Agent Loop:从一句话到完成任务

一句话理解

想象你雇了一个非常能干的助手。你说"帮我整理一下房间",这个助手不是听完就干完了——他会反复循环:先看看房间什么样,再决定先收拾哪里,收拾完一个地方再看看下一步该干什么,直到整个房间干净为止。

Claude Code 的 Agent Loop 就是这样一个**"观察 → 思考 → 行动 → 再观察"的循环**。

整体架构

用户输入 Prompt │ ▼ ┌─────────────────────────────┐ │ QueryEngine.submitMessage() │ ← 每轮对话的入口 │ │ │ 1. 解析用户输入 │ │ 2. 拼装 System Prompt │ │ 3. 进入 query() 主循环 ──────────┼──┐ └─────────────────────────────┘ │ │ ┌────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────┐ │ query() —— 无限循环 │ │ │ │ ┌──────────┐ ┌──────────┐ ┌────────┐ │ │ │ 调用 API │───▶│ 解析响应 │───▶│执行工具 │ │ │ │ (Claude) │ │(流式处理) │ │(Bash等)│ │ │ └──────────┘ └──────────┘ └────────┘ │ │ ▲ │ │ │ │ 工具结果 │ │ │ └──────────────────────────────┘ │ │ │ │ 直到:模型不再调用工具 / 达到上限 / 用户中断 │ └─────────────────────────────────────────────┘

核心文件导航

文件职责大小
src/entrypoints/cli.tsxCLI 入口,启动 REPL302 行
src/bootstrap/state.ts会话状态初始化1758 行
src/QueryEngine.ts每轮对话的控制器1295 行
src/query.ts核心无限循环1729 行

第一步:启动(cli.tsx)

当你在终端输入 claude 时,一切从这里开始:

// src/entrypoints/cli.tsx
async function main() {
  // 快速路径:--version, --help 等不需要完整启动
  if (args.version) { /* 直接输出版本号,退出 */ }
  if (args.dumpSystemPrompt) { /* 输出 system prompt,退出 */ }

  // 完整启动:加载主模块
  const { cliMain } = await import('../main.js')
  await cliMain(/* ... */)
}

比喻:这就像一个餐厅的前台。如果你只是问"几点关门",前台直接告诉你;如果你要吃饭,前台才会把你领到座位上,启动完整的服务流程。

第二步:初始化会话状态(state.ts)

每次启动 Claude Code,都会创建一个全新的会话状态。这个状态就像一个"黑板",整个运行过程中所有信息都写在上面:

// src/bootstrap/state.ts (lines 44-256)
type State = {
  // 你在哪个目录工作
  originalCwd: string
  cwd: string
  projectRoot: string

  // 这次对话花了多少钱
  totalCostUSD: number

  // 每一轮用了多少工具
  turnToolCount: number

  // 会话唯一标识
  sessionId: SessionId

  // 上一次 API 请求的缓存(用于续传)
  lastAPIRequest: /* ... */ | null
  lastAPIRequestMessages: /* ... */ | null

  // 定时任务
  sessionCronTasks: SessionCronTask[]

  // ...更多字段
}

初始化函数会生成唯一的 session ID:

// src/bootstrap/state.ts (line 330)
sessionId: randomUUID() as SessionId

第三步:QueryEngine —— 每轮对话的控制器

QueryEngine一轮对话的总指挥。它负责:接收用户输入 → 组装上下文 → 调用核心循环 → 返回结果。

// src/QueryEngine.ts (lines 184-198)
export class QueryEngine {
  private mutableMessages: Message[]       // 所有历史消息
  private abortController: AbortController // 中断控制
  private permissionDenials: SDKPermissionDenial[] // 权限拒绝记录
  private totalUsage: NonNullableUsage     // 累计用量
}

关键方法是 submitMessage(),它是一个异步生成器(可以持续产出消息):

// src/QueryEngine.ts (lines 209+)
async *submitMessage(
  prompt: string | ContentBlockParam[],
  options?: { uuid?: string; isMeta?: boolean },
): AsyncGenerator<SDKMessage, void, unknown> {

  // 1. 解析用户输入,处理斜杠命令
  const { shouldQuery } = await processUserInput(prompt)

  // 2. 如果是本地命令(如 /help),直接返回
  if (!shouldQuery) { yield localResult; return }

  // 3. 进入核心循环
  for await (const message of query({
    messages,
    systemPrompt,
    canUseTool: wrappedCanUseTool,
    maxTurns,
    taskBudget,
    // ...
  })) {
    // 4. 处理循环产出的每条消息
    switch (message.type) {
      case 'assistant': yield message; break
      case 'progress': yield message; break
      case 'stream_event': updateUsage(message); break
      // ...
    }
  }
}

比喻QueryEngine 就像一个项目经理。客户(用户)提了需求,项目经理先判断是不是简单问题(斜杠命令),如果是就直接答复。如果是复杂需求,就启动完整的项目流程(query() 循环)。

第四步:query() —— 核心无限循环

这是整个 Agent 系统最核心的部分。它是一个 while(true) 无限循环:

