Sherry's Blog


  • 首页

  • 关于我

  • 标签

  • 目录

  • 吐槽

  • 文章列表

你选了 Opus,但干活的是 Haiku:从三个 md 文件看 Claude Code 的模型路由

发表于 2026-04-15 | 分类于 技术 , agent , claude-code

本文基于以下三份资料:

  1. Claude Code v2.1.88 反混淆源码(sherry255/civil-engineering-cloud-claude-code-source-v2.1.88,2026年4月初)
  2. anthropics/claude-code 官方仓库 v2.1.88 tag 下的 plugin 文件
  3. 本地已安装的 Claude Code plugin marketplace

后续版本可能已有改动。文章经过一轮事实核对修订,subagent 路由这套机制本身在快速迭代,如发现与当前版本不符欢迎指正。

起因是我在看 Claude Code 的 plugin 目录时,发现了三个看起来都叫”code review”的文件:

1
2
3
plugins/code-review/commands/code-review.md
plugins/feature-dev/agents/code-reviewer.md
plugins/pr-review-toolkit/agents/code-reviewer.md

想弄清楚这三个文件和底层源码之间是什么关系——md 里写的 model: sonnet 到底是怎么被执行的,谁说了算。顺着这条线挖下去,发现了一些有意思的事情。


一、三个文件是三种不同的东西

文件 A:slash command(工作流编排 prompt)

code-review/commands/code-review.md 是你输入 /code-review 时触发的 slash command。严格来说,它不是”硬编码的工作流”,而是一段强引导的 prompt:命令正文被 loadSkillsDir.ts 加载后喂给主模型,主模型按正文里的指示去调用子 agent。不是编译期强制,是 LLM 按指令执行。

v2.1.88 tag 下实际的步骤(来自 官方仓库):

1
2
3
4
5
6
7
8
9
10
11
12
13
Step 1:  Haiku agent × 1  — 资格检查(PR 是否关闭/草稿/不需要 review 等)
Step 2: Haiku agent × 1 — 收集相关 CLAUDE.md 路径
Step 3: Sonnet agent × 1 — PR 摘要
Step 4: 4 个并行 agent:
- Agent 1+2: Sonnet × 2(CLAUDE.md 合规检查)
- Agent 3: Opus × 1(bug 扫描)
- Agent 4: Opus × 1(逻辑/安全问题)
Step 5: 每个 Step 4 发现的 issue 并行启动一个验证 subagent:
- bug 类 issue → Opus
- CLAUDE.md 类 issue → Sonnet
Step 6: 过滤未通过验证的 issue
Step 7: 输出摘要(有 --comment 才继续)
Step 8-9: 组织并发内联评论

也就是说,Opus 在这个流程里是参与的——step 4 的 bug agent 和 step 5 的 bug 验证都是 Opus,只是审阅维度被拆分成了”合规用 Sonnet、bug 用 Opus”。我之前写的”流程里没有一个子任务是 Opus”是错的,在此更正。

你的 /model 选择对这个流程的影响:命令正文里用”Launch a haiku agent”、”Launch 4 agents in parallel: … Opus bug agent”这种语言强引导主模型,主模型按指令调用 Agent 工具(源码里正式名是 AgentTool,旧名 Task,两者是同一个东西;下文统一写成 Agent/Task 工具 避免混淆)并传对应的 model 参数。用户手动 /model sonnet 不会直接覆盖这些传参——因为 Agent/Task 工具的 model 参数优先级高于父模型继承(见第二节的优先级表)。不过这不是”编译期写死”,理论上主模型可以违背指令,只是正常情况下它会遵守。

文件 B 和 C:subagent(被调用的积木)

另外两个是 subagent,结构简单:接收代码 diff,返回 confidence ≥ 80 的 issue 列表。它们不是给用户直接触发的,是被别的 command 内部调用的。

文件 B(feature-dev) 文件 C(pr-review-toolkit)
模型 model: sonnet model: opus
触发时机 /feature-dev Phase 6,并行起 3 个做质量检查 /pr-review-toolkit:review-pr 命令正文里默认总会包含它
使用节奏 开发内循环,频繁 PR 提交前,低频

文件 A 和文件 B/C 完全没有关系。 /code-review Step 4 里的 4 个并行 agent(2 Sonnet + 2 Opus)是 inline 临时 agent,不是 code-reviewer subagent。三个文件只是名字相似,彼此独立。

为什么 B 和 C 各自带一份而不共用?

Claude Code 的 plugin 系统其实支持跨包依赖——src/utils/plugins/dependencyResolver.ts 有完整的 manifest.dependencies 解析、依赖校验、跨 marketplace 依赖规则。所以说 plugin 系统”禁止跨包依赖”是错的,这是我之前的误读。

但 feature-dev 和 pr-review-toolkit 的作者选择了各自带一份 code-reviewer,可能的原因是:两个 plugin 的使用场景节奏不同,需要不同的模型默认值和 system prompt,直接复用会导致行为冲突;而且 plugin 作者之间没有协调,各写各的更简单。

所以两份文件实际上有差异:

  • feature-dev 的 code-reviewer.md 默认 model: sonnet,用于开发循环的快速检查
  • pr-review-toolkit 的 code-reviewer.md 默认 model: opus,用于提交前的精细审阅

内容大约 95% 相同,但 frontmatter 的 model 字段和 description 里的 “proactive use” 措辞不同。


二、md 文件里的 model 字段,在源码里怎么执行

这是我最想弄清楚的问题。

源码在 src/utils/model/agent.ts 的 getAgentModel() 函数,优先级从高到低:

1
2
3
4
1. CLAUDE_CODE_SUBAGENT_MODEL 环境变量(全局强制覆盖)
2. Agent/Task 工具调用时显式传的 model 参数
3. Agent 定义里的 model 字段(md frontmatter 或代码里写死)
4. 默认:'inherit',继承父 agent 的模型

所以 md 文件里写 model: sonnet,就是第 3 层。优先级不算高,可以被第 2 层(Agent/Task 工具调用时的显式 model 参数)覆盖——这也是 /code-review 能做到”调用同一个 agent 类型但传不同模型”的机制。

需要说明的是:Agent/Task 工具的 model 参数允许每次调用时动态传入(src/tools/AgentTool/AgentTool.tsx:86),它的优先级高于 md frontmatter。主模型是否主动传参,取决于当前 prompt 有没有引导它这样做——比如 slash command 正文里明确写”Launch a haiku agent”时,主模型就会传 model: haiku。所以 md frontmatter 的模型设定不是板上钉钉,只是在没人覆盖时的默认值。

同族继承是一个细节:如果你选了 claude-opus-4-6,md 里写 model: opus,subagent 继承的是精确型号 claude-opus-4-6,而不是 provider 默认的 opus 版本。这防止了 Vertex/Bedrock 用户意外降级。


三、md 文件是怎么被加载和调用的

只有理解了完整的加载和调用链,才能回答下一个关键问题:哪些 md 文件什么时候会被实际触发?

加载链路

启动时 Claude Code 扫描所有 plugin 的 agents/ 目录,调用链如下:

1
2
3
4
5
6
7
8
9
10
main.tsx:2029
→ getAgentDefinitionsWithOverrides(currentCwd)
→ loadPluginAgents() // src/utils/plugins/loadPluginAgents.ts:231-344
→ loadAllPluginsCacheOnly() // 从 pluginLoader 拿到启用的 plugin 列表
→ 遍历每个 plugin 的 agentsPath
→ walkPluginMarkdown() // src/utils/plugins/walkPluginMarkdown.ts:21-69
// 递归找所有 .md,记录 namespace 子路径
→ loadAgentFromFile() // loadPluginAgents.ts:65-229
→ fs.readFile + parseFrontmatter
→ 组装 AgentDefinition 对象

关键:agentType 的命名规则(loadPluginAgents.ts:89-90):

1
2
const nameParts = [pluginName, ...namespace, baseAgentName]
const agentType = nameParts.join(':')

所以 feature-dev/agents/code-reviewer.md 加载后的 agentType 是 feature-dev:code-reviewer。两个 plugin 各自有 code-reviewer.md 时,因为 plugin 名前缀不同,agentType 不会冲突。

AgentDefinition 数据结构

每个 md 文件加载后变成一个对象(loadAgentsDir.ts:106-159),核心字段:

字段 来源 用途
agentType plugin名 + namespace + name 精确匹配的 ID
whenToUse frontmatter 的 description 给主 agent 看的”何时使用我”
model frontmatter 的 model subagent 用什么模型
tools frontmatter 的 tools subagent 能用哪些工具
getSystemPrompt md 正文(去掉 frontmatter) subagent 启动时的 system prompt

加载完后存在 toolUseContext.options.agentDefinitions.activeAgents 里,所有 Agent/Task 工具调用都从这里查。

主 agent 怎么知道有哪些 subagent

这是最关键的一步。源码在 src/tools/AgentTool/prompt.ts:43-46:

1
2
3
function formatAgentLine(agent: AgentDefinition): string {
return `- ${agent.agentType}: ${agent.whenToUse} (Tools: ${toolsDescription})`
}

每个 plugin agent 被拼成一行:

1
2
3
- feature-dev:code-reviewer: Reviews code for bugs, logic errors... (Tools: Glob, Grep, Read, ...)
- pr-review-toolkit:code-reviewer: Use this agent when you need to review code... (Tools: ...)
- pr-review-toolkit:silent-failure-hunter: ...

这些行会通过两条路径之一被主 agent 看到(具体走哪条由 feature flag 控制,见 prompt.ts:190、attachments.ts:1490):

  1. 直接注入到 Agent/Task 工具的 tool description 里——主 agent 在每次需要调用 Agent/Task 工具时能看到这个完整列表
  2. 作为 agent_listing_delta attachment 单独发送——不放在 tool description,而是作为消息 attachment 传递给主 agent

不管走哪条路径,最终效果相同:主 agent 能看到这个 agent 列表,并据此选择 subagent_type 参数。

主 agent 怎么决定调哪个

它读 description 做语义判断。 不是规则匹配,是 LLM 自己决定。

主 agent 看到这一长串 agent 列表,根据当前用户的需求(”帮我审一下这段代码”),自己挑一个最合适的 agentType,然后传给 Agent/Task 工具:

1
2
3
4
Agent({
subagent_type: "pr-review-toolkit:code-reviewer",
prompt: "...",
})

匹配是精确字符串相等(AgentTool.tsx:345):

