当大多数人谈论 AI Agent 架构时,ReAct(Reasoning + Acting)几乎是唯一的答案。但 Claude Code 选择了一条不同的路——Async Generator 状态机。这个设计决策背后有着深刻的思考,它解决了 ReAct 的根本性限制,为流式交互和优雅恢复奠定了基础。
导读:ReAct 的困境
如果你熟悉 AI Agent 开发,一定对 ReAct 模式不陌生:
1 | 思考(Thought) → 行动(Action) → 观察(Observation) → 思考 → ... |
这个模式直观且易于理解,已经成为 LangChain、AutoGPT 等框架的标配。但当你深入使用时,会发现它有几个难以回避的问题:
问题一:串行瓶颈
每一轮”思考”必须等待模型生成完整响应后才能开始执行工具。用户盯着屏幕等待,体验割裂。
问题二:无法利用流式传输
模型支持流式输出,但 ReAct 模式下,流式传输的价值被大大削弱——你必须等待完整的 action 才能执行。
问题三:恢复困难
当 API 超时、Token 溢出或工具失败时,ReAct 没有统一的状态表示来支持自动恢复。
Claude Code 的解决方案是:放弃 ReAct,使用 Async Generator 状态机。
一、状态机的核心设计
1.1 State 数据结构
src/query.ts 定义了状态机的核心状态(第 204-217 行):
1 | type State = { |
关键洞察:transition 字段记录了每一轮状态转换的原因。这使得调试和测试变得非常清晰——你可以精确知道为什么 Agent 从一个状态跳转到另一个状态。
1.2 核心循环:五个阶段
整个 while (true) 循环(第 307-1728 行)分为五个阶段:
1 | ┌─────────────────────────────────────────────────────────────┐ |
1.3 状态更新的优雅之处
这是整个设计最优雅的部分——通过状态赋值而非递归调用驱动循环:
1 | // src/query.ts:1715-1728 |
没有递归,没有回调地狱,只是简单的 state = next 然后 continue。
为什么这很重要?
- 内存稳定:不会因为深度递归导致栈溢出
- 状态可追溯:每一轮的状态转换原因都被记录
- 恢复可控:任何阶段的错误都可以通过修改 state 来恢复
二、流式优先的执行模型
2.1 StreamingToolExecutor 的设计
Claude Code 的一个关键创新是 StreamingToolExecutor——当模型生成 tool_use 块时,工具立即开始运行,而不是等模型生成完整响应。
1 | // src/services/tools/StreamingToolExecutor.ts |
对比 ReAct:
| 模式 | 工具执行时机 | 用户体验 |
|---|---|---|
| ReAct | 等待模型完整响应 | 割裂,需要等待 |
| Async Generator | 流式传输中即时执行 | 流畅,实时反馈 |
2.2 工具编排策略
工具执行不是简单的逐个运行,而是有精心设计的编排策略(src/services/tools/toolOrchestration.ts):
1 | 工具调用列表 |
设计原理:
- 只读工具没有副作用,可以安全并行
- 写入工具可能相互影响,必须串行保证顺序
- 10 个并发限制防止资源耗尽
三、六种故障恢复策略
这是 Claude Code 最精妙的设计之一。核心循环内置了 6 种恢复策略,确保用户体验的稳定性:
3.1 恢复策略详解
| 恢复策略 | 触发条件 | 恢复方式 |
|---|---|---|
collapse_drain_retry |
prompt 过长 | 排空已暂存的上下文折叠,重试 |
reactive_compact_retry |
仍然过长 | 通过 Claude 生成摘要,重试 |
max_output_tokens_escalate |
触及 8k 默认限制 | 升级到 64k 限制重试 |
max_output_tokens_recovery |
触及任何限制 | 注入”继续”提示,重试(最多 3 次) |
stop_hook_blocking |
Stop 钩子阻塞 | 将阻塞错误注入上下文,重试 |
token_budget_continuation |
预算尚余 | 注入预算提示,继续执行 |
3.2 恢复代码示例
每种恢复都通过修改 state 实现:
1 | // 例:prompt 过长恢复 |
3.3 为什么这些恢复策略重要?
想象一个场景:用户正在让 Claude 修改一个大型代码库,对话已经进行了 50 轮,积累了大量上下文。突然:
- Token 溢出 → 自动压缩,用户无感知
- API 超时 → 自动重试,用户无感知
- 模型达到输出限制 → 注入”继续”,自动续写
用户几乎感觉不到任何中断。这是 Claude Code 能提供流畅体验的关键。
四、与 LangChain Agent 的具体差异
4.1 代码对比
LangChain Agent(简化):1
2
3
4agent = initialize_agent(tools, llm, agent="zero-shot-react-description")
result = agent.run("do something")
# 内部:LLM → parse → tool → LLM → parse → tool → ... → final answer
# 每一步都是独立的 LLM 调用
Claude Code Agent(简化):1
2
3
4
5for await (const msg of query({ messages, tools, systemPrompt })) {
yield msg // 实时产出消息
// 内部:流式 LLM → 流式工具执行 → 状态更新 → 继续
// 单次 API 调用可以触发多个工具,工具在流式中执行
}
4.2 关键差异
| 维度 | LangChain | Claude Code |
|---|---|---|
| 每一轮 | 独立的 LLM 调用 | 流式 API 调用 |
| 工具解析 | OutputParser 解析文本 | 原生 tool_use 块 |
| 执行方式 | 等待完整响应 | 流式即时执行 |
| 错误处理 | 手动 try-catch | 内置 6 种恢复 |
| 并行工具 | 需要显式编排 | 自动分区并行 |
4.3 与 LangGraph 的对比
LangGraph 是 LangChain 的升级版,引入了图结构:
| 维度 | LangGraph | Claude Code |
|---|---|---|
| 状态流转 | 显式图节点 + 边 | 隐式状态机(while + continue) |
| 可视化 | 可导出为图 | 状态转换原因可追溯 |
| 持久化 | Checkpoint + State | 文件系统 + 消息历史 |
| 人机交互 | interrupt_before/after | 权限系统 + 钩子 |
| 多 Agent | 需要显式编排 | AgentTool 统一接口 |
Claude Code 的优势在于简单性——不需要定义图结构,一个 while 循环就能处理所有情况。
五、设计原则总结
从源码分析中,我们可以总结出以下核心设计原则:
5.1 最小抽象原则
与 LangChain 的”万物皆抽象”不同,Claude Code 的核心只有:
- 一个循环(
while (true)inquery()) - 一个状态(
State对象) - 一个接口(
Tool类型)
没有 Agent → AgentExecutor → Chain → Memory → Callback 的嵌套抽象层。
5.2 原生 API 集成
Claude Code 直接使用 Anthropic API 的原生能力:
- 原生工具调用:无需 OutputParser,直接使用
tool_use块 - 原生流式传输:无需包装层,直接消费 SSE 流
- 原生缓存:利用 API 的 prompt caching 特性
- 原生思维链:直接使用 extended thinking
这避免了”框架税”——LangChain 等框架在 LLM 和开发者之间增加的抽象层。
5.3 可观测性设计
transition 字段的设计体现了对可观测性的重视:
1 | type Continue = { |
每一轮循环都知道自己为什么继续,这对于调试和测试至关重要。
六、关键源文件索引
| 文件 | 行数 | 职责 |
|---|---|---|
src/query.ts |
~1730 | Agent 主循环,状态机核心 |
src/QueryEngine.ts |
~687 | 高层封装,对外 API |
src/services/tools/StreamingToolExecutor.ts |
~200 | 流式工具执行器 |
src/services/tools/toolOrchestration.ts |
~150 | 工具编排策略 |
src/query/transitions.ts |
~50 | 状态转换类型定义 |
src/query/tokenBudget.ts |
~100 | Token 预算管理 |
src/query/stopHooks.ts |
~200 | Stop 钩子处理 |
七、总结
Claude Code 的 Async Generator 状态机设计解决了 ReAct 模式的根本性限制:
- 流式执行:工具在模型生成过程中就开始运行
- 状态可控:统一的
State对象,恢复只需修改状态 - 自动恢复:6 种内置策略确保用户体验稳定
- 缓存友好:静态部分全局缓存,动态部分最小化
- 并行能力:只读工具自动并行,写入工具串行保序
这个设计选择体现了 Claude Code 团队对产品体验的深刻理解:用户不应该等待,也不应该因为技术问题中断。
系列文章导航: