news 2026/4/29 20:02:26

Claude Code 深度拆解:Agent 执行内核 3 — 从 API 调用到安全退出

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Claude Code 深度拆解:Agent 执行内核 3 — 从 API 调用到安全退出

Hi,大家好,欢迎来到维元码簿。

本文属于《Claude Code 源码 Deep Dive》系列,专注于 Agent 执行内核中的API 调用、流式处理、工具执行、错误恢复与生命周期收尾板块。如果你想了解整个系列,可以先看系列开篇 | Claude Code 源码架构概览:51万行代码的模块地图。

API 调用不只是"发出请求、等待回复"——真正的工程挑战是出意外了怎么兜底。413 爆了、token 超了、模型挂了、用户按了 Ctrl+C……Claude Code 不是靠一把 try-catch 包办一切,而是在循环体不同位置设置了 7 个精准的 continue 点,每种异常都有专属的恢复路径。与此同时,代码还在流式响应中同步执行工具——模型还在输出,工具已经在跑了。

读完全文,你将能回答这几个问题:

  • 模型输出到一半,工具已经在执行了——怎么做到的?答案:StreamingToolExecutor 的addTool()在流中识别到完整的 tool_use block 后立即加入执行队列——不等待 message_stop。
  • API 返回 413,Agent 会崩溃吗?答案:不会——Collapse Drain 机制立即将待折叠的对话内容提交压缩以释放上下文窗口;若空间仍不足,再启动 Reactive Compact 做更激进的摘要压缩。两次自救都不行才放弃。错误全程 withhold,用户根本看不到。
  • 7 个 continue 点分别处理什么?为什么不是一把 try-catch?答案:PTL 恢复、OTK 升级/恢复、Fallback 切换、Stop Hook 阻断、Token Budget 续命——每种异常有自己的精准恢复路径。

本篇覆盖的源码范围

模块核心文件核心代码行文件总行职责
API 调用src/query.tsL558-9971730 行callModel 流式请求、assistant 消息累积、事件分发
流式工具执行src/services/tools/StreamingToolExecutor.tsL1-519531 行边收边执行、并发调度、siblingAbort
工具编排src/services/tools/toolOrchestration.tsL1-189189 行只读并行/写入串行分区
错误恢复src/query.tsL1062-13581730 行PTL/OTK/Fallback 恢复路径
Stop Hookssrc/query/stopHooks.tsL1-474474 行Stop/TeammateIdle/TaskCompleted 三元组
Token 预算src/query/tokenBudget.tsL1-9494 行预算追踪与 nudge 注入

前情提要:从消息压缩到"真正干活"

在子命题 2中,四层压缩已经把messagesForQuery从 85,000 token 瘦身到 ~12,000 token。「① 准备」阶段到此收尾——接下来进入 while(true) 循环的另外三个阶段:

  • ② 调用:拼装messages + systemPrompt + tools发起请求 →for await接收流式响应、边收边解析 tool_use 块、主模型异常时设标志位切换 fallbackModel 重试。对应代码:deps.callModel(...)+onStreamingFallback
  • ③ 执行:已完成的工具结果按接收顺序产出;Bash 出错触发级联取消兄弟工具;Hook 阻断消息、工具结果等附件一并收集 yield。对应代码:StreamingToolExecutor/runTools+attachment
  • ④ 转移:Stop → TeammateIdle → TaskCompleted 三元组依次执行,阻断则注入反馈让模型修正;最后更新 state 决定continuereturn { reason }——5 种终止原因中只有 1 种是正常出口,另外 4 种都是异常兜底。对应代码:stopHookResult+state = { ...next }

接下来三章按 ② → ③ → ④ 顺序拆解。


API 调用全景:18+ 个参数的一次请求

deps.callModel() 的参数清单

query.tsL659 的deps.callModel()调用一次性传入 18+ 个参数——远超一个普通 HTTP 请求的复杂度:

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: { model, fastMode, toolChoice, isNonInteractiveSession, fallbackModel, onStreamingFallback, querySource, agents, allowedAgentTypes, hasAppendSystemPrompt, maxOutputTokensOverride, fetchOverride, mcpTools, hasPendingMcpServers, queryTracking, effortValue, advisorModel, skipCacheWrite, agentId, addNotification, taskBudget: { total, remaining } } }))

