agentmemory 技术拆解:AI Coding Agent 如何捕获、检索、注入和遗忘记忆

32 阅读13分钟

AI 编程助手越来越能干,但它还有一个很影响体验的问题:会“断片”。

今天你刚告诉它项目怎么启动、测试怎么跑、某个坑为什么不能碰;明天换一个会话,它可能又像新来的同事一样,从头问一遍。

这不是模型不聪明,而是很多 Coding Agent 的长期记忆还停留在比较原始的阶段:靠几份 Markdown 文件、几条规则、几段上下文硬塞。

最近走红的开源项目 agentmemory,正好提供了一个值得拆开的样本。它不是简单给 Agent 加一份“备忘录”,而是试图把 AI 编程助手的记忆做成一套工程系统:能自动捕获开发过程,能按问题检索历史,能把关键内容注入上下文,也能让旧信息过期、被覆盖、被遗忘。

顺着它的源码往下看,一套 Coding Agent 记忆系统的基本轮廓会变得很清楚。

记忆不是更长的提示词

很多项目里都会放 CLAUDE.mdAGENTS.md.cursorrules 之类的文件。

这些文件很有用。它们告诉 Agent:项目结构是什么、常用命令是什么、用户偏好是什么、哪些操作不要乱做。

但如果只靠这种方式,长期记忆很快会遇到几个问题。

问题传统 Markdown 记忆的表现
捕获主要靠人工维护,Agent 做过什么不会自动沉淀
检索通常是整段注入,不是按问题召回
成本内容越写越长,占用上下文越来越多
更新新旧规则混在一起,不清楚谁覆盖谁
治理缺少过期、版本、访问强化和出处追踪

长期记忆要解决的,不是“把更多历史塞进 prompt”。

它要解决的是:当 Agent 面对一个新问题时,能不能从过去的开发过程里找出相关证据,再用有限的 token 预算带回当前上下文。

agentmemory 的整体流程大概是这样:

开发过程事件
  -> Hook 自动捕获
  -> 隐私清洗
  -> 压缩成 observation
  -> BM25 / Vector / Graph 检索
  -> 按 token budget 注入上下文
  -> 访问记录、版本覆盖、反思和遗忘

这已经不是“记笔记”,而是一个面向 Agent 的检索与状态管理系统。

第一步:先把开发过程捕获下来

记忆系统的第一步不是总结,而是采集。

agentmemorysrc/functions/observe.ts 里监听 Agent 生命周期里的关键 Hook:

Hook捕获内容
session_start会话启动信息
prompt_submit用户提交的 prompt
pre_tool_use工具调用前的上下文
post_tool_use工具名、输入、输出
post_tool_failure工具失败和错误信息
pre_compact上下文压缩前的记忆注入点
subagent_start / subagent_stop子 Agent 生命周期
stop / session_end会话结束信息

这意味着,一次文件读取、一次命令执行、一次测试失败、一次用户补充要求,都可以成为记忆候选。

这些事件会先变成 RawObservation,里面包含 sessionIdtimestamphookTypetoolNametoolInputtoolOutputuserPrompt 等字段。

这里有两个设计很关键。

第一,它会做去重。Coding Agent 经常反复读同一个文件、重复执行类似命令,如果每次都写入长期记忆,记忆库很快会变成噪音堆。

第二,它在落库前做隐私清洗。src/functions/privacy.ts 会处理 <private>...</private>、API key、Bearer token、GitHub token、AWS key、JWT、npm token 等敏感模式。

这一步的位置很重要。隐私保护不能只发生在“展示结果”时,更应该发生在“写入记忆”之前。

第二步:把事件压成可检索的 observation

原始工具输出通常很乱:有命令、有日志、有报错、有文件路径,也可能有大段无关文本。

如果直接把这些内容存起来,后面很难高质量检索。

agentmemorysrc/prompts/compression.ts 里定义了一套 observation 压缩格式:

<observation>
  <type>file_read | file_write | command_run | error | decision | ...</type>
  <title>Short descriptive title</title>
  <subtitle>One-line context</subtitle>
  <facts>
    <fact>Specific factual detail</fact>
  </facts>
  <narrative>2-3 sentence summary</narrative>
  <concepts>
    <concept>technical concept or pattern</concept>
  </concepts>
  <files>
    <file>path/to/file</file>
  </files>
  <importance>1-10</importance>
</observation>

这套格式不是为了好看,而是为了让记忆以后能被找回来。

facts 记录具体事实,concepts 提供可复用搜索词,files 保留精确路径,narrative 解释事件的意义,importance 帮系统判断这条记忆以后是否值得注入上下文。

在代码场景里,files 尤其重要。

因为很多开发问题不是纯语义问题,而是精确路径问题:哪个配置文件改过、哪个测试文件失败过、哪个函数曾经报错过。文件路径、函数名、错误码,往往比一句模糊总结更有价值。

源码里还限制了输入长度:toolInput 最多 4000 字符,toolOutput 最多 4000 字符,userPrompt 最多 2000 字符。

这说明它不追求原样保存一切,而是在保存成本和未来可检索性之间做取舍。

还有一个现实的工程细节:per-observation LLM compression 从 v0.8.8 开始是 opt-in。默认路径会使用 buildSyntheticCompression(raw),避免每次工具调用都消耗大模型 token。

高频 Coding Agent 每天可能产生大量工具事件。如果每条都调用 LLM 压缩,系统很快会被成本和延迟拖住。

第三步:检索不能只靠向量

很多人一提长期记忆,就会想到向量数据库。

但在 Coding Agent 场景里,纯向量检索并不够。代码世界里有大量精确信号,比如文件名、函数名、命令、错误码、包名,这些内容用关键词检索反而更稳。

agentmemory 的检索系统分成三路:

检索流对应文件主要作用
BM25src/state/search-index.ts命中文件名、函数名、错误码、路径等精确信号
Vectorsrc/state/vector-index.ts处理语义相似、表达不同但意思接近的问题
Graphsrc/functions/graph-retrieval.ts根据实体关系做跨记忆扩展

BM25 是其中很重要的一路。

SearchIndex 实现了倒排索引,BM25 参数是 k1 = 1.2b = 0.75。它索引的字段包括 titlesubtitlenarrativefactsconceptsfilestype

这类查询很适合 BM25:

  • docker-compose.yml
  • auth middleware
  • Prisma schema
  • rate_limit
  • HTTP 403
  • src/functions/search.ts

向量检索负责补上另一部分:当用户说“上次那个登录失败的问题”,它未必包含当时的精确错误码,但 embedding 可能能找到语义相关记录。

项目推荐的本地 embedding 是 all-MiniLM-L6-v2384 维,不需要 API key。源码里的 VectorIndex 采用逐条扫描 embedding 的方式,所以更适合个人或小团队规模;如果记忆量继续扩大,后面可能需要更专业的向量索引。

Graph 负责实体关系。它会从 query 中抽实体,在 KV.graphNodesKV.graphEdges 上做 BFS,默认深度 2 。这一路更适合回答“这个模块和哪些 bug、文件、决策有关”。

三路结果最后在 src/state/hybrid-search.ts 里融合。

它使用 RRF,也就是 Reciprocal Rank Fusion:

combinedScore =
  BM25_weight   * 1 / (60 + bm25Rank)
+ Vector_weight * 1 / (60 + vectorRank)
+ Graph_weight  * 1 / (60 + graphRank)

默认权重是 BM25 0.4 、Vector 0.6 、Graph 0.3 。如果某一路没有结果,权重会重新归一化。

还有一个很贴近开发场景的细节:session diversify。

系统默认每个 session 最多返回 3 条结果,避免同一次会话里的相似 observation 刷满列表。开发记忆天然按会话聚集,如果不限制,很容易搜出来全是同一段历史的碎片。

第四步:记忆分层,不同内容走不同通道

一个成熟的记忆系统,不能把所有内容都放进同一个篮子。

用户偏好、项目约定、一次报错、长期模式,性质完全不同。如果都混成一份文本,迟早会失控。

agentmemory 把记忆拆成了三类:可检索记忆、强制注入记忆、高阶抽象记忆。

