返回首页

Runtime Anatomy / Claude Code source notes

Agent 不是聊天机器人,而是一台会反复接线的运行时机器

最近我系统读了一遍 Claude Code 这类 Agent 工具的源码。读完以后,我的注意力从“模型会不会回答”转到了另一件事:模型如何把下一步意图交给运行时。触发工具、收集结果、整理上下文,然后继续推理。这篇文章重点拆三件事:工具调用、上下文管理、循环推理。

1. Model

模型先给出下一步意图

它可能输出文本,也可能输出一个结构化的 tool_use block。运行时看到 tool_use,会把它解释成下一步动作。

2. Tool

工具层接管真实世界

工具定义负责 schema 校验、权限判断、并发策略、错误包装,以及把执行结果转换回 tool_result。

3. Context

上下文被重新装配

新的 assistant 消息、工具结果、附件、记忆、压缩边界一起进入下一轮消息队列。

4. Loop

继续推理直到没有工具需求

只要本轮出现工具调用,运行时就继续循环。没有 tool_use、没有阻塞 hook、没有恢复重试,回合才结束。

01 / Loop

一次 Agent 回合的生命线

普通聊天的心智模型是:用户发一句,模型答一句。Agent 的心智模型完全不同:用户发出目标后,运行时会进入一个循环。每次循环都把当前消息、系统提示、工具定义和上下文状态发给模型;如果模型返回工具调用,就暂停回答,先执行工具;拿到结果后再把结果作为新的用户侧消息放回上下文,继续请求模型。

query loop sketchsrc/query.ts
let state = {
  messages: initialMessages,
  toolUseContext: initialToolUseContext,
  turnCount: 1,
}

while (true) {
  const { messages, toolUseContext, turnCount } = state

  messagesForQuery = compactAndPrepare(messages)
  assistantMessages = streamModel(messagesForQuery)

  if (!assistantMessages.hasToolUse) {
    return completed
  }

  toolResults = runTools(assistantMessages.toolUseBlocks)
  attachments = collectMemoryAndNotifications()

  const nextTurnCount = turnCount + 1

  if (maxTurns && nextTurnCount > maxTurns) {
    return maxTurnsReached
  }

  const next = {
    messages: [
      ...messagesForQuery,
      ...assistantMessages,
      ...toolResults,
      ...attachments,
    ],
    toolUseContext: toolUseContext,
    turnCount: nextTurnCount,
    transition: 'next_turn',
  }
  state = next
}

这里最关键的是 state = next。Agent 的连续性来自运行时的回填纪律。每一轮观察结果都会被整理进下一轮输入;模型每次都像站在新的现场,只是这个现场由运行时提前布置好。

02 / Nested query

子 Agent 其实是在工具里再跑一遍 query

理解了主循环以后,子 Agent 就容易多了。AgentTool 并没有启动一套完全不同的运行时;同步子 Agent 更像一个特殊工具:主 Agent 调用 AgentTool 时,主循环会停在这次 tool_use 上,进入 runAgent,然后 runAgent 再调用同一个 query()。内层 query 自己完成多轮模型请求、工具执行和上下文回填,结束后把结果包装成 tool_result 还给外层 query。

这就是“嵌套 query”的意思:它发生在当前函数调用栈里,里面再套一层完整的 Agent loop;主 Agent 并没有打开新聊天窗口,也没有另起 CLI 进程。外层 query 负责主任务,内层 query 负责子任务。两层用同一套引擎,但参数不同:子 Agent 有自己的 messages、system prompt、tool allowlist、agentId 和 queryTracking depth。

这个设计很克制。子 Agent 仍然遵守同一套工具协议、权限协议和上下文治理,并没有变成一套神秘的新抽象。区别只是它被当成一个 tool 调起来,跑完以后再把一段更高层的工作结果交回主 Agent。

1主 query

用户目标进入外层循环,模型决定下一步要不要调用工具。