其中几个关键参数值得展开:

  • fallbackModel + onStreamingFallback:当主模型流式出错(非 4xx/5xx),回调onStreamingFallback设置标志位,外层 while 循环检测后切换模型重试。
  • taskBudget:服务端预算追踪机制。remaining由客户端在 compact 时跨边界传递——服务端看不到压缩前的完整历史,需要客户端主动汇报。
  • signaltoolUseContext.abortController.signal,用户 Ctrl+C 时触发 abort,所有正在执行的工具、正在进行的 API 调用同步取消。

流式事件的四种类型

for await (const message of deps.callModel(...))的每次迭代产生四种可能的消息类型:

消息类型何时产生处理方式
assistant模型的每次内容输出累积到assistantMessages[];提取 tool_use block 加入 StreamingToolExecutor;yield 给 REPL
system系统级通知(如 “Switched to fallback model”)yield 给 REPL 展示
attachmentHook 阻断、工具结果等附件yield + 检查是否 preventContinuation
tombstone流式 Fallback 时标记废弃消息yield 给 REPL 移除 UI 中的废弃消息

StreamingToolExecutor:边收边做的并发引擎

这是 Claude Code Agent 循环中最巧妙的设计。传统 Agent 是"等模型说完 → 解析 tool_use → 执行工具"。Claude Code 更进一步——模型还在输出时,工具已经在跑了。

下图把「流式事件接收」与「工具执行」画在同一条时间轴上——模型 tool_use block 一识别完整,工具就立即开跑,不等 message_stop。

addTool():流中触发

当流式响应中出现content_block_starttype: 'tool_use')时,查询循环立即调用streamingToolExecutor.addTool(block, message)

// query.ts L760-770 — 流式循环中的工具触发if(streamingToolExecutor){for(constblockoftoolBlocks){streamingToolExecutor.addTool(block,message)}}

addTool()的职责是:检查并发安全性 → 如果可以立即执行 → 启动 tool.call();否则排入队列等待。

并发控制:只读并行,写入串行

StreamingToolExecutor的核心规则只有一条——canExecuteTool(isConcurrencySafe)

可以执行新工具,当且仅当: - 没有正在执行的工具(空闲),或者 - 新工具安全 且 所有正在执行的工具也安全

这个简单的规则产生了三种调度场景:

  • 只读工具(Glob、Grep、FileRead)isConcurrencySafe = true→ 可以并行
  • 写入工具(Edit、Write)isConcurrencySafe = false→ 必须独占
  • 执行工具(Bash)isConcurrencySafe = false→ 必须独占,且出错时取消兄弟

下面这张甘特图展示了 5 个工具在 StreamingToolExecutor 下的并发调度时序。

siblingAbortController:Bash 出错的级联取消

当 BashTool 执行出错时,StreamingToolExecutor会取消所有正在执行的兄弟工具:

// StreamingToolExecutor.ts — 错误传播逻辑if(toolName===BASH_TOOL_NAME&&error){this.siblingAbortController.abort('sibling_error')}

这是因为 Bash 命令通常有前后依赖——如果npm install失败了,后续依赖它的工具也没有执行意义。而 FileRead 或 Glob 的失败是独立的——它们不会触发级联取消。

结果顺序:先完成不一定先产出

一个精妙的设计:StreamingToolExecutor缓冲已完成的结果,按接收顺序产出——不是完成顺序。这保证了模型在下一轮看到 tool_result 时有确定性的顺序。

discard():Fallback 时清空

当流式 Fallback 触发时:

// query.ts L733-739 — Fallback 时清理if(streamingToolExecutor){streamingToolExecutor.discard()// 清空所有 pending 工具streamingToolExecutor=newStreamingToolExecutor(// 重建一个新的toolUseContext.options.tools,canUseTool,toolUseContext,)}

旧的 StreamingToolExecutor 被丢弃——它的工具调用使用的是旧模型的 tool_use_id,在新模型下毫无意义。


runTools:非流式 Fallback 模式

config.gates.streamingToolExecution关闭时,走runTools()路径。它执行partitionToolCalls()分区:

  • 并发安全组runToolsConcurrently()并行执行
  • 非安全组runToolsSerially()逐个执行(因为前一个可能影响后一个的 context)

注意runTools()的调用时机——它是在整个流式响应结束后才执行的,不像 StreamingToolExecutor 那样边收边做。这就是为什么 StreamingToolExecutor 是默认路径:延迟更低。


错误恢复全景:7 个 continue 点的精准矩阵

现在进入最重要的话题:错误恢复。Claude Code 不是用一把 try-catch 包住整个循环——它在循环体的不同位置设置了7 个精准的 continue 点,每种异常有自己的恢复路径。

下面这张决策树展示了 7 个 continue 点从 while(true) 出发的完整恢复网络。

Continue 1:AutoCompact 继续

压缩完成后,通过state = { ... }+continue回到循环顶部——这是最"正常"的 continue,不是错误恢复。

Continue 2:Collapse Drain(413 恢复第一层)

当 API 返回 413(prompt_too_long)时,错误被 withhold 拦截,不 yield 给用户。第一层恢复是contextCollapse.recoverFromOverflow()。这里的 “drain” 含义是:Context Collapse 系统预先标记了可折叠的消息对但尚未提交——Collapse Drain 就是把所有待处理的折叠操作一次性执行,用压缩后的摘要替换原文,从而释放上下文窗口空间让 API 重试成功:

// query.ts L1093-1116 — Collapse Drainif(feature('CONTEXT_COLLAPSE')&&contextCollapse&&state.transition?.reason!=='collapse_drain_retry'){constdrained=contextCollapse.recoverFromOverflow(messagesForQuery,querySource)if(drained.committed>0){state={...nextState,transition:{reason:'collapse_drain_retry'}}continue// ← 排水成功,重试}}

这是零额外 API 成本的恢复——只利用已有的折叠数据。

Continue 3:Reactive Compact(413 恢复第二层)

如果 Collapse Drain 失败(没有可折叠的内容了),进入reactiveCompact.tryReactiveCompact()

// query.ts L1119-1165 — Reactive Compactconstcompacted=awaitreactiveCompact.tryReactiveCompact({...})if(compacted){state={...nextState,hasAttemptedReactiveCompact:true,transition:{reason:'reactive_compact_retry'}}continue// ← 压缩成功,重试}// 失败 → return { reason: 'prompt_too_long' }

这里的关键标志是hasAttemptedReactiveCompact——如果 RC 已经尝试过一次且仍然 413,不再重试,直接退出。

下面这张对比图并列展示了 PTL(prompt-too-long)和 OTK(output token 超限)两种恢复路径的完整流程。

Continue 4:流式 Fallback 切换

当主模型流式响应中出现FallbackTriggeredError时:

  1. 设置streamingFallbackOccured = true
  2. 外层检测到标志 → yield tombstone 清理废弃消息
  3. streamingToolExecutor.discard()清空旧工具
  4. attemptWithFallback = true→ 用 fallbackModel 重新 API 调用
  5. yield system message “Switched to fallback model”
  6. continue回到循环顶部(retry 已经由外层 while(attemptWithFallback) 完成)

Continue 5:OTK Escalate(输出 token 超限升级)

当模型输出达到 output token 上限(isWithheldMaxOutputTokens)时,第一策略是升级上限

// query.ts L1199-1220 — OTK Escalateif(capEnabled&&maxOutputTokensOverride===undefined&&!process.env.CLAUDE_CODE_MAX_OUTPUT_TOKENS){state={...nextState,maxOutputTokensOverride:ESCALATED_MAX_TOKENS,transition:{reason:'max_output_tokens_escalate'}}continue// ← 用更大的 max_tokens 重试同一请求}

这是一个聪明的优化——不需要多轮对话,同一个请求用更大的输出预算重试。

Continue 6:OTK Recovery(注入恢复消息)

如果 64k 上限仍然不够,降级为多轮恢复——注入一条 meta 消息让模型继续:

// query.ts L1223-1251 — OTK Recoveryif(maxOutputTokensRecoveryCount<MAX_OUTPUT_TOKENS_RECOVERY_LIMIT){constrecoveryMessage=createUserMessage({content:`Output token limit hit. Resume directly — no apology, no recap...`,isMeta:true,})state={...nextState,maxOutputTokensRecoveryCount:maxOutputTokensRecoveryCount+1,transition:{reason:'max_output_tokens_recovery'}}continue}

