第三章:Agent 循环——大脑如何思考与行动

这是整个 Claude Code 项目最核心的部分。理解了 Agent 循环,就理解了 Coding Agent 的本质。
本章深入 query.ts 的 1729 行代码,拆解"思考→行动→观察"循环的每一个环节。


目录


3.1 Agent 循环是什么?

一个 Coding Agent 的工作方式可以概括为一句话:

用户提问 → LLM 思考 → 调用工具 → 获取结果 → LLM 再思考 → … → 最终回复

这个"思考→行动→观察→再思考"的循环就是 Agent Loop。它是所有 AI Agent 系统的核心——无论是 Claude Code、Cursor 还是其他 Coding Agent,底层都遵循这个模式。

flowchart LR User[/"用户提问"/] --> Think["LLM 思考"] Think --> Decide{"需要工具?"} Decide -- "是" --> Act["调用工具
(Read/Edit/Bash...)"] Act --> Observe["获取结果"] Observe --> Think Decide -- "否" --> Reply[/"输出回复"/]

Claude Code 的独特之处在于它用 TypeScript 的 异步生成器async function*)来实现这个循环,使得循环过程中可以实时向 UI 推送中间结果——LLM 的流式文本、工具调用的进度、状态变化事件等。


3.2 核心文件关系图