1
const found = agents.find(agent => agent.agentType === effectiveType)

找不到就报错 Agent type 'X' not found。

两条完全独立的触发路径

所以 plugin subagent 会在两种情况下被调用:

路径 A:slash command 内部显式调用

slash command 的 md 正文里写”用 X agent 做这件事”,主 agent 按指令调。比如 /feature-dev 的 Phase 6 写明:

Launch 3 code-reviewer agents in parallel with different focuses

主 agent 看到这条,去找 feature-dev:code-reviewer,调三次。

路径 B:主 agent 自主语义判断

没有 slash command,纯日常对话。你说”帮我看看这段代码有没有问题”,主 agent 看到 pr-review-toolkit:code-reviewer 的 description 写着”should be used proactively after writing or modifying code”,可能会决定调它——但也可能判断这个任务太简单,自己直接看就行。不需要你输入任何斜杠命令。

这意味着:你装的 plugin 的 agents,其中一部分会被暴露到主 agent 的”工具列表”里。实际暴露前还会经过几层过滤(见 loadAgentsDir.ts:359、attachments.ts:1503):

  • MCP 依赖过滤:agent 声明依赖的 MCP 工具不可用时会被跳过
  • 权限 deny 规则:被 deny 列表禁用的 agent 不会出现在列表里
  • allowedAgentTypes 白名单:如果配置了白名单,只有白名单里的 agent 会被暴露

通过过滤的 agent 会出现在主 agent 看到的列表里,主 agent 可能在你不知道的情况下调用它们——那个 agent 用什么模型,取决于 md frontmatter 和调用时传的 model 参数。

怎么追踪某次任务实际调了哪些 agent

如果你想知道每次 Claude Code 实际调了哪些 subagent,最直接的方法是配一个 PostToolUse hook。PostToolUse hook 的输入是 stdin 的 JSON(不是环境变量),需要用 jq 读:

1
2
3
4
5
6
7
8
9
10
11
{
"hooks": {
"PostToolUse": [{
"matcher": "Agent|Task",
"hooks": [{
"type": "command",
"command": "jq -c '{tool_name, subagent_type: (.tool_input.subagent_type // null), model: (.tool_input.model // null), prompt: (.tool_input.prompt // null)}' >> /tmp/agent-calls.log"
}]
}]
}
}

这样每次 Agent/Task 调用都会记录 tool_name、subagent_type、model、prompt。特别是 model 字段——它会告诉你这次调用实际传了什么模型参数,和 frontmatter 默认值是否一致。


四、所有 plugin subagent 的全貌

之前只看了三个 code-review 相关的 md,是因为搜关键词搜出来的。但实际上本地 claude-plugins-official marketplace 子树里有 19 个 subagent,分布在 7 个不同的 plugin 里。

统计范围:仅限 claude-plugins-official 这一个官方 marketplace 的本地安装目录快照(路径 ~/.claude/plugins/marketplaces/claude-plugins-official/plugins/)。如果你装了其他 marketplace(比如 claude-plugins-community),它们下的 subagent 不在本次统计内,数字会不一样。

统计快照:2026-04-15。统计命令:

1
2
3
4
find ~/.claude/plugins/marketplaces/claude-plugins-official/plugins -path "*/agents/*.md" | while read f; do
model=$(grep "^model:" "$f" | head -1 | awk '{print $2}')
echo "${model:-inherit} | $f"
done

官方 marketplace 还在持续更新,数字可能随时变化。

