AI 编程助手越来越能干,但它还有一个很影响体验的问题:会“断片”。
今天你刚告诉它项目怎么启动、测试怎么跑、某个坑为什么不能碰;明天换一个会话,它可能又像新来的同事一样,从头问一遍。
这不是模型不聪明,而是很多 Coding Agent 的长期记忆还停留在比较原始的阶段:靠几份 Markdown 文件、几条规则、几段上下文硬塞。
最近走红的开源项目 agentmemory,正好提供了一个值得拆开的样本。它不是简单给 Agent 加一份“备忘录”,而是试图把 AI 编程助手的记忆做成一套工程系统:能自动捕获开发过程,能按问题检索历史,能把关键内容注入上下文,也能让旧信息过期、被覆盖、被遗忘。
顺着它的源码往下看,一套 Coding Agent 记忆系统的基本轮廓会变得很清楚。
记忆不是更长的提示词
很多项目里都会放 CLAUDE.md、AGENTS.md、.cursorrules 之类的文件。
这些文件很有用。它们告诉 Agent:项目结构是什么、常用命令是什么、用户偏好是什么、哪些操作不要乱做。
但如果只靠这种方式,长期记忆很快会遇到几个问题。
| 问题 | 传统 Markdown 记忆的表现 |
|---|---|
| 捕获 | 主要靠人工维护,Agent 做过什么不会自动沉淀 |
| 检索 | 通常是整段注入,不是按问题召回 |
| 成本 | 内容越写越长,占用上下文越来越多 |
| 更新 | 新旧规则混在一起,不清楚谁覆盖谁 |
| 治理 | 缺少过期、版本、访问强化和出处追踪 |
长期记忆要解决的,不是“把更多历史塞进 prompt”。
它要解决的是:当 Agent 面对一个新问题时,能不能从过去的开发过程里找出相关证据,再用有限的 token 预算带回当前上下文。
agentmemory 的整体流程大概是这样:
开发过程事件
-> Hook 自动捕获
-> 隐私清洗
-> 压缩成 observation
-> BM25 / Vector / Graph 检索
-> 按 token budget 注入上下文
-> 访问记录、版本覆盖、反思和遗忘
这已经不是“记笔记”,而是一个面向 Agent 的检索与状态管理系统。
第一步:先把开发过程捕获下来
记忆系统的第一步不是总结,而是采集。
agentmemory 在 src/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,里面包含 sessionId、timestamp、hookType、toolName、toolInput、toolOutput、userPrompt 等字段。
这里有两个设计很关键。
第一,它会做去重。Coding Agent 经常反复读同一个文件、重复执行类似命令,如果每次都写入长期记忆,记忆库很快会变成噪音堆。
第二,它在落库前做隐私清洗。src/functions/privacy.ts 会处理 <private>...</private>、API key、Bearer token、GitHub token、AWS key、JWT、npm token 等敏感模式。
这一步的位置很重要。隐私保护不能只发生在“展示结果”时,更应该发生在“写入记忆”之前。
第二步:把事件压成可检索的 observation
原始工具输出通常很乱:有命令、有日志、有报错、有文件路径,也可能有大段无关文本。
如果直接把这些内容存起来,后面很难高质量检索。
agentmemory 在 src/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 的检索系统分成三路:
| 检索流 | 对应文件 | 主要作用 |
|---|---|---|
| BM25 | src/state/search-index.ts | 命中文件名、函数名、错误码、路径等精确信号 |
| Vector | src/state/vector-index.ts | 处理语义相似、表达不同但意思接近的问题 |
| Graph | src/functions/graph-retrieval.ts | 根据实体关系做跨记忆扩展 |
BM25 是其中很重要的一路。
SearchIndex 实现了倒排索引,BM25 参数是 k1 = 1.2、b = 0.75。它索引的字段包括 title、subtitle、narrative、facts、concepts、files、type。
这类查询很适合 BM25:
docker-compose.ymlauth middlewarePrisma schemarate_limitHTTP 403src/functions/search.ts
向量检索负责补上另一部分:当用户说“上次那个登录失败的问题”,它未必包含当时的精确错误码,但 embedding 可能能找到语义相关记录。
项目推荐的本地 embedding 是 all-MiniLM-L6-v2, 384 维,不需要 API key。源码里的 VectorIndex 采用逐条扫描 embedding 的方式,所以更适合个人或小团队规模;如果记忆量继续扩大,后面可能需要更专业的向量索引。
Graph 负责实体关系。它会从 query 中抽实体,在 KV.graphNodes 和 KV.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:
| Slot | Scope | 默认 pinned | 用途 |
|---|---|---|---|
persona | global | true | Agent 的角色、语气、行为准则 |
user_preferences | global | true | 用户编码风格、工具偏好 |
tool_guidelines | global | true | 工具选择和执行规则 |
project_context | project | true | 架构约定、构建/测试命令 |
guidance | project | true | 下一次 session 的建议和风险 |
pending_items | project | true | 未完成任务 |
session_patterns | project | false | 近期重复行为模式 |
self_notes | project | false | Agent 自己的临时笔记 |
当 AGENTMEMORY_SLOTS=true 时,context.ts 会读取 pinned slots,并渲染成:
# agentmemory pinned slots
## persona
...
## user_preferences
...
检索型记忆解决“需要时想起”,slots 解决“每次都必须知道”。这两类记忆分开,是很重要的设计。
### 高阶抽象记忆:reflect 和 insights
还有一类记忆,不是单次事件,而是跨多次事件才能看出来的模式。
比如:某个项目每次升级依赖都容易卡在 CI 缓存;某类 bug 总是和同一个配置文件有关;某个用户总是偏好先写测试再改实现。
src/functions/reflect.ts 做的就是这一层。
它会读取 KV.graphNodes、KV.graphEdges、KV.semantic、KV.lessons、KV.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_recallmemory_savememory_smart_searchmemory_sessionsmemory_consolidatememory_reflectmemory_lesson_savememory_slot_list / get / create / append / replace / deletememory_graph_querymemory_team_sharememory_action_create / updatememory_signal_send / readmemory_verifymemory_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/toolsPOST /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,不是最终问答正确率。
结果如下:
| System | R@5 | R@10 | R@20 | NDCG@10 | MRR |
|---|---|---|---|---|---|
| agentmemory BM25+Vector | 95.2% | 98.6% | 99.4% | 87.9% | 88.2% |
| agentmemory BM25-only | 86.2% | 94.6% | 98.6% | 73.0% | 71.5% |
| MemPalace raw vector-only | 96.6% | 约 97.6% | — | — | — |
BM25-only 的 R@5 已经有 86.2% ,说明关键词检索在开发记忆里仍然很有价值。加上 vector 后,R@5 到 95.2% ,提升约 9 个百分点 。
但这个指标不能被误读。它衡量的是 gold session 是否出现在 top-K 召回结果里,不是模型最终回答是否正确。
另一个内部评估在 benchmark/QUALITY.md:
| System | Recall@10 | Precision@5 | NDCG@10 | Tokens/query |
|---|---|---|---|---|
| Built-in CLAUDE.md / grep | 55.8% | 78.0% | 80.3% | 22,610 |
| Built-in 200-line MEMORY.md | 37.8% | 63.0% | 56.4% | 7,938 |
| BM25-only | 55.9% | 95.0% | 82.7% | 3,142 |
| Dual-stream | 58.6% | 90.0% | 84.7% | 3,142 |
| Triple-stream | 58.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 降低了成本和延迟,但也可能牺牲 facts、concepts、narrative 的质量。真实项目里的召回效果,还需要更多使用数据验证。
这些边界反而让它更像真实工程项目:方向明确,但仍有取舍。
AI 编程助手需要一套记忆系统
当 Coding Agent 只是偶尔帮你补几行代码,记忆问题没那么明显。
但当它开始参与长期项目,记忆就会变成基础设施。
它至少要回答四个问题:
- 过去的开发行为能不能自动捕获?
- 需要历史时,能不能按问题检索,而不是全量注入?
- 用户偏好、项目约定、错误记录、长期模式,能不能分开管理?
- 旧信息能不能过期,新经验能不能覆盖旧经验?
agentmemory 的答案,是把记忆拆成 observation、memory、slot、insight,再用 Hook、BM25、vector、graph、MCP、decay 串起来。
它还没有把所有问题都解决,但它提供了一个很清晰的方向:
AI Coding Agent 的长期记忆,不应该只是更厚的提示词,而应该是一套可检索、可治理、可演化的状态系统。
---
参考链接
- GitHub:<github.com/rohitg00/ag…
- npm:
@agentmemory/agentmemory - 项目主页:<agent-memory.dev>
- LongMemEval:<arxiv.org/abs/2410.10…
- LoCoMo:<snap-stanford.github.io/LoCoMo/>