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 = {
messages: Message[]
toolUseContext: ToolUseContext
turnCount: number
transition: Continue | undefined// 上一次迭代为何继续
maxOutputTokensRecoveryCount: number
hasAttemptedReactiveCompact: boolean
// ...
}
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 |
|
max_output_tokens_recovery |
|
aborted_streaming |
|
stop_hook_prevented |
|
token_budget_early_stop |
|
同样,”为何继续”也有枚举:
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
exportasyncfunction* runAgent({ 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 会超出上下文窗口。系统有多种压缩策略,按优先级依次尝试:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
压缩后,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 function(async function*)。
消费者不拉取数据,生成器就不执行。
// REPL 中:每产生一条消息立即渲染
forawait (const message ofquery({...})) {
setMessages(prev => [...prev, message])
}
对于子 Agent,父循环可以随时通过 AbortController 中止子循环执行,不会有”停不下来”的问题。
十、设计总结
|
|
|
while(true)
|
|
async function*) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
结语
Claude Code 的 Agent Loop 本质上是一个有限状态机,包裹在生成器里,每个 turn 是一次状态转移。它没有用复杂的框架,而是用最朴素的 while(true) + 显式状态,把流式 API、工具调用、错误恢复、子 Agent 递归全部整合进来。
如果你在设计自己的 Agent 系统,这套模式值得借鉴:
-
1. 把循环状态集中在一个对象里,而不是散落在多个变量中 -
2. 给每个退出/继续点命名,让行为可追踪 -
3. 用 Generator 驱动流式输出,UI 响应更快 -
4. 不可变性不只是风格,它保护了你的 cache 效率

夜雨聆风