Agent 循环由”上下文装配→模型决策→工具执行→结果回填→终止判断”五个阶段串成,Claude Code 把它跑成生产级 Agent 框架的骨架;每轮循环都会重新装配上下文、判断是否调用工具,再决定继续推进或收尾。理解这五个阶段各自的职责边界,是调优 prompt、设权限、设计并行工具的前提。
一、循环在做什么
一次 query() 启动后,循环在 while not stopped 框架里反复跑。官方的”五步视角”把循环拆成:接收 prompt、评估响应、执行工具、重复、返回结果。底层伪代码则把每轮进一步细化为 8 步:发送开始事件、控制工具输出体积、压缩历史、追加 system context、调 API、解析流、运行工具、把结果塞回历史。
不论粒度如何拆,循环的”主干语义”始终是装配→决策→执行→回填→终止。下面按阶段说明每个环节在做什么、什么时候调用工具。
把这五阶段用一段伪代码写直白点:
# one turn of the agent loop
while not stopped:
# (a) assemble -- what the model sees
context = assemble(
system_prompt, # instructions header
tool_schemas, # callable tool signatures
history, # prior turn messages
hook_additions, # pushed in by hooks
)
# (b) model -- pick the next action
action = model(context, tools)
if action.is_text_only():
stopped = run_stop_hooks(action) # may veto
continue
# (c) execute -- gate and run the tool call
if not permitted(action): # permission layer
continue
action = run_pre_tool_hooks(action) # block / write
result = execute(action) # tool runs here
result = run_post_tool_hooks(result) # mutate / annotate
history.append(action, result)
这段代码的价值是把”权限闸门”和”hooks”在循环里出现的位置标出来——前者挤在执行阶段的最前面,后者分别在工具调用前后插入。
二、五个阶段的职责边界
2.1 上下文装配(assemble)
每轮开始时把模型能看到的所有信息拼成一个 prompt:系统提示、工具签名、历史消息、Hook 注入的额外段、CLAUDE.md 记忆文件、Skills 列表、MCP 资源。系统提示由 buildEffectiveSystemPrompt() 拼装,环境信息用 computeSimpleEnvInfo() 计算并缓存。整段 prompt 体量常常破万 token,因此后续要做压缩。
这一步不调用工具,职责只是”把模型该看的东西准备好”。任何想影响模型决策的代码——Hook 注入、Skill 启用、MCP 工具注册——都发生在这一阶段。
2.2 模型决策(model)
把装配好的 context 喂给 Claude,让它选择下一步动作。结果只有两类:
- 仅文本:模型直接回答,循环进入”是否停止”判断;
- 含
tool_use:模型决定调用工具,循环进入执行阶段。
文本块直接进 UI 流式显示;思考块被内部消费;工具调用块则被提取出来交给下一阶段。模型决策也不真正执行工具——它只是表达意图,权限闸门和实际执行都在后面。
2.3 工具执行(execute)
拿到 tool_use 后先过四道闸:
- 黑名单(reject):如
rm -rf等敏感命令立即拒绝; - 白名单(allow):如
Read、Glob走快通道免审; - 分类器:异步启发式判定读/写;
- 交互式确认:分类器无法判定时向用户问询。
通过后由 execute(action) 真正运行工具,再依次跑 pre_tool_hooks 和 post_tool_hooks 做拦截、修改或追加上下文。结果会包成 tool_result 块,塞回 user 消息。批量执行时,partitionToolCalls() 会把只读工具并发(默认 10 并发)、写工具串行,避免竞态。
2.4 结果回填(feedback)
工具结果回填到 history 后,循环回到装配阶段进入下一轮。模型在下一次响应里能看见全部历史,包括之前的工具输出、错误信息、Hook 改动。压缩策略在这一阶段触发:剩余上下文窗口低于阈值时,启动 micro-compression(裁剪冗长解释)、对话记忆压缩(早期对话换成摘要)、失败后的反应式压缩(连续三次失败后停止压缩尝试)。
回填的”颗粒”很重要——既要保留工具的原始输出供模型回看,也要避免一次性塞满上下文;这段平衡是 Claude Code 相对裸 Messages API 的一大价值。
2.5 终止判断(stop)
每轮末尾判断”是否继续”:
- 模型仅文本输出:通过
stop_hooks跑停止钩子,没被否决就退出; - 达到
max_turns或max_budget_usd:硬性停机; - 工具拒绝、异常抛错:按错误类型决定是否熔断。
终止后 SDK 发出 final AssistantMessage(仅文本)+ ResultMessage(含 token 用量、成本、session id)。从这里开始,UI 可以做”任务完成”展示,调用方可以做计费统计。
三、循环的内部小步:8 步伪代码
把单轮放大看,代码侧真实的步骤是这八步,按顺序串成:
- emit
stream_request_start事件; - 裁剪本轮工具输出体积,避免单条结果撑爆窗口;
- 必要时压缩上一条响应;
- 追加 system context(环境、预算、策略等);
- 调 Claude API 并流式接收;
- 解析流,分出 text / thinking / tool_use 块;
- 对 tool_use 跑权限闸门 + hooks + 实际执行;
- 把工具结果回填到消息列表,准备进入下一轮。
这八步与上面五阶段是”粗粒度 vs 细粒度”的关系:装配=1+3+4,决策=5+6,执行=7,回填=8,终止判断横跨 6 和 7 的尾段。理解粗粒度便于优化策略,理解细粒度便于定位问题。
四、阶段与工具调用时机的对应
工具被调用的时刻永远在”模型决策”阶段之后、”结果回填”阶段之前。把它写成时间轴就是:装配 → 决策(含 tool_use)→ 执行(含 hook)→ 回填 → 终止判断 → 回到装配。
| 阶段 | 触发工具 | 不触发工具 | 关键动作 |
|---|---|---|---|
| 装配 | 否 | 否 | 拼 prompt、加载 CLAUDE.md、注册 tools |
| 决策 | 否 | 否 | 调 API、解析流 |
| 执行 | 是 | 否 | 权限闸门、hooks、execute() |
| 回填 | 否 | 是 | 写 history、压缩 |
| 终止判断 | 否 | 是 | 跑 stop_hooks、产出 result |
注意:装配与决策阶段虽不直接执行工具,但决定了工具是否被使用——装配阶段通过 tool schema 限制可选范围,决策阶段通过 prompt 上下文影响模型偏好。优化循环时,要从这两段入手而不是改 execute()。
五、典型流程演示
用 prompt “修一下 auth.ts 里的失败测试” 演示循环完整跑一遍:
- 装配:把 CLAUDE.md、可用工具、当前目录、失败测试列表拼好;
- 决策 turn 1:模型给出 assistant 块,含
Bash工具调用npm test; - 执行:权限通过 → 跑 npm test → 输出三个失败;
- 回填:把失败输出塞回 history;
- 决策 turn 2:模型决定读
auth.ts与auth.test.ts; - 决策 turn 3:模型调
Edit改文件,再Bash跑测试; - 回填:测试通过;
- 决策 final:模型给纯文本回复”已修复,全部通过”;
- 终止:跑 stop_hooks,产出
ResultMessage,计费统计落账。
整个过程是 4 轮(3 轮带工具 + 1 轮纯文本)。maxTurns=2 会把循环卡在”已修代码但还没跑回归”那一刻——这是配置项最直观的副作用。
六、循环里的并发与权限
并行工具只在执行阶段被决定,并发上限默认 10 个。三个并发规则:
- 只读工具(
Read、Glob、Grep)并发跑,按提交顺序返回结果; - 写工具(
Edit、Write)必须串行,避免顺序错乱; - 一次 assistant 响应里若同时有读和写,SDK 会先批处理读、写一次只提交一个。
权限层级的”四道闸门”在 execute() 入口处运行。bypassPermissions 模式会跳过白名单与分类器,但 reject 仍然生效——这意味着无论权限怎么开,危险命令都拦得住。
七、循环的可调旋钮
调优循环时真正能动的参数集中在 query 入口和 system context:
maxTurns/max_turns:限制带工具的轮数;maxBudgetUsd/max_budget_usd:按花费熔断;permissionMode:plan / acceptEdits / bypassPermissions / default;canUseTool自定义回调:把分类器升级为业务级决策;includePartialMessages:开stream_event增量;systemPrompt/appendSystemPrompt:装配阶段可控注入。
调任何一个旋钮都等价于在循环某一阶段插入策略——比如改 canUseTool 就是在执行阶段换闸门。
到这里,循环从”装配到终止”的五个阶段、每阶段的工具调用时机、可调的旋钮都串完了。设计 Agent 时,把策略放在对的阶段,比把策略塞进 system prompt 更可控。
常见问题(FAQ)
Q1:maxTurns 限制的是带工具的轮次还是所有轮次?
只统计带 tool_use 的轮次,纯文本回复那一轮不计入预算;纯文本轮次会自然结束循环。
Q2:执行阶段的权限闸门能完全关掉吗?
黑名单不能关,白名单与分类器可在 bypassPermissions 下跳过;想接管决策应自定义 canUseTool 而不是改设置。
Q3:循环压缩会丢历史吗?
micro-compression 只裁冗长解释,工具输入输出保留;对话记忆压缩会把早期对话换成摘要,原文不再可见;失败连续三次后停止自动压缩。