// src/query.ts (lines 218-238)
export async function* query(params: QueryParams) {
  // 循环状态
  let state = {
    messages: [],
    turnCount: 0,
    hasAttemptedReactiveCompact: false,
    maxOutputTokensRecoveryCount: 0,
    // ...
  }

  while (true) {
    // ====== 阶段 1:调用 Claude API ======
    for await (const message of callModel({
      messages: state.messages,
      systemPrompt,
      tools: availableTools,
      // ...
    })) {
      yield message  // 流式输出给用户
    }

    // ====== 阶段 2:错误恢复 ======
    // 如果 prompt 太长,尝试压缩后重试
    // 如果输出被截断,提高 max_tokens 重试

    // ====== 阶段 3:执行工具 ======
    for await (const update of runTools(toolUseBlocks)) {
      yield update.message
    }

    // ====== 阶段 4:决定是否继续 ======
    if (noMoreToolCalls) break  // 模型没有调用工具 → 任务完成
    if (turnCount >= maxTurns) break  // 达到轮次上限
    if (aborted) break  // 用户中断

    // 否则:把工具结果加入消息,继续下一轮
    state.messages.push(...toolResults)
    state.turnCount++
    continue  // 回到 while(true) 顶部
  }
}

用一张图来表示这个循环:

┌───────────────────┐ │ while (true) │ └─────────┬─────────┘ │ ┌─────────▼─────────┐ │ 调用 Claude API │ │ (发送消息+工具列表)│ └─────────┬─────────┘ │ ┌─────────▼─────────┐ │ 流式接收响应 │ │ (文字 + 工具调用) │ └─────────┬─────────┘ │ ┌─────────▼─────────┐ │ 有工具调用吗? │ └────┬─────────┬────┘ No │ │ Yes ┌────▼───┐ ┌───▼──────────┐ │ 结束循环 │ │ 并行执行工具 │ │ 返回结果 │ │ (Bash/Read等) │ └────────┘ └───┬──────────┘ │ ┌─────────▼─────────┐ │ 把工具结果加入消息 │ │ turnCount++ │ └─────────┬─────────┘ │ │ continue └──────────▶ 回到循环顶部

关键设计:流式处理 + 并行工具执行

Claude Code 不是等 API 完全响应后才执行工具。它使用了一个叫 StreamingToolExecutor 的组件,边接收 API 响应边开始执行工具

时间线 ──────────────────────────────────────▶ API 响应流: [文字..] [工具A的参数..] [工具B的参数..] [结束] │ │ 工具执行: 开始执行A 开始执行B │ │ A完成 B完成

这样做的好处是:当模型还在输出工具 B 的参数时,工具 A 可能已经跑完了。节省的时间 = 工具 A 的执行时间

关键设计:错误恢复

循环中有多种自动恢复机制:

Prompt 太长 → 自动压缩

// src/query.ts (lines 1119-1166)
// 如果 API 返回 "prompt too long" 错误
if (withheldError?.type === 'prompt_too_long') {
  // 尝试压缩上下文(详见第6章)
  await compactConversation(messages, context)
  // 压缩成功 → 用新消息重试
  state.messages = compactedMessages
  continue  // 回到循环顶部
}

输出被截断 → 提高限制重试

// src/query.ts (lines 1188-1256)
// 如果输出超过了 max_output_tokens
if (withheldError?.type === 'max_output_tokens') {
  // 从 8K 升级到 64K
  state.maxOutputTokensOverride = 64_000
  // 注入恢复消息:"你的输出被截断了,请继续"
  state.messages.push(recoveryMessage)
  state.maxOutputTokensRecoveryCount++
  continue  // 重试(最多 3 次)
}

比喻:这就像考试时发现答题纸不够用了。第一次给你一张小纸,写不下就自动换一张大纸,最多换 3 次。

关键设计:两级生成器嵌套

整个消息流使用了两层异步生成器的设计:

外层:QueryEngine.submitMessage() │ │ 消费内层生成器的消息 │ → 规范化格式 │ → 记录到会话历史 │ → 产出给外部(UI/SDK) │ └── 内层:query() │ │ 产出原始消息 │ → stream_event(token 用量) │ → assistant(模型回复) │ → user(工具结果) │ → attachment(附件信息)

这种设计的好处:内层专注于"循环执行",外层专注于"对外呈现",职责分离非常清晰。

一次完整执行的时序

让我们跟踪一个真实场景:"帮我修复 bug.ts 文件中的类型错误"

时间 ──────────────────────────────────────────────────▶ 用户: "帮我修复 bug.ts 中的类型错误" │ ▼ QueryEngine.submitMessage() │ ├─ 解析输入(不是斜杠命令) ├─ 拼装 System Prompt │ ▼ query() 循环 - 第 1 轮 │ ├─ 调用 Claude API ├─ Claude 回复: "让我先看看这个文件" + 调用 Read("bug.ts") ├─ 执行 Read 工具 → 返回文件内容 ├─ 把结果加入消息 │ ▼ query() 循环 - 第 2 轮 │ ├─ 调用 Claude API(带上文件内容) ├─ Claude 回复: "我发现了问题" + 调用 Edit("bug.ts", ...) ├─ 执行 Edit 工具 → 修改文件 ├─ 把结果加入消息 │ ▼ query() 循环 - 第 3 轮 │ ├─ 调用 Claude API ├─ Claude 回复: "已修复,类型错误是因为..."(没有工具调用) ├─ 没有工具调用 → 跳出循环 │ ▼ 返回最终结果给用户

小结

Agent Loop 的核心思想可以归结为三个字:"循环调用"

  1. 不是一次性完成:模型可以反复调用工具,逐步推进任务
  2. 流式处理:边接收响应边执行工具,减少等待时间
  3. 自动恢复:遇到上下文超长、输出截断等问题,自动压缩或重试
  4. 生成器架构:两层嵌套的异步生成器,干净地分离了执行逻辑和对外接口

下一章我们来看:循环中执行的那些"工具"到底是怎么定义的?它们是怎么做到安全执行的?