CrabNote螃蟹笔记

2026 年每个 AI Agent 构建者都必须使用的 Memory Stack(译)

2026 年每个 AI Agent 构建者都必须使用的 Memory Stack(译)

2026 年每个 AI Agent 构建者都必须使用的 Memory Stack(译) 2026 年每个 AI Agent 构建者都必须使用的 Memory Stack(译) Modified April 29 我完成了13轮对抗性代码审查。此类攻击手段反复出现,直至第三道防护机制部署完成后才得以遏制。 实话实说:该防护手段无法阻挡拥有终端访问权限、蓄意发起攻击的入侵者。它主要用于防范意外异常,以及从恶意镜像仓库拉取到的被篡改提交记录,这也正是该威胁模型的设计目标。 Two phase write 与 catch up reconciliation Git commit 很慢,大约 10ms。SQLite index 写入很快,大约 1ms。简单的单事务处理方式,会让每一次写入操作都被迫采用两者中速度更慢的执行方式。 所以我用了下面这种方式: 1. 先写入 git(source of truth,持久化层)。 2. 再写入 index(best effort,更快)。 3. 如果进程在两个操作之间异常终止,<code catch up index</code 会在下次打开时执行数据同步修复。 生产环境里的做法大概长这样: 三条 reconciliation path: • Fast path:watermark 与 HEAD 一致,并且 event count 一样,什么都不做。 • Orphan path:index 里有 git 没有的 event。说明历史被重写了,或者导入了外来的 index。直接从 git 全量重建。 • Replay path:index 只是 git 的严格子集。那就从第一个缺失 event 开始往后 replay,依靠 ingest 的 idempotency 来保持 projection ordering 正确。 真正花了不少 review 才想透的细节,是“从最早缺失事件开始整体往前 replay”。 那个看起来显然正确的版本,也就是“只 ingest 缺失的事件”,在缺失事件和已索引事件交错出现时会出问题。 它会把当前 tip projection 用过时数据覆盖掉。 只有从最早缺失的事件开始,按顺序整体重放到最新,projection ordering 才能保持单调正确。 MCP server:tool description 比 implementation 更重要 Brain 暴露了 5 个 MCP 工具。一旦接入,所有兼容的 AI 工具都可以直接使用。 在 brain mcp/src/lib.rs 中: Code block Rust Copy [tool router(server handler)] impl BrainMcp { [tool(description = "Ping the brain server; returns 'pong' if alive.")] async fn ping(&self) String { "pong".to string() } [tool(description = "\ Save a persistent note to the user's long term memory. CALL THIS WHENEVER: the user states a preference or convention the user shares a decision or rationale you'd want to recall next session you finish a non trivial task and a lesson emerged the user asks you to remember something Do NOT call this for ephemeral context that only matters in the current turn. Written to a git backed event log, durable across sessions and tools.")] async fn note(&self, args: NoteArgs) Result<String, ErrorData { / ... / } [tool(description = "\ Search the user's long term memory. Returns up to 5 matches. CALL THIS PROACTIVELY at the start of non trivial tasks: before picking a library, pattern, or config value before running a migration, deploy, or schema change when the user references prior decisions when the user asks 'didn't we fix this before?' Prefix matching is automatic — typing finds .")] async fn ask(&self, args: AskArgs) Result<String, ErrorData { / ... / } // log and doctor similarly prescriptive } Agent 读取的是 description。它会根据 description 来决定自己要不要调用这个工具。 一个写着“当你不确定时就调用我”的 description,会比一个只写“返回 top 5 匹配结果”的 description 被调用得多得多。 每个工具的 description 里,我写的都不只是 WHAT,而是 WHEN。这才是真正的杠杆点。 FTS5 query rewriting FTS5 默认是 exact token match。如果你输入 fast,哪怕明明有很多关于 fastapi 的 note,它也可能什么都搜不到。 这是一个致命的 UX bug。 修复代码在 brain index/src/query.rs: 如果一个 query 没有显式 operator,就按 tokenizer boundary 把它拆开,并给每个 token 自动追加 。这样裸词就会自动变成前缀查询。 Code block Rust Copy fn escape fts(raw: &str) String { let has explicit operator = raw .chars() .any(|c| matches!(c, ' ' | '"' | '(' | ')' | ':')) || raw .split whitespace() .any(|w| matches!(w, "AND" | "OR" | "NOT")); if has explicit operator { // Balanced quotes → phrase query, pass through. Odd count → // stray quote, double for safety. let quote count = raw.chars().filter(|&c| c == '"').count(); if quote count.is multiple of(2) { return raw.to string(); } return raw.replace('"', "\"\""); } // Split on tokenizer boundaries (non alphanumeric + not ) and // append to each token. Bare words become prefix queries. let mut tokens = Vec::new(); let mut current = String::new(); for c in raw.chars() { if c.is alphanumeric() || c == ' ' { current.push(c); } else if !current.is empty() { tokens.push(format!("{current} ")); current.clear(); } } if !current.is empty() { tokens.push(format!("{current} ")); } tokens.join(" ") } 这样一来: • ask fast → fast → 能匹配到 fastapi • ask fastapi users → fastapi users → 两个 token 都能匹配 • ask cargo build → cargo build → 隐式 AND,同时找两个词 • ask "exact phrase" → 原样透传 • ask auth → 原样透传,不会变成双星号 FTS5 parse error 在 search path 中会被静默捕获,并返回空结果,而不是冒泡成一个 500。这样就算某个 MCP client 不小心传进来一个 foo(bar,也不会把整个 session 直接撞挂。 通过 git push / pull 来同步 Brain 本身就是一个 git repo。所以同步,就是 git push 和 git pull。我只是把它们包装成了 CLI 命令。 直接 shell out 到 git,本身就能白拿 SSH key、HTTPS credential manager、2FA、macOS Keychain,以及企业级 SSO。 不需要额外配置。不需要 daemon。不需要 cloud。所有脏活累活都让 git 来做。 跨工具测试 一旦这 5 个 harness 全都指向同一个 /.brain,那么 Claude Code 写下的一条 note,就会对 Cursor、Codex、OpenClaw、Hermes 同时可见。 这是我用来验证安装是否真的成功的测试: Code block Bash Copy In Claude Code: use brain.note to save: "we use authlib for PKCE, not fastapi users" [brain.note is called] Saved. In Cursor, new session, different project: what did we decide about auth? [Cursor's agent calls brain.ask "auth" via MCP] Based on brain memory: you chose authlib over fastapi users for PKCE. In Codex CLI: $ codex "what's our auth stack?" [Codex calls brain.ask "auth"] You use authlib for PKCE (not fastapi users). 三个不同工具。三个不同进程。一个共享 memory。 这正是我真正想要的东西。 这个构建过程里到底发生了什么 第 1 周。我先把 core 搭起来:types、store、index。整整两天没有任何东西能编译通过,因为 libgit2 的 Repository 是 !Send,而我又必须把它穿过 async code。最后的解法是在 orchestration layer 使用 spawn blocking,而不是硬跟 lifetime 对抗。 第 2 到第 3 周。我完成了 MCP server。第一版的 tool description 写的是 “Save a note.”。结果 agent 根本不会主动调用它,除非我明确提示。于是我把 description 重写成强制式、指令式的“CALL THIS WHENEVER”风格。然后 agent 开始在没人提醒的情况下主动调用。 对 MCP 来说,tool description 的重要性,高于 tool implementation 本身。 也是在第 3 周,我差点直接放弃。brain ask fast 明明应该命中 10 条提到 fastapi 的 note,但结果什么也没有。我重建了 3 次 index,写了一个 ingest 100 条 event 并断言 retrieval 的测试。测试通过了。CLI 依然什么都搜不到。 后来我更仔细地读了 FTS5 文档。它默认就是 exact token match。那天晚上我把 query rewriter 写了出来。第二天早上,整个系统的可用性直接提升了 10 倍。 第 4 到第 5 周。进入 Codex review 的第 5 到第 11 轮。一共修了 12 个问题,从 secret prefilter bypass 到 commit trailer injection,再到 watermark race condition。新增了 30 个 regression test。 整个模式几乎总是一样:Codex 指出一段只有 5 行的代码,我会先想“这没问题吧”,然后我写一个专门试图破坏它的测试,而那个测试通常真的能把它打爆。 第 6 周。我给 5 个 harness 全都写好了 adapter。大部分工作其实是 system prompt template 的编写,以及把每个 harness 的 include 语法调对。光是和 Cursor 的 .mdc frontmatter 打架,我就花了半天,因为它的 alwaysApply 想稳定工作,就得老老实实用字面量的 [" / "] glob。 第 7 周。我写了 brain push、brain pull 和 brain remote。整个过程花了 30 分钟,因为真正干活的是 git。 第 8 周。做 TUI 的打磨:按天分组、tool glyph、filter key。起因很简单,我突然意识到我已经从 4 个工具里收集了 200 多条 note,却没有任何方式按工具维度把它们看清楚。 第 9 周。我撞上了一堵墙:source pollution bug。每一条 note 都会匹配每一个 query。brain ask brain 会返回所有 note。brain ask fastapi 也会返回所有 note。 我不想把它夸得太神。这个系统并不魔法,甚至也谈不上特别聪明。它真正做到的,是一致性。 它会把每一条 note 都 commit 下来。Index 出错时它会重建 index。它拒绝写入 secret。它不会今天记得、明天就忘记上周学到的东西。 这种一致性,在几个月时间里不断累积之后,会带来一种与无状态 agent 明显不同的体验,即便底层模型根本没有变化。 使用时要特别注意什么 Index drift • 问题:index 里有 git 没有的 event,或者反过来。搜索会返回 ghosts,或者漏掉真实 note。 • 解决办法:brain doctor deep。它会从 git 一次性重建 index。 Source field FTS pollution • 问题:某个 metadata field 被错误索引进 body 列,导致每个 query 都匹配每条 note。 • 解决办法:只 allow list 那些真正应该进入 FTS 的字段。再写一个专门搜“本不该命中任何结果的 token”的测试。 Detached HEAD writes • 问题:一次写入如果落在 detached HEAD 上,下次 branch checkout 就会直接蒸发。 • 解决办法:在 open time 和 append time 都拒绝 detached HEAD。Brain 会直接拒绝写入,直到你重新 attach。 Concurrent writer races • 问题:两个 agent 恰好同一毫秒 commit,其中一个会输掉 HEAD compare and swap。 • 解决办法:exponential backoff retry loop,加上 idempotency key 的预检查,防止 retry 时双重落地。 Schema mismatch on upgrade • 问题:新版本 brain 提升了 FTS schema,旧的 index 文件已经不兼容。 • 解决办法:rebuild self heal。open 时检测到 mismatch,删除旧 sqlite 文件,再从 git 重建。 Secret leaked into commit subject • 问题:用户输入的 free form 文本如果进了 commit subject,就会永久存在于 git log oneline 里。 • 解决办法:对 Observe、Lesson、Redact 做 subject scrubbing。直接用 event id,永远不要回显用户原文。 如果重新来一遍,我会做哪些不同的决定 第一天就把 retrieval test 写出来。source pollution bug 本来可以很早就被发现,只要我一开始就有这样一个测试:“一条完全没提到 brain 的 note,不应该被 ask brain 命中。”但我直到第 9 周才写出这个测试。 写测试时,要验证用户体验,而不只是单元正确性。 每写 500 行代码就跑一次 adversarial review,而不是每 5000 行才做一次。我把 13 轮 Codex review 全都堆到了后面。如果我在每个 major feature 之后就跑一轮,大多数修复都只会是 5 行,而不是后面那种 50 行级别的重构。 一开始就使用 typed errors。StoreError::InvalidState { detail: String } 把 6 种完全不同的情况都塞进了同一个 stringly typed variant。调用方根本无法分辨哪些是 recoverable,哪些已经接近 corruption。 一开始就把 tool description 写成 prescriptive 风格。那种泛泛而谈的描述,例如 “Save a note”,根本不会被调用。只有那种明确告诉 agent WHEN 该调用的描述,才会触发主动使用。这是我做过杠杆最高的一次改动。 我完成了13轮对抗性代码审查。此类攻击手段反复出现,直至第三道防护机制部署完成后才得以遏制。 实话实说:该防护手段无法阻挡拥有终端访问权限、蓄意发起攻击的入侵者。它主要用于防范意外异常,以及从恶意镜像仓库拉取到的被篡改提交记录,这也正是该威胁模型的设计目标。 Two phase write 与 catch up reconciliation Git commit 很慢,大约 10ms。SQLite index 写入很快,大约 1ms。简单的单事务处理方式,会让每一次写入操作都被迫采用两者中速度更慢的执行方式。 所以我用了下面这种方式: 1. 先写入 git(source of truth,持久化层)。 2. 再写入 index(best effort,更快)。 3. 如果进程在两个操作之间异常终止,<code catch up index</code 会在下次打开时执行数据同步修复。 生产环境里的做法大概长这样: 三条 reconciliation path: • Fast path:watermark 与 HEAD 一致,并且 event count 一样,什么都不做。 • Orphan path:index 里有 git 没有的 event。说明历史被重写了,或者导入了外来的 index。直接从 git 全量重建。 • Replay path:index 只是 git 的严格子集。那就从第一个缺失 event 开始往后 replay,依靠 ingest 的 idempotency 来保持 projection ordering 正确。 真正花了不少 review 才想透的细节,是“从最早缺失事件开始整体往前 replay”。 那个看起来显然正确的版本,也就是“只 ingest 缺失的事件”,在缺失事件和已索引事件交错出现时会出问题。 它会把当前 tip projection 用过时数据覆盖掉。 只有从最早缺失的事件开始,按顺序整体重放到最新,projection ordering 才能保持单调正确。 MCP server:tool description 比 implementation 更重要 Brain 暴露了 5 个 MCP 工具。一旦接入,所有兼容的 AI 工具都可以直接使用。 在 brain mcp/src/lib.rs 中: Agent 读取的是 description。它会根据 description 来决定自己要不要调用这个工具。 一个写着“当你不确定时就调用我”的 description,会比一个只写“返回 top 5 匹配结果”的 description 被调用得多得多。 每个工具的 description 里,我写的都不只是 WHAT,而是 WHEN。这才是真正的杠杆点。 FTS5 query rewriting FTS5 默认是 exact token match。如果你输入 fast,哪怕明明有很多关于 fastapi 的 note,它也可能什么都搜不到。 这是一个致命的 UX bug。 修复代码在 brain index/src/query.rs: 如果一个 query 没有显式 operator,就按 tokenizer boundary 把它拆开,并给每个 token 自动追加 。这样裸词就会自动变成前缀查询。 这样一来: • ask fast → fast → 能匹配到 fastapi • ask fastapi users → fastapi users → 两个 token 都能匹配 • ask cargo build → cargo build → 隐式 AND,同时找两个词 • ask "exact phrase" → 原样透传 • ask auth → 原样透传,不会变成双星号 FTS5 parse error 在 search path 中会被静默捕获,并返回空结果,而不是冒泡成一个 500。这样就算某个 MCP client 不小心传进来一个 foo(bar,也不会把整个 session 直接撞挂。 通过 git push / pull 来同步 Brain 本身就是一个 git repo。所以同步,就是 git push 和 git pull。我只是把它们包装成了 CLI 命令。 直接 shell out 到 git,本身就能白拿 SSH key、HTTPS credential manager、2FA、macOS Keychain,以及企业级 SSO。 不需要额外配置。不需要 daemon。不需要 cloud。所有脏活累活都让 git 来做。 跨工具测试 一旦这 5 个 harness 全都指向同一个 /.brain,那么 Claude Code 写下的一条 note,就会对 Cursor、Codex、OpenClaw、Hermes 同时可见。 这是我用来验证安装是否真的成功的测试: 三个不同工具。三个不同进程。一个共享 memory。 这正是我真正想要的东西。 这个构建过程里到底发生了什么 第 1 周。我先把 core 搭起来:types、store、index。整整两天没有任何东西能编译通过,因为 libgit2 的 Repository 是 !Send,而我又必须把它穿过 async code。最后的解法是在 orchestration layer 使用 spawn blocking,而不是硬跟 lifetime 对抗。 第 2 到第 3 周。我完成了 MCP server。第一版的 tool description 写的是 “Save a note.”。结果 agent 根本不会主动调用它,除非我明确提示。于是我把 description 重写成强制式、指令式的“CALL THIS WHENEVER”风格。然后 agent 开始在没人提醒的情况下主动调用。 对 MCP 来说,tool description 的重要性,高于 tool implementation 本身。 也是在第 3 周,我差点直接放弃。brain ask fast 明明应该命中 10 条提到 fastapi 的 note,但结果什么也没有。我重建了 3 次 index,写了一个 ingest 100 条 event 并断言 retrieval 的测试。测试通过了。CLI 依然什么都搜不到。 后来我更仔细地读了 FTS5 文档。它默认就是 exact token match。那天晚上我把 query rewriter 写了出来。第二天早上,整个系统的可用性直接提升了 10 倍。 第 4 到第 5 周。进入 Codex review 的第 5 到第 11 轮。一共修了 12 个问题,从 secret prefilter bypass 到 commit trailer injection,再到 watermark race condition。新增了 30 个 regression test。 整个模式几乎总是一样:Codex 指出一段只有 5 行的代码,我会先想“这没问题吧”,然后我写一个专门试图破坏它的测试,而那个测试通常真的能把它打爆。 第 6 周。我给 5 个 harness 全都写好了 adapter。大部分工作其实是 system prompt template 的编写,以及把每个 harness 的 include 语法调对。光是和 Cursor 的 .mdc frontmatter 打架,我就花了半天,因为它的 alwaysApply 想稳定工作,就得老老实实用字面量的 [" / "] glob。 第 7 周。我写了 brain push、brain pull 和 brain remote。整个过程花了 30 分钟,因为真正干活的是 git。 第 8 周。做 TUI 的打磨:按天分组、tool glyph、filter key。起因很简单,我突然意识到我已经从 4 个工具里收集了 200 多条 note,却没有任何方式按工具维度把它们看清楚。 第 9 周。我撞上了一堵墙:source pollution bug。每一条 note 都会匹配每一个 query。brain ask brain 会返回所有 note。brain ask fastapi 也会返回所有 note。 我不想把它夸得太神。这个系统并不魔法,甚至也谈不上特别聪明。它真正做到的,是一致性。 它会把每一条 note 都 commit 下来。Index 出错时它会重建 index。它拒绝写入 secret。它不会今天记得、明天就忘记上周学到的东西。 这种一致性,在几个月时间里不断累积之后,会带来一种与无状态 agent 明显不同的体验,即便底层模型根本没有变化。 使用时要特别注意什么 Index drift • 问题:index 里有 git 没有的 event,或者反过来。搜索会返回 ghosts,或者漏掉真实 note。 • 解决办法:brain doctor deep。它会从 git 一次性重建 index。 Source field FTS pollution • 问题:某个 metadata field 被错误索引进 body 列,导致每个 query 都匹配每条 note。 • 解决办法:只 allow list 那些真正应该进入 FTS 的字段。再写一个专门搜“本不该命中任何结果的 token”的测试。 Detached HEAD writes • 问题:一次写入如果落在 detached HEAD 上,下次 branch checkout 就会直接蒸发。 • 解决办法:在 open time 和 append time 都拒绝 detached HEAD。Brain 会直接拒绝写入,直到你重新 attach。 Concurrent writer races • 问题:两个 agent 恰好同一毫秒 commit,其中一个会输掉 HEAD compare and swap。 • 解决办法:exponential backoff retry loop,加上 idempotency key 的预检查,防止 retry 时双重落地。 Schema mismatch on upgrade • 问题:新版本 brain 提升了 FTS schema,旧的 index 文件已经不兼容。 • 解决办法:rebuild self heal。open 时检测到 mismatch,删除旧 sqlite 文件,再从 git 重建。 Secret leaked into commit subject • 问题:用户输入的 free form 文本如果进了 commit subject,就会永久存在于 git log oneline 里。 • 解决办法:对 Observe、Lesson、Redact 做 subject scrubbing。直接用 event id,永远不要回显用户原文。 如果重新来一遍,我会做哪些不同的决定 第一天就把 retrieval test 写出来。source pollution bug 本来可以很早就被发现,只要我一开始就有这样一个测试:“一条完全没提到 brain 的 note,不应该被 ask brain 命中。”但我直到第 9 周才写出这个测试。 写测试时,要验证用户体验,而不只是单元正确性。 每写 500 行代码就跑一次 adversarial review,而不是每 5000 行才做一次。我把 13 轮 Codex review 全都堆到了后面。如果我在每个 major feature 之后就跑一轮,大多数修复都只会是 5 行,而不是后面那种 50 行级别的重构。 一开始就使用 typed errors。StoreError::InvalidState { detail: String } 把 6 种完全不同的情况都塞进了同一个 stringly typed variant。调用方根本无法分辨哪些是 recoverable,哪些已经接近 corruption。 一开始就把 tool description 写成 prescriptive 风格。那种泛泛而谈的描述,例如 “Save a note”,根本不会被调用。只有那种明确告诉 agent WHEN 该调用的描述,才会触发主动使用。这是我做过杠杆最高的一次改动。 在 API 稳定之前,不要急着拆 repo。我曾经试图在代码稳定之前,就把 brain 从 agentic stack 的 parent repo 里拆出去。结果来回 re sync 了 3 次。应该再等等。 最终结论 现在,这套系统还只是为一个人、在一组工具之间提供 memory。但工具会来,也会走。模型会被淘汰。Harness 会因为新的 IDE 插件而改变,旧的会逐渐腐烂。你的 skill 会随着项目一起演化。 真正会在多年时间里持续积累价值的,只有 memory:你做过什么决定、试过什么、失败过什么、什么有效、什么是你永远不会再做第二次的。 • 模型,可以在出现更好的东西时随时替换。 • Skills 和 protocols,可以随着工作方式一起重写。 • 但 memory 无法替代。它编码的是你独有的错误、你独有的决策、你独有的工作方式。 把你的 memory 掌握在自己手里。把你的索引掌握在自己手里。把它们保存在 plain file 和 git 里,放在任何公司都拿不走的地方。 致谢 这篇文章的 理论 受到 Harrison Chase(LangChain CEO)的启发,也被 Mem0 的 Taranjeet 进一步 sharpen 了。正是他先说出了 “memory lock in” 这个词,而那时我还没意识到这正是我需要的 framing。 我在一个下午写出了第一版。但完整 stack 花了三个月。并且从那之后,它几乎每周都在变得更好,甚至很多时候我根本没有再去碰它。 声明 这篇文章由作者本人完成研究与写作,由 Minimax M2.7 编辑。缩略图取自 Pinterest。 Harrison Chase: “memory should be open!” — https://x.com/hwchase17/status/2046308913939919232Harrison Chase: “Your Harness, Your Memory” — https://www.langchain.com/blog/your harness your memoryVivek Trivedi: “The Anatomy of an Agent Harness” — https://www.langchain.com/blog/the anatomy of an agent harness 原帖链接:https://x.com/Av1dlive/status/2048800691943309698 这里有一个几乎没人会告诉 AI 构建者的真相。 你不需要自己造模型。 你真正需要构建的 是一个存在于所有模型之下的 memory layer。 TL;DR:如果你不想通读全文,就把这个链接丢给你的 agent,然后直接问它问题:github.com/codejunkie99/brain github.com/codejunkie99/brain 核心论点:memory 应该是可移植的 无论是在 Claude Code 里,还是在 Cursor 里,都该使用同一套存储。 一个 index,能够在会话重置、模型下线、工具切换之后依然存活。 一个 protocol,让所有 harness 都能连接到同一个 brain。 Harrison Chase 在 4 月 21 日用三句话精准点出了这一点。 Taranjeet from Mem0 在 4 月 24 日把它说得更锋利了。 我同意他们两位的观点。我只是把它再往前推了一步。 记忆不应仅做到开放,也不应只实现可迁移。它还应当具备类型定义、可索引、可审计、可同步与高安全性。智能体不该只依托无法检视的标记文本块和向量数据库运行。 我花了一个多月时间,构建了正好符合这些要求的东西: • 基于 Git 的事件日志,每条笔记对应一次提交。 • 搭载 BM25 排序与前缀匹配功能、由 SQLite FTS5 构建的索引。 • 一款 MCP 标准输入输出服务器,向所有兼容智能代理开放五项工具能力。 • 面向人工介入场景的命令行工具(CLI)与全屏终端交互界面(TUI)。 这篇文章会解释我是怎么把它做出来的,以及为什么每一层都存在。 如果你只想安装它: github.com/codejunkie99/brain。它支持 Claude Code、Cursor、Codex、OpenClaw、Hermes,或者任何能讲 MCP 或 shell 的工具。 github.com/codejunkie99/brain 如果你想真正理解你装进去的是什么,那就继续往下读。 项目整体形态 7 个 Rust crate。5