一个 Agent 框架拆到最底层,只剩一个 while 循环:调模型、拿工具调用、执行工具、把结果喂回去、再调模型。三十行代码就能跑起来。
Claude Code 把这个循环写了几千行代码,不是在优化循环本身,是在补「出错了怎么办」——每一种可能的失败都有一条恢复路径,每一条恢复路径都有次数上限和降级策略。这些决策分支堆在一起,就是 Agent 的中枢层。
中枢管什么?拿到用户一句话之后,理解意图、拆任务、组织执行路径、调度工具、出问题兜底。不管记忆怎么存、不管上下文怎么压、不管工具具体怎么实现,而是全权负责管理解、规划、调度、控制。
一、中枢的职责
Claude Code 整体分五层,中枢是第二层,夹在交互入口和能力执行之间:
graph TD
A["交互入口层 CLI / IDE / SDK / Headless"]
A --> B
subgraph B["核心中枢层"]
IR[意图路由] --- WS[工作流选择] --- WE[工作流推进]
AD[Agent 调度] --- TD[工具调度] --- PG[权限门控]
HD[Hook 调度] --- RC[运行控制] --- RS[结果汇总]
end
B --> C
B --> D
E --> B
subgraph C["能力扩展层"]
C1[Built-in Tools] --- C2[Skills] --- C3[Subagents] --- C4[MCP Servers] --- C5[Hooks]
end
subgraph D["状态与记忆层"]
D1[Session] --- D2["CLAUDE.md"] --- D3[Auto Memory] --- D4[Settings]
end
subgraph E["安全控制层"]
E1[Permissions] --- E2[User Approval] --- E3["Allow/Deny Rules"] --- E4[企业策略]
end
中枢层有 9 个组件,切分原则只有一条:决策权在中枢,执行权在其他模块。
| 组件 | 职责 |
|---|---|
| Intent Router | 意图理解,把用户输入翻译成可执行路径 |
| Workflow Selector | 判断走哪种执行模式 |
| Workflow Engine | 推进执行循环,决定继续还是停止 |
| Agent Dispatcher | 分配Agent执行,判断哪些任务该拆给子代理 |
| Tool Dispatcher | 决定选哪个工具、什么时机调、能否并发 |
| Permission Gate | 决定何时触发权限检查、根据结果继续还是阻断 |
| Hook Dispatcher | 决定在哪些生命周期节点触发 Hook |
| Runtime Control | 出错后的重试、降级、熔断、补偿决策 |
| Result Synthesizer | 多轮工具调用后整合结果、判断任务是否完成 |
二、中枢引擎的物理实现
中枢的决策跑在一个具体的循环里。
2.1 中枢循环就是工作流引擎
Claude Code 的工作流没有独立引擎。中枢循环本身就撑起了全部工作流的推进:
while (model returns tool_calls):
execute tools
feed results back to model
return final text
这个循环意味着每一轮模型都在推进工作流——根据上一轮的结果决定下一步。没有预定义的工作流模板等着被填充,工作流是在执行过程中动态生成的。
2.2 事件驱动的双层循环
(1)分层
逻辑上是一个中枢循环,内部靠事件驱动模式来分发控制权。物理实现是两个文件、两层职责:
| 层 | 文件 | 职责 |
|---|---|---|
| QueryEngine | src/QueryEngine.ts | 会话管理:多轮状态、transcript 持久化、SDK 协议适配、usage 累积 |
| QueryLoop | src/query.ts | 单轮执行:API 调用、工具执行、错误恢复、终止判断 |
QueryEngine 不是引擎本身——引擎在 QueryLoop 里。QueryEngine 是包装器和外层的会话管家,它管会话的起点和终点,中间每一轮交给 QueryLoop 跑。
为什么拆成两层而不是一个循环?按生命周期拆分。跨会话恢复(关终端再打开、重连)需要保留会话历史重建执行状态。两层分离后,QueryEngine 持有跨轮状态(会话历史、usage、transcript),QueryLoop 只持有单轮执行状态。恢复会话时重新创建 QueryLoop,从 QueryEngine 拿历史——恢复不需要重新跑循环。
所有界面——CLI、Headless、SDK、IDE——汇聚到同一个 QueryLoop,共享同一条代码路径。一套核心逻辑,支撑所有入口;行为一致,维护成本不随入口数量增长。
(2)事件驱动
事件驱动模型。 中枢不是顺序执行完一个操作再做下一个,它通过预定义的生命周期事件来分发控制权。
中枢注册了以下关键事件:
- 用户消息到达 → QueryLoop 收到新输入
- 工具返回 tool_use → 触发工具分发和权限检查
- 模型返回 end_turn → 进入结果整合和停止判断
- API 错误或异常 → 进入恢复路径选择
- 用户中断 Ctrl+C → 触发级联取消
每个事件被 QueryEngine 截获后,分发到对应的处理分支——这就是隐含在 QueryLoop 里的 handler 机制。它没有用声明式的事件总线,而是用 while(true) 中不同的 continue 分支实现了同样的效果:事件到达 → 匹配 handler → 执行处理 → 更新状态 → 继续等待下一个事件。
这和 Linux epoll 的核心有异曲同工之妙。epoll 的核心就三件事:注册 fd 和感兴趣的事件 → epoll_wait 阻塞等待事件到达 → 事件到了,根据事件类型分发给对应的 handler 处理。handler 处理完更新状态,回到 epoll_wait 继续等待。queryLoop 做的事本质一样:注册了 5 种关键事件(用户消息到达、模型返回 tool_use、模型返回 end_turn、API 错误、用户中断),然后 while(true) 循环处理分发事件。
事件驱动带来的好处和 epoll 一脉相承:
- 主流程不会被具体业务逻辑打散
- 状态推进更加清晰
- 可以很好地处理长链路任务
- 中断、错误、重试都可以统一纳入事件模型
- 异步、高效
- 可插拔
2.3 AsyncGenerator:中枢的消息传输机制
queryLoop 是 AsyncGenerator,不是 async function。它是一个可暂停、可恢复的消息流。
AsyncGenerator 是异步生成器,可以边生产边产出数据,而不是等所有结果都准备好再一次性返回。
- 三个硬需求驱动了这个选择:
(1)背压 AsyncGenerator 让 queryLoop 每完成一个工具就 yield 一条消息出去,调用方按需消费——用户可以实时看到每一步的进展。如果换成 async function,只能等整轮结束才返回——模型连续调 10 个工具时,10 个结果全产出之后 UI 才能刷新,用户体验上就是「卡住了」。
(2)中断。 用户按下 Ctrl+C。AsyncGenerator 的 .return() 方法在 generator 内部触发 finally 块,级联关闭所有嵌套的子 generator。取消是级联的、确定的,不需要在每个函数调用链上手动传 AbortController。子 Agent 的 runAgent 也是 AsyncGenerator,中断同样级联到底。
(3)流式组合。 父 Agent 调用子 Agent 时,用 yield* 直接把子 Agent 的输出流嵌套进父 Agent 的流中——不需要中间缓冲区存储完整结果再转发。整条链的背压和中断沿同一条 AsyncGenerator 链传播。
把这三种能力放在一起看:AsyncGenerator 让中枢变成了一条可中断的消息管道,而不是一个「调一次、等一次」的同步函数。这是 Claude Code 能在长会话中保持流畅交互的根本原因。
2.4 隐式状态机:State 全量替换
queryLoop 内部没有 switch(state) 枚举,没有显式的状态转换图。它通过一个集中的 State 结构体做隐式状态管理:
State {
messages // 当前消息数组
toolUseContext // 工具上下文
autoCompactTracking // 压缩追踪
maxOutputTokensRecoveryCount // OTK 恢复重试次数(上限 3 次)
hasAttemptedReactiveCompact // 413 恢复守卫(每轮限 1 次)
maxOutputTokensOverride // OTK 升档后的 token 配额(8,192 → 65,536)
pendingToolUseSummary // 待处理摘要
stopHookActive // Stop Hook 守卫(阻断同一回复 ≤ 2 次)
turnCount // 当前轮次
transition // 上一轮为什么 continue
}
关键的工程决策不在有什么字段——在怎么更新 State:不增量修改,每个 continue 点都重建完整的新 State。
trade-off :为什么这条路更安全?
举个例子。假设第 N 轮触发流式降级到 fallback 模型,降级过程中需要丢弃部分消息、清除工具执行器、切换模型、重新调用。如果之前是用 state.messages.push(newMsg) 这样的增量修改,每条恢复路径都需要精确知道「之前改了哪些字段、要回退到哪个值」。一条路径,一套回退逻辑。七条恢复路径,七套回退逻辑。维护成本高,而且一旦某条路径漏掉了某个字段,就会导致状态不一致的 bug。
全量替换的方式下,每个 continue 点的 State 是自包含的:它声明了「这一轮之后,所有状态应该是什么」。 恢复路径不需要回退,只需要基于当前 State 重建一个新 State。N 条恢复路径共用一套替换逻辑。
这个选择的代价是每轮都要为 State 结构体做一次完整的内存分配。但在 Agent 循环里,每轮至少涉及一次网络调用(几十到几百毫秒),对比之下一次结构体分配的几百纳秒可以忽略不计。
三、意图路由
中枢的入口和出口。进来的是自然语言,出去的是结构化结果。
3.1 意图理解
Claude Code 的意图理解没有分类模型,没有路由表。模型本身就是路由器——它读取用户输入,根据自己对系统 prompt 的理解,判断接下来应该怎么走。
system prompt 里专门设计了行为约束段:"Doing tasks" 段定义了模型在收到请求后的行为边界——默认不加功能、不过度抽象、不设计假设性需求。"Executing actions with care" 段定义了执行破坏性操作前的审慎标准。CLAUDE.md 提供项目级上下文——同样一句「帮我加个登录功能」,在 Flask 项目里和在 Next.js 项目里有完全不同的约束。
它干的事不是「识别用户意图」。同一个需求在不同项目里,约束完全不同。它要做的是「在这个项目里、这个约束下,理解用户要干什么」。prompt 写得再详细,上下文和记忆给不到有效信息,理解也偏。
3.2 结果整合
多轮工具调用之后,中枢把分散在各轮的结果合并成最终输出。这不是简单拼接——模型从多轮工具调用中提取关键发现,丢弃中间过程,只输出用户关心的结论。
中枢在这里的判断是:「信息够了,可以输出了」还是「还需要再调一次工具」。当模型返回的不是 tool_use 而是 end_turn 文本时,中枢理解为任务完成。没有额外的完成度评分模块,没有「置信度 ≥ 0.9 才放行」的闸门——模型说完了就是完了。这个设计把「什么时候该结束」的判断也交给了模型,中枢只负责检查终止条件是否被触发。
四、任务拆解与执行规划
理解了意图后,接下来把模糊请求变成可执行方案。
4.1 任务拆解
从模糊需求到具体执行,第一步是拆。Claude Code 用 TodoWrite 作为任务追踪的脚手架。模型把用户需求拆成子任务列表,每次工具调用后更新状态:
TodoWrite(
todos: [
{ content: "添加登录路由", status: "pending" },
{ content: "实现 JWT 验证", status: "in_progress" },
{ content: "添加登录页面", status: "pending" },
{ content: "更新 API 文档", status: "pending" }
]
)
不让模型凭记忆记住做到了哪一步。让它用数据记住。任务拆解是模型的推理能力,但拆解结果的持久化和更新是工具的确定性能力。 模型负责拆,TodoWrite 负责记。
任务依赖与动态调整。 TodoWrite 不只是清单,它还能表达任务间的依赖关系。一个任务可以被标记为依赖另一个任务完成后才能开始。更重要的是,当执行过程中发现新信息时,模型可以动态调整——在列表里追加新任务、修改已有任务的描述、或者标记某个任务为「不需要了」。这种动态调整能力是固定流水线做不到的:规划不是一次性完成的,是在执行过程中持续修正的。
4.2 执行规划:模型即 Planner
Claude Code 没有独立的 Planner 模块——没有 DAG 编排器,没有预定义模板。模型自己决定「接下来该做什么」。
对比两种做法:
| 方案 | 做法 | 代价 |
|---|---|---|
| 独立 Planner | 先跑规划模型产出 DAG,再跑执行模型按图施工 | 多一次模型调用。更致命的是:规划和执行分离后,执行中遇到意外无法调整计划——因为 Planner 已经退出了 |
| 模型即 Planner | 模型每轮自己判断下一步 | 模型需要同时具备规划和执行能力 |
Claude Code 选后者。规划不是独立的阶段——它分布在每一轮的「我该调哪个工具」中。每次工具选择都是一次微规划。这是隐式规划——不需要显式的「规划阶段」,因为模型的推理能力已经足够把规划藏在工具调用的决策链里。
这种做法的前提是模型足够强。如果底层模型不够好,模型即 Planner 会暴露出规划质量不稳定的问题。但 Claude Code 的基线是 Claude 4.X 系列——这些模型的推理能力已经能稳定承担这个角色。
五、工作流选择与推进
任务拆完了,方向有了。中枢下一步是组织执行路径。Claude Code 的工作流没有一个是预定义的模板——它根据场景动态生成。
5.1 不写模板
Claude Code 没有「调试模板」「开发模板」「审查模板」。没有 DAG,没有固定步骤序列。模型每一轮根据当前上下文自己决定下一步该干什么。
对比两种做法:
| 方案 | 做法 | 代价 |
|---|---|---|
| 预定义工作流模板 | 为每种场景写固定的步骤序列:「调试 = 启动服务 → 调接口 → 看日志 → 改代码」 | 只能处理预期内的场景。遇到变体(日志在远程服务器上、没有测试接口)就卡住 |
| 生成式工作流 | 模型每轮根据当前上下文动态决定下一步 | 对模型的要求高。但同一个「调试 bug」的起点,路径可以完全不同 |
Claude Code 选后者。意图路由阶段准确理解场景之后,不套模板——让模型自己组织路径。
5.2 典型的工作流模式
虽然模型是自由推进的,但推进出来的路径往往收敛到几种典型模式。以下是高频出现的三种:
调试型工作流。 用户报了一个 bug。典型推进路径:
搜索报错信息 → 找到相关代码 → 读代码定位根因 → 修改 → 验证
假设用户说「登录接口返回 500」。中枢第一轮大概率用 Grep 搜索错误日志或报错关键词,发现堆栈指向 auth 模块。第二轮读 auth 模块的代码,发现 token 过期判断有一个边界条件没处理。第三轮用 Edit 修掉那个边界条件。第四轮跑测试验证修复。
关键特征:起点是「症状」(报错日志、异常行为),终点是「修复 + 验证」。中间每一轮的推进方向由新获得的信息决定——看日志发现涉及模块 A,下一轮就去读模块 A 的代码;读代码发现某个边界条件没覆盖,下一轮就去改。路径不是预先知道的,是一步一步挖出来的。
开发型工作流。 用户提了一个新功能需求。典型推进路径:
读文档/现有代码 → 找到待实现的接口或模块 → 补充实现 → 跑测试
假设用户说「给用户模块加上头像上传功能」。中枢前几轮大量读代码——读路由定义、读现有的文件上传逻辑、读用户模型——先摸清现状。然后定位到需要修改的三个文件,按依赖顺序逐个改:先扩展用户模型加字段,再加路由和上传处理,最后更新前端组件。
和调试型的区别是:起点是「目标」(要实现什么),不是「问题」(出了什么错)。开发型工作流的探索阶段比调试型更长——中枢在读代码、理解现状上的轮次更多。
探索型工作流。 用户问「这个项目里 X 是怎么做的」。典型推进路径:
搜索关键词 → 读核心文件 → 提取架构关系 → 报告结论
假设用户问「这个项目的权限校验是怎么实现的」。中枢搜 auth/middleware/permission 关键词,读几个核心文件,理出权限校验的调用链:middleware → auth service → permission checker → role resolver。最后输出一段结构化的描述。
和前面两种的关键区别是:不修改代码。工作流在「报告结论」这一步终止。
这三种不是配置项——中枢不显式选择「现在是调试模式」。模型在每一轮根据当前的上下文自动靠近某条路径。同一个会话中可以无缝切换:用户先问了一个探索性问题(探索型路径),紧接着说「那帮我改一下这里」(切换到开发型路径)。
5.3 工作流的终止判断
无论走哪条路径,中枢每轮结束时都做同一个判断:继续还是停?
继续: 模型返回了 tool_use。但不是无条件无限循环——某些恢复路径有次数上限,到了上限也会强制停止。
停止: 模型返回了 end_turn(任务完成)、用户按了 Ctrl+C、Hook 显式阻断、token 硬上限触发。十条终止路径中,只有 completed 一种正常退出。其他九种都是各种防线耗尽后的降级终止。
5.4 是否需要 workflow
在设计 Agent 系统中最容易做的设计是给每种场景写一个固定流程:「用户说 X → 走调试模板 → 按步骤 1234 执行」。这种做法稳定可控,但每遇到一个模板覆盖不了的场景就要加一个新模板,三个月后维护成本超过开发成本。
Claude Code 的选择是不写模板——把场景判断交给意图路由,把执行路径交给模型。前提是模型足够强。如果底层模型还达不到这个水平,一个折中是:高频场景用模板兜底,低频场景让模型自由推进。关键是中枢不把模板当成唯一的执行路径——模板是兜底策略,不是唯一策略。
六、调度控制
中枢推动工作流的过程中,有四个需要和其他模块协作的决策点。中枢只做决策那一半:什么情况触发、怎么判断结果。
6.1 Tool Dispatcher:怎么选工具、排顺序、定并发
模型返回的是结构化的 tool_use block——函数名 + 参数 JSON。中枢拿到之后做三件事。
选工具: 中枢不需要选——模型已经选了。每一轮模型从工具池中挑出它认为最合适的工具。但如果模型选的工具不在允许列表中(被 deny 规则过滤了),中枢直接拦截——工具调用不会到达执行层。
排顺序与并发决策: 模型一次返回多个 tool_use block 时,它们已经有序。中枢不重排顺序,但做并发判断——每个工具通过 isConcurrencySafe() 声明自己能否并行执行:
模型输出:[Read(a), Glob, Read(b), Edit, Grep]
中枢分区:
分区1(并行): [Read(a), Glob, Read(b)]
分区2(串行): [Edit]
分区3(并行): [Grep]
连续的并发安全工具组成并行分区,遇到非并发安全的就起新分区。Read、Grep、Glob 是只读操作所以天然可以并行;Edit 和 Write 不行。Bash 的执行要看具体命令。中枢的判断是:「这几步之间有没有数据依赖?没有就并行。」这个判断依靠工具自己声明的 isConcurrencySafe 属性——中枢不分析命令内容,只检查声明。
定时机: 默认走 StreamingToolExecutor——API 流式响应中每收到一个 tool_use block 立刻开始执行,不等待整个响应完成。这能利用模型流式生成的时间窗口,把多个工具调用的墙钟时间从 N 次串行压到约单次流式延迟。中枢的角色是决定走流式执行路径还是 fallback 执行路径。
6.2 Permission Gate:中枢什么时候触发权限检查
中枢在三个精确时机触发权限检查,不是「每次调工具前都检查」:
预过滤: 被 deny 规则拒绝的工具,在发给模型看之前就剔掉了。模型根本不知道这些工具存在,自然也不会调用它们。这是成本最低的权限控制——在被调用之前就扼杀了。
前置检查: 模型返回 tool_use 后,中枢查权限模式决定走什么路径。Plan 模式下每个工具调用都要用户确认——即便权限表里写了 allow。ByPass 模式下几乎不弹窗,但 deny 规则仍然生效——权限表覆盖不了的危险操作在 deny 层依然被拦截。
后置检查: PreToolUse Hook 在工具执行前跑最后一次检查。如果 Hook 返回 exit code 2,中枢直接阻断——不弹窗、不让用户绕过去。Hook 是中枢的最后一个决策点:前面的路线都已经跑完了,这个点可以一票否决。
中枢在这里只管两件事:在正确的时机触发检查,根据检查结果决定继续还是阻断。检查逻辑怎么写,中枢不关心。
6.3 Hook Dispatcher:中枢在什么生命周期点触发
Hook 触发后,中枢根据返回值做决策:exit 0 放行,exit 1 弹窗问用户,exit 2 硬阻断。中枢不关心 Hook 脚本怎么写的——只关心返回值。Hook 脚本在什么语言下实现、检测了什么模式、日志写了什么,都和中枢无关。
flowchart TD
Start([触发 Hook 脚本]) --> Check{检测 Exit Code}
Check -- "0 (Success)" --> ActionPass[静默放行]
ActionPass --> ResultPass[继续执行原计划]
Check -- "1 (Warning)" --> ActionAsk[弹窗询问用户]
ActionAsk --> ResultAsk[等待用户指令]
Check -- "2 (Error/Danger)" --> ActionBlock[硬阻断操作]
ActionBlock --> ResultBlock[中止执行并记录日志]
Hook的触发时间点:
| 生命周期点 | Hook 类型 | 用途示例 | 返回值影响 |
|---|---|---|---|
| 会话启动 | session_start | 初始化日志、设置环境 | 不影响主流程 |
| 调用 LLM 前 | pre_llm | 检测 prompt 是否包含敏感信息 | exit 1 → 弹窗确认 |
| 工具调用前 | pre_tool | 检测是否调用危险工具(如 execute) | exit 2 → 硬阻断 |
| 执行命令前 | pre_command | 检测 rm -rf /、sudo 等危险操作 | exit 2 → 硬阻断 |
| 编辑文件前 | pre_edit | 检测是否修改只读文件/核心配置 | exit 1 → 弹窗确认 |
| 会话结束 | session_end | 清理资源、统计 token | 不影响主流程 |
6.4 Agent Dispatcher:什么情况拆给子代理
中枢在三种场景下判断「这件事该拆出去」:
- 搜索验证类: 中间过程极多但最终结论很短。比如「找到项目里所有调用了旧 API 的地方」——可能要读三十个文件,结论就五个文件路径。中枢的判断是:这三十个文件的读取过程不值得污染主对话。
- 并行独立任务: 两个任务之间没有依赖关系。中枢可以同时 fork 多个子代理各自跑各自的部分——修 bug A 和修 bug B 互不干扰。
- 探索性工作: 不确定需要读多少文件、看多少代码。中枢用一个子代理去摸清情况,只拿结论回来,不把探索过程中的所有尝试都带进主上下文。
判断依据是任务的独立性和上下文成本——独立且不会污染主上下文就拆。具体的隔离和通信机制不在中枢层。
七、运行时控制
运行时控制指的是 Claude Code 中枢在执行任务过程中,动态干预、调整或中断执行流程的能力,包括应对失败、超时、异常等情况的能力。
恢复路径全景
七条恢复路径,从代价最低的开始排:
| # | 名称 | 异常场景 | 触发条件 | 代价 | 上限 |
|---|---|---|---|---|---|
| 1 | AutoCompact(自动压缩) | 上下文窗口接近上限 | token 占用达到阈值(约 83.5% 有效窗口) | 高(需额外 API 调用) | 连续 3 次失败熔断 |
| 2 | Collapse Drain(折叠消耗) | 上下文溢出,请求体超限 | API 返回 413 prompt_too_long | 零(复用已计算的折叠数据) | 无可折叠内容时直接跳过 |
| 3 | Reactive Compact(反应式压缩) | 上下文溢出且折叠空间不足 | 413 且 Drain 未能释放足够空间 | 中(额外 API 调用) | 每轮仅 1 次 |
| 4 | Fallback Switch(模型降级) | 主模型服务端过载 | Anthropic 后端主动发出降级信号 | 中(丢弃当前工具执行器,重建) | 无次数上限 |
| 5 | OTK Escalate(输出配额升档) | 模型输出被截断,首次触发 | stop_reason = max_tokens | 零(重试同一请求,无额外上下文变化) | 仅 1 次,配额 8,192 → 65,536 |
| 6 | OTK Recovery(多轮续接) | 升档后仍被截断 | 65,536 token 仍不足以完成输出 | 低(向模型注入续接提示) | 连续 3 次 |
| 7 | Stop Hook(Hook 阻断) | 用户自定义 Hook 拦截本轮输出 | PreToolUse / Stop Hook 返回 exit 2 | 低(将 Hook 的错误原因反馈给模型) | 同一回复最多阻断 2 次 |
这张表的核心逻辑是廉价本地操作优先,昂贵 API 调用靠后。Collapse Drain 和 OTK Escalate 是零额外成本的——中枢先试不花钱的办法,不行再逐步加码。
7.1 重试策略
不是所有错误都值得重试。中枢区分四类:
| 错误类型 | 中枢决策 | 原因 |
|---|---|---|
| 模型输出被截断(max_tokens) | 重试同一请求,升档配额 | 不是模型错误,是配额不够 |
| API 临时不可用(429/503) | 重试 + 退避 | 服务端抖动,等一下就好 |
| 模型侧过载 | 降级而非重试 | 切 fallback 模型比重试同一个过载的模型更可靠 |
| 上下文溢出(413) | 先压缩再重试 | 不是 API 故障,上下文太大 |
Claude Code 的做法是 OTK 升档(8,192 → 65,536),重试同一个请求,不增加轮次。 不改变上下文,不重新执行工具,只给模型更多 token 空间。模型一次连续输出中的思路是连贯的,截断了再续接,连贯性就丢了。给够配额比断掉重来划算。
7.2 降级策略
主模型不可用 → fallback 模型
流式输出中断 → 切非流式调用
所有模型不可用 → 终止
降级触发信号是模型侧过载时主动发出的降级请求,不是普通的 4xx/5xx 错误。中枢收到后不会再重试主模型——直接切 fallback。
降级期间的关键操作:清掉 UI 上残留的部分消息(tombstone 事件)、丢弃当前 tool executor 的缓存、创建新 executor。然后从同一轮从头调用——不是断点续跑,是整轮重新来过。
7.3 超时控制
Bash 命令超过 2 分钟默认自动后台化,不阻塞主循环。显式设置 timeout: 600000 则禁用后台化,让命令跑满 10 分钟。
会话级超时更狠:maxTurns 到达后强制终止,不管任务是否完成。这不是性能优化,是硬资源控制——防止一个会话无限跑下去。
7.4 熔断机制
每个自动恢复路径都有失败上限。不是可以无限重试的:
| 恢复路径 | 上限 | 超限行为 |
|---|---|---|
| OTK 恢复 | 连续 3 次 | 终止,返回错误 |
| Reactive Compact | 每轮 1 次 | 终止,reason=prompt_too_long |
| AutoCompact | 连续 3 次失败 | 降级为强制截断,截断再熔断就清空会话 |
| Stop Hook | 阻断同一回复 2 次 | preventContinuation 触发,放弃本轮 |
设计原则:自动恢复可以赌三次,赌不赢就认输。 无限重试不仅浪费 API 成本,还会遮蔽问题的真实根因——如果是 prompt 设计缺陷导致连续失败,重试一百次也不会好。生产数据曾发现 1,279 个会话有 50 次以上的连续 AutoCompact 失败,每天浪费约 25 万次 API 调用。熔断就是为了防止这种情况。
7.5 补偿机制
有些操作执行后需要修正上下文状态,否则会影响后续操作的正确性:
上下文修饰。 Bash 执行 cd /path 后,后续 Bash 命令自动在 /path 下执行——不需要每次重新切换目录。这个修饰只对非并发安全工具生效,并发工具不能互相修改上下文。
缺失 tool_result 补全。 用户 Ctrl+C 中断工具执行时,有些 tool_use block 已经发出去但结果还没来得及返回。如果就这样结束,消息数组会留下缺口——每个 tool_use 必须对应一个 tool_result,API 不接受不完整的消息数组。中枢在终止前自动补全缺失的 tool_result,确保契约完整。
7.6 人工介入
不同模式下的介入深度不同——不是「有弹窗」和「没有弹窗」两档:
| 模式 | 介入程度 | 中枢行为 |
|---|---|---|
| Plan Mode | 每阶段结束暂停确认 | 四阶段(Explore → Plan → Implement → Commit),每个阶段结束中枢不推进,等用户确认。Explore 只读,Plan 出方案,Implement 执行,Commit 提交。本质是给模型加一层确认壳,不改变模型即 Planner 的内核 |
| Default | 不可逆操作弹窗 | 暂停循环,等用户点击允许/拒绝 |
| AcceptEdits | 文件编辑自动通过,Shell 仍需确认 | 区分文件操作和 Shell 操作 |
| ByPass | 几乎不弹窗 | 跳过所有非 deny 检查,但 deny 规则仍生效 |
还有一个不讲道理的设计:权限永远不跨会话恢复。 每次新会话,信任从零开始。不是对用户不信任,是上次会话的安全上下文已不适用于新的上下文窗口——模型的推理状态不同了,上次被允许的操作换到新的上下文里可能是危险的。
7.7 质量门禁
两个检查点。第一个是任务完成度检查:模型发现自己开始重复读取已读过的文件、重复提已确认过的决策——这些都是上下文质量退化的信号。中枢的做法是在这些信号出现时推进压缩,而不是指望模型自己「克服」退化。
第二个是输出合规检查:PostToolUse Hook 在工具执行后核查产出。比如文件修改后 Hook 跑 linter,不通过就回退这个修改。中枢的角色是执行检查、收集返回值、根据返回值决定接受还是回退。
八、可以借鉴的设计思路
8.1 中枢与执行必须解耦
中枢只做决策,不做执行。这不只是架构上的整洁,有几个实际工程必要性:
- 可替换性: 工具实现可以独立升级,中枢的调度逻辑不需要动。如果中枢直接调用执行代码,每次换工具都要改中枢。
- 可测试性: 中枢的决策逻辑可以独立测试,把工具执行 mock 掉,验证中枢在各种错误条件下选了哪条恢复路径,不需要真的跑 Shell 命令或调 API。
- 并发安全: 中枢决定哪些工具并发、哪些串行,但不参与执行。中枢直接管理执行时,并发控制会和决策逻辑耦合,并发 bug 变成中枢 bug,极难定位。
- 权限边界清晰: Permission Gate 是中枢的一部分,但权限检查逻辑不在中枢。一旦权限检查逻辑混进中枢,权限规则的变更就会牵连中枢的其他决策。
- 跨平台复用: 同一套中枢决策逻辑可以在 CLI、IDE、SDK、Headless 模式下共用——正是因为中枢不直接处理 UI 渲染和工具执行,Claude Code 所有界面才能共享同一个 queryLoop。
- 防止单点问题: 使用事件机制让中枢更加的灵活,但是过重的负担会让中枢成为整个agent的瓶颈
8.2 生成式工作流比固定流水线更重要
Claude Code 最值得借鉴的不是某一条路径的设计——是路径可以运行时动态切换。调试、开发、探索不是三个按钮,是模型根据场景自动靠近的三种推进模式,同一个会话里可以无缝切换。自研 Agent 很容易做成「所有任务走同一条流水线」,但复杂任务和简单任务的中枢行为应该不同。
8.3 按生命周期拆分
QueryEngine 管会话状态,queryLoop 管单轮执行状态——这个双层分离是做持久化 Agent 的前提。会话状态和执行状态混在一起,跨会话恢复会很痛:要么全部清空,要么需要把整个执行状态序列化。分开之后,恢复会话时只需重建 queryLoop,历史从 QueryEngine 读取,执行不需要重新跑。
8.4 用全量替换代替增量修改管理状态
State 全量替换可以直接复用。自研 Agent 有多条错误恢复路径时,增量修改的问题是每条路径都需要自己写回退逻辑——七条路径,七套回退,漏掉一个字段就出 bug。全量替换让每个 continue 点只声明「这一轮之后所有状态是什么」,恢复路径只负责重建,不需要知道之前改了什么。
8.5 错误要扣住,到最终失败才暴露
可恢复的错误不应该实时暴露给上层调用方——上层一旦收到错误就会终止会话,但恢复循环可能正在进行中。Claude Code 把 413、OTK 这类中间错误扣住,等恢复链路全部跑完、确实无法恢复了才往上抛。自研 Agent 里这个细节很容易被忽略:框架层把错误直接透传,业务层断开连接,恢复尝试变成了无人在听的独角戏。
8.6 熔断与补偿
熔断机制-没有上限的自动恢复或者尝试是可怕的。
补偿机制——操作之后不修正上下文状态,一次错误的影响会级联到后续所有操作。
人工介入-粒度控制不是「弹窗」和「不弹窗」两个档位,而是不同模式、不同工具类型、不同程度。