### 可检索记忆:observations 和 memories

observations 来自自动 Hook 捕获,memories 来自显式保存,比如 MCP 工具 memory_save

src/functions/remember.ts 支持六类手动记忆:

pattern | preference | architecture | bug | workflow | fact

写入前,它会扫描已有 KV.memories,对新旧内容算 Jaccard similarity。如果相似度大于 0.7 ,就认为新记忆覆盖旧记忆。

覆盖不是简单删除,而是形成版本链:

  • 旧 memory:isLatest = false
  • 新 memory:version = old.version + 1
  • 新 memory:parentId = old.id
  • 新 memory:supersedes = [old.id]

这比在 Markdown 末尾追加一句更可控。

当然,Jaccard 是词面相似。如果同一条偏好被换一种说法表达,系统不一定能识别出覆盖关系。这是它目前的边界。

### 强制注入记忆:slots

有些内容不能等检索命中,必须每次都带着。

比如用户偏好、工具禁令、项目硬约束、未完成事项。

src/functions/slots.ts 里有一组默认 slots:

SlotScope默认 pinned用途
personaglobaltrueAgent 的角色、语气、行为准则
user_preferencesglobaltrue用户编码风格、工具偏好
tool_guidelinesglobaltrue工具选择和执行规则
project_contextprojecttrue架构约定、构建/测试命令
guidanceprojecttrue下一次 session 的建议和风险
pending_itemsprojecttrue未完成任务
session_patternsprojectfalse近期重复行为模式
self_notesprojectfalseAgent 自己的临时笔记

AGENTMEMORY_SLOTS=true 时,context.ts 会读取 pinned slots,并渲染成:

# agentmemory pinned slots

## persona
...

## user_preferences
...

检索型记忆解决“需要时想起”,slots 解决“每次都必须知道”。这两类记忆分开,是很重要的设计。

### 高阶抽象记忆:reflect 和 insights

还有一类记忆,不是单次事件,而是跨多次事件才能看出来的模式。

比如:某个项目每次升级依赖都容易卡在 CI 缓存;某类 bug 总是和同一个配置文件有关;某个用户总是偏好先写测试再改实现。

src/functions/reflect.ts 做的就是这一层。

它会读取 KV.graphNodesKV.graphEdgesKV.semanticKV.lessonsKV.crystals,先构造 concept cluster,再让 LLM 生成 insight:

<insights>
  <insight confidence="0.0-1.0" title="Short descriptive title">
    actionable and non-obvious observation
  </insight>
</insights>

如果同一个 insight 再次出现,系统不会重复保存,而是强化已有 insight:reinforcements++,confidence 逐步接近 1.0

如果 insight 长期没有被强化,mem::insight-decay-sweep 会按周衰减 confidence。低到一定程度且从未被强化的 insight,会被 soft delete。

这一步让记忆从“历史记录”进一步变成“经验沉淀”。

MCP:把记忆能力暴露给不同 Agent

记忆系统如果只能给一个客户端用,价值会打折。

agentmemory 通过 MCP 暴露工具,让 Claude Code、Codex CLI、Cursor 等不同客户端都能接入同一套记忆能力。

src/mcp/tools-registry.ts 里定义了 51 个 MCP tools,包括:

  • memory_recall
  • memory_save
  • memory_smart_search
  • memory_sessions
  • memory_consolidate
  • memory_reflect
  • memory_lesson_save
  • memory_slot_list / get / create / append / replace / delete
  • memory_graph_query
  • memory_team_share
  • memory_action_create / update
  • memory_signal_send / read
  • memory_verify
  • memory_obsidian_export

默认不会全部打开。

AGENTMEMORY_TOOLS=all 会返回全部 51 个;默认 core 模式只暴露 8 个 essential tools:

memory_save
memory_recall
memory_consolidate
memory_smart_search
memory_sessions
memory_diagnose
memory_lesson_save
memory_reflect

MCP 接入分两层。

完整 server 提供:

  • GET /agentmemory/mcp/tools
  • POST /agentmemory/mcp/call

standalone MCP 可以这样启动:

npx @agentmemory/agentmemory mcp
npx @agentmemory/mcp