把所有 agents/*.md 的 model 字段全部扒出来:

模型 数量 哪些 agent
硬编码 sonnet 6 feature-dev:code-architect、feature-dev:code-explorer、feature-dev:code-reviewer、agent-sdk-dev:agent-sdk-verifier-py、agent-sdk-dev:agent-sdk-verifier-ts、plugin-dev:agent-creator
硬编码 opus 3 pr-review-toolkit:code-reviewer、pr-review-toolkit:code-simplifier、code-simplifier:code-simplifier
inherit(继承主模型) 10 hookify:conversation-analyzer、plugin-dev:plugin-validator、plugin-dev:skill-reviewer、pr-review-toolkit:comment-analyzer、pr-review-toolkit:pr-test-analyzer、pr-review-toolkit:silent-failure-hunter、pr-review-toolkit:type-design-analyzer、skill-creator:analyzer、skill-creator:comparator、skill-creator:grader

几个观察:

  1. feature-dev 整个 plugin 全是 Sonnet。只要被触发,feature-dev 的 subagent 默认跑 Sonnet(除非调用方传了 model 参数覆盖)。
  2. pr-review-toolkit 是混合。核心的 code-reviewer 和 code-simplifier 是 Opus,其余四个分析器都是 inherit。
  3. inherit 不是多数。10/19 是 inherit,剩下 9 个在 frontmatter 里写了具体模型。

这里要小心一个推论。“装 plugin 越多,/model 能影响的范围越小” 这个说法不严谨:

  • 首先,一个 agent 即使 frontmatter 写了具体 model,调用方仍然可以在 Agent/Task 工具里传参覆盖
  • 其次,新装的 plugin 可能全是 inherit
  • 最后,很多 plugin agent 可能因为过滤或根本不被主 agent 选中,完全不影响实际路由

更准确的说法是:你不容易预先知道”当我选 Opus 时,哪些子任务会用 Opus”——因为这取决于 plugin 作者在 md 里写的默认值 + 主 agent 当时的调用决策 + 调用时是否传了 model 覆盖参数。除非你开 hook 日志实际跑一遍,否则只能靠猜。


五、Explore agent:外部用户和内部员工的不同待遇

挖源码时发现了一个更有意思的地方。

src/tools/AgentTool/built-in/exploreAgent.ts,第 76-78 行:

1
2
3
// Ants get inherit to use the main agent's model; external users get haiku for speed
// Note: For ants, getAgentModel() checks tengu_explore_agent GrowthBook flag at runtime
model: process.env.USER_TYPE === 'ant' ? 'inherit' : 'haiku',

ant 是 Anthropic 内部员工的简称——Anthropic 缩写成 ant。公司内部项目代号是 Tengu(天狗),源码里的 feature flag 大多以 tengu_ 开头(比如 tengu_amber_stoat)。

要诚实地说明几件事:

  1. 上面代码里的第二行注释说 “For ants, getAgentModel() checks tengu_explore_agent GrowthBook flag at runtime”,但我查了 src/utils/model/agent.ts 的 getAgentModel() 完整代码,没有任何对 tengu_explore_agent 的实际判断。这句话只是注释,不是验证过的实现。可能是注释忘了更新,也可能是运行时判断在别的地方。我之前把注释当作已验证的事实引用,不严谨。

  2. 同样要修正的是关于 USER_TYPE 的说法。src/tools/REPLTool/constants.ts 的注释说 “USER_TYPE is a build-time –define”,但这只针对 ant-native binary 的特定场景。源码里其他地方(比如 src/tools.ts:214)仍然直接运行时读 process.env.USER_TYPE === 'ant' 来决定是否加载某些工具——也就是说 USER_TYPE 同时存在编译时 define 和运行时读环境变量两条路径。我之前断言”运行时无法修改”是过头了。

那事实层面到底是什么?

可以确定的:在 v2.1.88 exploreAgent.ts 这一行,process.env.USER_TYPE === 'ant' 是在模块定义时求值的(ES module 顶层),所以如果你拿到的二进制在启动时 USER_TYPE 不是 'ant',这里就是 'haiku'。

不能确定的:是否存在某种运行时配置能绕开这里。你可以试着用 USER_TYPE=ant claude 启动看看 Explore agent 会不会变成 inherit——我没有实测过。

确定的底线:这行代码在两种 USER_TYPE 下行为不同,默认的外部发布构建里,Explore agent 的 model 字段是 'haiku'。

  • USER_TYPE === 'ant' 时:Explore agent 的 model 字段是 'inherit'
  • 默认(外部发布):model 字段是 'haiku'

这不意味着”搜代码一定用 Haiku”

需要澄清一个我之前写过头的地方。我之前说”Explore agent 是日常编码里最高频的 subagent——搜文件、找实现,背后全是它在跑”,这是错的。

主 agent 本身就有 Bash、Glob、Grep、FileRead 这些工具(见 src/tools.ts:195-220),完全可以自己搜索代码,不一定要通过 Explore subagent。Explore 只是一个可选的委托对象,不是搜索的必经路径。

主 agent 什么时候会选择 Explore?通常是:需要大范围探索、希望保护主上下文不被污染、或者明确知道要快速返回结果时。简单的”grep 一下这个字符串”,主 agent 大概率会自己用 Grep 工具做,不会委托。

怎么绕过 Explore 的 Haiku 硬编码

两种方式:

  1. 全局覆盖(粗粒度):

    1
    CLAUDE_CODE_SUBAGENT_MODEL=opus claude

    或在 settings.json 里设 env.CLAUDE_CODE_SUBAGENT_MODEL。所有 subagent 强制 Opus,没有细粒度。

  2. Agent/Task 工具调用时显式传 model(细粒度):
    Agent/Task 工具 schema 允许传 model: 'opus' | 'sonnet' | 'haiku',优先级高于 agent frontmatter/代码硬编码(见第二节优先级表)。但这需要主 agent 主动传参,你没法直接控制。

第 1 条是目前最稳妥的绕过方式。


六、把这些串起来

从三个 md 文件出发,挖到的东西是:

md 文件是 Claude Code plugin 体系的”声明层”,用 frontmatter 的 model 字段声明这个 agent 用什么模型。源码的 getAgentModel() 是”执行层”,负责把声明翻译成实际的 API 调用模型,同时处理优先级覆盖、同族继承、跨平台前缀等逻辑。

两层之间的关系是清晰的,md 里写什么基本就是什么——除非被更高优先级的机制覆盖。

但有一类 agent 不走 md,而是在 TypeScript 代码里直接定义:内置 agent(Explore、Plan、general-purpose 等)。这些 agent 的模型设定在源码里写死,其中 Explore agent 在默认情况下对外部用户是 Haiku——不过它仍然可以被 CLAUDE_CODE_SUBAGENT_MODEL 环境变量、Agent/Task 工具调用时的 model 参数、或用户/项目/策略层的同名 agent 文件覆盖(见第二节优先级表和第九节的 activeAgents 覆盖链)。

所以 /model opus 的实际影响范围是:

  • ✅ 主对话(Claude 直接回答你的问题)
  • ✅ general-purpose agent(model 字段未设定,默认 inherit)
  • ✅ Plan agent(model: 'inherit' 写死在代码里)
  • ✅ plugin subagent 里写了 model: inherit 或不写 model 的
  • ⚠️ Explore agent:默认外部构建下是 'haiku',除非你跑 USER_TYPE=ant 或设 CLAUDE_CODE_SUBAGENT_MODEL
  • ⚠️ /code-review slash command 里的子任务:正文用”Launch a haiku agent / Opus bug agent”这种语言强引导主模型去传 model 参数。主模型通常会照做,但这是 prompt 引导不是编译期强制
  • ⚠️ plugin subagent 里写了具体模型的(feature-dev:* 是 sonnet,pr-review-toolkit:code-reviewer 是 opus 等):这些是默认值,调用方可以传 model 参数覆盖

你以为选了 Opus 就是全程 Opus。实际上,主 agent 是 Opus,但它干很多事情是通过委托完成的,委托出去的那部分用什么模型,取决于 plugin 作者、slash command 正文、和主 agent 当时的调用决策。


七、如何强制全程 Opus

既然 /model opus 管不到 subagent,真正的解决方案是用优先级最高的环境变量——在 ~/.claude/settings.json 里设:

1
2
3
4
5
6
{
"env": {
"CLAUDE_CODE_SUBAGENT_MODEL": "opus"
},
"model": "opus"
}

优先级关系:CLAUDE_CODE_SUBAGENT_MODEL 是源码里第一层判断,任何 md frontmatter 里写的 model: sonnet、代码里硬编码的 model: haiku,都会被它覆盖。

代价:无法细粒度控制。/code-review 里原本被指示用 Haiku 的资格检查、原本用 Sonnet 做 CLAUDE.md 合规检查的任务,全部会变成 Opus。如果你按 token 付费,成本会显著上升。如果是 Max 订阅,无所谓。

重启生效:settings.json 在 Claude Code 启动时读取,已经在跑的进程需要退出重启(Ctrl+C 后重新运行 claude)。


八、四种配置组合的完整对照

用户能控制的旋钮只有两个:/model(主对话模型)和 CLAUDE_CODE_SUBAGENT_MODEL(subagent 全局覆盖)。但用户不知道自己日常的每个操作背后触发的是哪个 agent。下面把两层都展开。

情况对照表

配置 主对话 Explore(搜代码/找文件) general-purpose / Plan feature-dev code-reviewer pr-review-toolkit code-reviewer /code-review inline agents
环境变量 opus + /model opus Opus Opus Opus Opus Opus 全 Opus
环境变量 opus + /model sonnet Sonnet Opus Opus Opus Opus 全 Opus
环境变量 sonnet + /model opus Opus Sonnet Sonnet Sonnet Sonnet 全 Sonnet
环境变量 sonnet + /model sonnet Sonnet Sonnet Sonnet Sonnet Sonnet 全 Sonnet

无环境变量时(默认):Explore 的 model 字段是 haiku(外部构建),feature-dev subagent 默认 Sonnet,pr-review-toolkit 的 code-reviewer/code-simplifier 默认 Opus,general-purpose/Plan 默认 inherit 主模型。但这些都是”默认值”,调用方传的 model 参数会覆盖它们。

用户能感知的操作 → 可能触发什么

需要说明:主 agent 是否委托给 subagent,是它自己的决策。以下是”在主 agent 选择委托时,通常会调哪个 agent”——不是每次都一定发生。

你在做什么 可能触发的 agent(也可能主 agent 自己处理)
直接问 Claude 问题,它回答 主对话(没有委托)
让 Claude 搜文件、找实现 主 agent 自己用 Grep/Glob/Read;只在大范围探索时才委托给 Explore
让 Claude 做复杂研究、跨文件分析 可能委托给 general-purpose
让 Claude 制定实现方案 可能委托给 Plan
用 /feature-dev 开发功能完成后的质量检查 按 slash command 正文指示调 feature-dev:code-reviewer 若干个
用 /pr-review-toolkit:review-pr pr-review-toolkit:code-reviewer + 其他专项 agent
用 /code-review review GitHub PR Haiku×2(资格/路径)+ Sonnet×3(摘要+CLAUDE.md 合规)+ Opus×2(bug 扫描/逻辑问题)+ 可变数量的验证 subagent(bug 用 Opus、合规用 Sonnet)

两个反直觉的组合

情况2(环境变量 opus + /model sonnet):主对话是 Sonnet,但所有 subagent 跑 Opus。搜个文件比你直接问问题还贵。

情况3(环境变量 sonnet + /model opus):你选了 Opus 主对话,但所有委托出去的任务——包括 pr-review-toolkit 里本来写了 model: opus 的 code-reviewer——都被压到 Sonnet。主 agent 是好的,但干活的全是次一级的。


九、未展开的三个更深入口

本文讲的是”模型路由”——声明层 md 和执行层 getAgentModel() 之间的关系。但 Claude Code 的真实调度架构还有几条本文没展开的线,下面只点到为止,细节留给后续文章。

1. activeAgents 的六层覆盖链(src/tools/AgentTool/loadAgentsDir.ts:193)

主 agent 实际看到的不是”plugin 写了什么就是什么”,而是 built-in / plugin / userSettings / projectSettings / flagSettings / policySettings 六层合并后的 activeAgents。后写的覆盖前写的——你可以在 ~/.claude/agents/ 放一个同名 agent 覆盖内置的 Explore,但企业策略层(policySettings)又可以反过来压你的用户层。不是”用户总能覆盖”,是”合并优先级”。

2. slash command frontmatter 里的 model 字段(src/utils/processUserInput/processSlashCommand.tsx:864)

本文第一节讲的是”命令正文通过 prompt 引导子 agent 用什么模型”。但 slash command 的 frontmatter 自己还能带一个 model 字段——processSlashCommand 把它作为本次 query 的主轮模型返回。也就是说,一条 /my-command 可以在自己的 frontmatter 里写 model: opus,直接把这次对话切到 Opus,不经过 /model。这和 subagent 的模型是两层不同的东西:一个是”这条命令本身用什么模型跑”,一个是”命令里再委托出去的 agent 用什么模型”。

配套还有一个 disableModelInvocation(src/types/command.ts:189),决定命令是否出现在主模型的 SkillTool 可调用列表里——也就是控制”主模型能不能自己想到这招”。这个属于能力暴露边界,不属于模型路由。

3. 隐式 fork subagent(src/tools/AgentTool/forkSubagent.ts:47)

不是所有委托都走命名 subagent。当 Agent/Task 调用的 subagent_type 为空且 fork 实验 flag 开启时,Claude Code 会合成一个 FORK_AGENT——继承父模型、继承父的完整工具池、权限冒泡回父终端,完全不读 plugin frontmatter,不在 builtInAgents 列表里。它的存在是为了 prompt cache 共享(父和 fork 子的 API 请求前缀字节一致)。这条路径绕开了本文第二到第五节讲的所有分析框架。

注意 fork 是 feature flag 驱动的,不是默认路径——大多数会话不会触发,但如果你开了 hook 日志看到 subagent_type=null 的 Task 调用,那就是它。


合起来看,Claude Code 的委托架构不只是一套”模型路由”,它还有覆盖链(多源合并)、能力暴露边界(slash command 的 model + disableModelInvocation)、和隐式委托路径(fork)三层机制。本文只讲了路由这一层,剩下三条每一条都够写一篇。


附录:原始文件

附录 A:三个 md 文件

A1. plugins/code-review/commands/code-review.md(slash command,v2.1.88 官方仓库实际内容)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
---
allowed-tools: Bash(gh issue view:*), Bash(gh search:*), Bash(gh issue list:*), Bash(gh pr comment:*), Bash(gh pr diff:*), Bash(gh pr view:*), Bash(gh pr list:*), mcp__github_inline_comment__create_inline_comment
description: Code review a pull request
---

Provide a code review for the given pull request.

**Agent assumptions (applies to all agents and subagents):**
- All tools are functional and will work without error.
- Only call a tool if it is required to complete the task.

To do this, follow these steps precisely:

1. Launch a haiku agent to check if any of the following are true:
- The pull request is closed
- The pull request is a draft
- The pull request does not need code review
- Claude has already commented on this PR
If any condition is true, stop and do not proceed.

2. Launch a haiku agent to return a list of file paths (not their contents) for all relevant CLAUDE.md files.

3. Launch a sonnet agent to view the pull request and return a summary of the changes.

4. Launch 4 agents in parallel to independently review the changes:
Agents 1 + 2: CLAUDE.md compliance sonnet agents (parallel)
Agent 3: Opus bug agent (parallel with agent 4) — scan for obvious bugs in the diff
Agent 4: Opus bug agent (parallel with agent 3) — security/logic issues in the changed code

5. For each issue found in step 4, launch parallel subagents to validate the issue.
Use Opus subagents for bugs and logic issues, and sonnet agents for CLAUDE.md violations.

6. Filter out any issues that were not validated in step 5.

7. Output a summary of the review findings to the terminal.
If `--comment` was NOT provided, stop here.

8. (If --comment) Create a list of comments to leave.

9. (If --comment) Post inline comments for each issue.

完整内容见 anthropics/claude-code 仓库 v2.1.88 tag。


A2. plugins/feature-dev/agents/code-reviewer.md(subagent,Sonnet)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
---
name: code-reviewer
description: Reviews code for bugs, logic errors, security vulnerabilities, code quality issues, and adherence to project conventions, using confidence-based filtering to report only high-priority issues that truly matter
tools: Glob, Grep, LS, Read, NotebookRead, WebFetch, TodoWrite, WebSearch, KillShell, BashOutput
model: sonnet
color: red
---

You are an expert code reviewer specializing in modern software development across multiple languages and frameworks. Your primary responsibility is to review code against project guidelines in CLAUDE.md with high precision to minimize false positives.

## Review Scope

By default, review unstaged changes from `git diff`. The user may specify different files or scope to review.

## Confidence Scoring

Rate each potential issue on a scale from 0-100. Only report issues with confidence ≥ 80.

A3. plugins/pr-review-toolkit/agents/code-reviewer.md(subagent,Opus)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
---
name: code-reviewer
description: Use this agent when you need to review code for adherence to project guidelines, style guides, and best practices. This agent should be used proactively after writing or modifying code, especially before committing changes or creating pull requests.
model: opus
color: green
---

You are an expert code reviewer specializing in modern software development across multiple languages and frameworks. Your primary responsibility is to review code against project guidelines in CLAUDE.md with high precision to minimize false positives.

## Issue Confidence Scoring

Rate each issue from 0-100. Only report issues with confidence ≥ 80.

Group issues by severity (Critical: 90-100, Important: 80-89).

附录 B:源码引用(v2.1.88)

B1. src/tools/AgentTool/built-in/exploreAgent.ts(关键行)

1
2
3
4
5
6
7
8
9
export const EXPLORE_AGENT: BuiltInAgentDefinition = {
agentType: 'Explore',
// ...
// Ants get inherit to use the main agent's model; external users get haiku for speed
// Note: For ants, getAgentModel() checks tengu_explore_agent GrowthBook flag at runtime
model: process.env.USER_TYPE === 'ant' ? 'inherit' : 'haiku',
omitClaudeMd: true,
getSystemPrompt: () => getExploreSystemPrompt(),
}

B2. src/utils/model/agent.ts(完整 getAgentModel 函数)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
/**
* Get the default subagent model. Returns 'inherit' so subagents inherit
* the model from the parent thread.
*/
export function getDefaultSubagentModel(): string {
return 'inherit'
}

