<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>集异璧之大成</title><description>Shelven Zhou 的技术博客，记录 AI Agent、Claude Code、工程实践与源码阅读。</description><link>https://e95e6364.shelven.pages.dev/</link><item><title>Claude Code Tools 拆解: 从 shell wrapper 到 Agent 执行边界</title><link>https://e95e6364.shelven.pages.dev/posts/cc-tools-implementation/</link><guid isPermaLink="true">https://e95e6364.shelven.pages.dev/posts/cc-tools-implementation/</guid><description>深入 Claude Code tools 层实现：它如何用 schema、语义校验、权限检查、并发语义、进度回调和结果管理，把裸 shell 包装成 agent runtime 可控的执行边界。</description><pubDate>Fri, 24 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;很多人第一次实现 coding agent，最容易想到的工具接口是：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;async function run(command: string) {
  return await sh(command)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;看起来已经够了。模型会写命令，系统执行命令，把 stdout/stderr 塞回上下文，下一轮继续推理。早期 demo 这么做完全没问题，但一旦进入真实工程环境，这个抽象很快就会崩。&lt;/p&gt;
&lt;p&gt;原因不是 shell 不够强，而是它太强。&lt;code&gt;sh&lt;/code&gt; 同时覆盖读取、搜索、编译、测试、网络访问、文件修改、后台任务、权限提升、进程管理、重定向、管道、子 shell、命令替换等行为。对 agent 来说，裸 shell 是一个没有边界的万能出口。&lt;/p&gt;
&lt;p&gt;对系统来说，问题在于一段 command string 几乎不携带结构化语义。系统看到 &lt;code&gt;npm test&lt;/code&gt;、&lt;code&gt;grep -R foo .&lt;/code&gt;、&lt;code&gt;sed -i ...&lt;/code&gt;、&lt;code&gt;git push&lt;/code&gt; 时，首先看到的都只是字符串；它不知道这个调用主要是读还是写、会影响哪些路径、能不能和其他操作并发、是否应该走权限弹窗、输出可能有多大、失败结果应该如何反馈给模型。除非系统重新解析 shell，并额外补上这些语义，否则它只能在“全部放行”和“每次都问”之间做粗糙选择。&lt;/p&gt;
&lt;p&gt;Claude Code 的 tools 层解决的正是这个问题：它不是把 shell 暴露给模型，而是在模型和操作系统之间建立一层&lt;strong&gt;可描述、可校验、可授权、可并发调度、可观测、可压缩上下文&lt;/strong&gt;的执行边界。&lt;/p&gt;
&lt;p&gt;本文基于 2026 年 3 月 31 日泄露的 Claude Code 源码（文末可免费下载）和 &lt;code&gt;references/proj-understand-harness/claude-code-main&lt;/code&gt; 中的实现，拆解 Claude Code tools 系统的设计。&lt;/p&gt;
&lt;h2&gt;为什么不能只给 Agent 一个 sh？&lt;/h2&gt;
&lt;p&gt;裸 shell 的第一个问题是&lt;strong&gt;权限不可表达&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;ls src&lt;/code&gt;、&lt;code&gt;cat package.json&lt;/code&gt;、&lt;code&gt;npm test&lt;/code&gt;、&lt;code&gt;rm -rf dist&lt;/code&gt;、&lt;code&gt;git push&lt;/code&gt;、&lt;code&gt;curl | sh&lt;/code&gt; 都是字符串。系统如果只看到一段 command string，就只能在执行前做粗糙的字符串匹配，或者干脆每次都问用户。前者容易漏，后者会把用户淹没在权限弹窗里。&lt;/p&gt;
&lt;p&gt;第二个问题是&lt;strong&gt;并发不可判断&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;一个模型回复里可能同时包含多个 tool use。&lt;code&gt;Read(a.ts)&lt;/code&gt; 和 &lt;code&gt;Grep(foo)&lt;/code&gt; 可以并发；&lt;code&gt;Edit(a.ts)&lt;/code&gt; 和 &lt;code&gt;Bash(npm test)&lt;/code&gt; 最好按顺序；两个写同一个文件的操作更不能乱跑。如果所有东西都是 &lt;code&gt;sh&lt;/code&gt;，主循环很难知道哪些调用可以并发，哪些必须串行。&lt;/p&gt;
&lt;p&gt;第三个问题是&lt;strong&gt;结果不可控&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;真实命令输出很容易爆炸：测试日志、构建日志、&lt;code&gt;grep -R&lt;/code&gt;、&lt;code&gt;find&lt;/code&gt;、&lt;code&gt;npm install&lt;/code&gt;。如果原样塞回 context window，几十次工具调用后上下文会被日志污染。上一篇 context engineering 里讲的 Snip、MicroCompact、AutoCompact，解决的是上下文膨胀；但 tools 层本身也要先做第一道结果管理。&lt;/p&gt;
&lt;p&gt;第四个问题是&lt;strong&gt;交互不可恢复&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;命令可能长时间运行、卡住、需要后台化、被用户中断、输出图片、写入文件、触发 hook、被权限拒绝。一个可靠的 agent runtime 不能只返回 &lt;code&gt;{ stdout, stderr }&lt;/code&gt;，它还要知道执行中发生了什么，如何展示进度，如何把失败反馈给模型，如何让用户介入。&lt;/p&gt;
&lt;p&gt;所以 Claude Code 没有把 tools 做成一组简单函数，而是定义了一套执行协议。&lt;/p&gt;
&lt;h2&gt;Tool.ts：工具调用的控制面&lt;/h2&gt;
&lt;p&gt;Claude Code 的核心抽象在 &lt;code&gt;src/Tool.ts&lt;/code&gt;。表面上看，一个 &lt;code&gt;Tool&lt;/code&gt; 只是 &lt;code&gt;name + inputSchema + call()&lt;/code&gt;；但真正重要的是，&lt;code&gt;Tool&lt;/code&gt; 把一次工具调用拆成了三组控制面：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;images/cc-tools-implementation/tool-boundary.svg&quot; alt=&quot;Claude Code Tool 执行边界&quot; /&gt;&lt;/p&gt;
&lt;p&gt;第一组是&lt;strong&gt;校验类字段&lt;/strong&gt;：&lt;code&gt;inputSchema&lt;/code&gt;、&lt;code&gt;validateInput&lt;/code&gt;、&lt;code&gt;checkPermissions&lt;/code&gt;。它们回答的是：模型传来的参数是否合法？这个调用在语义上是否成立？当前权限模式下是否允许执行？&lt;/p&gt;
&lt;p&gt;第二组是&lt;strong&gt;执行编排字段&lt;/strong&gt;：&lt;code&gt;call&lt;/code&gt;、&lt;code&gt;onProgress&lt;/code&gt;、&lt;code&gt;isReadOnly&lt;/code&gt;、&lt;code&gt;isConcurrencySafe&lt;/code&gt;。它们回答的是：工具如何执行？执行中如何反馈？这个调用是否会改变外部状态？能否和其他工具并发？&lt;/p&gt;
&lt;p&gt;第三组是&lt;strong&gt;上下文工程字段&lt;/strong&gt;：&lt;code&gt;prompt&lt;/code&gt;、&lt;code&gt;inputJSONSchema&lt;/code&gt;、&lt;code&gt;maxResultSizeChars&lt;/code&gt;、&lt;code&gt;mapToolResultToToolResultBlockParam&lt;/code&gt;。它们回答的是：模型在 context window 里看到什么工具说明？MCP 等外部工具如何直接提供 JSON Schema？结果以什么形式回到模型？大结果如何避免污染上下文？&lt;/p&gt;
&lt;p&gt;把这些字段放在一起看，&lt;code&gt;Tool&lt;/code&gt; 就不再是一个函数接口，而是 agent runtime 对真实世界操作的边界协议。&lt;/p&gt;
&lt;h3&gt;1. 校验：先证明这个调用可以执行&lt;/h3&gt;
&lt;p&gt;工具执行前，Claude Code 会先用 schema 校验模型传来的参数：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const parsedInput = tool.inputSchema.safeParse(input)
if (!parsedInput.success) {
  return tool_use_error
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这一步处理的是&lt;strong&gt;数据格式&lt;/strong&gt;问题。模型生成 tool call 参数并不总是合法，schema 失败时，Claude Code 不会猜测执行，而是把结构化错误作为 tool result 返回给模型，让模型自己修正参数。&lt;/p&gt;
&lt;p&gt;schema 之后还有语义校验。&lt;code&gt;validateInput&lt;/code&gt; 不是权限系统，而是工具自己的“这个调用有没有意义”。例如 BashTool 会拦截较长的 &lt;code&gt;sleep N&lt;/code&gt;，提示模型使用后台任务或 Monitor tool，而不是让前台命令长时间占住主循环。&lt;/p&gt;
&lt;p&gt;最后才是权限检查。通用权限逻辑在 &lt;code&gt;utils/permissions/permissions.ts&lt;/code&gt;，但每个工具可以实现自己的 &lt;code&gt;checkPermissions&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;async checkPermissions(input, context) {
  return bashToolHasPermission(input, context)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这意味着权限不是简单的 “Bash 允许/拒绝”。BashTool 会继续解析 command，匹配 &lt;code&gt;Bash(git status:*)&lt;/code&gt; 这样的内容规则，识别管道、重定向、复合命令、路径约束、安全 wrapper、sandbox auto-allow、deny/ask/allow 规则和 classifier 结果。&lt;/p&gt;
&lt;p&gt;通用权限流程大致是：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;先看整个 tool 是否被 deny。&lt;/li&gt;
&lt;li&gt;再看整个 tool 是否 always ask。&lt;/li&gt;
&lt;li&gt;调用 tool 自己的 &lt;code&gt;checkPermissions&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;工具返回 deny 或安全检查 ask 时，直接尊重。&lt;/li&gt;
&lt;li&gt;bypass / acceptEdits / auto 等模式再介入。&lt;/li&gt;
&lt;li&gt;仍然是 passthrough 的调用转成 ask，让用户决定。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这个顺序体现了一个原则：&lt;strong&gt;工具内的内容级安全判断优先于全局便利模式&lt;/strong&gt;。某些敏感路径检查即使在 bypass 模式也必须 prompt。&lt;/p&gt;
&lt;h3&gt;2. 执行编排：并发由 runtime 决定，不由模型决定&lt;/h3&gt;
&lt;p&gt;模型可能一次返回多个 tool use，但能不能并发，不应该由模型自己说了算。Claude Code 的 &lt;code&gt;toolOrchestration.ts&lt;/code&gt; 会把一次 assistant message 中的多个 tool use 分批：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if (isConcurrencySafe &amp;amp;&amp;amp; previousBatch.isConcurrencySafe) {
  previousBatch.blocks.push(toolUse)
} else {
  batches.push({ isConcurrencySafe, blocks: [toolUse] })
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;并发安全的连续工具会并行跑，非并发安全工具会串行跑。默认值很保守：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;isConcurrencySafe: () =&amp;gt; false
isReadOnly: () =&amp;gt; false
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;也就是说，一个工具如果没有明确声明自己安全，就不会被并发调度。&lt;/p&gt;
&lt;p&gt;BashTool 的实现很典型：只有当一个 shell 命令被判定为 read-only，它才可能和其他 read-only 工具并发。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;isConcurrencySafe(input) {
  return this.isReadOnly?.(input) ?? false
}

isReadOnly(input) {
  const compoundCommandHasCd = commandHasAnyCd(input.command)
  const result = checkReadOnlyConstraints(input, compoundCommandHasCd)
  return result.behavior === &apos;allow&apos;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里有一个容易误解的点：Claude Code 并没有做“路径级 read/write 冲突图”。例如理论上 &lt;code&gt;Write(a.ts)&lt;/code&gt; 和 &lt;code&gt;Read(b.ts)&lt;/code&gt; 可以并发，&lt;code&gt;Write(a.ts)&lt;/code&gt; 和 &lt;code&gt;Read(a.ts)&lt;/code&gt; 不应该并发；两个写不同文件的操作也未必一定冲突。但源码里的调度策略没有细到这个层面。&lt;/p&gt;
&lt;p&gt;它的实际规则更简单，也更保守：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;FileReadTool&lt;/code&gt; 明确声明 &lt;code&gt;isConcurrencySafe() = true&lt;/code&gt;、&lt;code&gt;isReadOnly() = true&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;FileWriteTool&lt;/code&gt; 和 &lt;code&gt;FileEditTool&lt;/code&gt; 没有声明并发安全，因此使用 &lt;code&gt;buildTool&lt;/code&gt; 默认的 &lt;code&gt;isConcurrencySafe() = false&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;partitionToolCalls()&lt;/code&gt; 只会把&lt;strong&gt;连续的 concurrency-safe tool use&lt;/strong&gt; 合并成并发批次。&lt;/li&gt;
&lt;li&gt;一旦遇到非并发安全工具，它会单独成为一个串行批次，前后的读操作也会被批次边界隔开。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;所以如果模型一次返回：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Read(a.ts), Read(b.ts), Write(a.ts), Read(a.ts)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;调度结果会是：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[Read(a.ts), Read(b.ts)] 并发
[Write(a.ts)] 串行
[Read(a.ts)] 串行等待前面的 Write 完成
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Claude Code 通过“写操作整体不并发”绕开了路径级冲突分析。文件写入还有另一层防护：&lt;code&gt;FileWriteTool&lt;/code&gt; / &lt;code&gt;FileEditTool&lt;/code&gt; 的 &lt;code&gt;validateInput()&lt;/code&gt; 会检查目标文件是否已经被读过，以及文件 mtime 是否在读取后被用户或 formatter/linter 修改过。如果文件变了，写入会失败并要求模型重新 Read。&lt;/p&gt;
&lt;h3&gt;3. 上下文工程：工具说明和工具结果都有预算&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;prompt()&lt;/code&gt; 返回的是工具描述，也就是 API tool schema 里的 &lt;code&gt;description&lt;/code&gt;。在 &lt;code&gt;toolToAPISchema&lt;/code&gt; 里，Claude Code 会把 tool 转成 Anthropic API 接受的 schema。内置工具通常从 Zod &lt;code&gt;inputSchema&lt;/code&gt; 转成 JSON Schema；MCP 或 synthetic output 这类外部/动态工具则可以直接提供 &lt;code&gt;inputJSONSchema&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;base = {
  name: tool.name,
  description: await tool.prompt(...),
  input_schema,
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里有一个容易被忽略的优化：tool schema 会做 per-session cache，避免 feature flag 或 &lt;code&gt;tool.prompt()&lt;/code&gt; 的细微变化导致工具数组字节变化，从而破坏 prompt cache。&lt;/p&gt;
&lt;p&gt;工具结果也有预算。每个工具声明 &lt;code&gt;maxResultSizeChars&lt;/code&gt;。执行完成后，&lt;code&gt;toolExecution.ts&lt;/code&gt; 会把工具输出映射成 API &lt;code&gt;tool_result&lt;/code&gt;，再经过 &lt;code&gt;processToolResultBlock&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if (size &amp;gt; threshold) {
  const result = await persistToolResult(content, toolUseId)
  return { ...toolResultBlock, content: buildLargeToolResultMessage(result) }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;大结果不会被硬截断，而是持久化到 session 的 &lt;code&gt;tool-results&lt;/code&gt; 目录，模型只看到路径和 preview。BashTool 还在自身内部处理超大 shell 输出：如果底层命令写出了 output file，它会复制或 hardlink 到 tool-results，并在结果里放 &lt;code&gt;persistedOutputPath&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;FileReadTool 则把阈值设成 &lt;code&gt;Infinity&lt;/code&gt;，因为 Read 本身已经有读取限制；如果把 Read 结果再持久化成文件，会形成 “Read → file → Read” 的循环。&lt;/p&gt;
&lt;p&gt;这说明 Claude Code 的上下文管理不是压缩阶段才开始，而是从工具说明和工具输出产生的那一刻就开始。&lt;/p&gt;
&lt;h2&gt;Case Study：BashTool 如何把 shell 变成可控工具&lt;/h2&gt;
&lt;p&gt;BashTool 是最能体现 Claude Code tools 层复杂度的工具。它看起来只是 &lt;code&gt;Run shell command&lt;/code&gt;，实际承担了 shell 语义解析、安全判断、沙箱、后台任务、输出管理和进度上报。&lt;/p&gt;
&lt;h3&gt;输入不是只有 command&lt;/h3&gt;
&lt;p&gt;BashTool schema 包含：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;command&lt;/code&gt;：要执行的命令。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;timeout&lt;/code&gt;：超时时间。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;description&lt;/code&gt;：模型对命令意图的短描述。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;run_in_background&lt;/code&gt;：是否后台运行。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;dangerouslyDisableSandbox&lt;/code&gt;：显式请求绕过沙箱。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;_simulatedSedEdit&lt;/code&gt;：内部字段，不暴露给模型。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;_simulatedSedEdit&lt;/code&gt; 是一个很好的例子。Claude Code 支持把某些 &lt;code&gt;sed&lt;/code&gt; 原地编辑识别成可预览的文件编辑。用户批准后，权限系统注入模拟后的新内容，BashTool 直接应用文件修改，而不是重新执行 &lt;code&gt;sed&lt;/code&gt;。同时 schema 会从模型可见字段里删掉 &lt;code&gt;_simulatedSedEdit&lt;/code&gt;，避免模型伪造内部字段绕过权限。&lt;/p&gt;
&lt;h3&gt;权限不是 regex，而是 shell 解析&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;bashToolHasPermission&lt;/code&gt; 先尝试 tree-sitter bash AST 解析。解析结果分几类：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;simple&lt;/code&gt;：能拆成清晰的 simple commands。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;too-complex&lt;/code&gt;：包含无法可靠静态分析的结构，例如复杂展开或控制流。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;parse-unavailable&lt;/code&gt;：tree-sitter 不可用时走 legacy shell-quote 路径。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;对于 &lt;code&gt;too-complex&lt;/code&gt;，Claude Code 的策略是 fail safe：先尊重 deny/ask/allow 的精确规则；如果无法证明安全，就 ask。&lt;/p&gt;
&lt;p&gt;对于 &lt;code&gt;simple&lt;/code&gt;，它会继续做语义检查，例如危险 builtin、wrapper stripping、路径约束、重定向、管道和复合命令。deny/ask 规则会做更激进的 env var stripping，防止 &lt;code&gt;FOO=bar rm ...&lt;/code&gt; 绕过 &lt;code&gt;Bash(rm:*)&lt;/code&gt;；allow 规则则更保守，只剥离安全 env vars，避免把危险环境变量下的命令误判为已允许。&lt;/p&gt;
&lt;p&gt;这里的细节很工程化：比如 &lt;code&gt;timeout -k$(id) 10 ls&lt;/code&gt; 这种 wrapper 参数如果用宽松正则剥离，就可能把危险部分藏在 wrapper flag 里，让剩余 &lt;code&gt;ls&lt;/code&gt; 命中 allow rule。源码里专门用 allowlist 限制 timeout flag value，避免这种解析差异。&lt;/p&gt;
&lt;h3&gt;执行不是 exec 一次就结束&lt;/h3&gt;
&lt;p&gt;BashTool 的 &lt;code&gt;call()&lt;/code&gt; 调用 &lt;code&gt;runShellCommand&lt;/code&gt; generator，边执行边消费 progress：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;onProgress({
  toolUseID: `bash-progress-${progressCounter++}`,
  data: {
    type: &apos;bash_progress&apos;,
    output: progress.output,
    elapsedTimeSeconds: progress.elapsedTimeSeconds,
    totalLines: progress.totalLines,
    totalBytes: progress.totalBytes,
  }
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这让终端 UI 可以显示滚动进度，也让系统知道工具仍然活着。对 coding agent 来说，这不是锦上添花：测试、构建、安装依赖、启动 dev server 都可能持续几十秒甚至几分钟，没有 progress 语义就很难做中断、后台化和用户反馈。&lt;/p&gt;
&lt;p&gt;执行完成后，BashTool 还会做几件事：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;追踪 git 操作，例如从 &lt;code&gt;git commit&lt;/code&gt; 输出中提取 commit id。&lt;/li&gt;
&lt;li&gt;对非零 exit code 做语义解释，不是所有非零都等价于工具异常。&lt;/li&gt;
&lt;li&gt;标注 sandbox failure。&lt;/li&gt;
&lt;li&gt;如果 cwd 被命令切到项目外，重置工作目录并提示。&lt;/li&gt;
&lt;li&gt;识别图片输出，转成 image tool result。&lt;/li&gt;
&lt;li&gt;对超大输出持久化，并只把 preview 放进上下文。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这就是为什么 BashTool 的输出 schema 不只是 stdout/stderr，还包含 &lt;code&gt;interrupted&lt;/code&gt;、&lt;code&gt;backgroundTaskId&lt;/code&gt;、&lt;code&gt;persistedOutputPath&lt;/code&gt;、&lt;code&gt;returnCodeInterpretation&lt;/code&gt;、&lt;code&gt;noOutputExpected&lt;/code&gt; 等字段。&lt;/p&gt;
&lt;h2&gt;内置工具池如何进入模型？&lt;/h2&gt;
&lt;p&gt;先看内置工具。Claude Code 的 base tools 由 &lt;code&gt;src/tools.ts&lt;/code&gt; 的 &lt;code&gt;getAllBaseTools()&lt;/code&gt; 组装。基础工具包括 Bash、PowerShell、文件读写、搜索、WebFetch/WebSearch、Todo、Skill、MCP resource tools、subagent/task tools，以及一些由 feature flag 或内部环境启用的 LSP、Cron、RemoteTrigger、Workflow、Snip、Monitor、WebBrowser 等工具。&lt;/p&gt;
&lt;p&gt;这不是一个固定列表。&lt;code&gt;getAllBaseTools()&lt;/code&gt; 会根据 &lt;code&gt;USER_TYPE&lt;/code&gt;、feature flag、环境变量、平台能力动态引入工具。随后 &lt;code&gt;getTools(permissionContext)&lt;/code&gt; 会继续过滤：简化模式只暴露简化工具集；被 deny rule blanket-deny 的工具会在进入模型前移除；REPL 模式会隐藏底层 primitive tools；&lt;code&gt;isEnabled()&lt;/code&gt; 为 false 的工具不会出现在工具池。&lt;/p&gt;
&lt;p&gt;最终在 &lt;code&gt;query.ts&lt;/code&gt; 里，主循环调用模型时传入：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;deps.callModel({
  messages,
  systemPrompt,
  thinkingConfig,
  tools: toolUseContext.options.tools,
  ...
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;services/api/claude.ts&lt;/code&gt; 再把每个 Tool 转成 API schema。所以内置 tools “进入模型” 并不是把源码塞进 prompt，而是把 &lt;code&gt;name + description + input_schema&lt;/code&gt; 作为模型可调用的工具定义发送给 API。模型返回 &lt;code&gt;tool_use&lt;/code&gt; block 后，Claude Code 再在本地找到同名 Tool 执行。&lt;/p&gt;
&lt;h2&gt;扩展能力如何渐进式披露？&lt;/h2&gt;
&lt;p&gt;再看扩展能力。MCP tools 会从 app state 进入 &lt;code&gt;assembleToolPool(permissionContext, mcpTools)&lt;/code&gt;，和内置工具合并；skills 则通过内置的 &lt;code&gt;SkillTool&lt;/code&gt; 暴露入口，但 skill 内容本身来自项目、本地、plugin、bundled 或 MCP commands。它们的问题不是“能不能作为工具进入模型”，而是数量和内容都可能很大，不能把完整定义一次性塞进 context window。&lt;/p&gt;
&lt;p&gt;Claude Code 在这里用了同一个思路：&lt;strong&gt;先给目录，再按需加载全文&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;Skill 是第一种渐进式披露。&lt;code&gt;SkillTool&lt;/code&gt; 本身是内置工具，会和其他内置工具一样进入 API tools schema；但完整 &lt;code&gt;SKILL.md&lt;/code&gt; 不会一开始塞进 context window。Claude Code 只先告诉模型“有哪些 skill 可能可用”，模型判断匹配后再调用 &lt;code&gt;Skill&lt;/code&gt; 工具加载完整说明。&lt;/p&gt;
&lt;p&gt;这条链路分三层：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;SkillTool&lt;/code&gt; 的 prompt 明确告诉模型：看到匹配 skill 时，必须先调用 Skill。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;skill_listing&lt;/code&gt; system reminder 只注入 skill 的 &lt;code&gt;name + description + when_to_use&lt;/code&gt; 摘要，不注入完整 &lt;code&gt;SKILL.md&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;模型真正调用 &lt;code&gt;Skill(&quot;&amp;lt;name&amp;gt;&quot;)&lt;/code&gt; 后，Claude Code 才读取并展开完整 skill 内容；声明 &lt;code&gt;context: fork&lt;/code&gt; 的 skill 甚至会在独立 subagent context 里运行，最后只把结果返回主会话。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;ToolSearch 是第二种渐进式披露，主要解决 MCP 和少数可选内置工具的 schema 膨胀。&lt;code&gt;isDeferredTool()&lt;/code&gt; 会把 MCP tools、&lt;code&gt;shouldDefer: true&lt;/code&gt; 的内置工具标记为 deferred；&lt;code&gt;alwaysLoad: true&lt;/code&gt; 的工具和 ToolSearchTool 自己不会 deferred。&lt;/p&gt;
&lt;p&gt;当 tool search 启用时，被 deferred 的工具仍然会以 &lt;code&gt;defer_loading: true&lt;/code&gt; 形式传给 API，但完整 schema 不进入初始 prompt。模型如果需要某个 deferred tool，先调用 ToolSearchTool：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;select:Read,Edit,Grep
notebook jupyter
+slack send
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;ToolSearchTool 返回匹配工具的完整 JSON Schema，之后模型才能调用它们。这个机制本质上是在做&lt;strong&gt;工具定义的按需分页&lt;/strong&gt;：常用核心工具留在首屏，长尾 MCP 和可选工具延迟加载。&lt;/p&gt;
&lt;p&gt;Skill 和 ToolSearch 看起来是两套机制，但目标一致：不要把所有潜在能力一次性塞进 context window。对 coding agent 来说，context engineering 不只发生在历史消息压缩阶段，也发生在“给模型暴露哪些工具和说明”的入口处。&lt;/p&gt;
&lt;h2&gt;关键设计取舍&lt;/h2&gt;
&lt;p&gt;Claude Code 的 tools 实现里，最值得借鉴的不是某个具体字段，而是几条设计取舍。&lt;/p&gt;
&lt;p&gt;第一，&lt;strong&gt;fail-closed 默认值&lt;/strong&gt;。&lt;code&gt;buildTool&lt;/code&gt; 给所有工具补默认实现，其中 &lt;code&gt;isConcurrencySafe=false&lt;/code&gt; 和 &lt;code&gt;isReadOnly=false&lt;/code&gt; 最关键。新工具如果忘了声明自己只读，主循环不会冒险并发。&lt;/p&gt;
&lt;p&gt;第二，&lt;strong&gt;权限系统分层&lt;/strong&gt;。用户/项目/策略/CLI/session 来源的 allow、deny、ask rules，工具自己的内容级 &lt;code&gt;checkPermissions&lt;/code&gt;，PreToolUse hook，permission mode，Bash/PowerShell classifier，共同组成决策链。简单读操作自动放行，高风险操作提示用户，明确 deny 永远优先。&lt;/p&gt;
&lt;p&gt;第三，&lt;strong&gt;hooks 是工具执行链的一等环节&lt;/strong&gt;。&lt;code&gt;toolExecution.ts&lt;/code&gt; 在权限前运行 PreToolUse hooks，在执行后运行 PostToolUse hooks。Pre hook 可以追加上下文、修改 tool input、给出权限结果或阻止执行；Post hook 可以处理工具输出。这让 tools 层成为 Claude Code hook 生态的插入点，而不只是执行函数。&lt;/p&gt;
&lt;p&gt;第四，&lt;strong&gt;context 管理前移&lt;/strong&gt;。上一篇文章讨论的 Snip、MicroCompact、AutoCompact 是历史消息层面的压缩；这一篇看到的是工具层的压缩：工具说明按需披露，工具结果超限持久化，空结果也会被替换成 &lt;code&gt;(&amp;lt;tool&amp;gt; completed with no output)&lt;/code&gt;，避免模型在空函数结果后异常停止。&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;Claude Code 的 tools 实现说明，一个成熟 coding agent 的工具层至少要回答五个问题：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;模型能传什么参数？&lt;/li&gt;
&lt;li&gt;这个调用是否允许执行？&lt;/li&gt;
&lt;li&gt;它是否会读写外部状态，能不能并发？&lt;/li&gt;
&lt;li&gt;执行过程中如何反馈、取消、后台化？&lt;/li&gt;
&lt;li&gt;结果如何进入上下文而不污染上下文？&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;裸 &lt;code&gt;sh&lt;/code&gt; 只能回答“怎么执行”。Claude Code 的 &lt;code&gt;Tool&lt;/code&gt; 抽象回答的是“如何把执行纳入 agent runtime 的控制面”。&lt;/p&gt;
&lt;p&gt;这也是 tools 层最值得借鉴的地方：工具不是模型能力的简单扩展，而是 agent 与真实世界交互时的边界协议。边界越清楚，agent 才越能在复杂工程环境里跑得久、跑得快，并且不把用户和 context window 一起拖进混乱里。&lt;/p&gt;
&lt;p&gt;本文所引用的 Claude Code 源码包含在下方附件中，欢迎自行探索更多细节。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;附件&lt;/strong&gt;：&lt;a href=&quot;/downloads/claude-code-main.zip&quot;&gt;claude-code-main.zip&lt;/a&gt; — 2026 年 3 月 31 日泄露的 Claude Code 源码（用于本文分析）&lt;/p&gt;
</content:encoded><category>AI Agent</category><author>Shelven Zhou</author></item><item><title>[译] 使用 Claude Code：会话管理与 100 万上下文</title><link>https://e95e6364.shelven.pages.dev/posts/cc-session-management/</link><guid isPermaLink="true">https://e95e6364.shelven.pages.dev/posts/cc-session-management/</guid><description>Claude Code 工程师 Thariq 分享会话管理最佳实践：何时开新会话、rewind vs 纠正、compact vs clear、以及如何用 subagents 管理上下文。</description><pubDate>Thu, 16 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;原文：&lt;a href=&quot;https://x.com/trq212/status/2044548257058328723&quot;&gt;Using Claude Code: Session Management &amp;amp; 1M Context&lt;/a&gt;
原作者：Thariq（&lt;a href=&quot;https://x.com/trq212&quot;&gt;@trq212&lt;/a&gt;）— Claude Code 工程师。曾就职 YC W20、MIT Media Lab。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;今天，我们为 &lt;code&gt;/usage&lt;/code&gt; 命令推出了一项全新更新，旨在帮助你更清晰地了解自己在 Claude Code 中的使用情况。这个决定的背后，是我们近期与用户进行的多次深入交流。&lt;/p&gt;
&lt;p&gt;在这些交流中，一个反复浮现的话题是：不同用户管理会话的方式差异极大，尤其是在 Claude Code 新增 100 万上下文之后。&lt;/p&gt;
&lt;p&gt;你是只在终端里保持一两个会话常驻？还是每次提示词都开一个新会话？什么时候该用 compact、rewind 或 subagents？什么会导致一次糟糕的 compact？&lt;/p&gt;
&lt;p&gt;这里面的门道比想象中多得多，而且会切实影响你使用 Claude Code 的体验。而几乎所有这些问题，本质上都归结为对上下文窗口的管理。&lt;/p&gt;
&lt;h2&gt;上下文、压缩与上下文腐化速览&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;images/cc-session-management/HF-nqWCbEAE3Oan.jpeg&quot; alt=&quot;上下文窗口示意&quot; /&gt;&lt;/p&gt;
&lt;p&gt;上下文窗口是模型在生成下一次回复时能一次性&quot;看见&quot;的全部内容，包括系统提示词、到目前为止的对话、每一次工具调用及其输出，以及所有被读取过的文件。Claude Code 的上下文窗口为 100 万 token。&lt;/p&gt;
&lt;p&gt;不过，使用上下文本身是有代价的——这种现象通常被称为上下文腐化（context rot）。简单来说，随着上下文不断膨胀，模型的注意力被摊薄到更多 token 上，早期那些不再相关的内容开始干扰当前任务，性能因此下降。&lt;/p&gt;
&lt;p&gt;上下文窗口存在硬性上限。当你快要触顶时，就需要将当前工作浓缩为一段更精简的描述，然后在新的上下文窗口中继续——这就是压缩（compaction）。你也可以手动触发压缩。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;images/cc-session-management/HF-ntaxboAAZuCm.jpeg&quot; alt=&quot;压缩示意&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;每一轮都是一个分岔点&lt;/h2&gt;
&lt;p&gt;假设你刚让 Claude 做了一件事，它也已经完成了。此时你的上下文里已经有了一些信息：工具调用、工具输出、你的指令。接下来你其实有相当多的选择：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;继续：在同一个会话里再发一条消息&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/rewind&lt;/code&gt;（Esc Esc）：跳回到之前某条消息，从那里重新尝试&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/clear&lt;/code&gt;：开启新会话，通常附上一段你从刚才的工作中提炼出的简要说明&lt;/li&gt;
&lt;li&gt;Compact：总结到目前为止的会话，然后基于这份总结继续&lt;/li&gt;
&lt;li&gt;Subagents：把下一段工作委派给一个拥有干净上下文的代理，只把它的结果带回当前会话&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;最自然的做法当然是继续发消息，但另外四个选项存在的目的，都是帮助你管理上下文。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;images/cc-session-management/HF-n6mMbEAEImhv.jpeg&quot; alt=&quot;会话分岔点&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;什么时候该开始新会话&lt;/h2&gt;
&lt;p&gt;什么时候该保留一个长会话，什么时候又该另起炉灶？我们的经验法则是：新任务，新会话。&lt;/p&gt;
&lt;p&gt;100 万上下文窗口确实意味着你现在可以更可靠地完成更长的任务，比如从零开始构建一个完整的全栈应用。&lt;/p&gt;
&lt;p&gt;但有时你会处理一些相关任务，其中一部分上下文仍然必要，但并非全部。例如，为你刚实现的功能编写文档。虽然你可以开启新会话，但 Claude 需要重新读取你刚实现的那些文件，这会更慢，也更贵。&lt;/p&gt;
&lt;h2&gt;用回退代替纠正&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;images/cc-session-management/HF-oDqjbEAI94h5.jpeg&quot; alt=&quot;回退 vs 纠正&quot; /&gt;&lt;/p&gt;
&lt;p&gt;如果只能推荐一个体现良好上下文管理的习惯，那就是 rewind。&lt;/p&gt;
&lt;p&gt;在 Claude Code 中，双击 Esc（或运行 &lt;code&gt;/rewind&lt;/code&gt;）可以让你跳回任意一条之前的消息，并从那里重新输入提示词。该时间点之后的消息会从上下文中移除。&lt;/p&gt;
&lt;p&gt;相比直接纠正，rewind 往往是更好的选择。举个例子：Claude 读取了五个文件，尝试了一种方案，但没有成功。你的直觉可能是输入&quot;这不行，换成 X 试试&quot;。但更好的做法是回退到刚读完文件之后的位置，然后带着你刚学到的信息重新提示：&quot;不要用方案 A，foo 模块没有暴露那个接口，直接走 B。&quot;&lt;/p&gt;
&lt;p&gt;你也可以使用 &quot;summarize from here&quot;，让 Claude 总结它的发现并创建一条交接消息——有点像未来的 Claude 给过去的自己留了张纸条：&quot;我试过了，这条路走不通。&quot;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;images/cc-session-management/HF-oKwBbEAAdb6I.jpeg&quot; alt=&quot;Rewind 流程&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;压缩与全新会话&lt;/h2&gt;
&lt;p&gt;当一个会话变得很长时，你有两种方式可以减负：&lt;code&gt;/compact&lt;/code&gt; 或 &lt;code&gt;/clear&lt;/code&gt;（然后重新开始）。它们感觉相似，但行为非常不同。&lt;/p&gt;
&lt;p&gt;Compact 会让模型总结到目前为止的对话，然后用这份总结替换掉历史记录。这是一个有损过程——你把&quot;什么值得保留&quot;的判断权交给了 Claude。好处是你不需要动手写任何东西，而且 Claude 在纳入关键发现和文件方面可能比你更全面。你也可以通过传入指令来引导它，例如：&lt;code&gt;/compact focus on the auth refactor, drop the test debugging&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;images/cc-session-management/HF-oPtxaAAAUKMr.jpeg&quot; alt=&quot;Compact 示意&quot; /&gt;&lt;/p&gt;
&lt;p&gt;使用 &lt;code&gt;/clear&lt;/code&gt; 时，你需要亲手写下重要内容：&quot;我们正在重构 auth middleware，约束是 X，相关文件是 A 和 B，我们已经排除了方案 Y&quot;——然后干净地重新开始。这更费事，但最终上下文中保留的内容完全由你决定。&lt;/p&gt;
&lt;h2&gt;什么会导致糟糕的 Compact？&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;images/cc-session-management/HF-oy22bEAE_Jd8.jpeg&quot; alt=&quot;糟糕的 Compact&quot; /&gt;&lt;/p&gt;
&lt;p&gt;如果你经常跑长会话，可能已经遇到过压缩效果特别差的情况。我们发现，糟糕的压缩往往发生在模型无法预判你接下来要做什么的时候。&lt;/p&gt;
&lt;p&gt;比如，自动压缩在一次漫长的调试会话之后触发，总结了这次排查过程。而你的下一条消息是：&quot;现在修一下我们在 bar.ts 里看到的另一个 warning。&quot;&lt;/p&gt;
&lt;p&gt;但由于这个会话之前聚焦在调试上，那个&quot;另一个 warning&quot;可能已经从总结中被丢掉了。&lt;/p&gt;
&lt;p&gt;这尤其棘手——受上下文腐化影响，模型在执行压缩时恰好处于它最不聪明的状态。好在有了 100 万上下文之后，你会有更充裕的时间主动使用 &lt;code&gt;/compact&lt;/code&gt;，并附带说明你接下来想做什么。&lt;/p&gt;
&lt;h2&gt;Subagents 与全新的上下文窗口&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;images/cc-session-management/HF-o6v1bQAA7pS6.jpeg&quot; alt=&quot;Subagents 示意&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Subagents 本质上也是一种上下文管理手段，适用于你提前知道某段工作会产生大量中间输出、而这些输出之后不再需要的场景。&lt;/p&gt;
&lt;p&gt;当 Claude 通过 Agent 工具派生出一个 subagent 时，这个 subagent 会获得自己的全新上下文窗口。它可以按需完成大量工作，然后综合结果，只将最终报告返回给父会话。&lt;/p&gt;
&lt;p&gt;我们的判断标准很简单：之后还需要这些工具输出本身，还是只需要结论？&lt;/p&gt;
&lt;p&gt;虽然 Claude Code 会自动调用 subagents，但你也可以主动要求它这样做。例如：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&quot;启动一个 subagent，根据下面这个 spec 文件验证这项工作的结果&quot;&lt;/li&gt;
&lt;li&gt;&quot;派生一个 subagent，阅读另一个代码库并总结它是如何实现 auth flow 的，然后你自己用同样方式实现&quot;&lt;/li&gt;
&lt;li&gt;&quot;派生一个 subagent，根据我的 git changes 为这个功能编写文档&quot;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;总之，每当 Claude 结束一轮回复、而你准备发送下一条消息时，你就站在了一个决策点上。&lt;/p&gt;
&lt;p&gt;未来，我们预期 Claude 能自主处理这些决策。但就目前而言，主动管理上下文仍然是你引导 Claude 产出更好结果的重要手段。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;images/cc-session-management/HF-qwt9bEAEa1eq.jpeg&quot; alt=&quot;总结&quot; /&gt;&lt;/p&gt;
</content:encoded><category>AI Agent</category><author>Shelven Zhou</author></item><item><title>Claude Code Context Engineering 拆解: Snip、MicroCompact 与 AutoCompact</title><link>https://e95e6364.shelven.pages.dev/posts/cc-context-engineering/</link><guid isPermaLink="true">https://e95e6364.shelven.pages.dev/posts/cc-context-engineering/</guid><description>深入 Claude Code 源码，拆解其三层递进式 context window 管理机制——Snip 拦截冗余输出、MicroCompact 外科手术式清理 tool results、AutoCompact LLM 驱动总结——以及它们如何配合 prompt cache 实现成本与能力的双重优化。</description><pubDate>Tue, 14 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;目前 Agent 的能力天花板往往不是模型本身，而是 context window 的管理质量。一个百万 token 的窗口看似宽裕，但在真实的 coding agent 场景下——动辄几十次 tool call、成百上千行的文件读取和 shell 输出——填满它只是时间问题。填满之后怎么办？粗暴截断会丢失关键上下文，导致 agent &quot;失忆&quot;；放任不管又会让模型淹没在噪音中，注意力被稀释，决策质量下降。&lt;/p&gt;
&lt;p&gt;Claude Code 对此做了三层递进式压缩：&lt;strong&gt;Snip → MicroCompact → AutoCompact&lt;/strong&gt;。这套机制不仅在省 token，降低模型端的服务压力，更直接提升了 agent 的任务完成质量——更少的噪音意味着更精准的注意力分配。本文基于 2026 年 3 月 31 日泄露的 Claude Code 源码（文末可免费下载）和开源社区项目，拆解这三层机制的设计与实现。&lt;/p&gt;
&lt;h2&gt;全局视角：三层压缩的执行链路&lt;/h2&gt;
&lt;p&gt;在 Claude Code 的 query 主循环中，三层压缩按以下顺序执行：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 1. Snip: 在输出进入 context 之前拦截
const snipResult = snipModule.snipCompactIfNeeded(messagesForQuery)
messagesForQuery = snipResult.messages
snipTokensFreed = snipResult.tokensFreed

// 2. MicroCompact: 精准清理旧的 tool results
const microcompactResult = await microcompactMessages(messagesForQuery, ...)
messagesForQuery = microcompactResult.messages

// 3. AutoCompact: 超过阈值时，用 LLM 总结整个对话
const shouldCompact = await shouldAutoCompact(
  messages, model, querySource, snipTokensFreed
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;三者不是互斥的，而是逐级递进——Snip 做源头拦截，MicroCompact 做存量清理，AutoCompact 是最后防线。整体设计哲学是：&lt;strong&gt;能不调 LLM 就不调 LLM&lt;/strong&gt;。Snip 和 MicroCompact 都是纯本地操作，零额外成本；只有当前两层都不够用时，才启动代价最高的 LLM 总结。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;images/cc-context-engineering/three-layers.svg&quot; alt=&quot;三层压缩执行链路&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;Layer 1: Snip — 在输出进入 context 之前就拦截&lt;/h2&gt;
&lt;p&gt;Claude Code 内部有一个 &lt;code&gt;snipCompact&lt;/code&gt; 模块，由 &lt;code&gt;feature(&apos;HISTORY_SNIP&apos;)&lt;/code&gt; 控制，其代码并未包含在公开的源码中。但从调用方式可以看出它的定位：&lt;strong&gt;在 MicroCompact 之前执行，返回 &lt;code&gt;tokensFreed&lt;/code&gt; 供后续阈值判断使用。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;由于原始代码不可用，这里参考 &lt;a href=&quot;https://github.com/edouard-claude/snip&quot;&gt;edouard-claude/snip&lt;/a&gt; 的实现来分析 snip 的设计思路。该项目是社区基于 Claude Code hook 机制构建的独立 shell 输出过滤工具，Claude Code 内部的 snip 逻辑在策略和细节上可能有所不同。&lt;/p&gt;
&lt;h3&gt;代理与拦截模式&lt;/h3&gt;
&lt;p&gt;Snip 的核心思想是在 AI Agent 和操作系统 Shell 之间建立一个拦截层：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Claude Code → [PreToolUse Hook] → snip → Shell → [过滤输出] → Claude Code
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通过 Claude Code 的 &lt;code&gt;PreToolUse&lt;/code&gt; 钩子，当 Claude 准备执行 &lt;code&gt;bash&lt;/code&gt; 工具时，hook 脚本会将命令改写为 &lt;code&gt;snip -- &amp;lt;original_command&amp;gt;&lt;/code&gt;，让 snip 代理执行并过滤输出。&lt;/p&gt;
&lt;h3&gt;命令变换与参数注入&lt;/h3&gt;
&lt;p&gt;Snip 不只是被动过滤，还会主动改造命令以获得更好的结构化输出。例如，对于 &lt;code&gt;go test&lt;/code&gt;，snip 会自动注入 &lt;code&gt;-json&lt;/code&gt; 参数，强制产生 JSON 格式的输出，便于后续精确过滤：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;- action: &quot;aggregate&quot;
  patterns:
    passed: &apos;&quot;Action&quot;:&quot;pass&quot;&apos;
    failed: &apos;&quot;Action&quot;:&quot;fail&quot;&apos;
  format: &quot;{{.passed}} passed, {{.failed}} failed&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;原本几百行的测试日志，经过 aggregate action 聚合后，变成一行 &lt;code&gt;12 passed, 0 failed&lt;/code&gt;。&lt;/p&gt;
&lt;h3&gt;各类命令的压缩效果&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;命令&lt;/th&gt;
&lt;th&gt;过滤策略&lt;/th&gt;
&lt;th&gt;压缩率&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;git status&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;分类统计文件状态，仅显示摘要&lt;/td&gt;
&lt;td&gt;~85%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;git log&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;提交信息重写为单行摘要&lt;/td&gt;
&lt;td&gt;~85%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;go test&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;注入 JSON 参数，聚合 Pass/Fail&lt;/td&gt;
&lt;td&gt;~97%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;cargo test&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;捕获进度条，仅保留失败堆栈&lt;/td&gt;
&lt;td&gt;~99%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;git diff&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;仅保留统计信息，截断超长 diff&lt;/td&gt;
&lt;td&gt;~80%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;优雅降级&lt;/h3&gt;
&lt;p&gt;Snip 的一个重要设计原则是&lt;strong&gt;绝不破坏主链路&lt;/strong&gt;。如果过滤器内部出错、找不到匹配的过滤器、或环境配置不全，它会自动退化为 passthrough 模式，原样返回输出。这确保了它作为黑盒代理的安全性。&lt;/p&gt;
&lt;h2&gt;Layer 2: MicroCompact — 外科手术式清理 tool results&lt;/h2&gt;
&lt;p&gt;如果说 Snip 是在源头减少输入，MicroCompact 则是对已经进入 context 的历史消息做精准清理。它只针对特定工具的结果进行处理：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const COMPACTABLE_TOOLS = new Set([
  FILE_READ_TOOL_NAME,    // 文件读取
  ...SHELL_TOOL_NAMES,    // Shell 命令
  GREP_TOOL_NAME,         // 搜索
  GLOB_TOOL_NAME,         // 文件匹配
  WEB_SEARCH_TOOL_NAME,   // 网页搜索
  WEB_FETCH_TOOL_NAME,    // 网页获取
  FILE_EDIT_TOOL_NAME,    // 文件编辑
  FILE_WRITE_TOOL_NAME,   // 文件写入
])
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;关键设计：&lt;strong&gt;清理时保留 tool_use 的 ID 和结构，只移除 content&lt;/strong&gt;。这确保模型知道「曾经执行过这个操作」，但不再为其内容占据 token 空间。&lt;/p&gt;
&lt;h3&gt;双轨机制：Cold Cache vs Warm Cache&lt;/h3&gt;
&lt;p&gt;要理解 MicroCompact 为什么要分两条路径，需要先了解 Prompt Caching 的工作方式。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Prompt Caching 快速回顾&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;当你向 Claude API 发送请求时，模型需要对 prompt 中的每个 token 计算 KV（Key-Value）对。Prompt Caching 的核心思想是：&lt;strong&gt;如果两次请求的 prompt 共享相同的前缀，那么这些前缀 token 的 KV 对可以被缓存和复用，无需重新计算。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;对于 Claude Code 这种多轮对话场景，每一轮新请求都会携带完整的对话历史作为前缀，只要前缀不变，后续请求就能享受 cache read 的低成本——&lt;strong&gt;cache read 的价格仅为 cache write（首次写入）的 1/10&lt;/strong&gt;。但反过来，&lt;strong&gt;任何对历史消息的修改都会破坏前缀匹配，导致修改位置之后的所有缓存失效。&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;images/cc-context-engineering/prompt-cache.svg&quot; alt=&quot;Prompt Caching 原理示意&quot; /&gt;&lt;/p&gt;
&lt;p&gt;理解了这个约束，MicroCompact 的双轨设计就顺理成章了——本质上就是在回答一个问题：&lt;strong&gt;当前的 prompt cache 是热的还是冷的？&lt;/strong&gt;&lt;/p&gt;
&lt;h4&gt;Time-based 路径（Cold Cache）&lt;/h4&gt;
&lt;p&gt;当对话停顿超过一定时间（Claude Code 默认阈值为 60 分钟），系统判定 prompt cache 已失效（注：Anthropic API 的标准 cache TTL 为 5 分钟，这里的 60 分钟是 Claude Code 自定义的触发阈值，采用更保守的策略来决定何时执行 time-based 清理）。既然下次请求注定要完整重发所有 token，不如趁机&quot;瘦身&quot;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function maybeTimeBasedMicrocompact(messages, querySource) {
  const trigger = evaluateTimeBasedTrigger(messages, querySource)
  if (!trigger) return null

  // 保留最近 N 个 tool results，清理其余
  const keepSet = new Set(compactableIds.slice(-keepRecent))
  const clearSet = new Set(compactableIds.filter(id =&amp;gt; !keepSet.has(id)))

  // 直接替换内容为占位符
  return messages.map(message =&amp;gt; {
    // ...
    if (clearSet.has(block.tool_use_id)) {
      return { ...block, content: &apos;[Old tool result content cleared]&apos; }
    }
    // ...
  })
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;逻辑直白：cache 反正冷了，直接改消息内容，物理减少发送的 token 量。&lt;/p&gt;
&lt;h4&gt;Cached MC 路径（Warm Cache）&lt;/h4&gt;
&lt;p&gt;活跃对话中，cache 仍然有效。如果此时直接修改历史消息，会破坏前缀匹配，导致之前所有 cache 失效——代价太大。&lt;/p&gt;
&lt;p&gt;Cached MC 的解法是&lt;strong&gt;将「缓存存储」与「模型可见性」解耦&lt;/strong&gt;：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;发送完整的历史消息到 API（保持 cache 命中）&lt;/li&gt;
&lt;li&gt;附带 &lt;code&gt;cache_edits&lt;/code&gt; 指令，告诉模型在推理时忽略特定 tool result 的内容&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;async function cachedMicrocompactPath(messages, querySource) {
  // 注册和追踪 tool results
  const toolsToDelete = mod.getToolResultsToDelete(state)

  if (toolsToDelete.length &amp;gt; 0) {
    // 创建 cache_edits 指令（不修改本地消息！）
    const cacheEdits = mod.createCacheEditsBlock(state, toolsToDelete)
    pendingCacheEdits = cacheEdits

    // 消息原样返回，cache_edits 在 API 层注入
    return { messages, compactionInfo: { pendingCacheEdits: { ... } } }
  }
  return { messages }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这实现了一个看似矛盾的目标：&lt;strong&gt;在不破坏已有 cache 的前提下，减少模型实际处理的 token 数量。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;cache_edits&lt;/code&gt; 支持两类操作：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;clear_tool_uses&lt;/code&gt;：屏蔽特定 tool call 的输入或输出&lt;/li&gt;
&lt;li&gt;&lt;code&gt;clear_thinking&lt;/code&gt;：清除旧的思维链（除最近 1-2 次外，早期推理过程通常已无必要）&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;cache_edits：Agent 与 Model 的协同优化&lt;/h3&gt;
&lt;p&gt;值得注意的是，&lt;code&gt;cache_edits&lt;/code&gt; 不是纯客户端的技巧——它要求模型推理引擎在底层配合，能够在保持 KV cache 完整的前提下，根据客户端指令在推理时跳过特定内容。这是一个 &lt;strong&gt;agent 端和 model 端协同优化&lt;/strong&gt;的典型案例，也是 Anthropic 作为同时掌控模型和 agent 产品的厂商的核心竞争力之一。目前 &lt;code&gt;cache_edits&lt;/code&gt; 是 Claude Code 内部使用的能力，尚未作为公开 API 提供。&lt;/p&gt;
&lt;p&gt;横向对比来看，各家在缓存管理上的深度差异很大：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;提供商&lt;/th&gt;
&lt;th&gt;缓存触发方式&lt;/th&gt;
&lt;th&gt;是否支持缓存编辑&lt;/th&gt;
&lt;th&gt;缓存失效机制&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Anthropic&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;显式标记 (&lt;code&gt;cache_control&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;是&lt;/strong&gt; (&lt;code&gt;cache_edits&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;按 TTL 或手动覆盖&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Google&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;显式创建 (&lt;code&gt;CachedContent&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;仅管理（TTL/删除）&lt;/td&gt;
&lt;td&gt;固定 TTL（默认 1h）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;DeepSeek&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;自动触发&lt;/td&gt;
&lt;td&gt;否（仅前缀匹配）&lt;/td&gt;
&lt;td&gt;动态过期（硬盘存储）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;OpenAI&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;自动触发&lt;/td&gt;
&lt;td&gt;否&lt;/td&gt;
&lt;td&gt;精确匹配失效&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;大多数提供商的 prompt cache 是&quot;只读&quot;的——你只能通过保持前缀不变来利用它，一旦中间有修改就全部失效。Anthropic 的 &lt;code&gt;cache_edits&lt;/code&gt; 打破了这个限制，允许在不破坏缓存的前提下对 context 做&quot;外科手术式&quot;编辑。这使得 Claude Code 能在活跃对话中实现 MicroCompact，而其他提供商上的 agent 只能等 cache 过期后才能清理。&lt;/p&gt;
&lt;h3&gt;Token 估算的保守策略&lt;/h3&gt;
&lt;p&gt;由于无法在发送前获得精确的 API token 计数，MicroCompact 对本地估算的 token 数做了 4/3 的加权处理：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;export function estimateMessageTokens(messages: Message[]): number {
  // ... 遍历所有 block 累加 token
  // 4/3 加权，确保在接近上限时提前触发压缩
  return Math.ceil(totalTokens * (4 / 3))
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;宁可多压缩一点，也不能让请求因溢出而失败。&lt;/p&gt;
&lt;h2&gt;Layer 3: AutoCompact — 最后防线&lt;/h2&gt;
&lt;p&gt;当 Snip 和 MicroCompact 都不够用时，AutoCompact 作为最后一道防线介入。它的触发条件是 token 使用量超过 &lt;code&gt;effectiveContextWindow - 13K buffer&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;export const AUTOCOMPACT_BUFFER_TOKENS = 13_000

export function getAutoCompactThreshold(model: string): number {
  const effectiveContextWindow = getEffectiveContextWindowSize(model)
  return effectiveContextWindow - AUTOCOMPACT_BUFFER_TOKENS
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;优先级：Session Memory &amp;gt; LLM Summarization&lt;/h3&gt;
&lt;p&gt;AutoCompact 并不直接调用 LLM 总结。它先尝试一个更轻量的方案：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;export async function autoCompactIfNeeded(messages, ...) {
  // 优先尝试 Session Memory Compaction
  const sessionMemoryResult = await trySessionMemoryCompaction(
    messages, agentId, autoCompactThreshold
  )
  if (sessionMemoryResult) {
    return { wasCompacted: true, compactionResult: sessionMemoryResult }
  }

  // fallback: 传统 LLM 总结
  const compactionResult = await compactConversation(messages, ...)
  return { wasCompacted: true, compactionResult }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Session Memory Compaction&lt;/strong&gt; 利用 Claude Code 在对话过程中持续维护的 session memory（一个结构化的会话记忆文件）。当需要压缩时，直接用这个已有的记忆作为摘要，保留最近 10K-40K tokens 的原始消息，无需额外 LLM 调用。&lt;/p&gt;
&lt;p&gt;只有当 session memory 不可用（未启用、内容为空、或压缩后仍超阈值）时，才 fallback 到传统的 LLM 总结。&lt;/p&gt;
&lt;h3&gt;传统 Compaction：fork agent 做总结&lt;/h3&gt;
&lt;p&gt;传统路径会 fork 一个 agent，用专门设计的 prompt 总结对话。这个 prompt 要求生成 9 个结构化 section：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;1. Primary Request and Intent
2. Key Technical Concepts
3. Files and Code Sections（含代码片段）
4. Errors and fixes
5. Problem Solving
6. All user messages（完整保留用户原话）
7. Pending Tasks
8. Current Work
9. Optional Next Step（含原文引用，防止任务漂移）
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中两个设计值得注意：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;analysis + summary 两阶段&lt;/strong&gt;：prompt 要求先在 &lt;code&gt;&amp;lt;analysis&amp;gt;&lt;/code&gt; 标签中整理思路，再在 &lt;code&gt;&amp;lt;summary&amp;gt;&lt;/code&gt; 中给出最终总结。&lt;code&gt;&amp;lt;analysis&amp;gt;&lt;/code&gt; 部分在使用时会被 &lt;code&gt;formatCompactSummary&lt;/code&gt; 函数剥离——它只是用来提升总结质量的 &quot;草稿纸&quot;，不会进入后续 context&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Partial Compact&lt;/strong&gt;：支持只总结旧消息、保留近期消息原文。这比全量总结保留了更多细节&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;安全机制&lt;/h3&gt;
&lt;p&gt;AutoCompact 有两个关键的安全设计：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Circuit Breaker&lt;/strong&gt;：连续 3 次压缩失败后，停止重试。这防止了 context 不可恢复地超限时，无意义的 API 调用风暴：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const MAX_CONSECUTIVE_AUTOCOMPACT_FAILURES = 3
// BQ 2026-03-10: 1,279 sessions had 50+ consecutive failures (up to 3,272)
// in a single session, wasting ~250K API calls/day globally.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;PTL Retry&lt;/strong&gt;：当 compact 请求本身因为 prompt 过长而失败时，逐步从最旧的消息组开始截断，直到请求能通过：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;export function truncateHeadForPTLRetry(messages, ptlResponse) {
  const groups = groupMessagesByApiRound(messages)
  // 根据 token gap 计算需要丢弃的组数，或 fallback 到 20%
  const dropCount = tokenGap !== undefined
    ? /* 精确计算 */ ...
    : Math.max(1, Math.floor(groups.length * 0.2))
  return groups.slice(dropCount).flat()
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;对于 agent 开发者来说，这篇文章的核心 takeaway 不只是&quot;怎么压缩 context&quot;，而是 &lt;strong&gt;context 管理本身就是 agent 能力的一部分&lt;/strong&gt;。具体来说：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;分层设计优于单一策略&lt;/strong&gt;——零成本的本地操作（Snip、MicroCompact）覆盖绝大多数场景，LLM 总结只是最后防线。不是每次都需要动用最重的工具&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cache-aware 是关键约束&lt;/strong&gt;——在有 prompt cache 的系统中，&quot;压缩 context&quot;和&quot;保持 cache 命中&quot;是一对张力。MicroCompact 的双轨设计展示了如何在这个约束下做工程权衡&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;精心管理的 context = 更好的任务完成质量&lt;/strong&gt;——更少的噪音意味着更精准的注意力分配，这直接影响 agent 的决策质量，而不仅仅是省钱&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;本文所引用的 Claude Code 源码包含在下方附件中，欢迎自行探索更多细节。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;附件&lt;/strong&gt;：&lt;a href=&quot;/downloads/claude-code-main.zip&quot;&gt;claude-code-main.zip&lt;/a&gt; — 2026 年 3 月 31 日泄露的 Claude Code 源码（用于本文分析）&lt;/p&gt;
</content:encoded><category>AI Agent</category><author>Shelven Zhou</author></item></channel></rss>