flowchart TD subgraph EntryLayer["入口层"] REPL["screens/REPL.tsx
交互式 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
2
3
4
5
6
7
8
9
10
11
// query.ts, line 219
export async function* query(
params: QueryParams,
): AsyncGenerator<
| StreamEvent // LLM 流式事件(文本 deltausage 等)
| RequestStartEvent // 每轮请求开始标记
| Message // 完整消息(助手/用户/系统)
| TombstoneMessage // 被废弃的消息标记
| ToolUseSummaryMessage, // 工具使用摘要(移动端 UI
Terminal // 终止原因(return 值)
>

query() 是一个异步生成器函数——它通过 yield 推送中间结果,通过 return 返回终止原因。

内部循环:queryLoop()

1
2
3
4
5
6
7
8
9
// query.ts, line 241
async function* queryLoop(
params: QueryParams,
consumedCommandUuids: string[],
): AsyncGenerator<
| StreamEvent | RequestStartEvent | Message
| TombstoneMessage | ToolUseSummaryMessage,
Terminal
>

query() 是薄包装器,核心逻辑全部在 queryLoop() 中。

参数类型:QueryParams

1
2
3
4
5
6
7
8
9
10
11
12
13
14
type QueryParams = {
messages: Message[] // 对话历史
systemPrompt: SystemPrompt // 系统提示词
userContext: Record<string, string> // CLAUDE.md、日期等
systemContext: Record<string, string> // git 状态等
canUseTool: CanUseToolFn // 权限判断函数
toolUseContext: ToolUseContext // 工具执行上下文
fallbackModel?: string // 备用模型
querySource: QuerySource // 请求来源标识
maxTurns?: number // 最大循环轮次
taskBudget?: { total: number } // Token 预算
deps?: QueryDeps // 可注入的依赖(测试用)
// ... 更多可选字段
}

循环状态:State

1
2
3
4
5
6
7
8
9
10
11
type State = {
messages: Message[] // 不断追加的消息列表
toolUseContext: ToolUseContext // 工具执行上下文(包含 abortController)
autoCompactTracking: AutoCompactTrack // 压缩追踪(轮次、连续失败数)
maxOutputTokensRecoveryCount: number // max_output_tokens 恢复尝试计数
hasAttemptedReactiveCompact: boolean // 响应式压缩是否已尝试
pendingToolUseSummary: Promise | null // 上一轮的工具摘要(后台计算)
stopHookActive: boolean // 停止钩子是否激活
turnCount: number // 当前轮次
transition: Continue // 上一次 continue 的原因
}

设计要点:循环使用 while(true) + state = { ... }; continue 而非递归调用,避免了长对话中的栈溢出风险。每次 continue 都携带一个 transition.reason,使得调试和测试可以精确追踪每次循环是因何继续的。


3.4 单轮循环的 6 个阶段

flowchart TD Start(("Turn 开始")) subgraph PhaseA["Phase A: 上下文准备
(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 设计了一条四层压缩管线,按顺序执行:

flowchart LR Raw["原始消息列表"] --> Snip subgraph Pipeline["压缩管线(每轮执行)"] Snip["① Snip
删除中间旧消息"] Micro["② Microcompact
清除旧工具结果"] Collapse["③ Context Collapse
折叠上下文"] Auto["④ Autocompact
全量摘要压缩"] Snip --> Micro --> Collapse --> Auto end Auto --> Ready["压缩后的消息
→ 发送给 API"]

① Snip(历史裁剪)

1
2
3
4
5
6
7
8
feature('HISTORY_SNIP') 门控

作用:删除对话中间范围的旧消息(不是头部,不是尾部,而是"中间")
触发:每轮循环开始时
输出:
- messages: 过滤后的消息数组
- tokensFreed: 释放的 Token 数(传递给后续阶段)
- boundaryMessage: 标记裁剪位置的系统消息

② Microcompact(微压缩)

微压缩在每次 API 调用前运行,替换旧工具结果的内容,不需要 LLM 参与。

1
2
3
4
5
6
7
8
9
10
11
12
可压缩的工具类型:
FileRead, Bash/Shell, Grep, Glob, WebSearch, WebFetch, FileEdit, FileWrite

替换策略:
旧工具结果内容 → '[Old tool result content cleared]'

三条路径(按优先级):
1. 时间基准微压缩:距上次助手消息 > 60 分钟时触发
(此时服务端 prompt 缓存已过期,可以大胆修改)
2. 缓存微压缩:使用 API 的 cache_edits 机制
(不修改本地消息,通过 API 参数告知服务端清除)
3. 无操作:交给 Autocompact 处理

③ Context Collapse(上下文折叠)

1
2
3
4
feature('CONTEXT_COLLAPSE') 门控

作用:将冗长的上下文投影为精简视图
特点:这是一个"读时投影",不修改原始消息

④ Autocompact(自动压缩)

当 Token 数超过阈值时触发完整的摘要压缩:

1
2
3
4
5
6
7
8
9
阈值公式:
effectiveWindow = contextWindow - min(maxOutputTokens, 20000)
threshold = effectiveWindow - 13000

执行优先级:
1. 先尝试 Session Memory Compaction(无 API 调用,使用磁盘上的会话记忆)
2. 失败则回退到 Traditional Compaction(调用 LLM 生成摘要)

熔断器:连续失败 3 次后跳过(防止死循环)

Blocking Limit 检查

1
2
3
4
5
6
当 auto-compact 被关闭,且没有其他恢复手段时:
阻塞阈值 = effectiveWindow - 3000

如果 Token 数超过此阈值:
yield 错误消息
return { reason: 'blocking_limit' }

3.6 Phase B: API 调用——流式响应与工具提取

流式调用流程

sequenceDiagram participant QL as queryLoop() participant API as claude.ts participant LLM as Claude API participant STE as StreamingToolExecutor QL->>API: callModel(params) API->>LLM: POST /messages (stream: true) loop 流式响应 LLM-->>API: SSE chunk (text_delta / tool_use) API-->>QL: yield StreamEvent QL-->>QL: 检查是否有 tool_use 块 alt 发现 tool_use 块 QL->>STE: addTool(block) STE->>STE: 如果可并发,立即开始执行 STE-->>QL: getCompletedResults() QL-->>QL: yield 已完成的工具结果 end end LLM-->>API: message_stop API-->>QL: yield 最终 AssistantMessage

工具调用的提取

1
2
3
4
5
6
7
8
// 从每个助手消息中提取 tool_use 块
const msgToolUseBlocks = message.content.filter(
content => content.type === 'tool_use',
)
if (msgToolUseBlocks.length > 0) {
toolUseBlocks.push(...msgToolUseBlocks)
needsFollowUp = true // 标记:循环需要继续
}

关键变量 needsFollowUp 决定了循环的走向:

  • true:进入 Phase E(工具执行路径)
  • false:进入 Phase D(无工具路径→准备终止)

Withholding 机制(错误扣留)

对于可恢复的错误(prompt-too-long、media-size、max-output-tokens),Claude Code 不会立即 yield 给 UI,而是扣留它们:

1
2
3
4
5
6
7
8
9
10
扣留的错误类型:
- prompt-too-long (HTTP 413) → 可通过压缩恢复
- media-size-error → 可通过移除媒体恢复
- max-output-tokens → 可通过提升限额恢复

流程:
1. 检测到可恢复错误 → 存入 withheldMessages(不 yield)
2. 进入 Phase D 尝试恢复
3. 恢复成功 → continue 循环(用户无感知)
4. 恢复失败 → 才 yield 错误给用户

这个设计使得 Agent 能够静默处理瞬态问题,用户只看到最终结果。


3.7 Phase C:后处理——错误恢复与回退

模型回退(Fallback)

flowchart TD Stream["流式响应进行中"] Err{"捕获 FallbackTriggeredError?"} HasFB{"有 fallbackModel?"} Switch["切换到备用模型
清除累积状态
丢弃 StreamingToolExecutor"] Retry["重新开始内层 while 循环"] Surface["yield 错误消息
return model_error"] Stream --> Err Err -- "是" --> HasFB HasFB -- "是" --> Switch --> Retry HasFB -- "否" --> Surface Err -- "否,其他错误" --> Surface

FallbackTriggeredError 在以下场景触发:

  • 连续 3 次 529(过载)错误
  • 模型不可用

回退时的操作:

  1. 所有已 yield 的孤立消息标记为 Tombstone
  2. 清除工具块和助手消息
  3. 如果是内部用户,剥离 thinking 签名
  4. 切换模型,重试

通用错误处理

1
2
3
4
5
6
try/catch 外层:
- ImageSizeError/ImageResizeError → return { reason: 'image_error' }
- 其他错误:
1. 为已发出但未完成的 tool_use 块生成缺失的 tool_result
2. yield 错误消息
3. return { reason: 'model_error', error }

中断信号检查

1
2
3
4
5
6
if (abortController.signal.aborted) {
1. 排空 StreamingToolExecutor 的剩余结果
2. 清理 Computer Use 锁
3. 如果不是 'interrupt' 原因 → yield 中断消息
4. return { reason: 'aborted_streaming' }
}

'interrupt' 是特殊的中断原因——它表示用户在流式响应中提交了新消息(submit-interrupt),此时不需要显示中断提示。


3.8 Phase D:无工具路径——停止判断与恢复策略

当 LLM 没有调用任何工具时(needsFollowUp === false),循环准备终止。但在真正终止前,需要经过多道检查:

flowchart TD NoTool["LLM 未调用工具"] PTL{"有扣留的
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),进入工具执行路径:

flowchart TD ToolBlocks["收到 N 个 tool_use 块"] HasSTE{"使用流式执行器?"} HasSTE -- "是" --> Remain["getRemainingResults()
等待所有工具完成"] 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
2
3
4
5
6
7
8
9
10
11
12
LLM 一次可能返回多个工具调用:

┌──────────────────────────────────┐
│ 分类:concurrencySafe? │
│ │
│ 只读工具(Read, Grep, Glob 等) │──→ 并行执行(最多 10 个)
│ 写入工具(Edit, Bash 等) │──→ 串行执行(避免冲突)
└──────────────────────────────────┘

分批规则(partitionToolCalls):
- 连续的 concurrencySafe 工具合成一批 → 并行
- 每个非 concurrencySafe 工具独立成批 → 串行

工具摘要的"错峰"生成

1
2
3
4
5
6
7
8
工具执行完成后,后台启动 Haiku 小模型生成摘要:
"Searched in auth/", "Fixed NPE in UserService" 等

这个摘要不是立即消费的——它存入 state.pendingToolUseSummary,
在 **下一轮** 循环的 Phase B 之后才 yield。

为什么?因为 Haiku 的调用(~1 秒)可以和主模型的流式响应(5-30 秒)
重叠执行,实现"错峰并行"。

3.10 Phase F:状态推进——构建下一轮

工具执行完成后,构建新的 State 对象:

1
2
3
4
5
6
7
8
9
10
11
12
state = {
messages: [...state.messages, ...assistantMessages, ...toolResults, ...attachments],
toolUseContext: updatedToolUseContext,
autoCompactTracking: updatedTracking,
maxOutputTokensRecoveryCount: 0, // 重置
hasAttemptedReactiveCompact: false, // 重置
pendingToolUseSummary: nextPending,
stopHookActive: false,
turnCount: state.turnCount + 1,
transition: { reason: 'next_turn' },
}
continue // 回到 while(true) 顶部

注意:不是修改旧 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
2
3
4
5
6
export type QueryDeps = {
callModel: typeof queryModelWithStreaming // API 调用
microcompact: typeof microcompactMessages // 微压缩
autocompact: typeof autoCompactIfNeeded // 自动压缩
uuid: () => string // UUID 生成
}

设计巧妙之处:类型定义使用 typeof 真实函数,确保接口签名与实现自动同步——如果 queryModelWithStreaming 的签名变化,QueryDeps.callModel 的类型会在编译期自动报错。

注入模式

flowchart LR subgraph Production["生产环境"] PD["productionDeps()"] Real1["queryModelWithStreaming"] Real2["microcompactMessages"] Real3["autoCompactIfNeeded"] Real4["crypto.randomUUID"] PD --> Real1 & Real2 & Real3 & Real4 end subgraph Test["测试环境"] TD["{ callModel: mockCallModel, ... }"] Mock1["返回预设响应"] Mock2["计数调用次数"] Mock3["跳过压缩"] Mock4["返回固定 UUID"] TD --> Mock1 & Mock2 & Mock3 & Mock4 end Query["query({ deps: ... })"] Production --> Query Test --> Query
1
2
// 使用:
const deps = params.deps ?? productionDeps() // 无注入时用生产实现

为什么只有 4 个依赖?

这是渐进式重构的体现。注释中明确说明这是 proof-of-concept,后续可以扩展到 runToolshandleStopHookslogEvent 等。选择这 4 个是因为它们是测试中最常被 mock 的函数。


3.13 不可变配置快照:QueryConfig

1
2
3
4
5
6
7
8
9
export type QueryConfig = {
sessionId: SessionId
gates: {
streamingToolExecution: boolean // 流式工具执行开关
emitToolUseSummaries: boolean // 工具摘要开关
isAnt: boolean // 是否内部用户
fastModeEnabled: boolean // 快速模式开关
}
}

关键设计决策

  1. 一次快照buildQueryConfig()query() 入口只执行一次,后续循环复用同一份配置
  2. 排除 feature() 门控feature() 来自 bun:bundle,必须留在使用处以支持 Tree Shaking(死代码消除)
  3. 容忍过期:Statsig 门控本身使用 CACHED_MAY_BE_STALE 语义,快照不引入额外过期风险

3.14 Token 预算系统

Token 预算(query/tokenBudget.ts)用于控制 Agent 在一次任务中消耗的总 Token 量:

flowchart TD Check["checkTokenBudget()"] SubAgent{"是子 Agent?"} SubAgent -- "是" --> Stop1["stop (null event)
子 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 消息:

flowchart TD subgraph PreHook["Phase 1: 预处理"] Cache["缓存安全参数快照
(供 /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 的上下文管理是一个五级递进体系,从轻量到重量排列:

flowchart TD subgraph L1["Level 1: Snip(最轻量)"] S1["删除对话中间的旧消息"] S1Note["无 API 调用 | 每轮自动执行"] end subgraph L2["Level 2: Microcompact"] S2["清除旧工具结果内容"] S2Note["无 API 调用 | 每轮自动执行"] end subgraph L3["Level 3: Session Memory Compaction"] S3["用磁盘上的会话记忆替代旧消息"] S3Note["无 API 调用 | Token 超阈值时触发"] end subgraph L4["Level 4: Traditional Compaction"] S4["调用 LLM 生成完整摘要"] S4Note["需要 API 调用 | Session Memory 失败时回退"] end subgraph L5["Level 5: Reactive Compaction"] S5["收到 413 错误后紧急压缩"] S5Note["需要 API 调用 | 最后的恢复手段"] end L1 --> L2 --> L3 --> L4 --> L5

Traditional Compaction 的完整流程

flowchart TD Start["compactConversation()"] PreHook["执行 PreCompact hooks"] Prompt["生成压缩提示词
(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
2
3
4
5
6
7
8
9
1. Primary Request and Intent     — 用户的主要目标
2. Key Technical Concepts — 涉及的技术概念
3. Files and Code Sections — 关键文件和代码片段
4. Errors and Fixes — 遇到的错误及修复
5. Problem Solving — 问题解决过程
6. All User Messages — 所有用户消息(非工具结果)
7. Pending Tasks — 待完成的任务
8. Current Work — 当前工作状态
9. Optional Next Step — 下一步建议

3.17 流式工具执行器:StreamingToolExecutor

StreamingToolExecutor 是 Agent 循环的性能优化核心——它允许在 LLM 还在输出后续内容时,提前开始执行已识别的工具

工具生命周期

stateDiagram-v2 [*] --> queued: addTool() queued --> executing: processQueue() executing --> completed: 工具执行完成 completed --> yielded: getCompletedResults() yielded --> [*] executing --> completed: 工具执行出错 note right of queued: 等待并发槽位 note right of executing: 实际执行中 note right of completed: 等待按序输出 note right of yielded: 已返回给调用方

并发控制模型

flowchart TD AddTool["addTool(block)"] IsSafe{"tool.isConcurrencySafe()?"} IsSafe -- "是" --> CheckRunning{"当前运行中的
都是 safe?"} CheckRunning -- "是" --> StartParallel["立即并行启动"] CheckRunning -- "否" --> Queue["排队等待"] IsSafe -- "否" --> CheckEmpty{"当前无运行中?"} CheckEmpty -- "是" --> StartExclusive["独占启动"] CheckEmpty -- "否" --> Queue Queue --> WaitComplete["等待运行中的工具完成"] WaitComplete --> ProcessQueue["processQueue() 重新扫描"]

中断级联架构

1
2
3
4
5
6
7
8
9
10
toolUseContext.abortController          ← 顶层:用户中断
└── siblingAbortController ← 中层:Bash 错误时取消兄弟
├── toolAbortController #1 ← 工具 1 独立 abort
├── toolAbortController #2 ← 工具 2 独立 abort
└── toolAbortController #3 ← 工具 3 独立 abort

关键规则:
- Bash 工具出错 → abort siblingAbortController → 取消所有兄弟工具
- 非 Bash 工具出错 → 不影响兄弟
- 用户中断 → abort 顶层 → 级联取消所有

输出顺序保证

getCompletedResults()同步生成器,它确保工具结果按原始顺序 yield,即使后面的工具先完成:

1
2
3
4
5
工具调用顺序: [A, B, C]
完成顺序: [B, A, C]
输出顺序: [A, B, C] ← 始终按原始顺序

规则:扫描到第一个非 concurrent 且未完成的工具时停止

3.18 QueryEngine:面向 SDK 的封装层

QueryEnginequery()类封装,为 SDK/Headless 场景提供完整的会话生命周期管理:

flowchart TD subgraph QE["QueryEngine 实例"] Config["config: QueryEngineConfig"] Messages["mutableMessages: Message[]"] Abort["abortController: AbortController"] Usage["totalUsage: NonNullableUsage"] SM["submitMessage(prompt)"] Int["interrupt()"] GetMsg["getMessages()"] SetModel["setModel(model)"] end subgraph Turn["单轮交互 (submitMessage)"] direction TB T1["1. 构建系统提示词"] T2["2. 处理用户输入
(斜杠命令/文件附件)"] 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
2
3
4
5
// 一次性使用的便捷函数
export async function* ask(config): AsyncGenerator<SDKMessage> {
const engine = new QueryEngine(config)
yield* engine.submitMessage(config.prompt)
}

3.19 API 调用层:重试、回退与流控

withRetry:异步生成器式的重试

flowchart TD Start["withRetry(operation)"] Attempt["执行 operation()"] Success{"成功?"} Success -- "是" --> Return["return 结果"] Success -- "否" --> Classify{"错误分类"} Classify -- "429 Rate Limit" --> Backoff429["指数退避
尊重 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
2
3
4
5
// 调用方式(在 queryModel 中):
for await (const retryMessage of withRetry(getClient, makeRequest, options)) {
yield retryMessage // 透传给 UI
}
// withRetry return 时,result 就是成功的 API 响应

3.20 预取与投机优化

Agent 循环中隐藏了多处预取优化,将等待时间转化为有用计算:

gantt title 单轮循环的时间线与并行任务 dateFormat X axisFormat %s section 主流程 上下文准备 :a1, 0, 2 LLM 流式响应 :a2, 2, 15 工具执行 :a3, 15, 20 状态推进 :a4, 20, 21 section 并行任务 记忆预取 :b1, 0, 8 技能发现预取 :b2, 2, 10 流式工具执行 :b3, 8, 15 工具摘要生成(Haiku) :b4, 20, 21

四大预取机制

机制 启动时机 消费时机 目的
记忆预取 用户消息进入时(仅首轮) 每轮工具执行后检查 提前加载相关记忆文件
技能发现 每轮迭代开始时 工具执行后收集 发现可用技能
流式工具执行 LLM 流中识别到 tool_use 时 流式输出期间 & 流结束后 减少工具等待时间
工具摘要 工具批次完成后(后台) 下一轮 Phase B 之后 利用模型思考时间生成摘要

记忆预取的去重

1
2
3
4
5
记忆预取返回的附件需要去重:
filterDuplicateMemoryAttachments(attachments, readFileState)

readFileState 跨迭代累积,记录 Agent 已经读取过的文件。
已读取的文件不会再作为记忆附件注入。

3.21 关键工程思想总结

思想 1:迭代而非递归

循环使用 while(true) + state = {...}; continue 而非递归调用。这避免了:

  • 长对话中的栈溢出(一次对话可能循环数百次)
  • 递归调用的上下文保持开销

每次 continue 都携带 transition.reason,使得状态转换可追踪、可测试。

思想 2:异步生成器实现生产者-消费者

1
2
3
4
5
6
7
query() 是生产者:yield 中间结果(流事件、消息、工具结果)
REPL/SDK 是消费者:for await 消费并渲染

好处:
- 计算与展示完全解耦
- 消费者可以控制消费速度(背压)
- 多层 yield* 委托实现透明的消息传递

思想 3:Withhold-then-Recover 模式

可恢复错误不立即暴露给用户,而是先扣留,尝试修复。只有修复失败才通知用户。

1
2
传统模式:错误 → 通知用户 → 用户决定
Claude Code:错误 → 静默恢复 → 成功则无感 / 失败才通知

这在 prompt-too-long 和 max-output-tokens 两种场景中特别有效。

思想 4:分层压缩的"漏斗"模型

五级压缩从轻到重排列,形成漏斗:

1
2
3
4
5
Snip       ← 最轻:删除中间消息(无 API 调用)
Microcompact ← 较轻:清除工具结果(无 API 调用)
SM Compact ← 中等:用本地记忆替换(无 API 调用)
Traditional ← 较重:调用 LLM 生成摘要
Reactive ← 最重:收到 413 后紧急压缩

大多数情况下,前两层就足够了。只有上下文真正膨胀时才触发重量级压缩。

思想 5:依赖注入的"最小侵入"

只注入 4 个最常被 mock 的依赖,而非重构整个系统。这是实用主义的工程决策

  • 足够解决测试痛点
  • 不引入过度抽象
  • 为后续渐进式扩展留了口

思想 6:错峰并行

利用不同操作的时间特征实现并行:

1
2
3
4
5
6
7
8
LLM 思考(5-30s)期间:
- 流式执行已识别的工具
- 消费记忆预取
- 消费技能发现

工具执行(1-10s)期间:
- 后台生成工具摘要(Haiku ~1s)
→ 在下一轮 LLM 思考期间消费

思想 7:中断信号的三级级联

1
2
3
4
5
6
7
8
用户中断 → 顶层 AbortController
→ siblingAbortController(Bash 错误时触发)
→ 每个工具的独立 AbortController

不同级别的 abort 有不同的语义:
- 用户中断:停止一切
- Bash 错误:只停止兄弟工具,不停止整个循环
- 单工具超时:只停止该工具

思想 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 行)