最多尝试MAX_OUTPUT_TOKENS_RECOVERY_LIMIT次,耗尽后 yield 错误并退出。

Continue 7:Stop Hook Blocking

当 Stop Hook 返回blockingErrors(exit code 2)时:

// query.ts L1282-1305 — Stop Hook Blockingif(stopHookResult.blockingErrors.length>0){state={...nextState,stopHookActive:true,transition:{reason:'stop_hook_blocking'}}continue// ← 注入错误消息,让模型修正后重试}

stopHookActive标志防止死循环——如果 hook 两次阻断同一个响应,说明模型无法满足 hook 的要求。

Bonus:Token Budget Continue

TOKEN_BUDGETfeature 开启且预算未耗尽时:

// query.ts L1316-1340 — Token Budget nudgeif(decision.action==='continue'){state={...nextState,transition:{reason:'token_budget_continuation'}}continue// ← 注入 nudge 消息提醒模型注意预算}

这不是"错误恢复",而是"主动续命"——在预算快耗尽但还没耗尽时,注入提醒消息让模型加快节奏。


模型 Fallback:优雅降级

Fallback 的完整链路值得单独展开。触发条件是:主模型流式响应中抛出FallbackTriggeredError(不是 4xx/5xx 网络错误,而是模型侧异常)。

完整处理流程(query.ts L712-750):

  1. 检测streamingFallbackOccured标志
  2. yield tombstone给所有assistantMessages——这些 partial 消息的 thinking blocks 有无效签名,不清理会导致后续 API 调用报"thinking blocks cannot be modified"错误
  3. 清空assistantMessagestoolResultstoolUseBlocksneedsFollowUp
  4. streamingToolExecutor.discard()+ 新建一个
  5. attemptWithFallback = true→ 外层 while 循环重新走 API 调用

关键设计:tombstone 事件通知 REPL 移除 UI 中的旧消息,保证用户界面的清洁。


用户中断:Ctrl+C 的优雅处理

toolUseContext.abortController.signal.aborted在两个关键位置被检查:

  1. 流式过程中deps.callModel()接收 signal,中断后isAbortedStreamingReason为 true → 补全缺失的 tool_result →return { reason: 'aborted_streaming' }
  2. 工具执行中getRemainingResults()runTools()内部检查 signal,已完成的工具结果正常产出,未开始的丢弃

yieldMissingToolResultBlocks()保证了中断时不会留下"悬空"的 tool_use——模型期望每个 tool_use 都有对应的 tool_result。


Stop Hooks 生命周期:三元组的协奏

needsFollowUp === false时,进入handleStopHooks()。这是三种 Hook 的完整执行流程:

Hook触发条件阻断后果
Stop每次 turn 结束(模型主动 end_turn)blockingErrors → continue 注入反馈消息
TeammateIdleTeammate agent 即将闲置阻断 → agent 继续工作
TaskCompletedTeammate 完成任务阻断 → agent 重新处理

执行顺序在stopHooks.ts中编排:先 Stop → 再 TeammateIdle → 最后 TaskCompleted。每个 Hook 可以返回三种结果:

  • Success(exit code 0):继续
  • Blocking(exit code 2):注入阻断消息 →continue回到循环
  • Prevent(exit code 1):直接终止,不重试

stopHookActive是防止死循环的关键——如果上一轮已经是 Stop Hook 阻断导致的 continue,这一轮 Stop Hook 再次阻断,preventContinuation机制会触发,防止无限循环。


循环终止:5 条出路

下面这张图展示了从 while(true) 出发的 5 条终止路径——只有一条是"正常出口"。

终止原因触发条件Terminal.reason颜色
completed模型主动 end_turn + stop hooks 通过 + token budget 耗尽'completed'🟢 绿色
max_turnsturnCount > maxTurns'max_turns'🟡 黄色
prompt_too_longPTL 恢复(Collapse Drain + Reactive Compact)全部失败'prompt_too_long'🔴 红色
aborted_streaming / aborted_tools用户 Ctrl+C'aborted_streaming'/'aborted_tools'🟠 橙色
model_errorAPI 返回非 PTL/非 fallback 的错误'completed'(走 stop hooks 后)🔴 深红

