乐于分享
好东西不私藏

Claude Code 源码来看 Agent Loop 设计解析

Claude Code 源码来看 Agent Loop 设计解析

我们专注于 AI agentic 的应用,持续分享我们在AI 时代的所见所得,Hellow AI World

Claude Code 的 Agent Loop 设计解析

一个工业级 AI Agent 是如何控制自己的”思考循环”的?


前言

Claude Code 。它的核心不是一次性的问答,而是一个持续运行的 Agent Loop——模型不断思考、调用工具、处理结果、再次思考,直到任务完成。

本文通过分析其源码,拆解这个设计哲学。

时序图


一、整体架构:三层结构

用户输入
   │
   ▼
REPL.tsx           ← UI 层(React/Ink 终端组件)
   │
   ▼
QueryEngine.ts     ← 会话管理层(状态、权限、计费)
   │
   ▼
query.ts           ← 核心循环层(真正的 Agent Loop)
   │
   ├── claude.ts   ← API 流式调用
   └── runAgent.ts ← 子 Agent 递归

每一层职责清晰。UI 不管推理逻辑,会话管理不管具体工具,核心循环只负责「下一步该怎么走」。


二、核心:状态机循环

Agent Loop 的核心在 src/query.ts,是一个 while(true) + 显式状态对象 的状态机:

typeState = {
messagesMessage[]
toolUseContextToolUseContext
turnCountnumber
transitionContinue | undefined// 上一次迭代为何继续
maxOutputTokensRecoveryCountnumber
hasAttemptedReactiveCompactboolean
// ...
}

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

// 1. 压缩上下文(如有必要)
// 2. 调用 API,流式处理响应
// 3. 执行工具调用
// 4. 决策:继续 or 返回

// 继续时:整体替换 state,不做局部 mutation
  state = { ...state, messages: newMessages, turnCount: turnCount + 1 }
}

关键设计:没有零散的变量赋值,所有变更都通过替换整个 state 对象完成。这既保证了不可变性,也让每次迭代的”入口状态”一目了然。


三、一次 Turn 的完整流程

┌──────────────────────────────────────┐
│ 1. 上下文压缩(Compaction)           │
│    检查 token 是否超限,按需压缩历史   │
└──────────────────────────────────────┘
              ↓
┌──────────────────────────────────────┐
│ 2. 流式 API 调用                      │
│    Anthropic SDK streaming           │
│    工具调用在流式传输中就开始执行      │
└──────────────────────────────────────┘
              ↓
┌──────────────────────────────────────┐
│ 3. 错误恢复                           │
│    prompt_too_long → 压缩后重试       │
│    max_output_tokens → 升级为 64k    │
│    fallback_model → 换模型重试        │
└──────────────────────────────────────┘
              ↓
┌──────────────────────────────────────┐
│ 4. 工具执行(Tool Execution)         │
│    收集所有 tool_use block           │
│    并发执行,等待全部结果              │
└──────────────────────────────────────┘
              ↓
┌──────────────────────────────────────┐
│ 5. 继续 or 终止                       │
│    有工具结果 → 继续下一 turn         │
│    无工具调用 → 任务完成,退出         │
└──────────────────────────────────────┘

四、退出机制:所有终止点

循环有明确的退出枚举,不会”神秘停止”:

退出原因
触发条件
completed
没有工具调用,任务完成
max_turns
超过最大轮次限制
prompt_too_long
413 错误且恢复失败
max_output_tokens_recovery
64k 升级后仍超限(3次)
aborted_streaming
用户中断(Ctrl+C)
stop_hook_prevented
Stop Hook 阻止继续
token_budget_early_stop
Token 预算耗尽

同样,”为何继续”也有枚举:

typeContinueReason =
  | 'next_turn'// 正常:有工具结果,继续
  | 'reactive_compact_retry'// 压缩后重试
  | 'max_output_tokens_escalate'// 升级 64k 重试
  | 'stop_hook_blocking'// Stop Hook 阻塞后继续
  | 'token_budget_continuation'// 注入 nudge message 后继续

这种设计让调试和日志变得极为友好——每次转换都有名字。


五、流式工具执行:减少等待

一个关键的性能优化:工具在模型还在流式输出时就开始执行

const streamingToolExecutor = newStreamingToolExecutor(tools, canUseTool, ctx)