2AgentTool

模型选择派子 Agent,外层 query 暂停在这次 tool_use 上。

3runAgent

构造子 Agent 的 system prompt、messages、toolUseContext 和工具集合。

4子 query

同一个 query() 再跑一遍,子 Agent 自己读文件、搜索、执行工具。

5tool_result

子 Agent 完成后,结果作为 AgentTool 的 tool_result 回到主 query。

普通 Bash tool 通常执行一次就返回;AgentTool 会启动一整段新的 Agent 循环。

子 Agent 可以继续思考、再调工具、再观察,直到自己的任务结束或到达 maxTurns。

隔离点在参数和上下文:子 Agent 不默认继承主对话全文,工具也可以被 allowlist 限制。

queryTracking.depth 让运行时知道当前是第几层调用,便于追踪嵌套链路。

03 / Tool use

工具调用是一段受控执行协议

从模型视角看,工具像一个 JSON 函数。但从运行时视角看,工具是一段受控执行协议:它要先被发现,再被校验,再经过权限和 hook,最后才会真正碰文件、shell、MCP 或网络资源。

1

发现工具

2

解析输入

3

校验 schema

4

权限判断

5

执行/并发控制

6

包装 tool_result

7

回填模型上下文

04 / Permission

权限校验是 tool_use 和真实执行之间的闸门

模型产出 tool_use 只代表“我想做这件事”,不代表运行时会立刻照做。真正执行前,runtime 会先把模型给的参数解析成结构化输入,再过工具自己的 schema 校验。连输入形状都不对的调用,不会进入权限阶段。

输入合法之后,权限才开始分层生效。工具本身可以声明只读、是否支持并发、是否需要额外确认;权限规则、自动分类器、hook 和 UI 确认会继续判断这次调用能不能落地。比如读文件和改文件不是同一种风险,跑一个 harmless grep 和执行会改状态的 shell 命令也不该走同一条路。

这里最重要的设计感是:权限不是一个单点判断,而是一条执行前流水线。任何一层都可以放行、改写输入、要求确认,或者阻塞调用。被阻塞的结果也不是简单丢掉,而是可以作为反馈回到模型上下文,让模型换一种更安全的做法继续推进。

Schema

先确认模型给的 JSON 输入能被工具 schema 接住,避免“看起来像调用”的脏数据进入执行层。

Tool policy

工具声明只读、并发安全、权限检查和结果格式;默认策略偏保守,敏感工具要显式收紧。

Rules / classifier

权限规则和自动分类器会把自然语言意图转成风险判断,尤其是文件修改、shell、网络和 MCP 调用。

Hooks / UI

PreToolUse hook 可以阻塞或补充反馈;需要人工确认时,UI 会让用户看到具体是谁想做什么。

Feedback

阻塞、错误、拒绝不会凭空消失,它们会被包装成 tool_result 或上下文反馈,供下一轮推理使用。

05 / Context

上下文是一张会被不断整理的工作台

一个长任务里,最容易吃掉上下文窗口的通常是工具结果、文件内容、附件、记忆和中间推理轨迹。从 Claude Code 的源码看,上下文治理是一整套分层机制:工具结果预算、micro-compact、auto-compact、reactive compact、session memory。它们当然能省 token,但更重要的目标是保住下一步推理所需的现场。

Context window as a workbench

概念示意,不是实测 token 分布
System Prompt14%
User / Assistant Turns34%
Tool Results24%
Attachments / Memory16%
Safety Buffer12%

Compact 不是把聊天记录压短这么简单

自动压缩的触发逻辑很像内存管理:先计算当前 token 使用量,再和模型有效窗口、预留输出空间、buffer 做比较。超过阈值时,运行时会尝试把历史折叠成摘要;如果压缩连续失败,还会有熔断,避免每一轮都浪费一次必然失败的 API 调用。真正难的地方在于压缩后的上下文仍然要能指导下一步行动:保留目标、约束、已读文件、关键错误、重要 diff 和未完成事项,把低价值 stdout、重复日志和已经过期的中间推理清掉。

