关键词:Tool System / ToolUseContext / System Prompt / CLAUDE.md / Auto Compact / Context Engineering
Tool 不是函数,上下文不是消息:Claude Code 的执行内核怎么设计
如果第一篇解决的是“Agent 怎么启动起来”,那第二篇要解决的是另外两个更难的问题:
- 模型怎么真正拥有双手;
- 它在一次次行动之后,怎么不失忆。
Claude Code 在第三章和第四章里给出的答案很明确:工具必须被设计成协议,上下文必须被设计成结构。
一、Tool 在 Claude Code 里不是函数,而是一整个生命周期对象
很多 Agent 示例会这样做:
const tools = [
{ name: "read_file", schema: ..., handler: ... }
]
这在 Demo 阶段够用,但在真实系统里远远不够。
Claude Code 里的 Tool 接口,至少会关心这些事情:
call(args, context, canUseTool, parentMessage, onProgress): Promise<ToolResult>
checkPermissions(input, context): Promise<PermissionResult>
prompt(options): Promise<string>
description(input, options): Promise<string>
isConcurrencySafe(input): boolean
isReadOnly(input): boolean
renderToolUseMessage(...)
renderToolResultMessage(...)
这说明一个 Tool 不只是“我能做什么”,还包括:
- 我什么时候能做;
- 我要不要先过权限;
- 我能不能并发;
- 我是不是只读;
- 我该怎么向模型介绍自己;
- 我该怎么在终端里显示。
这和“函数列表”是两个层级的设计。
二、call() 的返回值,已经说明工具不是孤立动作
Claude Code 的 Tool 执行结果不是一段字符串,而是结构化结果:
export type ToolResult<T> = {
data: T
newMessages?: Message[]
contextModifier?: (context: ToolUseContext) => ToolUseContext
}
这里最关键的是 contextModifier。
它意味着工具执行完成后,不只是“交一份结果回来”,还可能修改后续工具看到的上下文。
这说明 Tool 不是 side effect 黑盒,而是 Agent 运行时的一部分。
三、为什么 prompt() 和 description() 必须拆开
Claude Code 把工具的模型可见文本拆成两层:
prompt():完整工具手册,进入系统提示;description():简短 API 描述,进入 tool schema。
这是一种非常实用的分层。
因为“教模型什么时候用工具”和“告诉协议层这个工具叫什么”根本不是一件事。
如果这两层混在一起,到后面很难维护,尤其是工具一多的时候。
四、isReadOnly() 和 isConcurrencySafe() 这两个接口,决定了系统能不能稳
Claude Code 的工具安全性不是按工具名字判断,而是按“工具 + 参数”判断。
比如 BashTool:
cat file.txt大概率是只读;rm -rf /tmp/x就已经是写操作;grep pattern src/可以并发;- 修改文件、执行副作用命令就应该串行。
所以它把这两个判断做成函数:
isConcurrencySafe(input): boolean
isReadOnly(input): boolean
这一步看起来只是接口设计,实际上会直接决定后面的:
- 权限系统怎么判;
- StreamingToolExecutor 怎么调度;
- plan 模式里哪些工具还能继续用。
五、ToolUseContext 不是“方便传参”,而是 Agent 的运行时世界观
Claude Code 的每个工具都会收到一个 ToolUseContext。里面装的不是一两个辅助字段,而是几乎整个运行现场:
- 当前消息历史;
- AppState 的 getter / setter;
- AbortController;
- 可用工具集合;
- MCP 客户端;
- 调试模式;
- 预算限制;
- 子 Agent 定义;
- 模型配置。
这意味着 Tool 的地位不是“被调用的工具函数”,而是“运行在 Agent 世界里的执行节点”。
也正因为有这个上下文背包,工具层才能做这些事情:
- 中途中断;
- 上报执行进度;
- 调整 UI 状态;
- 读取当前权限模式;
- 和 MCP、子 Agent、内容替换系统协作。
六、System Prompt 在 Claude Code 里不是一段长字符串,而是三层结构
Claude Code 构建系统提示时,不是把所有东西直接拼接,而是先并行拉三部分:
const [defaultSystemPrompt, userContext, systemContext] = await Promise.all([
getSystemPrompt(...),
getUserContext(),
getSystemContext(),
])
三层分别是:
defaultSystemPrompt:角色、工具说明、运行规则;userContext:用户和项目的长期约定;systemContext:当前环境快照,比如 Git 状态。
这一步非常关键,因为它把“Prompt 工程”从写文案,变成了搭结构。
七、CLAUDE.md 的价值,不在配置,而在持久记忆
Claude Code 有个非常重要的设计:允许通过 CLAUDE.md 给 Agent 提供长期指令。
const claudeMd = shouldDisableClaudeMd
? null
: getClaudeMds(filterInjectedMemoryFiles(await getMemoryFiles()))
它会沿着目录树向上搜:
- 项目级
CLAUDE.md - 父目录级
CLAUDE.md - 用户级
~/.claude/CLAUDE.md - 企业级策略文件
graph TD
P["项目级 CLAUDE.md"] --> M["合并 userContext"]
F["父目录 CLAUDE.md"] --> M
U["用户级 CLAUDE.md"] --> M
E["企业级策略"] --> M
这件事非常像“给 Agent 加长期记忆层”。
因为很多约定不应该每轮都靠用户重新解释,比如:
- 这个仓库用什么包管理器;
- 哪些目录不能改;
- 提交信息遵循什么规范;
- 哪类测试默认不要跑。
把这些东西结构化放进 CLAUDE.md,等于把“用户习惯”和“项目规矩”从临时对话里抽出来了。
八、Git 状态注入,不是实时同步,而是“会话开始时的快照”
Claude Code 在系统上下文里会注入 Git 信息,但会明确标注:
This is the git status at the start of the conversation.
Note that this status is a snapshot in time, and will not update during the conversation.
这个提示非常专业。它避免模型误以为:
- 这份 Git 状态会自动更新;
- 它可以一直依赖同一份状态做判断。
这本质上是在给模型划信息边界:
你现在拿到的是起始快照,不是实时世界。
九、对话历史不能当字符串堆,它必须保持 tool_use / tool_result 成对完整
Claude Code 的消息历史里,一个工具调用会展开成这种结构:
assistant: [tool_use: { id: "abc", name: "Bash", input: {...} }]
user: [tool_result: { tool_use_id: "abc", content: "..." }]
这里最关键的不是格式,而是约束:
每个
tool_use都必须有匹配的tool_result。
这意味着做历史裁剪和上下文压缩时,不能把这对消息切断。
不然到 API 那一层,整段对话结构就会非法。
很多自制 Agent 在这一步出问题,就是因为只把消息当文本,而没把它当结构化事件流。
十、真正难的不是“塞更多上下文”,而是“在有限窗口里保留正确的东西”
Claude Code 的上下文管理至少有两道防线。
第一层:工具结果预算
工具返回太大时,不允许原样塞回模型上下文,而是经过 applyToolResultBudget() 做截断、替换或文件引用化。
第二层:自动压缩
一旦消息逼近窗口上限,就在每轮循环入口主动触发 autocompact:
const { compactionResult } = await deps.autocompact(
messagesForQuery,
toolUseContext,
{ systemPrompt, userContext, systemContext, ... },
querySource,
tracking,
snipTokensFreed,
)
flowchart TD
A["新一轮 queryLoop"] --> B["取 compact boundary 后的消息"]
B --> C["applyToolResultBudget"]
C --> D{"接近 token 上限?"}
D --> |"是"| E["autocompact 压缩"]
D --> |"否"| F["继续调用模型"]
E --> F
关键不是“会压缩”,而是它在入口就压,不是等模型报错以后再补救。
这体现的是主动预防,而不是被动恢复。
十一、第三章和第四章真正讲的,不是 Tool 和 Prompt,而是执行内核
把 Claude Code 的第三、四章压成一句话,其实就是:
Tool 必须结构化,上下文也必须结构化。
因为只有这样,Agent 才能同时满足:
- 工具可调度;
- 权限可判定;
- UI 可渲染;
- 上下文可压缩;
- 长对话不失真。
最后
很多人做 Agent,最大的误判是把复杂度都归因到模型上。
但 Claude Code 在这一段真正告诉我们的,是另一件事:
- Tool 不是函数;
- Prompt 不是文案;
- 上下文不是聊天记录;
- 历史也不是字符串数组。
它们全都是执行内核的一部分。
只有按这个标准来设计,Agent 才可能从“能跑”走向“能长期工作”。