export function getAgentModel(
agentModel: string | undefined,
parentModel: string,
toolSpecifiedModel?: ModelAlias,
permissionMode?: PermissionMode,
): string {
// 优先级 1:环境变量全局覆盖
if (process.env.CLAUDE_CODE_SUBAGENT_MODEL) {
return parseUserSpecifiedModel(process.env.CLAUDE_CODE_SUBAGENT_MODEL)
}

// 优先级 2:Agent/Task 工具调用时显式传的 model 参数
if (toolSpecifiedModel) {
if (aliasMatchesParentTier(toolSpecifiedModel, parentModel)) {
return parentModel
}
const model = parseUserSpecifiedModel(toolSpecifiedModel)
return applyParentRegionPrefix(model, toolSpecifiedModel)
}

const agentModelWithExp = agentModel ?? getDefaultSubagentModel()

// 优先级 3:agent 定义里的 model 字段,'inherit' 则继承父模型
if (agentModelWithExp === 'inherit') {
return getRuntimeMainLoopModel({
permissionMode: permissionMode ?? 'default',
mainLoopModel: parentModel,
exceeds200kTokens: false,
})
}

// 同族继承:父是 opus-4-6,子写 model: opus,则继承精确型号而非 provider 默认值
if (aliasMatchesParentTier(agentModelWithExp, parentModel)) {
return parentModel
}
const model = parseUserSpecifiedModel(agentModelWithExp)
return applyParentRegionPrefix(model, agentModelWithExp)
}

B3. src/tools/REPLTool/constants.ts(USER_TYPE 说明)

1
2
3
4
5
/**
* USER_TYPE is a build-time --define, so the ant-native
* binary would otherwise force REPL mode on every SDK subprocess regardless
* of the env the caller passes.
*/

2026年Agent上下文管理生态全景:从源码到架构决策

发表于 2026-04-14 | 分类于 技术 , agent , context

2026年Agent上下文管理生态全景:从源码到架构决策

一项基于源码阅读的实证性技术调研

摘要:本文对2026年4月Agent上下文管理(context management)领域的主要开源项目进行了源码级分析。调研覆盖了10个项目,共阅读约30万行源码,涉及TypeScript、Python、Go三种语言。通过对比各项目的架构设计、核心算法、接口耦合度和工程成熟度,本文得出结论:该领域已进入增量创新阶段,不存在可行的从0到1的独立项目方向。本文所有技术判断均基于对源码的直接阅读,每个关键论断附有文件路径和行号引用。


1 引言

1.1 研究动机

Agent系统在长时运行(long-running)场景中面临上下文窗口管理的核心挑战:对话历史、工具调用结果、任务状态的累积增长与有限的context window之间存在根本矛盾。本研究旨在回答一个实际问题:在2026年4月的技术生态中,是否存在一个值得从0到1构建的独立项目方向?

1.2 方法论

本研究采用源码阅读(source code reading)作为主要调研方法,而非依赖文档、README或博客文章。具体做法:

  1. 克隆目标项目的完整仓库到本地
  2. 从入口文件出发,沿调用链阅读核心实现
  3. 记录关键架构决策的具体文件位置(file:line格式)
  4. 对比不同项目在相同问题域的设计选择

1.3 调研范围

类别 项目 语言 核心代码量 阅读深度
宿主平台 OpenClaw TypeScript 350k+ star主仓库 ContextEngine接口、Registry、压缩管线
ContextEngine插件 lossless-claw TypeScript ~23,000行 全部核心模块
ContextEngine插件 Headroom Python ~211,000行 压缩管线、SmartCrusher、plugin层
工具插件 context-mode TypeScript plugin manifest + benchmark Manifest验证、工具定义
ContextEngine插件 OpenViking TypeScript plugin manifest + examples Manifest验证、能力声明
独立agent Hermes Agent Python ~10,000行(run_agent.py) context_compressor.py全部
记忆框架 Mem0 Python ~2,600行核心 Memory类、MemoryGraph类
记忆框架 Letta/MemGPT Python ~32,000行核心模块 Agent主循环、三层记忆工具、Summarizer
记忆框架 Zep Go ~7,900行legacy代码 全部store/model/graphiti层
MCP工具 ContextVault TypeScript ~1,500行 Server、VaultManager、IndexManager

2 宿主平台分析:OpenClaw ContextEngine接口

OpenClaw是当前最广泛使用的AI编码agent平台(350k+ GitHub stars)。其v2026.3.7版本引入了可插拔的ContextEngine接口,成为上下文管理插件的主要竞争平台。

2.1 ContextEngine接口定义

ContextEngine的完整生命周期合约定义于 src/context-engine/types.ts[^1],包含以下方法:

1
2
3
4
5
6
7
8
9
10
11
interface ContextEngine {
bootstrap(ctx: RuntimeContext): Promise<void>;
ingest(entry: AgentMessage, ctx: RuntimeContext): Promise<IngestResult>;
assemble(ctx: AssembleContext): Promise<AssembleResult>;
compact(ctx: CompactContext): Promise<CompactResult>;
afterTurn?(ctx: RuntimeContext): Promise<void>;
maintain?(ctx: RuntimeContext): Promise<void>;
prepareSubagentSpawn?(ctx: SubagentSpawnContext): Promise<SubagentPrepareResult>;
onSubagentEnded?(ctx: SubagentEndedContext): Promise<void>;
dispose?(): Promise<void>;
}

关键设计决策:

  • ownsCompaction?: boolean(types.ts:51):引擎可以声明自己拥有压缩逻辑,阻止runtime的自动压缩
  • runtimeContext.rewriteTranscriptEntries()(types.ts:93-95):提供安全的transcript重写通道
  • systemPromptAddition?: string(AssembleResult,types.ts:11):允许引擎向system prompt注入内容

2.2 排他性Slot系统

src/plugins/slots.ts[^2] 定义了两个排他性slot:

1
2
3
4
5
6
7
8
9
const SLOT_BY_KIND = {
memory: "memory",
"context-engine": "contextEngine",
}; // slots.ts:12-15

const DEFAULT_SLOT_BY_KEY = {
memory: "memory-core",
contextEngine: "legacy",
}; // slots.ts:17-20

applyExclusiveSlotSelection()(slots.ts:76-162)实现winner-takes-all语义:当一个插件占据slot,其他同类插件被自动禁用。

架构含义:这意味着lossless-claw、Headroom和任何其他ContextEngine插件无法同时作为独立插件共存。如果要组合多个引擎的能力,必须构建一个meta-engine占据唯一的contextEngine slot,内部编排子引擎逻辑。

2.3 LegacyContextEngine:默认实现的空壳

src/context-engine/legacy.ts[^3] 实现了默认的LegacyContextEngine:

  • ingest() → 返回 { ingested: false },纯no-op(legacy.ts:~35)
  • assemble() → 原样透传消息,estimatedTokens: 0(legacy.ts:~45)
  • compact() → 委托给 delegateCompactionToRuntime()(legacy.ts:~55,调用 delegate.ts:16-63)

真正的压缩逻辑在 src/agents/compaction.ts,硬编码了 BASE_CHUNK_RATIO = 0.4、MIN_CHUNK_RATIO = 0.15、SAFETY_MARGIN = 1.2——不可配置、不可组合。

2.4 五个架构缺陷

缺陷1:压缩是纯reactive的。run.ts中的auto-compaction只在API返回context overflow error之后才触发,tool-use循环中没有pre-prompt的context size检查。Issue #24800记录了session从196K涨到200K后永久卡死的案例。

缺陷2:safeguard模式默认开启但静默失败。src/config/defaults.ts:368设置 mode: "safeguard"[^4]。180K+ token时产生 "Summary unavailable due to context limits",上下文直接丢失,无警告。

缺陷3:两套hook系统不互通。src/hooks/internal-hooks.ts 的 triggerInternalHook() 和 src/plugins/hooks.ts 的 hookRunner.runSessionEnd() 是独立系统。session-memory hook只监听 command:new 和 command:reset(src/hooks/bundled/session-memory/handler.ts:55-57[^5]),不监听 session_end。

缺陷4:compaction后token计数归零。clearStaleAssistantUsageOnSessionMessages()(src/agents/pi-embedded-subscribe.handlers.compaction.ts:115-132[^6])清除所有assistant message的usage数据,而非只清除compaction之前的。

缺陷5:Registry是God Object。src/plugins/registry.ts 管理20+种注册类型(工具、hooks、CLI、HTTP路由、speech、媒体、channel、gateway、memory、contextEngine等),单文件混合认证、slot分配、provider冲突解决。


3 ContextEngine插件生态

3.1 lossless-claw:DAG无损压缩

仓库:Martian-Engineering/lossless-claw
代码量:~23,000行TypeScript
核心架构:DAG-based conversation summarization with incremental compaction

3.1.1 依赖注入设计

LcmContextEngine 类(src/engine.ts:1199[^7])通过 LcmDependencies 接口(src/types.ts:105-165[^8])获取所有外部能力:

1
2
3
4
5
6
7
8
9
10
11
12
13
export interface LcmDependencies {
config: LcmConfig;
complete: CompleteFn; // LLM调用
callGateway: CallGatewayFn; // Gateway RPC
resolveModel: ResolveModelFn; // 模型解析
getApiKey: GetApiKeyFn; // 密钥获取
requireApiKey: RequireApiKeyFn;
parseAgentSessionKey: ParseAgentSessionKeyFn;
isSubagentSessionKey: IsSubagentSessionKeyFn;
normalizeAgentId: (id?: string) => string;
log: { info, warn, error, debug };
// ... 更多
}

engine.ts零OpenClaw import——核心逻辑链完全独立:

模块 文件 职责 OpenClaw耦合
存储层 conversation-store.ts, summary-store.ts 纯SQLite读写 无
压缩引擎 compaction.ts 摘要生成与DAG构建 仅通过CompleteFn类型
组装器 assembler.ts 上下文装配 仅type推导
检索 retrieval.ts FTS5全文搜索 无
插件胶水 plugin/index.ts 390行注册逻辑 完全耦合

3.1.2 上下文组装算法

ContextAssembler.assemble()(src/assembler.ts:886-1059[^9])的核心流程:

  1. 从SQLite获取所有context items(消息 + 摘要),按ordinal排序
  2. 将每个item解析为 AgentMessage(获取底层消息或摘要记录)
  3. 将items分为可驱逐前缀和受保护的fresh tail(默认64条消息,src/db/config.ts:255[^10])
  4. 如果超出token budget,从最旧的非fresh items开始丢弃
  5. 当prompt可用时,使用BM25-lite相关性评分决定保留优先级
  6. 摘要格式化为XML:<summary id="..." kind="leaf" depth="0">...

返回结果包含 messages、estimatedTokens、systemPromptAddition和详细统计。

3.1.3 压缩配置

CompactionConfig(src/compaction.ts:33[^11])提供精细的压缩参数:

1
2
3
4
5
6
7
8
9
export interface CompactionConfig {
contextThreshold: number; // 默认0.75(budget的75%触发压缩)
freshTailCount: number; // 默认64(保护的最新消息数)
leafMinFanout: number; // depth-0摘要最少数量才触发condensation
condensedMinFanout: number; // depth>=1摘要condensation最少数量
leafChunkTokens: number; // 叶节点chunk的目标token数
leafTargetTokens: number; // 叶节点摘要目标长度
condensedTargetTokens: number; // 聚合摘要目标长度
}

支持四种压缩级别:"normal" | "aggressive" | "fallback" | "capped"。

3.1.4 插件注册

src/plugin/index.ts[^12] 执行以下注册:

  • api.registerContextEngine("lossless-claw", ...) 和 api.registerContextEngine("default", ...)——同时注册为命名引擎和默认引擎
  • api.registerTool() × 4:describe(查看摘要树)、expand(展开摘要为原始消息)、expand-query(按查询展开)、grep(全文搜索)
  • api.on("before_reset")、api.on("session_end"):生命周期事件监听
  • api.registerCommand():CLI命令注册

工程判断:lossless-claw是调研范围内架构最干净的项目。其核心可以通过约100-150行adapter代码独立于OpenClaw使用,只需构造满足LcmDependencies接口的依赖对象。


3.2 Headroom:内容感知压缩管线

仓库:chopratejas/headroom
代码量:~211,000行Python(465个.py文件)
核心架构:内容类型路由 + 统计驱动的压缩策略

3.2.1 压缩管线

headroom/transforms/pipeline.py:70-125[^13] 定义了默认的三级管线:

1
CacheAligner → ContentRouter → IntelligentContextManager
  • CacheAligner:prompt cache前缀稳定化,确保缓存字节前缀在turn间保持不变
  • ContentRouter:基于内容类型的智能路由(pipeline.py:81-89):
    • JSON数组 → SmartCrusher
    • 纯文本 → Kompress(ML-based)
    • 代码 → CodeCompressor(AST-aware)
    • 日志 → LogCompressor
    • 搜索结果 → SearchCompressor
    • HTML → HTMLExtractor
  • IntelligentContextManager:语义感知的上下文管理,策略链为 COMPRESS_FIRST → SUMMARIZE → DROP_BY_SCORE(pipeline.py:116-119)

降级路径(pipeline.py:91-123):

1
2
3
ContentRouter 不可用 → SmartCrusher(仅JSON)
SmartCrusher 不可用 → ToolCrusher(固定规则)
IntelligentContextManager 不可用 → RollingWindow(基于位置的滚动窗口)

3.2.2 SmartCrusher:统计驱动的JSON压缩

headroom/transforms/smart_crusher.py[^14](3,657行)是Headroom的核心创新:

文件头部的docstring明确了scope(smart_crusher.py:1-46):

“SCOPE: SmartCrusher handles JSON arrays of ANY type — dicts, strings, numbers, mixed types, and nested arrays. Non-JSON content (plain text, search results, logs, code, diffs) passes through UNCHANGED.”

“SCHEMA-PRESERVING: Output contains only items from the original array. No wrappers, no generated text, no metadata keys.”

关键安全保证:

  • First K + Last K items始终保留(K是自适应的,基于Kneedle算法,非硬编码)
  • 错误条目(包含’error’、’exception’、’failed’、’critical’)永远不丢弃
  • 异常数值(>2 std)始终保留
  • 变化点(change points)附近的items被保护
  • 基于RelevanceScorer的高相关性items保留(ML-powered或BM25-based)

支持的JSON类型(smart_crusher.py:25-31):

类型 压缩策略
dict数组 完整统计分析 + 自适应K(Kneedle)
字符串数组 去重 + 自适应采样 + 错误保留
数值数组 统计摘要 + 异常/变化点保留
混合类型数组 按类型分组,各组独立压缩
扁平对象(多key) Key级别自适应采样
嵌套对象 递归压缩内部数组/对象

3.2.3 OpenClaw集成

plugins/openclaw/src/engine.ts(240行)实现了 HeadroomContextEngine:

  • 声明 ownsCompaction: true
  • assemble() 将 AgentMessage 转为OpenAI格式 → 调用 compress() → 转回
  • compact() 本质上是no-op(”applies on next assemble()”)
  • 通过 import { compress } from "headroom-ai" 调用Python库(HTTP proxy方式)

engine.ts同样零OpenClaw import——核心压缩逻辑完全在Python库中。


3.3 context-mode:MCP工具方案

仓库:mksglu/context-mode

关键发现:context-mode的manifest(openclaw.plugin.json)声明 "kind": "tool"[^15]——它不是ContextEngine插件。它通过MCP tools(ctx_execute_file、ctx_index、ctx_search)提供功能,不参与ContextEngine的assemble/compact生命周期。

BENCHMARK.md声明整体节省率为96%[^16](非98%),主要针对tool output场景。

3.4 OpenViking:长期记忆

仓库:volcengine/OpenViking(字节跳动)

manifest(examples/openclaw-plugin/openclaw.plugin.json)确认 "kind": "context-engine"[^17]。声明了完整的assemble/afterTurn/compact生命周期实现。功能包括:autoCapture、autoRecall、bypassSessionPatterns、commitTokenThreshold。


4 记忆框架:源码级分析

4.1 Mem0:LLM-in-the-loop的记忆CRUD

仓库:mem0ai/mem0(48k stars)
核心文件:mem0/memory/main.py(2,597行)、mem0/memory/graph_memory.py(744行)

4.1.1 架构概述

Mem0的核心类 Memory(main.py:258[^18])包含一个vector store和一个可选的graph store(Neo4j),两条路径通过 ThreadPoolExecutor 并行执行。

4.1.2 add() 方法调用链

Memory.add()(main.py:383[^19])的真实执行流程:

  1. LLM提取facts:发送一次LLM调用(tool call模式),让模型从输入文本中抽取结构化记忆条目
  2. 搜索已有记忆:对每个抽取的fact做vector search,找到相似的已有记忆
  3. LLM决策:再发一次LLM调用,让模型对每条记忆做ADD/UPDATE/DELETE决策
  4. 执行存储操作:根据决策写入vector store

如果启用了graph store,步骤4还会并行执行 _add_to_graph():

1
2
3
4
# main.py - Memory.add() 内部
with concurrent.futures.ThreadPoolExecutor() as executor:
future1 = executor.submit(self._add_to_vector_store, ...)
future2 = executor.submit(self._add_to_graph, ...)

一次 memory.add() 最少需要2次LLM调用(fact提取 + 决策),启用graph后增加到5次。

4.1.3 Graph Memory实现

MemoryGraph类(graph_memory.py:29[^20])使用 langchain_neo4j.Neo4jGraph 做图存储。

add()方法(graph_memory.py:76[^21])的调用链:

  1. _retrieve_nodes_from_data()(graph_memory.py:219)— LLM tool call提取实体和类型
  2. _establish_nodes_relations_from_data()(graph_memory.py:252)— LLM tool call建立实体间关系
  3. _search_graph_db()(graph_memory.py:294)— 对每个实体做embedding cosine相似度搜索:
1
2
3
4
5
6
7
8
# graph_memory.py:312
cypher_query = f"""
MATCH (n {self.node_label} {{{node_props_str}}})
WHERE n.embedding IS NOT NULL
WITH n, round(2 * vector.similarity.cosine(n.embedding, $n_embedding) - 1, 4) AS similarity
WHERE similarity >= $threshold
...
"""
  1. _get_delete_entities_from_search_output()(graph_memory.py:347)— LLM tool call决定删除
  2. 执行删除和新增

图的删除是软删除(graph_memory.py:423-428[^22]):

1
SET r.valid = false, r.invalidated_at = datetime()

这保留了关系的时间线,支持temporal reasoning。

搜索实现(graph_memory.py:96-130[^23])使用BM25重排序:

1
2
3
4
from rank_bm25 import BM25Okapi
bm25 = BM25Okapi(search_outputs_sequence)
tokenized_query = query.split(" ")
reranked_results = bm25.get_top_n(tokenized_query, search_outputs_sequence, n=5)

4.1.4 架构局限

Mem0是一个”记忆数据库”,不是”上下文引擎”。它不感知token budget、不做context压缩、不参与prompt组装。开发者需要自己将Mem0的检索结果手动塞进prompt。这与OpenClaw的ContextEngine插件、lossless-claw的assembler是完全不同的抽象层次。


4.2 Letta/MemGPT:Agent-as-OS的三层记忆

仓库:letta-ai/letta
核心文件:letta/agents/letta_agent.py(1,983行)、letta/functions/function_sets/base.py、letta/schemas/block.py、letta/schemas/memory.py、letta/services/summarizer/summarizer.py