只有completed是正常出口。其他 4 条都是各种异常场景的最终兜底。每一次 exit 之前,代码都保证了两件事:

  1. 缺失的 tool_result 被补全(yieldMissingToolResultBlocks
  2. Stop Hooks(或 Stop Failure Hooks)被调用

本章小结

本文拆解了消息压缩完成后的完整处理链路:

  • API 调用一次性传入 18+ 个参数——这不是一个简单的 HTTP 请求,而是一个携带了系统 Prompt、工具定义、预算信息、Fallback 配置的完整上下文包。
  • StreamingToolExecutor实现了"边收边做"——模型还在输出时,工具已经在运行。只读并行、写入串行,Bash 出错级联取消兄弟。
  • 7 个 continue 点不是补丁——是精心设计的恢复策略矩阵。PTL 有 Collapse Drain → Reactive Compact 两层自救;OTK 有 Escalate(8k→64k)→ Recovery 消息注入两层;Fallback 有模型切换……
  • Stop Hooks 三元组在每次 turn 结束后运行——Stop → TeammateIdle → TaskCompleted,每个 Hook 可以阻断并让 Agent 继续。
  • 5 条出路——只有completed是正常出口。每一次退出都保证了 tool_result 完整 + Hooks 执行。

系列导航

本文属于《Claude Code 源码 Deep Dive》系列中「Agent 执行内核」命题的子篇章,专注于API 调用、流式处理与安全退出

姊妹篇(可独立阅读):

  • Claude Code 深度拆解:Agent 执行内核 1 — 主循环与状态机
  • Claude Code 深度拆解:Agent 执行内核 2 — Pipeline 与上下文压缩

如果这篇文章对你有帮助,欢迎点赞收藏支持一下。如果你对 Claude Code 源码感兴趣,欢迎关注本系列后续更新。有任何想法或疑问,欢迎评论区留言讨论👋

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/4/29 19:56:35

解锁电脑静音新境界:Fan Control让你的风扇变得聪明又安静

解锁电脑静音新境界&#xff1a;Fan Control让你的风扇变得聪明又安静 【免费下载链接】FanControl.Releases This is the release repository for Fan Control, a highly customizable fan controlling software for Windows. 项目地址: https://gitcode.com/GitHub_Trendin…

作者头像 李华
网站建设 2026/4/29 19:56:33

3分钟从零开始:用AICoverGen制作专业级AI翻唱的完整指南

3分钟从零开始&#xff1a;用AICoverGen制作专业级AI翻唱的完整指南 【免费下载链接】AICoverGen A WebUI to create song covers with any RVC v2 trained AI voice from YouTube videos or audio files. 项目地址: https://gitcode.com/gh_mirrors/ai/AICoverGen 想让…

作者头像 李华
网站建设 2026/4/29 19:49:44

前端新范式:用 AI 提效开发,用 EE 保证迭代质量

1.概述 在人工智能快速发展的今天&#xff0c;AI不再仅仅是回答问题的聊天机器人&#xff0c;而是正在演变为能够主动完成复杂任务的智能代理。OpenAI的Codex CLI就是这一趋势的典型代表——一个跨平台的本地软件代理&#xff0c;能够在用户的机器上安全高效地生成高质量的软件…

作者头像 李华
网站建设 2026/4/29 19:49:17

吉时利Keilthley 2400 通用数字源表 高精度数字万用表

吉时利Keilthley 2400 通用数字源表 高精度数字万用表 Keithley 标准系列 2400 源测量单元 (SMU) 仪器提供四象限精密电压和电流源/负载&#xff0c;外加测量。每个 SMU 仪器均同时提供高度稳定的直流电源和一台真正的仪器级 6 位万用表。电源特性包括低噪声、高精度和回读。万…

作者头像 李华
网站建设 2026/4/29 19:39:23

如何在15分钟内完成EspoCRM开源CRM系统的终极部署指南

如何在15分钟内完成EspoCRM开源CRM系统的终极部署指南 【免费下载链接】espocrm EspoCRM – Open Source CRM Application 项目地址: https://gitcode.com/GitHub_Trending/es/espocrm 想要一个功能完整且完全免费的开源CRM系统来管理客户关系吗&#xff1f;EspoCRM正是…

作者头像 李华