// 模型流式输出中,tool_use block 一出现就立即执行
forawait (const message ofcallModel({...})) {
const toolBlocks = extractToolUseBlocks(message)

for (const block of toolBlocks) {
    streamingToolExecutor.addTool(block, message)
// ↑ 启动工具执行,不等模型输出完毕
  }

// 同时获取已完成的工具结果
for (const result of streamingToolExecutor.getCompletedResults()) {
yield result.message
  }
}

假设模型要调用 3 个工具,前 2 个已经在 tool_use block 输出后就开始执行了,不用等第 3 个输出完才开始。


六、子 Agent 递归:query() 调用 query()

子 Agent 通过 runAgent() 实现,本质是递归调用 query()

// src/tools/AgentTool/runAgent.ts
exportasyncfunctionrunAgent({ agentDefinition, ... }) {
// 初始化子 Agent 专属的 MCP 服务
const { tools } = awaitinitializeAgentMcpServers(agentDefinition)

// 递归:子 Agent 有自己独立的 query() 循环
forawait (const message ofquery({
messages: initialMessages,
systemPrompt: agentSystemPrompt,
toolUseContext: agentToolUseContext,
maxTurns: agentDefinition.maxTurns,
  })) {
awaitrecordSidechainTranscript([message], agentId)
yield message
  }

awaitmcpCleanup()
}

隔离机制

  • • 每个子 Agent 有独立的 agentId
  • • 独立的 AbortController(异步 Agent 可独立中断)
  • • 独立的 transcript sidechain(互不污染历史)
  • • 深度通过 queryTracking.depth 追踪

七、上下文压缩:长对话的生命线

随着对话增长,token 会超出上下文窗口。系统有多种压缩策略,按优先级依次尝试

策略
说明
History Snip
剪掉旧轮次的部分 content,保留结构
Microcompact
对 prompt cache 中的内容做原地压缩
Context Collapse
把整段对话历史折叠为摘要消息
Reactive Compact
API 返回 413 时触发,激进压缩后重试

压缩后,state.messages 被替换为压缩版本,循环从下一 turn 继续。


八、不可变性:为何如此重要

整个 Agent Loop 严格遵守不可变原则,原因不只是”代码风格好”:

原因:Prompt Cache 命中率

Anthropic API 支持 prompt caching——对相同前缀的请求只计费一次。如果在循环中直接 mutate 消息对象,会导致缓存键发生变化,无法命中。

// ✓ 正确:创建新对象,缓存键不变
state = {
messages: [...state.messages, newMessage],
}

// ✗ 错误:原地修改,破坏 prompt cache
state.messages.push(newMessage)

九、生成器模式:惰性执行的威力

query() 和 runAgent() 都是 async generator functionasync function*)。

消费者不拉取数据,生成器就不执行。

// REPL 中:每产生一条消息立即渲染
forawait (const message ofquery({...})) {
setMessages(prev => [...prev, message])
}

对于子 Agent,父循环可以随时通过 AbortController 中止子循环执行,不会有”停不下来”的问题。


十、设计总结

设计选择
解决的问题
while(true)

 + State 对象
避免深层递归,状态管理集中
Generator(async function*
惰性执行、流式 UI 更新
显式退出/继续枚举
调试友好,行为可观测
不可变 State 替换
保护 Prompt Cache 命中率
流式工具执行
减少工具等待延迟
子 Agent 递归隔离
多 Agent 并行不干扰
多层压缩策略
支撑超长任务不崩溃

结语

Claude Code 的 Agent Loop 本质上是一个有限状态机,包裹在生成器里,每个 turn 是一次状态转移。它没有用复杂的框架,而是用最朴素的 while(true) + 显式状态,把流式 API、工具调用、错误恢复、子 Agent 递归全部整合进来。

如果你在设计自己的 Agent 系统,这套模式值得借鉴:

  1. 1. 把循环状态集中在一个对象里,而不是散落在多个变量中
  2. 2. 给每个退出/继续点命名,让行为可追踪
  3. 3. 用 Generator 驱动流式输出,UI 响应更快
  4. 4. 不可变性不只是风格,它保护了你的 cache 效率

我们专注于 AI agentic 的应用,持续分享我们在AI 时代的所见所得,Hellow AI World