4.2.1 设计哲学

Letta的架构思想是把Agent当OS、把记忆当文件系统。三层记忆是三种不同的”内存地址空间”,Agent通过function calling(tool call)自主决策何时读写哪一层。

4.2.2 第一层:Core Memory(Block系统)

Block类(schemas/block.py:67[^24])是LLM上下文窗口内的一块可写区域:

1
2
3
4
5
class Block(BaseBlock):
value: str = Field(..., description="Value of the block.")
limit: int = Field(CORE_MEMORY_BLOCK_CHAR_LIMIT, ...)
label: Optional[str] = Field(None, description="Label of the block")
read_only: bool = Field(False)

预置两个Block:Human(block.py:117,label=”human”)和 Persona(label=”persona”)。

Agent通过以下tool call修改Block:

  • core_memory_append(agent_state, label, content)(base.py:246[^25]):
1
2
3
current_value = str(agent_state.memory.get_block(label).value)
new_value = current_value + "\n" + str(content)
agent_state.memory.update_block_value(label=label, value=new_value)
  • core_memory_replace(agent_state, label, old_content, new_content)(base.py:263[^26]):
1
2
3
4
current_value = str(agent_state.memory.get_block(label).value)
if old_content not in current_value:
raise ValueError(f"Old content '{old_content}' not found in memory block '{label}'")
new_value = current_value.replace(str(old_content), str(new_content))
  • rethink_memory(agent_state, new_memory, target_block_label)(base.py:283):整体重写Block

  • memory(agent_state, command, ...)(base.py:10[^27]):类文件系统操作(create/str_replace/insert/delete/rename)。函数体是 raise NotImplementedError——实际路由在server端。

关键区别:这些是tool定义,Agent通过function calling自主决策写什么。不是自动提取,是Agent-driven的记忆管理。

4.2.3 第二层:Archival Memory(向量搜索)

archival_memory_insert(self, content, tags)(base.py:164[^28])和 archival_memory_search(self, query, tags, tag_match_mode, top_k, start_datetime, end_datetime)(base.py:194[^29])。

两个函数体都是 raise NotImplementedError("This should never be invoked directly.")——实际执行由Agent框架在tool call时路由到Passage store(PostgreSQL + 向量索引)。函数签名和docstring是给LLM看的,真正的存储逻辑在server端。

4.2.4 第三层:Recall Memory(消息搜索)

conversation_search(self, query, roles, limit, start_date, end_date)(base.py:87[^30])是真正实现的:

1
2
3
4
5
6
messages = self.message_manager.list_messages_for_agent(
agent_id=self.agent_state.id,
actor=self.user,
query_text=query,
roles=roles, limit=limit,
)

使用hybrid search(文本 + 语义),支持按role、时间范围过滤。

4.2.5 Agent主循环

LettaAgent.step()(letta_agent.py:174[^31])→ _step() → 循环(最多max_steps轮):

  1. _build_and_request_from_llm()(letta_agent.py:328)— 组装上下文 + LLM调用
  2. 解析response的tool call
  3. _handle_ai_response()(letta_agent.py:1714)— 执行tool call

Agent通过强制tool calling驱动(letta_agent.py:360-362[^32]):

1
2
3
if not response.choices[0].message.tool_calls:
stop_reason = LettaStopReason(stop_reason=StopReasonType.no_tool_call.value)
raise ValueError("No tool calls found in response, model must make a tool call")

每轮必须产出tool call,否则报错。这意味着每一轮交互都是一次完整的LLM调用,即使只是写一条记忆。

4.2.6 Summarization实现

Summarizer类(summarizer/summarizer.py:36[^33])支持两种模式(enums.py[^34]):

模式一:STATIC_MESSAGE_BUFFER(summarizer.py:244-300[^35])

固定buffer。当 len(all_in_context_messages) > message_buffer_limit 时,丢弃最旧消息,保留最近 message_buffer_min 条。如果有summarizer agent,异步触发背景摘要。

模式二:PARTIAL_EVICT_MESSAGE_BUFFER(summarizer.py:136-242[^36])

原始MemGPT方案。按比例(默认30%,partial_evict_summarizer_percentage,summarizer.py:49)驱逐最旧消息,用一次LLM调用生成摘要(simple_summary(),summarizer.py:188),插入到 context[1] 位置(system_message后的第一条)。

1
2
3
# summarizer.py:241-242
updated_in_context_messages = all_in_context_messages[assistant_message_index:]
return [all_in_context_messages[0], summary_message_obj, *updated_in_context_messages], True

关键观察:摘要触发是message数量驱动的(message_buffer_limit),不是token数量驱动的。这与现代大模型的context window管理方式脱节——不同消息的token数差异可达100x(一条简单回复 vs 一个大型tool output)。

4.2.7 ContextWindowOverview

schemas/memory.py[^37] 定义了 ContextWindowOverview,包含四种记忆的统计:

  • core_memory:当前Block内容和token数
  • archival_memory:passages总数
  • recall_memory:messages总数
  • summary_memory:最近的摘要(如有)

4.2.8 架构评价

  • 概念完整度最高(三层 + Agent自主管理),但每层都需要LLM调用,延迟和成本累积很快
  • Core Memory的”Agent自主写”模式对system prompt的prompt engineering要求极高
  • Summarizer按message数量触发,不感知实际token使用
  • Archival和Recall的存储实现在server端(ORM + PostgreSQL),client SDK拿到的是间接接口

4.3 Zep:从Go服务器到Graphiti HTTP壳

仓库:getzep/zep(25k stars)
核心文件:legacy/src/store/memory_ce.go、legacy/src/lib/graphiti/service_ce.go(300行)、legacy/src/store/memory_common.go、legacy/src/store/message_common.go(547行)

4.3.1 项目现状

Zep分为两个阶段:

  • 阶段一(开源Go服务器):包含消息存储、NER、摘要、嵌入、搜索的完整记忆服务
  • 阶段二(当前):开源代码移入 legacy/ 目录,核心产品变为Zep Cloud(闭源SaaS)。当前仓库的README(README.md:56-58[^38])明确说明:

“Note: This repository is currently a work in progress. This repository contains examples, integrations, and tools…”

4.3.2 CE版核心:Graphiti HTTP Client

读了legacy代码后的关键发现:即使是”完整的”Go服务器,CE版的记忆逻辑也只是一个Graphiti HTTP客户端。

memory_ce.go[^39] 的 _get() 方法(第15-57行):

1
2
3
4
5
6
7
8
9
10
11
12
13
func (dao *memoryDAO) _get(ctx context.Context, session *models.Session,
messages []models.Message, _ models.MemoryFilterOptions) (*models.Memory, error) {
mForRetrieval := messages
if len(messages) > maxMessagesForFactRetrieval { // maxMessagesForFactRetrieval = 4
mForRetrieval = messages[len(messages)-maxMessagesForFactRetrieval:]
}
memory, err := graphiti.I().GetMemory(ctx, graphiti.GetMemoryRequest{
GroupID: groupID,
MaxFacts: 5,
Messages: mForRetrieval,
})
// ...转换Graphiti返回的facts为本地Fact结构
}

_initializeProcessingMemory()(memory_ce.go:59-72[^40])同样只是HTTP POST:

1
2
3
4
5
6
7
func (dao *memoryDAO) _initializeProcessingMemory(...) error {
err := graphiti.I().PutMemory(ctx, session.SessionID, memoryMessages.Messages, true)
if session.UserID != nil {
err = graphiti.I().PutMemory(ctx, *session.UserID, memoryMessages.Messages, true)
}
return err
}

4.3.3 Graphiti服务接口

graphiti/service_ce.go[^41] 定义的 Service 接口(第80行)全是HTTP调用:

1
2
3
4
5
6
7
8
9
10
type Service interface {
GetMemory(ctx context.Context, payload GetMemoryRequest) (*GetMemoryResponse, error)
PutMemory(ctx context.Context, groupID string, messages []models.Message, ...) error
Search(ctx context.Context, payload SearchRequest) (*SearchResponse, error)
AddNode(ctx context.Context, payload AddNodeRequest) error
GetFact(ctx context.Context, factUUID uuid.UUID) (*Fact, error)
DeleteFact(ctx context.Context, factUUID uuid.UUID) error
DeleteGroup(ctx context.Context, groupID string) error
DeleteMessage(ctx context.Context, messageUUID uuid.UUID) error
}

每个方法内部都是 s.newRequest(ctx, method, path, body) + s.doRequest(req, &resp)——标准HTTP client模式(service_ce.go:118-165)。

Graphiti本身是一个独立的Python temporal knowledge graph框架(同为Zep团队的开源项目),支持valid_at/invalid_at时间戳的关系管理。Zep CE只是它的一个Go包装层。

4.3.4 消息存储

message_common.go[^42] 使用PostgreSQL(bun ORM)。GetLastN()(message_common.go:135-184)按ID倒序取N条消息。metadata更新支持JSON merge + advisory lock防并发(message_common.go:394-451)。

4.3.5 数据模型

Memory(models/memory_common.go:86-92[^43]):

1
2
3
4
5
type MemoryCommon struct {
Messages []Message `json:"messages"`
RelevantFacts []Fact `json:"relevant_facts"`
Metadata map[string]any `json:"metadata,omitempty"`
}

Fact(models/fact_common.go[^44]):只有UUID、CreatedAt、Fact(字符串)、Rating(可选浮点)。

Session(models/session_common.go:10-22[^45]):标准会话模型,含SessionID、UserID、Metadata、时间戳。

4.3.6 架构评价

能力 实现情况
知识图谱 ❌ 无自有实现,全部委托外部Graphiti服务
会话摘要 ❌ GetMemory只返回最后N条消息 + 最多5个facts
Token budget管理 ❌ 完全不存在
Context压缩 ❌ 完全不存在
消息搜索 ❌ 无embedding、无FTS,只有Graphiti代理搜索
消息存储 ✅ PostgreSQL CRUD

结论:评估Zep的真实能力边界需要去读 getzep/graphiti 仓库的Python代码,不是这个Go服务器。Go服务器只是一个REST API壳 + PostgreSQL消息CRUD + Graphiti HTTP client。


4.4 ContextVault:Markdown文件系统 + MCP工具

仓库:ahmadzein/ContextVault
核心文件:contextvault-mcp/src/vault/manager.ts(493行)、contextvault-mcp/src/vault/index-manager.ts(452行)、contextvault-mcp/src/vault/types.ts

