claude-code-learn_
02AGENT LOOP

02 - Agent Loop

Agent Loop 是 Claude Code 的中枢控制流。它把"用户输入一句话"映射为"模型推理 + 零到多次工具执行 + 回写结果"的循环,直到模型返回终止消息。

读完你能回答

问题
一次 API 请求里同时有哪几段内容?
为什么 tool_resultroleuser 而不是 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
}

上面有三条值得看一眼的细节:

  1. system数组不是字符串,每段可以独立标记 cache_control
  2. toolsinput_schema 就是 JSON Schema,模型严格按它生成参数
  3. 真实请求一般 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": "<序列化后的结果>"
    }
  ]
}

为什么 roleuser 因为 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 在"模型响应"和"用户可见"之间插入了一个任意长度的工具循环。用户看到的最终回答,是模型经过若干轮"调工具—看结果—再思考"后的收束产物。