第三章:Agent 循环——大脑如何思考与行动
这是整个 Claude Code 项目最核心的部分。理解了 Agent 循环,就理解了 Coding Agent 的本质。
本章深入query.ts的 1729 行代码,拆解"思考→行动→观察"循环的每一个环节。
目录
- 3.1 Agent 循环是什么?
- 3.2 核心文件关系图
- 3.3 函数签名与类型体系
- 3.4 单轮循环的 6 个阶段
- 3.5 Phase A:上下文准备——四层压缩管线
- 3.6 Phase B:API 调用——流式响应与工具提取
- 3.7 Phase C:后处理——错误恢复与回退
- 3.8 Phase D:无工具路径——停止判断与恢复策略
- 3.9 Phase E:工具执行路径——并行与串行
- 3.10 Phase F:状态推进——构建下一轮
- 3.11 十二种终止条件
- 3.12 依赖注入:可测试性的基石
- 3.13 不可变配置快照:QueryConfig
- 3.14 Token 预算系统
- 3.15 Stop Hooks:停止时的后处理管线
- 3.16 上下文管理:五级压缩体系
- 3.17 流式工具执行器:StreamingToolExecutor
- 3.18 QueryEngine:面向 SDK 的封装层
- 3.19 API 调用层:重试、回退与流控
- 3.20 预取与投机优化
- 3.21 关键工程思想总结
3.1 Agent 循环是什么?
一个 Coding Agent 的工作方式可以概括为一句话:
用户提问 → LLM 思考 → 调用工具 → 获取结果 → LLM 再思考 → … → 最终回复
这个"思考→行动→观察→再思考"的循环就是 Agent Loop。它是所有 AI Agent 系统的核心——无论是 Claude Code、Cursor 还是其他 Coding Agent,底层都遵循这个模式。
(Read/Edit/Bash...)"] Act --> Observe["获取结果"] Observe --> Think Decide -- "否" --> Reply[/"输出回复"/]
Claude Code 的独特之处在于它用 TypeScript 的 异步生成器(async function*)来实现这个循环,使得循环过程中可以实时向 UI 推送中间结果——LLM 的流式文本、工具调用的进度、状态变化事件等。
3.2 核心文件关系图
交互式 UI"] QE["QueryEngine.ts
SDK/Headless 入口"] end subgraph CoreLoop["核心循环层"] Q["query.ts
★ query() 异步生成器"] QL["query.ts
queryLoop() 内部循环"] end subgraph DI["依赖注入层"] Deps["query/deps.ts
QueryDeps 接口"] Config["query/config.ts
QueryConfig 快照"] Budget["query/tokenBudget.ts
Token 预算"] Hooks["query/stopHooks.ts
停止钩子"] end subgraph Execution["执行层"] API["services/api/claude.ts
queryModelWithStreaming()"] STE["services/tools/
StreamingToolExecutor"] Orch["services/tools/
toolOrchestration.ts"] Exec["services/tools/
toolExecution.ts"] end subgraph Context["上下文管理层"] Snip["services/compact/
snipCompact"] Micro["services/compact/
microCompact.ts"] Auto["services/compact/
autoCompact.ts"] SM["services/compact/
sessionMemoryCompact.ts"] Trad["services/compact/
compact.ts"] end REPL --> QE QE --> Q Q --> QL QL --> Deps QL --> Config QL --> Budget QL --> Hooks QL -- "Phase B" --> API QL -- "Phase E" --> STE QL -- "Phase E (fallback)" --> Orch STE --> Exec Orch --> Exec QL -- "Phase A" --> Snip QL -- "Phase A" --> Micro QL -- "Phase A" --> Auto Auto --> SM Auto --> Trad
| 文件 | 行数 | 角色 |
|---|---|---|
query.ts |
~1729 | ★ Agent 循环主实现 |
QueryEngine.ts |
~1177 | 类封装,管理会话生命周期 |
query/deps.ts |
~30 | 依赖注入接口(4 个核心依赖) |
query/config.ts |
~40 | 不可变配置快照 |
query/tokenBudget.ts |
~120 | Token 预算追踪与续写决策 |
query/stopHooks.ts |
~470 | 停止钩子处理(异步生成器) |
services/api/claude.ts |
~2200 | 流式 API 调用引擎 |
services/tools/StreamingToolExecutor.ts |
~531 | 流式工具执行器 |
3.3 函数签名与类型体系
公开入口:query()
1 | // query.ts, line 219 |
query() 是一个异步生成器函数——它通过 yield 推送中间结果,通过 return 返回终止原因。
内部循环:queryLoop()
1 | // query.ts, line 241 |
query() 是薄包装器,核心逻辑全部在 queryLoop() 中。
参数类型:QueryParams
1 | type QueryParams = { |
循环状态:State
1 | type State = { |
设计要点:循环使用 while(true) + state = { ... }; continue 而非递归调用,避免了长对话中的栈溢出风险。每次 continue 都携带一个 transition.reason,使得调试和测试可以精确追踪每次循环是因何继续的。
3.4 单轮循环的 6 个阶段
(lines 307-598)"] A1["Snip 历史裁剪"] A2["Microcompact 微压缩"] A3["Context Collapse 折叠"] A4["Autocompact 自动压缩"] A5["Blocking Limit 检查"] A1 --> A2 --> A3 --> A4 --> A5 end subgraph PhaseB["Phase B: API 调用
(lines 650-953)"] B1["构建请求参数"] B2["流式调用 LLM API"] B3["实时提取 tool_use 块"] B4["流式工具执行
(边流边执行)"] B1 --> B2 --> B3 --> B4 end subgraph PhaseC["Phase C: 后处理
(lines 864-997)"] C1["Fallback 模型回退"] C2["错误处理与恢复"] C3["中断信号检查"] end subgraph PhaseD["Phase D: 无工具路径
(lines 1062-1358)"] D1["Prompt-too-long 恢复"] D2["Max output tokens 恢复"] D3["Stop Hooks 执行"] D4["Token Budget 检查"] D5["正常完成 → return"] end subgraph PhaseE["Phase E: 工具执行路径
(lines 1360-1700)"] E1["执行剩余工具"] E2["生成工具摘要"] E3["收集附件消息"] E4["刷新工具列表"] end subgraph PhaseF["Phase F: 状态推进
(lines 1705-1728)"] F1{"超过 maxTurns?"} F2["构建新 State"] end Terminal(("return Terminal")) Start --> PhaseA --> PhaseB --> PhaseC PhaseC --> HasTools{"有工具调用?"} HasTools -- "否" --> PhaseD HasTools -- "是" --> PhaseE PhaseD --> Terminal PhaseE --> PhaseF F1 -- "是" --> Terminal F1 -- "否" --> F2 F2 --> Start
3.5 Phase A:上下文准备——四层压缩管线
每次调用 LLM 之前,都需要确保上下文不会超过 Token 限制。Claude Code 设计了一条四层压缩管线,按顺序执行:
删除中间旧消息"] Micro["② Microcompact
清除旧工具结果"] Collapse["③ Context Collapse
折叠上下文"] Auto["④ Autocompact
全量摘要压缩"] Snip --> Micro --> Collapse --> Auto end Auto --> Ready["压缩后的消息
→ 发送给 API"]
① Snip(历史裁剪)
1 | feature('HISTORY_SNIP') 门控 |
② Microcompact(微压缩)
微压缩在每次 API 调用前运行,替换旧工具结果的内容,不需要 LLM 参与。
1 | 可压缩的工具类型: |
③ Context Collapse(上下文折叠)
1 | feature('CONTEXT_COLLAPSE') 门控 |
④ Autocompact(自动压缩)
当 Token 数超过阈值时触发完整的摘要压缩:
1 | 阈值公式: |
Blocking Limit 检查
1 | 当 auto-compact 被关闭,且没有其他恢复手段时: |
3.6 Phase B: API 调用——流式响应与工具提取
流式调用流程
工具调用的提取
1 | // 从每个助手消息中提取 tool_use 块 |
关键变量 needsFollowUp 决定了循环的走向:
true:进入 Phase E(工具执行路径)false:进入 Phase D(无工具路径→准备终止)
Withholding 机制(错误扣留)
对于可恢复的错误(prompt-too-long、media-size、max-output-tokens),Claude Code 不会立即 yield 给 UI,而是扣留它们:
1 | 扣留的错误类型: |
这个设计使得 Agent 能够静默处理瞬态问题,用户只看到最终结果。
3.7 Phase C:后处理——错误恢复与回退
模型回退(Fallback)
清除累积状态
丢弃 StreamingToolExecutor"] Retry["重新开始内层 while 循环"] Surface["yield 错误消息
return model_error"] Stream --> Err Err -- "是" --> HasFB HasFB -- "是" --> Switch --> Retry HasFB -- "否" --> Surface Err -- "否,其他错误" --> Surface
FallbackTriggeredError 在以下场景触发:
- 连续 3 次 529(过载)错误
- 模型不可用
回退时的操作:
- 所有已 yield 的孤立消息标记为 Tombstone
- 清除工具块和助手消息
- 如果是内部用户,剥离 thinking 签名
- 切换模型,重试
通用错误处理
1 | try/catch 外层: |
中断信号检查
1 | if (abortController.signal.aborted) { |
'interrupt' 是特殊的中断原因——它表示用户在流式响应中提交了新消息(submit-interrupt),此时不需要显示中断提示。
3.8 Phase D:无工具路径——停止判断与恢复策略
当 LLM 没有调用任何工具时(needsFollowUp === false),循环准备终止。但在真正终止前,需要经过多道检查:
prompt-too-long?"} PTL -- "是" --> Collapse{"尝试 Context
Collapse Drain"} Collapse -- "成功" --> Continue1["continue: collapse_drain_retry"] Collapse -- "失败" --> Reactive{"尝试 Reactive
Compact"} Reactive -- "成功" --> Continue2["continue: reactive_compact_retry"] Reactive -- "失败" --> ReturnPTL["return: prompt_too_long"] PTL -- "否" --> MOT{"有扣留的
max_output_tokens?"} MOT -- "是" --> Escalate{"尝试提升到
64K Token"} Escalate -- "成功" --> Continue3["continue: max_output_tokens_escalate"] Escalate -- "失败" --> MultiTurn{"多轮恢复
(最多 3 次)"} MultiTurn -- "未耗尽" --> Continue4["continue: max_output_tokens_recovery"] MultiTurn -- "已耗尽" --> YieldErr["yield 扣留的错误"] MOT -- "否" --> ApiErr{"是 API 错误?"} ApiErr -- "是" --> ReturnOK["return: completed"] ApiErr -- "否" --> StopHook["执行 Stop Hooks"] StopHook --> Prevented{"被阻止?"} Prevented -- "是" --> ReturnHook["return: stop_hook_prevented"] Prevented -- "否" --> HasBlocking{"有阻塞错误?"} HasBlocking -- "是" --> Continue5["continue: stop_hook_blocking"] HasBlocking -- "否" --> TokenBudget{"Token 预算检查"} TokenBudget -- "继续" --> Continue6["continue: token_budget_continuation"] TokenBudget -- "停止" --> ReturnDone["return: completed"]
Max Output Tokens 恢复策略
分三步递进:
| 步骤 | 策略 | 说明 |
|---|---|---|
| 1 | Token 限额提升 | 从默认值升到 64K(ESCALATED_MAX_TOKENS),单次尝试 |
| 2 | 多轮恢复 | 注入 meta 消息"请直接继续,不要总结",最多重试 3 次 |
| 3 | 放弃 | yield 扣留的错误消息给用户 |
Stop Hooks 处理
Stop Hooks 是在 Agent 完成一轮回复后执行的后处理管线,详见 3.15 节。
3.9 Phase E:工具执行路径——并行与串行
当 LLM 调用了工具(needsFollowUp === true),进入工具执行路径:
等待所有工具完成"] HasSTE -- "否" --> RunTools["runTools()
传统编排器"] subgraph Legacy["传统编排:runTools()"] Partition["partitionToolCalls()
分批"] Concurrent["并行批次
all() 最多 10 并发"] Serial["串行批次"] Partition --> Concurrent Partition --> Serial end RunTools --> Legacy Remain --> Yield["yield 每个工具结果消息"] Legacy --> Yield Yield --> Summary["后台生成工具摘要
(Haiku 小模型)"] Summary --> AbortCheck{"中断检查"} AbortCheck -- "已中断" --> ReturnAbort["return: aborted_tools"] AbortCheck -- "未中断" --> Attach["收集附件消息"] subgraph Attachments["附件收集"] QCmd["队列命令快照"] FileNotif["文件变更通知"] Memory["记忆预取消费"] Skill["技能发现消费"] end Attach --> Attachments Attachments --> Refresh["refreshTools()
刷新工具列表"] Refresh --> MaxTurns{"超过 maxTurns?"} MaxTurns -- "是" --> ReturnMax["return: max_turns"] MaxTurns -- "否" --> NextState["构建新 State → continue"]
工具分类与并发策略
1 | LLM 一次可能返回多个工具调用: |
工具摘要的"错峰"生成
1 | 工具执行完成后,后台启动 Haiku 小模型生成摘要: |
3.10 Phase F:状态推进——构建下一轮
工具执行完成后,构建新的 State 对象:
1 | state = { |
注意:不是修改旧 State,而是构建全新对象。这保证了每次循环的状态是独立的。
3.11 十二种终止条件
Agent 循环通过 return Terminal 终止。以下是所有可能的终止原因:
| 终止原因 | 触发场景 | 类别 |
|---|---|---|
completed |
LLM 不再调用工具,任务自然完成 | 正常终止 |
max_turns |
达到 maxTurns 限制 | 安全阀 |
blocking_limit |
Token 超限且 auto-compact 关闭 | 资源限制 |
prompt_too_long |
上下文超长且所有恢复策略失败 | 资源限制 |
aborted_streaming |
用户在流式响应中中断 | 用户中断 |
aborted_tools |
用户在工具执行中中断 | 用户中断 |
model_error |
API 抛出未捕获异常 | 错误 |
image_error |
图片大小/格式错误 | 错误 |
hook_stopped |
工具级 Hook 阻止继续 | Hook 控制 |
stop_hook_prevented |
停止钩子明确阻止续写 | Hook 控制 |
以及通过 continue 继续循环的 9 种 transition(过渡原因):
| 过渡原因 | 说明 |
|---|---|
next_turn |
正常的下一轮 |
collapse_drain_retry |
Context Collapse 释放空间后重试 |
reactive_compact_retry |
响应式压缩后重试 |
max_output_tokens_escalate |
提升 Token 限额后重试 |
max_output_tokens_recovery |
多轮恢复重试 |
stop_hook_blocking |
Stop Hook 返回阻塞错误,需要模型修正 |
token_budget_continuation |
Token 预算未用完,注入 nudge 继续 |
fallback_retry |
模型回退后重试 |
model_switch |
模型切换后重试 |
3.12 依赖注入:可测试性的基石
接口定义(query/deps.ts)
1 | export type QueryDeps = { |
设计巧妙之处:类型定义使用 typeof 真实函数,确保接口签名与实现自动同步——如果 queryModelWithStreaming 的签名变化,QueryDeps.callModel 的类型会在编译期自动报错。
注入模式
1 | // 使用: |
为什么只有 4 个依赖?
这是渐进式重构的体现。注释中明确说明这是 proof-of-concept,后续可以扩展到 runTools、handleStopHooks、logEvent 等。选择这 4 个是因为它们是测试中最常被 mock 的函数。
3.13 不可变配置快照:QueryConfig
1 | export type QueryConfig = { |
关键设计决策
- 一次快照:
buildQueryConfig()在query()入口只执行一次,后续循环复用同一份配置 - 排除
feature()门控:feature()来自bun:bundle,必须留在使用处以支持 Tree Shaking(死代码消除) - 容忍过期:Statsig 门控本身使用
CACHED_MAY_BE_STALE语义,快照不引入额外过期风险
3.14 Token 预算系统
Token 预算(query/tokenBudget.ts)用于控制 Agent 在一次任务中消耗的总 Token 量:
子 Agent 不参与预算"] SubAgent -- "否" --> CalcPct["计算: turnTokens / budget"] CalcPct --> Under90{"< 90%?"} Under90 -- "是" --> Diminish{"连减检测:
连续 3 次 +
两次 delta < 500?"} Diminish -- "是" --> Stop2["stop (diminishing)"] Diminish -- "否" --> Continue["continue
注入 nudge 消息"] Under90 -- "否" --> Stop3["stop (budget reached)"]
关键常量
| 常量 | 值 | 含义 |
|---|---|---|
COMPLETION_THRESHOLD |
0.9 (90%) | 达到预算 90% 即认为"足够" |
DIMINISHING_THRESHOLD |
500 | 两次检查间 Token 增量低于此值视为"收益递减" |
Nudge 消息
当预算未用完但 LLM 已停止(end_turn),系统注入一条 nudge 消息:
“Stopped at X% of token target (N / M). Keep working – do not summarize.”
这条消息引导 LLM 继续工作而非过早总结。
3.15 Stop Hooks:停止时的后处理管线
Stop Hooks 在 LLM 完成回复后、循环终止前执行。它本身也是一个异步生成器,可以在执行过程中 yield 消息:
(供 /btw 命令使用)"] Job["模板任务分类
(写入状态到磁盘)"] Cache --> Job end subgraph Background["Phase 2: 后台任务(fire-and-forget)"] Suggest["提示词建议"] Memory["记忆提取"] Dream["自动整理"] Suggest ~~~ Memory ~~~ Dream end subgraph Hooks["Phase 3: 执行用户定义的 Stop Hooks"] ExecHook["executeStopHooks()"] Progress["progress 消息"] Attach["attachment 消息"] Block["blocking errors"] Prevent["preventContinuation"] ExecHook --> Progress & Attach & Block & Prevent end subgraph Teammate["Phase 4: 多 Agent Hooks(如果是 Teammate)"] TaskDone["TaskCompleted hooks"] Idle["TeammateIdle hooks"] TaskDone --> Idle end Result["返回 StopHookResult:
{ blockingErrors, preventContinuation }"] PreHook --> Background --> Hooks --> Teammate --> Result
StopHookResult 的处理
| 结果 | query.ts 的处理 |
|---|---|
preventContinuation = true |
return { reason: 'stop_hook_prevented' } |
blockingErrors.length > 0 |
注入错误消息,continue 让 LLM 修正 |
| 全部通过 | 进入 Token Budget 检查 |
3.16 上下文管理:五级压缩体系
Claude Code 的上下文管理是一个五级递进体系,从轻量到重量排列:
Traditional Compaction 的完整流程
(9 个必填段落模板)"] Stream["流式调用 LLM 生成摘要"] PTL{"摘要本身
prompt-too-long?"} PTL -- "是" --> Truncate["truncateHeadForPTLRetry()
删除最旧的消息组
(最多重试 3 次)"] Truncate --> Stream PTL -- "否" --> Strip["去除 analysis 标签
格式化摘要"] Strip --> PostAttach["生成后压缩附件"] subgraph Attachments["后压缩附件"] Files["重新读取最近 5 个文件
(每个 ≤5000 tokens)"] Plan["当前规划附件"] Skills["已调用技能附件
(每个 ≤5000 tokens)"] MCP["MCP 指令增量"] end PostAttach --> Attachments Attachments --> SessionStart["执行 SessionStart hooks
(重注入 CLAUDE.md 等)"] SessionStart --> Boundary["创建压缩边界标记"] Boundary --> Cleanup["runPostCompactCleanup()
重置 ~12 种缓存"] Cleanup --> Done(("完成"))
Session Memory Compaction vs Traditional
| 维度 | Session Memory | Traditional |
|---|---|---|
| 需要 API 调用 | 否 | 是 |
| 摘要来源 | 磁盘上的会话记忆文件 | LLM 实时生成 |
| 保留策略 | 保留最近 N 条消息(≥10K tokens, ≥5 条文本消息, ≤40K) | 全部替换为摘要 |
| 速度 | 毫秒级 | 秒级(取决于 API 延迟) |
| 质量 | 依赖已有的会话记忆质量 | 实时分析完整上下文 |
摘要模板的 9 个段落
Traditional Compaction 要求 LLM 按以下结构输出摘要:
1 | 1. Primary Request and Intent — 用户的主要目标 |
3.17 流式工具执行器:StreamingToolExecutor
StreamingToolExecutor 是 Agent 循环的性能优化核心——它允许在 LLM 还在输出后续内容时,提前开始执行已识别的工具。
工具生命周期
并发控制模型
都是 safe?"} CheckRunning -- "是" --> StartParallel["立即并行启动"] CheckRunning -- "否" --> Queue["排队等待"] IsSafe -- "否" --> CheckEmpty{"当前无运行中?"} CheckEmpty -- "是" --> StartExclusive["独占启动"] CheckEmpty -- "否" --> Queue Queue --> WaitComplete["等待运行中的工具完成"] WaitComplete --> ProcessQueue["processQueue() 重新扫描"]
中断级联架构
1 | toolUseContext.abortController ← 顶层:用户中断 |
输出顺序保证
getCompletedResults() 是同步生成器,它确保工具结果按原始顺序 yield,即使后面的工具先完成:
1 | 工具调用顺序: [A, B, C] |
3.18 QueryEngine:面向 SDK 的封装层
QueryEngine 是 query() 的类封装,为 SDK/Headless 场景提供完整的会话生命周期管理:
(斜杠命令/文件附件)"] T3["3. 持久化到 transcript"] T4["4. 调用 query() 异步生成器"] T5["5. for await 消费输出"] T6["6. 转换为 SDKMessage yield"] T7["7. 生成最终 result"] T1 --> T2 --> T3 --> T4 --> T5 --> T6 --> T7 end SM --> Turn subgraph Switch["消息类型处理 (step 5 的 switch)"] SA["assistant → 推入历史 + yield"] SU["user → 推入历史 + yield"] SE["stream_event → 累加 usage"] SS["system → 处理压缩边界"] ST["tool_use_summary → yield"] end T5 --> Switch
便捷封装:ask()
1 | // 一次性使用的便捷函数 |
3.19 API 调用层:重试、回退与流控
withRetry:异步生成器式的重试
尊重 Retry-After"] Classify -- "529 Overloaded" --> Count529{"连续 529
≥ 3 次?"} Count529 -- "是" --> Fallback["throw FallbackTriggeredError"] Count529 -- "否" --> Backoff529["退避重试"] Classify -- "401/403" --> Refresh["刷新凭证
重建客户端"] Classify -- "5xx" --> Backoff5xx["退避重试"] Classify -- "Context Overflow" --> Adjust["调整 maxTokens
重试"] Classify -- "ECONNRESET" --> DisableKA["禁用 Keep-Alive
重试"] Backoff429 --> YieldMsg["yield SystemAPIErrorMessage
(UI 显示 'Retrying in Xs...')"] Backoff529 --> YieldMsg Refresh --> Attempt Backoff5xx --> YieldMsg Adjust --> Attempt DisableKA --> Attempt YieldMsg --> MaxRetry{"超过最大重试次数
(默认 10)?"} MaxRetry -- "否" --> Attempt MaxRetry -- "是" --> Throw["throw 最终错误"]
关键重试参数
| 参数 | 默认值 | 说明 |
|---|---|---|
| 最大重试次数 | 10 | 可通过 CLAUDE_CODE_MAX_RETRIES 覆盖 |
| 基础退避 | 500ms | 指数增长 |
| 最大退避 | 32s | 正常模式 |
| Jitter | 25% | 避免重试风暴 |
| 529 触发回退阈值 | 3 次连续 | 之后切换备用模型 |
| 流空闲超时 | 90s | 挂起的流会被自动终止 |
withRetry 为什么是生成器?
传统的重试函数只能在内部等待,调用方无法感知重试过程。而 withRetry 作为异步生成器,可以在等待期间 yield 重试状态消息给 UI,让用户看到 “Retrying in 5 seconds…” 而非黑屏等待。
1 | // 调用方式(在 queryModel 中): |
3.20 预取与投机优化
Agent 循环中隐藏了多处预取优化,将等待时间转化为有用计算:
四大预取机制
| 机制 | 启动时机 | 消费时机 | 目的 |
|---|---|---|---|
| 记忆预取 | 用户消息进入时(仅首轮) | 每轮工具执行后检查 | 提前加载相关记忆文件 |
| 技能发现 | 每轮迭代开始时 | 工具执行后收集 | 发现可用技能 |
| 流式工具执行 | LLM 流中识别到 tool_use 时 | 流式输出期间 & 流结束后 | 减少工具等待时间 |
| 工具摘要 | 工具批次完成后(后台) | 下一轮 Phase B 之后 | 利用模型思考时间生成摘要 |
记忆预取的去重
1 | 记忆预取返回的附件需要去重: |
3.21 关键工程思想总结
思想 1:迭代而非递归
循环使用 while(true) + state = {...}; continue 而非递归调用。这避免了:
- 长对话中的栈溢出(一次对话可能循环数百次)
- 递归调用的上下文保持开销
每次 continue 都携带 transition.reason,使得状态转换可追踪、可测试。
思想 2:异步生成器实现生产者-消费者
1 | query() 是生产者:yield 中间结果(流事件、消息、工具结果) |
思想 3:Withhold-then-Recover 模式
可恢复错误不立即暴露给用户,而是先扣留,尝试修复。只有修复失败才通知用户。
1 | 传统模式:错误 → 通知用户 → 用户决定 |
这在 prompt-too-long 和 max-output-tokens 两种场景中特别有效。
思想 4:分层压缩的"漏斗"模型
五级压缩从轻到重排列,形成漏斗:
1 | Snip ← 最轻:删除中间消息(无 API 调用) |
大多数情况下,前两层就足够了。只有上下文真正膨胀时才触发重量级压缩。
思想 5:依赖注入的"最小侵入"
只注入 4 个最常被 mock 的依赖,而非重构整个系统。这是实用主义的工程决策:
- 足够解决测试痛点
- 不引入过度抽象
- 为后续渐进式扩展留了口
思想 6:错峰并行
利用不同操作的时间特征实现并行:
1 | LLM 思考(5-30s)期间: |
思想 7:中断信号的三级级联
1 | 用户中断 → 顶层 AbortController |
思想 8:重试的可观测性
withRetry 作为异步生成器,在等待重试期间 yield 状态消息。这实现了:
- 用户可以看到 “Retrying in 5s…”
- UI 不会卡死
- 重试过程完全透明
思想 9:状态转换的显式性
每次 continue 都写入完整的 State 对象并携带 transition.reason。这带来了:
- 调试时可以看到"为什么这次循环没有终止"
- 测试可以断言特定的恢复路径被触发
- 日志可以追踪完整的状态转换链
思想 10:配置的"快照不可变"
QueryConfig 在入口处一次性快照,整个循环复用。这避免了:
- 循环中途配置变化导致的不一致
- 每轮重复读取配置的开销
- Statsig 门控的并发安全问题
下一步
- 第四章:斜杠命令系统——/resume、/clear、/compact 的完整实现剖析
- 第五章:工具系统——Agent 的双手
- 第六章:权限与安全——信任的边界
本文档基于 Claude Code 源码分析生成,核心源码路径:
claude-code/src/query.ts(1729 行)
