Claude Code 源码解读之 Agent Loop
·
02 - Claude Code Agent Loop
重点章节 — 本文档深入讲解 Claude Code 最核心的 Agent Loop(代理循环)机制。
一、概念解释
什么是 Agent Loop?
传统 LLM 的交互是单轮请求-响应模式:
用户提问 → LLM 回答 → 结束
但现实中的复杂任务(如"重构这个模块")需要多步操作:读取文件、理解代码、修改文件、运行测试。Agent Loop 将单轮交互升级为循环:
用户提问 → LLM 思考 → 需要工具?→ 调用工具 → 结果喂回 LLM → 继续思考 → ... → 最终回答
↑___________________________________________________|
循环直到任务完成
为什么需要 Agent Loop?
| 没有 Agent Loop | 有 Agent Loop |
|---|---|
| 每次只能做一件事 | 可以连续完成多步任务 |
| 无法使用工具 | 可以调用文件读写、命令执行等工具 |
| 上下文在单轮内固定 | 上下文随工具结果不断丰富 |
| 无法自我纠正 | 可以根据工具反馈调整策略 |
核心设计哲学
Claude Code 的 Agent Loop 采用 while(true) 循环而非递归,原因:
- 状态管理更清晰 — 每次循环顶部解构状态,避免递归栈过深
- 资源控制更精确 — 可以精确追踪 token 消耗和迭代次数
- 错误恢复更简单 —
continue跳到下一次迭代,而非递归展开
二、核心流程图
三、解决什么问题?
Agent Loop 解决的核心问题:让 LLM 能够自主完成需要多步操作、工具调用和自我纠正的复杂任务。
具体来说:
- 多步任务编排 — 一次用户请求可能需要读文件、搜索代码、编辑文件、运行测试等多个步骤
- 工具调用与反馈 — LLM 的输出可能包含工具调用,执行后结果需要喂回 LLM 继续推理
- 上下文溢出管理 — 长对话中 token 会超限,需要智能压缩
- 错误恢复 — API 调用失败、token 超限、用户中断等场景需要优雅处理
四、核心代码详解
4.1 循环状态定义
// src/query.ts — 循环状态,每次迭代顶部解构
type State = {
messages: Message[] // 当前消息列表
toolUseContext: ToolUseContext // 工具使用上下文
autoCompactTracking: AutoCompactTrackingState | undefined // 自动压缩追踪
maxOutputTokensRecoveryCount: number // max_output_tokens错误恢复计数
hasAttemptedReactiveCompact: boolean // 是否已尝试响应式压缩
maxOutputTokensOverride: number | undefined // 输出token覆盖值
pendingToolUseSummary: Promise<ToolUseSummaryMessage | null> | undefined
stopHookActive: boolean | undefined // 停止钩子是否激活
turnCount: number // 当前轮次
transition: Continue | undefined // 上一次迭代为什么继续
}
设计要点:状态是不可变的 — 每次 continue 用 state = { ... } 创建新对象,而非就地修改。
4.2 主循环结构
// src/query.ts — 核心循环(简化版,保留关键逻辑)
async function* queryLoop(
params: QueryParams,
consumedCommandUuids: string[],
): AsyncGenerator<StreamEvent | Message, Terminal> {
// 不可变参数 — 循环中永不修改
const { systemPrompt, userContext, systemContext, canUseTool, fallbackModel } = params
const deps = params.deps ?? productionDeps()
// 可变跨迭代状态
let state: State = {
messages: params.messages,
toolUseContext: params.toolUseContext,
maxOutputTokensOverride: params.maxOutputTokensOverride,
autoCompactTracking: undefined,
maxOutputTokensRecoveryCount: 0,
hasAttemptedReactiveCompact: false,
turnCount: 1,
pendingToolUseSummary: undefined,
transition: undefined,
}
// ========== 主循环 ==========
while (true) {
let { toolUseContext } = state // 每次迭代顶部解构状态
const { messages, autoCompactTracking, turnCount, ... } = state
// --- ① 消息准备阶段 ---
let messagesForQuery = [...getMessagesAfterCompactBoundary(messages)]
// 工具结果预算裁剪(防止工具返回过大内容撑爆上下文)
messagesForQuery = await applyToolResultBudget(messagesForQuery, ...)
// 历史消息裁剪(移除过旧的中间消息)
if (feature('HISTORY_SNIP')) {
const snipResult = snipModule!.snipCompactIfNeeded(messagesForQuery)
messagesForQuery = snipResult.messages
}
// 微压缩(小粒度的上下文压缩)
const microcompactResult = await deps.microcompact(messagesForQuery, toolUseContext, ...)
messagesForQuery = microcompactResult.messages
// 上下文折叠(合并相似消息)
if (feature('CONTEXT_COLLAPSE')) {
const collapseResult = await contextCollapse.applyCollapsesIfNeeded(messagesForQuery, ...)
messagesForQuery = collapseResult.messages
}
// --- ② 构建系统提示 ---
const fullSystemPrompt = asSystemPrompt(
appendSystemContext(systemPrompt, systemContext)
)
// --- ③ 自动压缩检查 ---
const { compactionResult } = await deps.autocompact(
messagesForQuery, toolUseContext, { systemPrompt, userContext, systemContext }, ...
)
// 如果压缩发生,用压缩后的消息替换
if (compactionResult) {
messagesForQuery = compactionResult.summaryMessages
// ... 压缩后处理
}
// --- ④ 调用 LLM API(流式) ---
yield { type: 'stream_request_start' } // 通知UI:API请求开始
const stream = deps.streamMessages({ // 流式调用 Anthropic API
messages: normalizeMessagesForAPI(messagesForQuery, ...),
systemPrompt: fullSystemPrompt,
model: getCurrentModel(...),
tools: toolUseContext.options.tools,
...
})
// --- ⑤ 处理流式响应 ---
let toolUseMessages: ToolUseBlock[] = []
for await (const event of stream) {
if (event.type === 'content_block_start') {
if (event.content_block.type === 'tool_use') {
toolUseMessages.push(event.content_block) // 收集工具调用
}
}
yield event // 实时yield给UI渲染(文字流式显示)
}
// --- ⑥ 工具执行 ---
if (toolUseMessages.length > 0) {
// 分区执行:只读工具并行,写操作串行
for await (const update of runTools(
toolUseMessages, assistantMessages, canUseTool, toolUseContext
)) {
if (update.message) {
yield update.message // yield工具结果给UI
state.messages = [...state.messages, update.message] // 不可变更新
}
if (update.newContext) {
toolUseContext = update.newContext
}
}
// --- 更新状态,继续循环 ---
state = {
...state,
messages: [...state.messages, /* 新的工具结果消息 */],
toolUseContext,
turnCount: turnCount + 1,
transition: /* 继续原因 */,
}
continue // ← 回到while(true)顶部,开始下一轮
}
// --- ⑦ 无工具调用,循环结束 ---
return { type: 'stop', reason: 'end_turn' }
}
}
4.3 函数签名与生成器模式
// src/query.ts — 注意这是一个 AsyncGenerator
export async function* query(
params: QueryParams,
): AsyncGenerator<
StreamEvent | RequestStartEvent | Message | TombstoneMessage | ToolUseSummaryMessage,
Terminal // 最终返回值
> {
const consumedCommandUuids: string[] = []
const terminal = yield* queryLoop(params, consumedCommandUuids)
// 循环正常结束后,通知已消费的命令
for (const uuid of consumedCommandUuids) {
notifyCommandLifecycle(uuid, 'completed')
}
return terminal
}
为什么用 AsyncGenerator?
yield实时推送流式事件给 UI(文字逐字显示)return表示循环最终结果- 调用方可以用
for await...of消费事件流 - 支持中途取消(
.return()方法)
4.4 工具编排(并发 vs 串行)
// src/services/tools/toolOrchestration.ts — 工具分区执行
export async function* runTools(
toolUseMessages: ToolUseBlock[],
assistantMessages: AssistantMessage[],
canUseTool: CanUseToolFn,
toolUseContext: ToolUseContext,
): AsyncGenerator<MessageUpdate, void> {
let currentContext = toolUseContext
// partitionToolCalls: 将工具分为可并发/不可并发两组
for (const { isConcurrencySafe, blocks } of partitionToolCalls(
toolUseMessages, currentContext
)) {
if (isConcurrencySafe) {
// 只读工具(Glob, Grep, Read等)→ 并行执行
for await (const update of runToolsConcurrently(blocks, ...)) {
yield { message: update.message, newContext: currentContext }
}
} else {
// 写操作工具(Edit, Write, Bash等)→ 串行执行
for await (const update of runToolsSerially(blocks, ...)) {
yield { message: update.message, newContext: update.newContext }
}
}
}
}
4.5 错误恢复策略
// src/query.ts — 多种错误恢复路径
// 1. max_output_tokens 错误:LLM 输出被截断
if (isWithheldMaxOutputTokens(msg)) {
if (state.maxOutputTokensRecoveryCount < MAX_OUTPUT_TOKENS_RECOVERY_LIMIT) {
state = {
...state,
maxOutputTokensRecoveryCount: state.maxOutputTokensRecoveryCount + 1,
maxOutputTokensOverride: /* 增加输出限制 */,
transition: 'max_output_tokens_recovery',
}
continue // 重新请求,增加输出长度
}
}
// 2. prompt_too_long 错误:输入超长
if (isPromptTooLongMessage(msg)) {
// 先尝试压缩
if (!state.hasAttemptedReactiveCompact) {
state = { ...state, hasAttemptedReactiveCompact: true }
continue // 压缩后重试
}
return { type: 'error', reason: 'prompt_too_long' } // 无法恢复
}
// 3. API 临时错误:指数退避重试
// src/services/api/withRetry.ts 自动处理
4.6 终止条件汇总
| 条件 | 行为 |
|---|---|
| LLM 不调用工具(end_turn) | return { type: 'stop' } — 正常结束 |
| 用户发送中断信号 | yield 中断消息,添加 tool_result 错误 |
| maxTurns 达到上限 | return { type: 'stop', reason: 'max_turns' } |
| token 预算耗尽 | return { type: 'stop', reason: 'budget_exhausted' } |
| AbortController 被 abort | 抛出 AbortError,退出循环 |
| prompt_too_long 不可恢复 | return { type: 'error', reason: 'prompt_too_long' } |
五、示例代码:简化版 Agent Loop
以下是一个教学用的简化版 Agent Loop,帮助学生理解核心概念:
// simplified-agent-loop.ts — 教学用简化版
// 演示 Agent Loop 的核心循环机制
// ========== 消息类型定义 ==========
type TextContent = { type: 'text'; text: string }
type ToolUseContent = { type: 'tool_use'; id: string; name: string; input: any }
type ToolResultContent = { type: 'tool_result'; tool_use_id: string; content: string; is_error?: boolean }
type Message = {
role: 'user' | 'assistant'
content: (TextContent | ToolUseContent | ToolResultContent)[]
}
// ========== 工具定义 ==========
interface Tool {
name: string
description: string
execute(input: any): Promise<string>
}
// 示例工具:文件读取
const readFileTool: Tool = {
name: 'read_file',
description: '读取文件内容',
async execute(input: { path: string }) {
return `文件 ${input.path} 的内容:console.log("hello")`
}
}
// 示例工具:文件编辑
const editFileTool: Tool = {
name: 'edit_file',
description: '编辑文件',
async execute(input: { path: string; old_text: string; new_text: string }) {
return `已将 ${input.path} 中的 "${input.old_text}" 替换为 "${input.new_text}"`
}
}
const tools: Tool[] = [readFileTool, editFileTool]
// ========== 模拟 LLM API ==========
async function callLLM(messages: Message[]): Promise<Message> {
// 模拟:第一次调用返回工具调用,第二次返回最终答案
const hasToolResult = messages.some(m =>
m.content.some(c => c.type === 'tool_result')
)
if (!hasToolResult) {
// 第一步:LLM 决定调用工具
return {
role: 'assistant',
content: [
{ type: 'text', text: '我需要先读取文件内容。' },
{ type: 'tool_use', id: 'call_1', name: 'read_file', input: { path: 'index.ts' } }
]
}
} else {
// 第二步:LLM 根据工具结果给出最终答案
return {
role: 'assistant',
content: [
{ type: 'text', text: '根据文件内容,我已完成分析。文件看起来正常。' }
]
}
}
}
// ========== 核心 Agent Loop ==========
async function agentLoop(userMessage: string): Promise<string> {
let messages: Message[] = [
{ role: 'user', content: [{ type: 'text', text: userMessage }] }
]
let maxTurns = 10 // 防止无限循环
let turnCount = 0
// ★ 主循环 — 核心的 while(true) 结构
while (turnCount < maxTurns) {
turnCount++
console.log(`\n--- 第 ${turnCount} 轮迭代 ---`)
// ① 调用 LLM
const response = await callLLM(messages)
messages.push(response)
console.log('LLM 响应:', response.content.map(c =>
c.type === 'text' ? `文本: ${c.text}` :
c.type === 'tool_use' ? `工具调用: ${c.name}` : ''
).join(' | '))
// ② 提取工具调用
const toolCalls = response.content.filter(
(c): c is ToolUseContent => c.type === 'tool_use'
)
// ③ 检查终止条件:无工具调用则结束
if (toolCalls.length === 0) {
console.log('\n✓ LLM 未调用工具,循环结束')
const finalText = response.content
.filter((c): c is TextContent => c.type === 'text')
.map(c => c.text)
.join('\n')
return finalText
}
// ④ 执行工具
const toolResults: ToolResultContent[] = []
for (const toolCall of toolCalls) {
const tool = tools.find(t => t.name === toolCall.name)
if (!tool) {
toolResults.push({
type: 'tool_result',
tool_use_id: toolCall.id,
content: `未知工具: ${toolCall.name}`,
is_error: true
})
continue
}
console.log(` 执行工具: ${tool.name}(${JSON.stringify(toolCall.input)})`)
const result = await tool.execute(toolCall.input)
console.log(` 工具结果: ${result}`)
toolResults.push({
type: 'tool_result',
tool_use_id: toolCall.id,
content: result
})
}
// ⑤ 将工具结果添加到消息列表(喂回 LLM)
messages.push({
role: 'user', // 注意:工具结果以 user 角色发送
content: toolResults
})
// ⑥ continue — 回到 while 循环顶部
}
return '达到最大迭代次数'
}
// ========== 运行示例 ==========
async function main() {
console.log('=== 简化版 Agent Loop 演示 ===\n')
const result = await agentLoop('帮我检查 index.ts 文件')
console.log('\n=== 最终结果 ===')
console.log(result)
}
main().catch(console.error)
运行方式:
# 保存为 simplified-agent-loop.ts,用 ts-node 或 bun 运行
bun run simplified-agent-loop.ts
预期输出:
=== 简化版 Agent Loop 演示 ===
--- 第 1 轮迭代 ---
LLM 响应: 文本: 我需要先读取文件内容。 | 工具调用: read_file
执行工具: read_file({"path":"index.ts"})
工具结果: 文件 index.ts 的内容:console.log("hello")
--- 第 2 轮迭代 ---
LLM 响应: 文本: 根据文件内容,我已完成分析。文件看起来正常。
✓ LLM 未调用工具,循环结束
=== 最终结果 ===
根据文件内容,我已完成分析。文件看起来正常。
六、深入讲解:设计哲学与工程取舍
6.1 为什么用 while(true) 而非递归?
递归方式:
function agentTurn() {
const response = callLLM()
if (hasToolUse(response)) {
executeTools(response) // 每次递归增加一层调用栈
return agentTurn() // ← 递归调用
}
return response // 递归展开
}
循环方式(Claude Code采用):
while (true) {
const response = callLLM() // 同一层调用栈
if (hasToolUse(response)) {
executeTools(response) // 执行工具
continue // ← 直接回到顶部
}
return response // 直接退出
}
循环的优势:
- 栈深度恒定 — 10次工具调用不会产生10层递归栈
- 状态集中管理 —
State对象在循环顶部统一解构 - 终止条件清晰 — 所有
return/continue点一目了然 - 资源可追踪 — 每次循环都可以检查 token 预算
6.2 为什么用 AsyncGenerator?
// AsyncGenerator 的优势
async function* query(params): AsyncGenerator<Event, Terminal> {
while (true) {
yield { type: 'stream_start' } // ← 实时推送给UI
for await (const chunk of stream) {
yield chunk // ← 流式推送每个token
}
if (toolCalls) {
for await (const result of runTools(toolCalls)) {
yield result.message // ← 推送每个工具结果
}
continue
}
return terminal // ← 最终结果
}
}
- 实时性 —
yield让 UI 可以即时显示文字和工具进度 - 可取消 — 调用方可以
.return()或.throw()中止循环 - 背压控制 — 消费方可以控制消费速度
6.3 四级压缩策略
当上下文接近上限时,Claude Code 逐级压缩:
6.4 QueryDeps 依赖注入
// src/query/deps.ts — 依赖注入,方便测试
type QueryDeps = {
streamMessages: (...) => AsyncIterable // LLM API 调用
uuid: () => string // UUID 生成
autocompact: (...) => Promise<...> // 自动压缩
microcompact: (...) => Promise<...> // 微压缩
// ...
}
// 生产环境使用真实依赖
function productionDeps(): QueryDeps { ... }
// 测试时可以注入 mock
const testDeps: QueryDeps = {
streamMessages: mockStreamMessages,
uuid: () => 'test-uuid',
autocompact: mockAutoCompact,
// ...
}
这种设计让核心循环可以独立测试,不需要真实的 API 调用。
6.5 完整数据流
七、关键文件索引
| 文件 | 行数 | 职责 |
|---|---|---|
src/query.ts | 1729 | 核心 agent loop 主循环 |
src/QueryEngine.ts | 1295 | 对话生命周期管理 |
src/services/tools/toolOrchestration.ts | ~200 | 工具分区并发编排 |
src/services/tools/toolExecution.ts | ~150 | 单个工具执行 |
src/services/api/claude.ts | ~500 | LLM API 流式调用 |
src/services/api/withRetry.ts | ~100 | API 重试(指数退避) |
src/services/compact/autoCompact.ts | ~200 | 自动上下文压缩 |
src/services/compact/compact.ts | ~300 | 压缩核心逻辑 |
src/services/compact/microCompact.ts | ~150 | 微压缩 |
src/query/config.ts | ~100 | QueryConfig 不可变配置 |
src/query/deps.ts | ~50 | 依赖注入类型定义 |
src/query/tokenBudget.ts | ~80 | Token 预算控制 |
src/cost-tracker.ts | ~100 | Token 成本追踪 |
更多推荐



所有评论(0)