4.4.1 本质

ContextVault不是context压缩框架或记忆管理系统。它是一个MCP server,把知识存在本地markdown文件里,通过MCP tools让AI助手读写这些文件。

4.4.2 存储模型

VaultSettings(types.ts:3-12[^46]):

1
2
3
4
5
6
7
8
9
10
export interface VaultSettings {
mode: 'local' | 'global' | 'full';
enforcement: 'light' | 'balanced' | 'strict';
limits: {
max_global_docs: number; // 默认50
max_project_docs: number; // 默认50
max_doc_lines: number; // 默认100
max_summary_words: number; // 默认15
};
}

两个tier:global(~/.contextvault/)和project(./.contextvault/)。每个tier有一个 index.md(markdown表格做索引)和若干 P001_*.md / G001_*.md 文件。

VaultManager(manager.ts[^47])核心操作都是 fs.readFileSync / fs.writeFileSync。没有数据库、没有向量索引、没有embedding。

4.4.3 搜索实现

IndexManager.search()(index-manager.ts:346-363[^48]):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
search(query: string): IndexEntry[] {
const entries = this.parseEntries();
const queryTerms = queryLower.split(/\s+/);
return entries
.map(entry => {
const text = `${entry.id} ${entry.topic} ${entry.summary}`.toLowerCase();
let score = 0;
for (const term of queryTerms) {
if (text.includes(term)) score++;
}
return { entry, score };
})
.filter(r => r.score > 0)
.sort((a, b) => b.score - a.score)
.map(r => r.entry);
}

搜索范围是index.md中每条entry的topic和summary(各15个词以内)。不搜索文档正文。这是纯关键词匹配,不是语义搜索。

4.4.4 Enforcement机制

VaultManager跟踪agent的编辑次数和探索范围(manager.ts:195-372[^49])。当编辑超过阈值(balanced模式:8次编辑 + 2个文件),在tool response末尾追加reminder:

1
2
3
4
5
6
7
getEnforcementReminder(): string | null {
if (editCount >= editThreshold && filesEdited.size >= fileThreshold) {
return `\n\n---\n**ContextVault Reminder:** You've made ${editCount} edits
across ${filesEdited.size} files without documenting...`;
}
return null;
}

这是一个行为nudge系统,不是自动化的context管理。

4.4.5 文档模板

generateDocContent()(manager.ts:376-440[^50])根据类型生成不同的markdown结构:

类型 模板结构
error Error / Root Cause / Solution / Prevention
decision Decision / Options Considered / Reasoning / Trade-offs
plan Goal / Steps / Status
handoff Completed / In Progress / Next Steps
intel Area Explored / Findings
explain Concept / Explanation

4.4.6 20+ MCP Tools

通过 ContextVaultServer(server.ts:35[^51])注册:ctx_init、ctx_doc、ctx_error、ctx_decision、ctx_search、ctx_read、ctx_plan、ctx_handoff、ctx_bootstrap、ctx_changelog、ctx_quiz、ctx_review、ctx_share、ctx_archive、ctx_import、ctx_upgrade等。

4.4.7 架构评价

ContextVault解决的是”session间知识持久化”问题——确保AI在新会话中能访问到之前积累的知识。它不应被归类为”上下文管理”或”记忆框架”,更准确的定位是AI笔记本工具。其真正价值在于结构化的文档模板和行为nudge机制,而非技术上的context management创新。


5 独立Agent分析

5.1 Hermes Agent:单次LLM调用的上下文压缩

仓库:NousResearch/hermes-agent
核心文件:agent/context_compressor.py(745行)、run_agent.py(9,660行)

ContextCompressor.compress()(context_compressor.py:612-745[^52])的流程:

  1. 修剪旧的tool results(保留结构但截断内容)
  2. 确定压缩边界(哪些消息需要摘要)
  3. 生成摘要——仅1次LLM调用(_generate_summary()内部,context_compressor.py:404)
  4. 组装压缩后的context

工程特征:run_agent.py是一个9,660行的单文件脚本,不是模块化的类结构。这使得复用或集成Hermes的上下文管理逻辑非常困难。


6 跨项目比较与分析

6.1 分类学

本文调研的项目可以按问题域分为四类:

类别 解决什么 代表项目
上下文引擎 管理LLM context window的完整生命周期(ingest/assemble/compact) lossless-claw, Headroom, OpenViking
记忆数据库 存储和检索跨session的记忆/知识 Mem0, Letta/MemGPT
记忆服务 提供hosted的记忆/知识图谱API Zep (Cloud)
知识持久化 在session间保存结构化文档 ContextVault

这四类解决的是上下文管理栈的不同层次,不是竞品关系。

6.2 LLM调用开销对比

项目 一次记忆写入的LLM调用数 说明
lossless-claw 1 compact时一次summarization call
Headroom 0 纯算法压缩(ML模型是本地的,非LLM API)
Mem0 2-5 2次(vector path) + 3次(graph path额外)
Letta/MemGPT 1+ 每次tool call是一轮LLM交互
Hermes Agent 1 单次summary call
ContextVault 0 纯文件读写

6.3 token budget感知

项目 感知token budget 主动压缩 压缩触发方式
lossless-claw ✅ contextThreshold: 0.75 ✅ budget 75%时自动触发 Token比例
Headroom ✅ IntelligentContextManager ✅ 管线自动执行 Token数量
OpenClaw默认 ❌ ❌ 仅在API overflow后reactive 错误触发
Mem0 ❌ ❌ N/A(不参与context组装)
Letta/MemGPT 部分 ✅ Message数量(非token)
Zep CE ❌ ❌ N/A
ContextVault ❌ ❌ N/A

6.4 耦合度与可复用性

项目 与宿主平台耦合度 独立复用所需adapter代码量
lossless-claw engine.ts 零import ~100-150行(构造LcmDependencies)
Headroom核心 零import ~50行(Python库直接import)
Letta三层记忆 深度耦合(server端ORM) 不可行(需要整个server)
Mem0 Memory类 松耦合 ~20行(直接pip install)
Zep CE 依赖外部Graphiti服务 不可行(需要部署Graphiti)
ContextVault MCP标准接口 ~10行(MCP client配置)

7 Meta-Engine可行性分析

7.1 动机

既然lossless-claw和Headroom的核心逻辑都可以独立于OpenClaw使用,是否可以构建一个meta-engine组合两者的能力?

7.2 技术可行性

可行。预计~500行代码:

  1. 占据OpenClaw的contextEngine slot
  2. 内部实例化lossless-claw的 LcmContextEngine(需要构造 LcmDependencies)
  3. 在 assemble() 的输出上运行Headroom的 compress() 管线
  4. 将lossless-claw的结构化摘要(XML格式)与Headroom的tool output压缩组合

7.3 价值分析

lossless-claw的assembler输出包含两类内容:

  1. 摘要(<summary> XML节点)——已经是压缩后的文本,Headroom再压缩的边际收益极低
  2. Fresh tail原始消息(默认64条)——其中tool output部分可以被Headroom的SmartCrusher有效压缩

因此组合价值仅限于fresh tail中的tool output部分——对tool-heavy的编码场景有意义,但对纯对话、写作等场景价值有限。

7.4 战略结论

技术上可行,但战略上不成立:

  • 目标用户群窄(仅tool-heavy场景受益)
  • 两个上游项目都在活跃开发,维护成本高
  • OpenClaw自身也在改进默认压缩(reactive → proactive的issue已被讨论)

8 为什么最终放弃找独立项目方向

8.1 排除矩阵

方向 排除原因 源码证据
通用压缩中间件 Headroom已做(211k行Python,Apache 2.0) headroom/transforms/pipeline.py
记忆存储框架 Mem0(48k星,AWS集成)、Letta(三层架构)已做 mem0/memory/main.py, letta/functions/function_sets/base.py
独立任务状态管理 天生与agent框架深度耦合 OpenClaw src/tasks/task-registry.ts内存Map
OpenClaw ContextEngine实现 lossless-claw/Headroom/OpenViking/context-mode已做 各项目manifest和engine.ts
Memory OS Letta概念完整,MemOS/MemoryOS学术实现已有 letta/schemas/memory.py
编码agent特化 Claude Code/Cursor/Windsurf定义了体验天花板 N/A(市场判断)
Meta-engine 仅对tool-heavy场景有价值(§7) lossless-claw assembler + Headroom SmartCrusher分析
知识持久化工具 ContextVault已做(MCP标准接口) contextvault-mcp/src/vault/manager.ts

8.2 最终判断

这个领域在2026年4月已进入增量创新(incremental innovation)阶段。

每个子问题域都有至少一个具备工程成熟度的开源解决方案。剩下的工作是:各方案在自己的场景内优化细节(lossless-claw改进BM25评分、Headroom增加新的ContentRouter类型、Letta优化Summarizer触发策略)。没有可以从0到1构建的、既有足够价值又没有人做的独立方向。

方向仍然是harness工程——在特定场景(特定模型、特定任务类型、特定部署约束)内,通过组合现有工具链构建深度优化的agent体验。


附录A:源码引用索引

以下所有引用基于2026年4月10-14日期间从各项目主分支克隆的代码版本。

[^1]: OpenClaw src/context-engine/types.ts — ContextEngine接口定义,含bootstrap/ingest/assemble/compact/afterTurn等生命周期方法。ownsCompaction字段位于第51行,rewriteTranscriptEntries()位于第93-95行,systemPromptAddition位于AssembleResult类型第11行。

[^2]: OpenClaw src/plugins/slots.ts — 排他性slot系统。SLOT_BY_KIND定义于第12-15行,DEFAULT_SLOT_BY_KEY定义于第17-20行,applyExclusiveSlotSelection()实现于第76-162行。

[^3]: OpenClaw src/context-engine/legacy.ts — LegacyContextEngine实现。ingest()返回{ingested: false},assemble()透传且estimatedTokens: 0,compact()委托给delegateCompactionToRuntime()。全文约85行。

[^4]: OpenClaw src/config/defaults.ts:368 — 默认compaction模式设为"safeguard"。

[^5]: OpenClaw src/hooks/bundled/session-memory/handler.ts:55-57 — session-memory hook仅监听event.type === "command"且event.action === "new" || "reset"。

[^6]: OpenClaw src/agents/pi-embedded-subscribe.handlers.compaction.ts:115-132 — clearStaleAssistantUsageOnSessionMessages()清除所有assistant message的usage数据。

