最近在研读 ClaudeCode 源码,其中 Agent 核心循环的设计堪称精巧,读完收获很大。这篇文章就从上到下完整梳理一遍核心循环的代码逻辑,既做一次学习记录,也为后续自己做类似项目积累可直接借鉴的设计思路。
全局概览
开始
│
▼
┌──────────────────┐
│ 1. 初始化 │
│ 状态/预取 │
└────────┬─────────┘
│
▼
┌────────┐
◀───│ while │─── 继续 ◀─────┐
│ (true) │ │
└────┬───┘ │
│ │
▼ │
┌──────────────────┐ │
│ 2. 上下文管理 │ │
│ (多层压缩链) │ │
└────────┬─────────┘ │
│ │
▼ │
┌──────────────────┐ │
│ 3. 调用 AI 模型 │ │
│ 流式处理 │ │
└────────┬─────────┘ │
│ │
▼ │
┌───────────┐ │
│ 有工具调用?│ │
└─────┬─────┘ │
是 │ 否 │
▼ │ ▼ │
┌──────┐ │ ┌──────┐ │
│执行 │ │ │后处理│ │
│工具 │ │ │返回 │ │
└──┬───┘ │ └──────┘ │
│ │ │
└─────┴──────────────────────┘
│
▼
┌──────────────────┐
│ 4. 组装并递归 │
│ state = next │
└────────┬─────────┘
│
└── continue ──▶ while(true)
初始化循环状态
state:记录了循环过程中所有可变的上下文信息,确保多轮循环的连贯性。
详情如下:
let state: State = {
//完整对话历史
//工具执行上下文
//最大输出token数
//自动压缩跟踪状态
//是否已激活停止钩子
//最大输出token恢复次数
//应急压缩标记
//当前轮数
//待处理工具使用摘要
//上一次迭代继续的原因
}
主循环
就是一个 while(true) 循环、进入循环可以依次看到:
上下文管理
压缩机制:
- applyToolResultBudget: 工具结果存储优化,过大结果截断并持久到磁盘
- snipCompact :裁剪过旧的工具结果内容,精简历史消息。
- microcompact:超时自动卸载缓存的工具结果到磁盘,释放内存。
- contextCollapse:如果上下文折叠特性开启,执行折叠(智能折叠代码块、测试文件等)
- autocompact:执行自动压缩,生成摘要替换大量历史消息。返回压缩结果和连续失败次数。
上下文压缩后续代码还有一层:reactive compact(应急压缩 ),相关逻辑在无工具调用章节错误处理部分。
构建系统提示词:
系统提示分为两部分拼接,结构非常清晰:
- systemPrompt:AI 身份、能力、MCP 工具规则、输出风格、语气等;
- systemContext:动态的环境信息,此上下文会添加到每段对话开头,并在对话期间进行缓存。包括 Git 状态、缓存破坏器(仅 Ant 环境,用于强制刷新缓存)等。
const fullSystemPrompt = asSystemPrompt(
appendSystemContext(systemPrompt, systemContext),
);
LLM 调用与流式处理
处理完上下文之后,就该调用LLM了。
调用 LLM:callModel(一大堆参数)
for await (const message of deps.callModel({ // ... 一大堆参数
messages: prependUserContext(messagesForQuery, userContext), //把用户上下文加到消息列表前面
systemPrompt: fullSystemPrompt, // 系统提示
thinkingConfig: toolUseContext.options.thinkingConfig, // 思考配置
tools: toolUseContext.options.tools, // 工具列表
signal: toolUseContext.abortController.signal, // 终止信号
options: { // 模型调用选项
// 获取工具权限上下文(异步)
// 当前使用的模型
// 快速模式(如果启用)
// 工具选择策略
// 是否非交互式会话
// 回退模型 :当主模型不可用 时(比如 Claude Opus 负载过高),自动切换到的 备用模型 (比如 Claude Sonnet)
// 查询来源
// Agent 定义
// 追加系统提示
// 最大输出 token 数
// MCP 工具列表
// 查询追踪
// 努力值和建议模型
// 跳过缓存写入 是否跳过缓存写入
// 任务预算
}))
消息处理逻辑:
- if (streamingFallbackOccured):如果发生流式回退,将之前生成的消息标记为"墓碑"(从界面移除),清空所有收集的消息,重置状态;
- 为工具调用块的输入字段;
- 检查消息是否是可恢复的错误(提示过长、最大输出 token 等)。
- 收集助手消息和工具调用块;
- 处理模型回退错误 切换到回退模型,处理已生成的消息;
回退模型 :当主模型不可用 时(比如 Claude Opus 负载过高),自动切换到的 备用模型 (比如 Claude Sonnet)。
- 处理模型调用错误: 记录错误,返回模型错误状态(退出循环)
后处理与恢复:
-
执行后采样钩子
executePostSamplingHooks,比如会话记忆(sessionMemory)/技能改进(skillImprovement); -
处理流式中断: 用户按 Escape 时,尽可能优雅地停止一切正在做的事情,给用户一个清晰的反馈,然后退出这一轮对话;
-
从上一轮对话中生成工具使用总结
pendingToolUseSummary。实现方法:这里使用的是 Claude 轻量级模型(Haiku),快速生成摘要。Haiku(约1秒),而主模型的流式响应需要 5-30 秒。
无工具调用时的处理
如果没有工具调用,处理最后一条消息,阻断式编程,先进行多种错误拦截与恢复:
-
处理 isWithheld413:prompt-too-long 错误,
-
首先尝试 context-collapse 恢复(上下文折叠)
-
如果 context-collapse 无法恢复,尝试 reactive compact(应急压缩 )
- 如果成功,更新状态并继续
- 如果失败,产出错误消息并退出return { reason: 'prompt_too_long' }
-
-
处理isWithheldMaxOutputTokens:max_output_tokens 错误:首先尝试升级重试(从 8k 提升到 64k),如果不行,进行多轮恢复(最多 3 次),如果恢复耗尽,产出错误消息
-
执行 stop hooks:
-
Token 预算控制机制:
- 预算未用完:添加提示消息 → 重置恢复计数 → 继续下一轮
- 预算已用完:记录完成事件 → 退出循环
- 边际收益递减:提前停止 → 记录日志 → 退出循环
如果没有错误,就正常完成 return { reason: 'completed' }, 退出循环
有工具调用时的处理
两种执行模式 ,根据是否使用了流式工具执行,选择不同的工具执行路径:
- 流式执行 vs 批量执行 :
| 执行模式 | 触发条件 | 执行逻辑 | 优势 |
|---|---|---|---|
| 流式执行(Streaming) | 存在 streamingToolExecutor | 多工具并行执行,谁先完成谁先返回结果 | 速度快,响应流畅,适合多工具调用场景 |
| 批量执行(Batch) | 无流式执行器 | 工具按顺序执行,所有工具完成后统一返回结果 | 逻辑简单,适合工具间有依赖的场景 |
-
批量工具执行完成后生成工具使用摘要,并将该摘要传递至下一次递归调用
- 提取最后一条助手消息的文本作为上下文
- 收集工具信息用于生成摘要
- 提取结果内容
- 生成工具使用摘要
generateToolUseSummary(异步,不阻塞下一轮 API 调用):生成摘要后保存到nextPendingToolUseSummary,在下一轮使用之前就可以生成.
-
处理工具执行中断:
- 清理计算机使用状态
- 产出中断消息
- 如果达到最大轮数,产出提示
- 返回工具中断状态 return { reason: 'aborted_tools' } // 退出循环
上下文组装与迭代
最后的阶段,确保了 Agent 的上下文能够无缝传递给下一轮:
-
获取命令队列快照,按优先级筛选 。主线程只取无 agentId 的命令,子 Agent 只取针对自己的任务;
-
产出附件消息(文件变更、任务通知、记忆、技能等)
-
记忆预取和消费
pendingMemoryPrefetch,保证记忆不被重复使用; -
注入 skill :注入技能发现(Skill Discovery)的结果;
-
从命令队列中移除已消费的命令;
-
刷新 mcp 工具:在每次轮次之间刷新工具,使新连接的MCP服务器可用;
-
轮次计数:每执行一轮工具调用,轮次计数 +1;
-
主 Agent 生成任务摘要(用于
claude ps); -
检查是否达到最大轮数限制
-
构建下一轮循环状态 state:
- 合并所有消息
messages,toolUseContext等; - 重置恢复计数器;
- 传递待处理的工具摘要,把本轮生成的摘要传给下一轮:
pendingToolUseSummary: nextPendingToolUseSummary; - 设置迭代原因为"下一轮",回到循环开头。
- 合并所有消息
到这里整个循环逻辑就从上到下走了一遍。
循环关键返回值总结
| 返回值 | 含义 |
|---|---|
{ reason: 'completed' } | 正常完成 |
{ reason: 'aborted_streaming' } | 流式中断 |
{ reason: 'aborted_tools' } | 工具执行中断 |
{ reason: 'blocking_limit' } | 超过阻塞限制 |
{ reason: 'prompt_too_long' } | 提示过长 |
{ reason: 'max_turns' } | 达到最大轮数 |
{ reason: 'stop_hook_prevented' } | stop hook 阻止 |
{ reason: 'hook_stopped' } | hook 停止 |
借鉴设计思想
整个代码看下来,不愧是“宇宙最强“,觉得可以借鉴一二:
- 多层上下文压缩:层层压缩,保证多轮对话质量与成本平衡;
- 流式工具执行:调用工具极快,在 LLM 生成的过程中,有些工具已经执行完毕。 体验极快,只是工程复杂度更高;
- 错误处理与恢复:多种错误处理与兜底、重试机制等;
- 异步处理: 工具摘要、记忆预取、技能刷新都异步做,不阻塞主交互流程。
最后也分享个彩蛋。在源代码开头有一段Claudecode开发者的注释,可以看到其精神状态,也是相当幽默,估计也踩过很多坑。取了其中一段翻译一下:
年轻的巫师,请谨记这些规则。它们是thinking的法则,而thinking的法则即是宇宙的法则。倘若你不遵守这些规则,将会遭受整日调试代码、抓耳挠腮的惩罚。
本人水平有限,文章难免有不严谨之处,后续还会继续深挖学习。