如果本地 [http://localhost:3111/agentmemory/livez](http://localhost:3111/agentmemory/livez) 可用,它会 proxy 到真正的 agentmemory server;如果服务不可达,它会退回本地 InMemoryKV,保留保存、搜索、导出等最小能力。

src/mcp/transport.ts 里还有一个协议细节:JSON-RPC notification 没有 id,MCP 规范要求 server 不回复 notification。注释里提到,严格客户端比如 Codex CLI 会把多余回复视为协议违规并关闭连接。

这种细节看起来不起眼,但对工具生态兼容很关键。

Benchmark:看召回率,也看 token 成本

agentmemory 自带了 benchmark 文档。

benchmark/LONGMEMEVAL.md 里,它跑的是 LongMemEval-S:

  • 500 个问题;
  • 每题约 48 个 sessions;
  • 每题约 115K tokens;
  • embedding 使用本地 all-MiniLM-L6-v2
  • 指标是 retrieval recall,不是最终问答正确率。

结果如下:

SystemR@5R@10R@20NDCG@10MRR
agentmemory BM25+Vector95.2%98.6%99.4%87.9%88.2%
agentmemory BM25-only86.2%94.6%98.6%73.0%71.5%
MemPalace raw vector-only96.6%97.6%

BM25-only 的 R@5 已经有 86.2% ,说明关键词检索在开发记忆里仍然很有价值。加上 vector 后,R@5 到 95.2% ,提升约 9 个百分点

但这个指标不能被误读。它衡量的是 gold session 是否出现在 top-K 召回结果里,不是模型最终回答是否正确。

另一个内部评估在 benchmark/QUALITY.md

SystemRecall@10Precision@5NDCG@10Tokens/query
Built-in CLAUDE.md / grep55.8%78.0%80.3%22,610
Built-in 200-line MEMORY.md37.8%63.0%56.4%7,938
BM25-only55.9%95.0%82.7%3,142
Dual-stream58.6%90.0%84.7%3,142
Triple-stream58.0%87.0%81.7%3,142

这里有两个信息值得放在一起看。

一方面,Triple-stream 没有稳定超过 Dual-stream,说明图检索不是天然加分项,图谱质量会直接影响结果。

另一方面,token 成本从 22,610 tokens/query 降到 3,142 tokens/query。长期记忆系统的价值,不只是召回率更高,也包括让 Agent 不必每次把全部历史塞回上下文。

边界也很清楚

agentmemory 的设计很完整,但它不是魔法。

第一,VectorIndex 当前是逐条扫描 embedding。个人或小团队规模问题不大,数据量继续扩大后,需要更专业的向量索引。

第二,compression、reflect、consolidation 中不少环节依赖 LLM 输出 XML。项目有 validator 和 retry,但结构化输出仍然可能失败。

第三,Graph 检索是否有效,取决于图谱抽取质量。内部 benchmark 里 Triple-stream 没有稳定压过 Dual-stream,这一点不应该被包装成“图检索全面领先”。

第四,默认 synthetic compression 降低了成本和延迟,但也可能牺牲 factsconceptsnarrative 的质量。真实项目里的召回效果,还需要更多使用数据验证。

这些边界反而让它更像真实工程项目:方向明确,但仍有取舍。

AI 编程助手需要一套记忆系统

当 Coding Agent 只是偶尔帮你补几行代码,记忆问题没那么明显。

但当它开始参与长期项目,记忆就会变成基础设施。

它至少要回答四个问题:

  • 过去的开发行为能不能自动捕获?
  • 需要历史时,能不能按问题检索,而不是全量注入?
  • 用户偏好、项目约定、错误记录、长期模式,能不能分开管理?
  • 旧信息能不能过期,新经验能不能覆盖旧经验?

agentmemory 的答案,是把记忆拆成 observation、memory、slot、insight,再用 Hook、BM25、vector、graph、MCP、decay 串起来。

它还没有把所有问题都解决,但它提供了一个很清晰的方向:

AI Coding Agent 的长期记忆,不应该只是更厚的提示词,而应该是一套可检索、可治理、可演化的状态系统。

---

参考链接