[^7]: lossless-claw src/engine.ts:1199 — LcmContextEngine implements ContextEngine,构造函数接受LcmDependencies + DatabaseSync。全文4,306行。

[^8]: lossless-claw src/types.ts:105-165 — LcmDependencies接口定义,包含config、complete、callGateway、resolveModel、getApiKey、requireApiKey、parseAgentSessionKey、isSubagentSessionKey、normalizeAgentId、log等字段。

[^9]: lossless-claw src/assembler.ts:886-1059 — ContextAssembler.assemble()方法。包含context items获取(SQLite)、fresh tail分割(默认64条)、token budget感知的选择、BM25-lite相关性评分、XML格式摘要输出。

[^10]: lossless-claw src/db/config.ts:255 — freshTailCount默认值为64。

[^11]: lossless-claw src/compaction.ts:33 — CompactionConfig接口,包含contextThreshold(默认0.75)、freshTailCount、leafMinFanout、condensedMinFanout等参数。CompactionLevel类型为"normal" | "aggressive" | "fallback" | "capped"。全文起始于第1行。

[^12]: lossless-claw src/plugin/index.ts — 插件注册胶水代码,390行。执行api.registerContextEngine("lossless-claw", ...)和api.registerContextEngine("default", ...),注册4个工具(describe, expand, expand-query, grep),监听before_reset和session_end事件。

[^13]: Headroom headroom/transforms/pipeline.py:70-125 — 默认管线:CacheAligner → ContentRouter(或SmartCrusher fallback)→ IntelligentContextManager(或RollingWindow fallback)。ContentRouter路由策略定义于第81-89行。全文387行。

[^14]: Headroom headroom/transforms/smart_crusher.py:1-46 — SmartCrusher模块docstring。声明scope为”JSON arrays of ANY type”,safety保证包含first/last K保留、error item永不丢弃、异常值保留、change point保护。全文3,657行。

[^15]: context-mode openclaw.plugin.json — manifest声明"kind": "tool"。通过MCP tools(ctx_execute_file, ctx_index, ctx_search)提供功能。

[^16]: context-mode BENCHMARK.md — 整体节省率为96%(非98%)。

[^17]: OpenViking examples/openclaw-plugin/openclaw.plugin.json — manifest声明"kind": "context-engine"。功能包含autoCapture、autoRecall、bypassSessionPatterns、commitTokenThreshold。

[^18]: Mem0 mem0/memory/main.py:258 — Memory类定义。包含vector_store(必选)和graph store(可选,Neo4j)。同文件还有AsyncMemory类。全文2,597行。

[^19]: Mem0 mem0/memory/main.py:383 — Memory.add()方法。LLM调用提取facts → vector search找相似记忆 → LLM决策ADD/UPDATE/DELETE → 写入存储。graph路径通过ThreadPoolExecutor并行。

[^20]: Mem0 mem0/memory/graph_memory.py:29 — MemoryGraph类定义。使用langchain_neo4j.Neo4jGraph。初始化时创建Neo4j索引(entity_single和entity_composite)。

[^21]: Mem0 mem0/memory/graph_memory.py:76 — MemoryGraph.add()方法。调用链:_retrieve_nodes_from_data()(LLM实体提取)→ _establish_nodes_relations_from_data()(LLM关系建立)→ _search_graph_db()(embedding搜索)→ _get_delete_entities_from_search_output()(LLM删除决策)→ 执行。

[^22]: Mem0 mem0/memory/graph_memory.py:423-428 — 图关系软删除实现:SET r.valid = false, r.invalidated_at = datetime()。

[^23]: Mem0 mem0/memory/graph_memory.py:96-130 — MemoryGraph.search()方法。使用rank_bm25.BM25Okapi对graph查询结果做BM25重排序。

[^24]: Letta letta/schemas/block.py:67 — Block类定义。字段包含value(str)、limit(CORE_MEMORY_BLOCK_CHAR_LIMIT)、label(如’human’、’persona’)、read_only。Human子类位于第117行。

[^25]: Letta letta/functions/function_sets/base.py:246 — core_memory_append()实现:new_value = current_value + "\n" + str(content)。

[^26]: Letta letta/functions/function_sets/base.py:263 — core_memory_replace()实现:精确字符串替换,找不到old_content时抛ValueError。

[^27]: Letta letta/functions/function_sets/base.py:10 — memory()函数,类文件系统操作(create/str_replace/insert/delete/rename)。函数体为raise NotImplementedError。

[^28]: Letta letta/functions/function_sets/base.py:164 — archival_memory_insert()。函数体为raise NotImplementedError("This should never be invoked directly."),实际路由在server端。

[^29]: Letta letta/functions/function_sets/base.py:194 — archival_memory_search()。支持query、tags、tag_match_mode(any/all)、top_k、start/end datetime。同为NotImplementedError。

[^30]: Letta letta/functions/function_sets/base.py:87 — conversation_search()。调用self.message_manager.list_messages_for_agent(),使用hybrid search,支持role和时间范围过滤。

[^31]: Letta letta/agents/letta_agent.py:174 — LettaAgent.step()方法入口。调用_step()执行循环,最多max_steps轮。

[^32]: Letta letta/agents/letta_agent.py:360-362 — 强制tool calling检查。无tool call时抛ValueError("No tool calls found in response, model must make a tool call")。

[^33]: Letta letta/services/summarizer/summarizer.py:36 — Summarizer类定义。接受SummarizationMode、summarizer_agent、message_buffer_limit(默认10)、message_buffer_min(默认3)、partial_evict_summarizer_percentage(默认0.30)。

[^34]: Letta letta/services/summarizer/enums.py — SummarizationMode枚举。两个值:STATIC_MESSAGE_BUFFER和PARTIAL_EVICT_MESSAGE_BUFFER。

[^35]: Letta letta/services/summarizer/summarizer.py:244-300 — _static_buffer_summarization()。固定buffer模式,超过limit时丢弃旧消息保留最近message_buffer_min条。

[^36]: Letta letta/services/summarizer/summarizer.py:136-242 — _partial_evict_buffer_summarization()。MemGPT原始方案,按30%比例驱逐最旧消息,生成摘要插入context[1]。

[^37]: Letta letta/schemas/memory.py — ContextWindowOverview定义。包含core_memory、archival_memory(passages数量)、recall_memory(messages数量)、summary_memory。

[^38]: Zep README.md:56-58 — “Note: This repository is currently a work in progress. This repository contains examples, integrations, and tools for building intelligent agent context with Zep.”

[^39]: Zep legacy/src/store/memory_ce.go:15-57 — memoryDAO._get()方法。取最后4条消息用于retrieval,调用graphiti.I().GetMemory()获取最多5个facts。

[^40]: Zep legacy/src/store/memory_ce.go:59-72 — _initializeProcessingMemory()。两个HTTP POST调用:graphiti.I().PutMemory(ctx, session.SessionID, ...)和graphiti.I().PutMemory(ctx, *session.UserID, ...)。

[^41]: Zep legacy/src/lib/graphiti/service_ce.go — Graphiti HTTP client。Service接口定义于第80行,包含GetMemory/PutMemory/Search/AddNode/GetFact/DeleteFact/DeleteGroup/DeleteMessage。每个方法内部是HTTP request(s.newRequest() + s.doRequest())。全文300行。

[^42]: Zep legacy/src/store/message_common.go — 消息存储实现。使用PostgreSQL(bun ORM)。GetLastN()位于第135-184行。metadata更新含advisory lock(第394-451行)。全文547行。

[^43]: Zep legacy/src/models/memory_common.go:86-92 — MemoryCommon结构体定义。包含Messages、RelevantFacts、Metadata三个字段。

[^44]: Zep legacy/src/models/fact_common.go — Fact结构体。字段:UUID、CreatedAt、Fact(string)、Rating(*float64)。

[^45]: Zep legacy/src/models/session_common.go:10-22 — SessionCommon结构体。字段:UUID、ID、CreatedAt、UpdatedAt、DeletedAt、EndedAt、SessionID、Metadata、UserID、ProjectUUID。

[^46]: ContextVault contextvault-mcp/src/vault/types.ts:3-12 — VaultSettings接口。mode(local/global/full)、enforcement(light/balanced/strict)、limits(max docs 50/50, max lines 100, max summary words 15)。

[^47]: ContextVault contextvault-mcp/src/vault/manager.ts — VaultManager类。核心方法:readDocument()(fs.readFileSync)、writeDocument()(fs.writeFileSync)、search()(委托IndexManager)、getEnforcementReminder()(编辑计数nudge)。全文493行。

[^48]: ContextVault contextvault-mcp/src/vault/index-manager.ts:346-363 — IndexManager.search()。对index.md中的entry进行纯关键词匹配(split → includes → 计分)。搜索范围仅限id、topic、summary字段。

[^49]: ContextVault contextvault-mcp/src/vault/manager.ts:195-372 — Enforcement机制。包含trackEdit()、trackResearch()、getEnforcementReminder()(edit-based)、getResearchReminder()(research-based)。域分类支持frontend/backend/database/testing/config/utils/services/types/docs。

[^50]: ContextVault contextvault-mcp/src/vault/manager.ts:376-440 — generateDocContent()。根据type(doc/error/decision/plan/snippet/intel/handoff/explain)生成不同的markdown模板。

[^51]: ContextVault contextvault-mcp/src/server.ts:35 — ContextVaultServer类。注册20+个MCP tools,通过withTracking()包装enforcement和reminder逻辑。

[^52]: Hermes Agent agent/context_compressor.py:612-745 — ContextCompressor.compress()。流程:修剪旧tool results → 确定边界 → 生成摘要(1次LLM调用,位于_generate_summary()第404行)→ 组装。


附录B:代码量统计

项目 核心模块代码量 计量范围
Headroom 211,243行 Python headroom/**/*.py(465文件)
Letta 31,955行 Python letta/{agents,schemas,services/summarizer,functions}/**/*.py
lossless-claw 22,967行 TypeScript src/**/*.ts
Hermes Agent 9,660行 Python run_agent.py(单文件)
Zep legacy 7,885行 Go legacy/src/**/*.go
Mem0核心 3,341行 Python mem0/memory/{main,graph_memory}.py
ContextVault ~1,500行 TypeScript contextvault-mcp/src/**/*.ts

本文所有技术判断基于对源码的直接阅读,写作时间2026年4月。各项目代码可能在此后发生变化。

123…24

54 日志
17 分类
44 标签
© 2026 Sherry