02 - Agent Loop
Agent Loop 是 Claude Code 的中枢控制流。它把"用户输入一句话"映射为"模型推理 + 零到多次工具执行 + 回写结果"的循环,直到模型返回终止消息。
读完你能回答
| 问题 |
|---|
| 一次 API 请求里同时有哪几段内容? |
为什么 tool_result 的 role 是 user 而不是 assistant? |
stop_reason 有哪几种取值,分别意味着下一步做什么? |
| 模型"想同时调三个工具"时,客户端会怎么调度? |
| 循环什么情况下会被强制打断? |
全貌:一次 turn 的四方协作
一次 turn 从"用户敲下回车"开始,到"最终文本显示在终端"结束。中间穿插若干次模型调用和工具调用。下面这张图展示了最常见的「一次带工具的 turn」。
关键细节一次说清:
- 图中 1、3、8 步都是同一次 HTTP 请求形态,区别只是
messages[]不断变长 - 第 4 步的
stop_reason=tool_use是触发工具调用的唯一信号,客户端不猜、不解析文本 - 第 6 步的工具结果以
role:user注入(见下文"结果回填") - 图中隐藏了权限弹窗、流式 SSE 解包、重试等旁路,真实实现比这复杂
逐步演练
下面是一个可交互的分步演示。点击 ▶ 自动播放,或点任意步骤跳转。每一步下方给出真实的请求/响应片段。
单次请求的字段结构
每次模型调用是一次 HTTP 请求,请求体同时携带三个字段。三者在同一次传输中送达,不存在"先发 prompt 再发 tools"这种分步调用。
| 字段 | 内容 | 稳定性 |
|---|---|---|
system[] |
规则、身份、环境信息 | 相对稳定,利于前缀缓存 |
messages[] |
历史 turns + 当前 user turn | 每轮增长 |
tools[] |
当前可用工具的 JSON Schema | 可能随模式切换变化 |
展开一次真实请求体的骨架
POST https://api.anthropic.com/v1/messages
{
"model": "claude-opus-4-5",
"system": [
{ "type": "text", "text": "You are Claude Code...", "cache_control": { "type": "ephemeral" } },
{ "type": "text", "text": "# 环境\n工作目录: /repo\nOS: darwin ..." }
],
"messages": [
{ "role": "user", "content": "读一下 README.md" }
],
"tools": [
{
"name": "Read",
"description": "Reads a file from the local filesystem.",
"input_schema": {
"type": "object",
"properties": {
"file_path": { "type": "string" },
"limit": { "type": "integer" },
"offset": { "type": "integer" }
},
"required": ["file_path"]
}
}
/* Edit, Write, Bash, Glob, Grep, ... */
],
"max_tokens": 8192,
"stream": true
}上面有三条值得看一眼的细节:
system是数组不是字符串,每段可以独立标记cache_controltools的input_schema就是 JSON Schema,模型严格按它生成参数- 真实请求一般
stream: true,客户端按 SSE 边收边解
终止判定
每次响应都带一个 stop_reason,它决定下一步做什么。整个循环的走向,由它驱动。
| stop_reason | 含义 | 客户端动作 |
|---|---|---|
end_turn |
模型主动结束这一轮 | 退出循环,渲染给用户 |
tool_use |
模型请求调用工具 | 进入工具派发 |
max_tokens |
达到输出上限 | 退出并提示截断 |
stop_sequence |
命中预设终止串 | 按上下文决定 |
展开 tool_use 响应的真实骨架
{
"id": "msg_01XYZ",
"type": "message",
"role": "assistant",
"model": "claude-opus-4-5",
"stop_reason": "tool_use",
"content": [
{ "type": "text", "text": "我来读一下。" },
{
"type": "tool_use",
"id": "toolu_01ABCD",
"name": "Read",
"input": { "file_path": "README.md" }
}
]
}content[]可能同时包含文本和 tool_use,这就是用户看到的"边说话边调工具"- 同一次响应里可以出现多个 tool_use,每个独立
id,并发执行见下节
并发工具执行
同一轮响应若包含多个 tool_use,调度器会尝试并行。并发的前提通常是:
| 条件 | 说明 |
|---|---|
| 工具可安全并发 | 只读工具(Read / Grep / Glob)天然可并发;写工具谨慎 |
| 工具之间无显式依赖 | 参数不互相引用彼此的结果 |
| 用户未禁用并发 | 可由 settings.json 或模式关闭 |
不满足任一条件则退回串行。
本节对"并发可行性"的描述是观察所得的经验规律,非 Anthropic 官方公开协议。实际行为以当前运行版本为准。
展开一次并发 tool_use 的响应骨架
{
"stop_reason": "tool_use",
"content": [
{ "type": "tool_use", "id": "toolu_A", "name": "Read", "input": { "file_path": "a.ts" } },
{ "type": "tool_use", "id": "toolu_B", "name": "Read", "input": { "file_path": "b.ts" } },
{ "type": "tool_use", "id": "toolu_C", "name": "Grep", "input": { "pattern": "TODO" } }
]
}客户端会同时派发三个读操作,按各自完成顺序收集结果。回填时,三个 tool_result 必须打包在同一条 role:user 消息的 content[] 里,一起送入下一次请求。
结果回填
每个 tool_use 都要对应一个 tool_result。下一次请求的 messages[] 尾端必须追加一条 role:user 消息,content[] 里是所有工具结果。
{
"role": "user",
"content": [
{
"type": "tool_result",
"tool_use_id": "toolu_01ABCD",
"content": "<序列化后的结果>"
}
]
}为什么 role 是 user? 因为 Anthropic API 把 assistant / user 严格对齐"模型输出 / 非模型输出"。工具结果不是模型产物,所以必须挂在 user 消息里。这是 API 约定,不是设计品味。
展开错误结果的回填格式
工具失败时用同一个信封,加 is_error: true。模型看到后可以自行重试或换策略。
{
"role": "user",
"content": [
{
"type": "tool_result",
"tool_use_id": "toolu_01ABCD",
"content": "ENOENT: no such file or directory, open 'README.md'",
"is_error": true
}
]
}循环边界
以下情况强制退出循环:
- 模型连续数轮无进展(同一工具用同一参数反复调用)
- 累计 token 超过会话预算
- 用户中断(Ctrl-C / 取消按钮)
- 工具执行触发致命错误且无重试策略
这些边界来自可观察行为,具体阈值随版本调整,不在公开文档中。遇到"Claude 突然停了"时,多半命中其中之一。
与普通聊天的差异
普通聊天只循环到"模型文本响应 → 返回用户"即止。Agent Loop 在"模型响应"和"用户可见"之间插入了一个任意长度的工具循环。用户看到的最终回答,是模型经过若干轮"调工具—看结果—再思考"后的收束产物。