Result budget

工具结果不能无限塞回上下文。长输出要截断、摘要或落盘,只把下一步推理需要的部分放回来。

Micro compact

局部整理单个过大的工具结果或片段,优先处理噪声最多、价值最低的部分。

Auto compact

当整体上下文接近窗口阈值时,把历史折叠成更短的工作摘要,给后续推理腾空间。

Reactive compact

遇到 prompt-too-long 这类错误时再压缩重试,属于失败后的恢复路径。

Circuit breaker

连续压缩失败要停手,否则每一轮都会浪费一次注定失败的模型调用。

Preserved scene

压缩的目标不是漂亮摘要,而是保住下一步行动需要的现场:路径、结论、约束、错误和待办。

06 / Reasoning

循环推理的本质是观察驱动

Agent 每一轮都在做同一件事:基于当前观察,选择下一步动作。工具结果可能证明假设错误,也可能暴露新文件、新错误、新权限问题。运行时不需要提前知道完整计划,它只要保证每次观察都被正确回填,模型就能把任务拆成一串可验证的小步。

Plan

把目标变成下一步可执行动作

Act

调用 Bash、文件编辑、MCP、子 Agent 等工具

Observe

把 stdout、diff、错误、附件重新写回上下文

读完 Claude Code 源码后,我带走的架构判断

第一,Agent 的产品体验主要由运行时决定。模型会提出工具调用,但权限提示是否清晰、并发是否安全、错误是否能恢复、上下文是否被压缩得足够保真,都是工程系统的责任。

第二,工具返回值必须被当成 prompt 资产管理。输出太长要截断或落盘,重要摘要要保留,低价值噪声要被替换,否则长任务会被自己的观察结果淹没。

第三,子 Agent 最好理解成“工具里启动的另一个 query 循环”。这样看,隔离上下文、限制工具集合、设置 maxTurns、追踪 depth 都会变得很自然。

第四,权限校验要做成流水线,而不是一个布尔开关。schema、工具策略、规则、classifier、hook 和 UI 确认分别解决不同层级的风险。

第五,循环推理需要明确的终止条件。没有工具调用时可以结束;hook 阻塞时要重试;token 或 prompt-too-long 错误可以恢复,但恢复必须有次数限制。否则 Agent 会从“自主推进”变成“自主空转”。

架构拆解路径

我不想把这篇写成源码导览,所以没有逐个列文件路径。下面这条线索更接近我读完后的理解:把运行时拆成几个层次,再看它们怎样接住一次 Agent 调用。

主循环

把一次用户请求扩展成多轮模型请求、工具执行和状态回填。

先看这里,才能理解 Agent 为什么会从“一问一答”变成持续推进。

工具协议

定义工具的输入 schema、权限检查、只读/破坏性判断和结果格式。

它决定模型能碰什么、怎么碰、失败时如何反馈。

子 Agent

在 AgentTool 的执行过程中再次进入 query(),用独立参数完成一个子任务。

它解释了“派助手”为什么仍然可以复用同一套 runtime。

工具编排

把可并发的只读工具批量执行,把会改变状态的工具串行执行。

这层保证速度和安全边界不会互相牺牲。

权限流水线

在工具真正执行前经过 schema、工具策略、规则、classifier、hook 和 UI 确认。

模型说“我要做”只是意图,runtime 负责判断“能不能做、怎么做”。

单次工具调用

负责输入校验、权限流、hook、进度事件、错误包装和 tool_result 生成。

这是模型意图进入真实系统前的最后一道运行时闸门。

上下文治理

通过工具结果预算、摘要压缩、恢复重试和 session memory 控制上下文增长。

长任务能不能继续推进,主要取决于这一层是否保真。