Claude Code Agent Loop:拆解 AI 编程助手的心脏
目录
- 什么是 Agent Loop
- 核心循环:一个 while(true)
- 流式响应:模型还在说话,工具已经开始干活
- 并行工具执行:不是所有工具都能并行
- 工具生命周期:三道关卡
- 五层压缩:上下文永远不会爆
- 第 1 层:Tool Result Budget
- 第 2 层:History Snip
- 第 3 层:Microcompact
- 第 4 层:Context Collapse(实验性)
- 第 5 层:Autocompact
- 七种恢复路径:尽一切可能不中断
- 1. Prompt-Too-Long(413 错误)
- 2. Reactive Compact
- 3. Max Output Tokens 升级
- 4. Max Output Tokens 恢复
- 5. Stop Hook 阻断
- 6. Token Budget 续航
- 7. 正常的下一轮
- 模型降级:流量高峰也不掉线
- 工具摘要:Haiku 打辅助
- 错误隐藏:先自己消化,消化不了再告诉用户
- 全景图
- 为什么这个设计值得学习
- 写在最后
什么是 Agent Loop
你在终端里输入一句话,Claude Code 就开始干活——读文件、改代码、跑测试、修 bug,循环往复,直到任务完成。
这个”循环往复”的东西,就是 Agent Loop。
它是 Claude Code 的心脏。没有它,Claude 只是一个聊天机器人;有了它,Claude 变成了一个能自主完成复杂任务的编程 Agent。
简单说,Agent Loop 做的事情就一句话:
用户输入 → 调 API → 模型回复 → 如果要用工具就执行工具 → 把结果喂回去 → 继续调 API → 直到不需要工具为止。
看起来简单,但魔鬼在细节里。
核心循环:一个 while(true)
Claude Code 的 Agent Loop 本质上是一个 AsyncGenerator 函数:
export async function* query(
params: QueryParams,
): AsyncGenerator<StreamEvent | Message, Terminal>
用 Generator 而不是普通函数,是因为它需要在执行过程中不断 yield 中间结果(流式消息、工具进度、状态更新),同时保持循环状态。
核心结构长这样:
while (true) {
1. 准备消息(压缩、裁剪、折叠)
2. 调用 Claude API(流式)
3. 收集模型回复
4. 有 tool_use?→ 执行工具 → 收集结果 → continue
5. 没有 tool_use?→ 检查是否需要恢复 → 否则退出
6. 把 [原消息 + 助手回复 + 工具结果] 组装成新消息
7. 进入下一轮
}
整个循环里有 7 个 continue 出口——每一个都对应一种”还不能停,继续跑”的场景。
流式响应:模型还在说话,工具已经开始干活
Claude Code 不是等模型说完再执行工具,而是边流式接收回复边执行工具。
当模型返回的 content block 中出现 tool_use 类型时,StreamingToolExecutor 会立即把它加入执行队列:
if (message.type === "assistant") {
const msgToolUseBlocks = message.message.content.filter((content) => content.type === "tool_use");
if (msgToolUseBlocks.length > 0) {
toolUseBlocks.push(...msgToolUseBlocks);
needsFollowUp = true;
}
// 模型还在输出,工具已经开始跑了
if (streamingToolExecutor) {
for (const toolBlock of msgToolUseBlocks) {
streamingToolExecutor.addTool(toolBlock, message);
}
}
}
这意味着:当 Claude 在一次回复中调用了 3 个工具(比如同时读 3 个文件),第一个文件在模型还没说完话的时候就已经开始读了。
这就是为什么 Claude Code 感觉比想象中快。
并行工具执行:不是所有工具都能并行
StreamingToolExecutor 管理着工具的并发执行,但不是无脑并行——它区分两种工具:
- Concurrent-safe:可以和其他 concurrent-safe 工具并行跑(比如同时读多个文件)
- Exclusive:必须独占执行(比如写文件、跑 Bash 命令)
private canExecuteTool(isConcurrencySafe: boolean): boolean {
const executingTools = this.tools.filter(t => t.status === 'executing')
return (
executingTools.length === 0 ||
(isConcurrencySafe && executingTools.every(t => t.isConcurrencySafe))
)
}
每个工具被跟踪为一个状态机:
queued → executing → completed → yielded
结果按添加顺序返回(不是完成顺序),保证消息流的确定性。
工具生命周期:三道关卡
Claude Code 有 40+ 内置工具(Bash、Read、Edit、Grep、Glob、Agent 等),每个工具都遵循统一的生命周期:
validateInput() → checkPermissions() → call()
第一关:输入校验。 参数格式对不对?路径合不合法?不合法直接拒绝,不浪费一秒。
第二关:权限检查。 这个操作允许吗?权限系统是三层的:
- Hook 预检查(用户配置的 PreToolUse 钩子)
- 规则引擎匹配(settings.json 里的 allow/deny 规则)
- 用户交互确认(弹窗问你 Yes/No)
第三关:执行。 前两关都过了,才真正执行工具逻辑。
这套设计保证了安全性——Claude 永远不会”先斩后奏”。
五层压缩:上下文永远不会爆
长对话最大的敌人是上下文窗口限制。Claude Code 为此设计了五层递进的压缩体系:
第 1 层:Tool Result Budget
每条工具结果有大小上限。比如你 cat 了一个 10 万行的文件,它不会把 10 万行全塞进上下文,而是裁剪到合理大小。
第 2 层:History Snip
对历史消息做轻量裁剪,去掉已经不重要的旧对话片段。
第 3 层:Microcompact
缓存感知的优化——在不破坏 API 缓存命中率的前提下,对消息做微调压缩。
第 4 层:Context Collapse(实验性)
把多轮对话”折叠”成摘要,保留关键信息但大幅减少 token 数。这是 feature flag 控制的实验功能。
第 5 层:Autocompact
最后一道防线。当 token 数逼近阈值时,自动触发全量压缩:
const AUTOCOMPACT_BUFFER_TOKENS = 13_000;
export function getAutoCompactThreshold(model: string): number {
const effectiveContextWindow = getEffectiveContextWindowSize(model);
return effectiveContextWindow - AUTOCOMPACT_BUFFER_TOKENS;
}
还有个断路器:连续失败 3 次就停止重试,防止无限循环。
这五层就像一个漏斗,从细粒度到粗粒度,确保无论对话多长,上下文永远在可控范围内。
你用 /compact 手动触发的,其实就是第 5 层。
七种恢复路径:尽一切可能不中断
Agent Loop 里最精妙的部分是错误恢复机制。遇到问题不是直接报错退出,而是有 7 条恢复路径:
1. Prompt-Too-Long(413 错误)
消息太长 API 拒绝了?先尝试 Context Collapse 排水(便宜,保留细节),不行再触发 Reactive Compact(全量摘要)。
2. Reactive Compact
413 或者媒体文件太大时的紧急压缩。和 Autocompact 不同,这是”已经出错了才触发”的应急方案。
3. Max Output Tokens 升级
模型输出被截断了?自动升级到更高的 token 上限重试:
if (isWithheldMaxOutputTokens(lastMessage)) {
state = {
maxOutputTokensOverride: ESCALATED_MAX_TOKENS,
transition: { reason: "max_output_tokens_escalate" },
};
continue;
}
4. Max Output Tokens 恢复
升级了还是被截断?注入一条”请从断点继续”的消息,让模型接着写:
const recoveryMessage = createUserMessage({
content: "Output token limit hit. Resume directly...",
});
5. Stop Hook 阻断
Post-sampling hook 返回了阻断信号?把错误信息喂回模型,让它调整行为后重试。
6. Token Budget 续航
token 预算还没用完?注入一条 nudge 消息继续。
7. 正常的下一轮
工具执行完了,组装新消息,进入下一个循环迭代。
这 7 条路径的设计哲学很明确:能恢复就恢复,能继续就继续,用户感知到的中断越少越好。
模型降级:流量高峰也不掉线
当主模型(比如 Opus)负载过高时,Claude Code 有自动降级机制:
if (innerError instanceof FallbackTriggeredError && fallbackModel) {
currentModel = fallbackModel
// 清理已经产生的消息(变成"墓碑")
for (const msg of assistantMessages) {
yield { type: 'tombstone', message: msg }
}
// 清空状态,用降级模型重试
assistantMessages.length = 0
toolResults.length = 0
yield createSystemMessage(
`Switched to ${renderModelName(fallbackModel)} due to high demand...`
)
continue
}
注意”墓碑”机制——已经流式输出到终端的消息会被标记为 tombstone,UI 层会把它们移除。用户看到的是:原来的输出消失,换成降级模型的新输出。
工具摘要:Haiku 打辅助
每次工具执行完,Claude Code 会异步调用 Haiku(最快最便宜的模型)生成一句话摘要:
nextPendingToolUseSummary = generateToolUseSummary({
tools: toolInfoForSummary,
signal: toolUseContext.abortController.signal,
});
这个摘要在下一轮 API 调用之前 yield 出来,用于在终端显示进度提示。
关键词是异步——Haiku 的摘要生成和主模型的下一轮思考是并行的。Haiku 只需要 ~1 秒,而主模型一个 turn 通常 5-30 秒,所以摘要永远不会成为瓶颈。
错误隐藏:先自己消化,消化不了再告诉用户
Agent Loop 有一个很有意思的设计——错误隐藏(Withholding):
let withheld = false;
if (contextCollapse?.isWithheldPromptTooLong(message)) withheld = true;
if (reactiveCompact?.isWithheldPromptTooLong(message)) withheld = true;
if (reactiveCompact?.isWithheldMediaSizeError(message)) withheld = true;
if (isWithheldMaxOutputTokens(message)) withheld = true;
if (!withheld) {
yield yieldMessage; // 只有恢复失败才让用户看到
}
当出现 413、输出截断等可恢复错误时,Claude Code 不会立即把错误展示给用户。它先尝试自动恢复——压缩上下文、升级 token 上限、从断点续写。只有恢复全部失败,用户才会看到错误。
这就像一个好的客服:后厨出了问题,先自己解决,实在解决不了才告诉顾客。
全景图
把所有模块串起来,这就是 Claude Code Agent Loop 的完整架构:
用户输入
│
▼
┌─────────────────────────────────────────────┐
│ while (true) │
│ │
│ ┌─ 消息准备 ─────────────────────────────┐ │
│ │ Tool Result Budget → History Snip │ │
│ │ → Microcompact → Context Collapse │ │
│ │ → Autocompact │ │
│ └────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─ Claude API(流式)──────────────────┐ │
│ │ for await (message of callModel()) │ │
│ │ ├─ yield 流式文本 │ │
│ │ └─ 发现 tool_use → 立即入队执行 │ │
│ └──────────────────────────────────────┘ │
│ │ │
│ 有 tool_use? │
│ ├─ Yes ──────────────────────────────┐ │
│ │ ┌─ StreamingToolExecutor ───────┐ │ │
│ │ │ validateInput() │ │ │
│ │ │ → checkPermissions() │ │ │
│ │ │ → call() │ │ │
│ │ │ 并行/独占 按需选择 │ │ │
│ │ └───────────────────────────────┘ │ │
│ │ 收集 toolResults │ │
│ │ Haiku 异步生成摘要 │ │
│ │ 组装新消息 → continue │ │
│ │ │ │
│ └─ No ───────────────────────────────┘ │
│ ├─ 413?→ 压缩恢复 → continue │
│ ├─ 输出截断?→ 升级/续写 → continue │
│ ├─ Hook 阻断?→ 喂回错误 → continue │
│ ├─ 预算未尽?→ nudge → continue │
│ └─ 全部搞定 → return Terminal │
└─────────────────────────────────────────────┘
│
▼
任务完成
为什么这个设计值得学习
Claude Code 的 Agent Loop 不是学术论文里的理论框架,而是经过大规模生产验证的工程实现。几个值得借鉴的设计理念:
- Generator 模式:用 AsyncGenerator 实现”边执行边输出”,比回调或事件驱动更清晰
- 流式并行:不等模型说完就开始执行工具,最大化利用等待时间
- 分级压缩:五层递进而不是一刀切,在精度和成本之间找到平衡
- 静默恢复:能自愈的错误不暴露给用户,降低认知负担
- 状态机式工具管理:queued → executing → completed → yielded,清晰可追踪
- 优雅降级:主模型不可用时自动切换,用户几乎无感
如果你正在构建自己的 AI Agent,这些模式都可以直接借鉴。
写在最后
Agent Loop 是 Claude Code 最核心的模块,整个 query.ts 有 1700+ 行,是项目中最大的单文件之一。
但它的核心思想其实很简单:循环调用 → 执行工具 → 自动恢复 → 直到完成。 复杂的是边界情况的处理——而这些边界情况,恰恰是一个生产级 Agent 和一个 demo 的区别。
下次你在终端里看着 Claude Code 忙前忙后地改代码、跑测试、修 bug 的时候,你知道了——在它的心脏里,是一个永不轻言放弃的 while(true)。
相关推荐
Claude Code settings.json 详解(一):配置文件在哪里、谁说了算
全面介绍 Claude Code 的配置文件体系——五个配置来源的路径、优先级规则、数组合并与单值覆盖的区别、企业管理设置的多种下发方式。
Claude Code settings.json 详解(二):permissions 权限系统全解析
深入解析 Claude Code 的 permissions 配置——allow/deny/ask 三类规则、通配符语法、MCP 工具权限、defaultMode 各模式含义,以及 additionalDirectories 的作用。
Claude Code settings.json 详解(三):hooks 钩子全解析
深入解析 Claude Code 的 hooks 配置——四种钩子类型、核心事件(PreToolUse/PostToolUse/Stop/Notification)、stdin/stdout 协议、exit code 语义,以及实用配置示例。
Claude Code settings.json 详解(四):env、模型、认证与其他实用字段
全面介绍 Claude Code settings.json 中的 env 环境变量注入、模型配置、身份认证辅助、Git 提交署名、会话清理、语言与界面、思考深度、自动更新、记忆系统等实用字段。
Claude Code /agents 详解:自定义 AI 子代理,各司其职
详细介绍 Claude Code 的 /agents 命令——查看、管理和创建自定义 Agent,让不同任务由专门的 AI 角色来执行,从代码探索到架构规划各司其职。