Claude Code 源码架构深度解析(二):拆解 Agent Runtime 的 1729 行——queryLoop 与 Streaming Tool Execution 如何驱动 TaoToke

发布时间:2026/10/12 9:58:56
Claude Code 源码架构深度解析(二):拆解 Agent Runtime 的 1729 行——queryLoop 与 Streaming Tool Execution 如何驱动 TaoToke
1. 从一次“帮我重构这个函数”说起queryLoop 到底在循环什么你在 Claude Code 里敲下“帮我重构这个函数”回车之后发生的事情远比“拼 prompt → 调 API → 打印结果”复杂。真实链路是cli.tsx 做 fast-path 分发加载 main.tsxmain.tsx 初始化状态、注册工具、构造 ToolUseContext你的输入进入 query()query() 调用 queryLoop()——一个while(true)主循环。每次迭代依次做压缩上下文、组装 system prompt、调用模型 API、流式处理响应、执行工具调用、注入附加信息、决定是否进入下一轮。这套机制的核心文件是 query.ts约 1729 行是整个 Agent Runtime 的心脏。它要解决的问题很具体一个复杂编码任务可能触发几十甚至上百轮模型调用如果每轮都用递归实现模型返回工具调用 → 执行工具 → 拼回消息 → 再调 query()调用栈会一层层堆积长会话下爆栈是真实存在的风险。所以现在的设计改成了while(true) 可变 state 对象用状态迁移替代递归。对做 Agent 的开发者来说这段源码值得逐行读。它回答了一个工程问题当你的 Agent 需要长时间运行、需要错误恢复、需要断点续传时循环该怎么写。本文会拆解 queryLoop 的 state 结构、9 个 continue 分支、Streaming Tool Execution 的并发控制并给出可复制的调用链路梳理和关键函数注释片段。最后用 TaoToken 统一 Key/API 通道https://taotoken.net/?utm_sourcetaotoken_aicg_blog_end做一次真实请求验证让你对照源码看 Agent 循环的实际行为。适合谁读正在自研 Agent Runtime 的后端/全栈工程师想理解 Claude Code 内部调度机制的进阶用户以及被“长会话爆栈”“工具执行串行太慢”“prompt 缓存命中率低”这三个问题困扰过的团队。下面从 state 定义开始一层层往下拆。2. queryLoop 的 state 设计与 9 个 continue 分支Agent Runtime 状态机拆解先看 query.ts 里跨迭代携带的状态定义。这段代码是理解整个 Runtime 的钥匙// src/query.ts // 跨迭代携带的可变状态 type State { messages: Message[] toolUseContext: ToolUseContext autoCompactTracking: AutoCompactTrackingState | undefined maxOutputTokensRecoveryCount: number hasAttemptedReactiveCompact: boolean maxOutputTokensOverride: number | undefined pendingToolUseSummary: PromiseToolUseSummaryMessage | null | undefined stopHookActive: boolean | undefined turnCount: number // Why the previous iteration continued. Undefined on first iteration. transition: Continue | undefined } async function* queryLoop(params: QueryParams, consumedCommandUuids: string[]) { // Mutable cross-iteration state. let state: State { messages: params.messages, toolUseContext: params.toolUseContext, maxOutputTokensOverride: params.maxOutputTokensOverride, autoCompactTracking: undefined, stopHookActive: undefined, maxOutputTokensRecoveryCount: 0, hasAttemptedReactiveCompact: false, turnCount: 1, pendingToolUseSummary: undefined, transition: undefined, } // eslint-disable-next-line no-constant-condition while (true) { let { toolUseContext } state const { messages, turnCount, hasAttemptedReactiveCompact, ... } state // ... 每一轮循环的处理逻辑 } }注意几个设计细节。第一queryLoop 是 async generator不是普通函数这意味着它可以在循环内部 yield 中间结果调用方可以边收边处理。第二state 是可变对象每轮迭代开头解构出需要的字段结尾重新组装。第三transition字段记录了“上一轮为什么继续”第一轮为 undefined这个字段对调试极其重要——出问题时你能直接看到循环是因为哪个原因又跑了一轮。代码里有 9 个不同的 continue 点每个对应一种“为什么要再跑一轮”的语义Continue 原因触发场景next_turn正常的工具调用后继续reactive_compactAPI 返回 413 后紧急压缩重试max_output_tokens模型输出超限注入恢复消息继续stop_hookstop hook 要求模型继续token_budgettoken 预算未用完继续工作context_collapse_drain上下文折叠后重试fallback_model模型降级后重试......这种“每个 continue 点都有明确语义”的设计好处是调试时不用猜。传统递归写法里异常在层与层之间传递堆栈信息混在一起你很难快速定位是“压缩失败”还是“模型降级”导致的循环。而状态机写法把每种继续原因显式编码日志里打出来一目了然。每轮循环的执行步骤按顺序是第一步上下文预处理四道压缩机制依次执行——snip compact → micro compact → context collapse → auto compact目的是在有限上下文窗口里塞进最有用的信息。第二步 Token 预算检查如果 auto compact 被关了检查是否接近硬限制。第三步组装 system prompt把静态区和动态区拼接。第四步调用模型 API把消息列表、system prompt、工具定义一起发出去。第五步流式处理响应关键点在于如果流中出现了 tool_use block不等模型说完就开始执行工具。第六步错误恢复分支非常多——prompt 太长先试 context collapse drain 再试 reactive compact输出 token 超限就注入恢复消息让模型继续模型降级就切 fallback model。第七步 Stop hooks模型停止输出后运行 stop hooks 决定要不要继续。第八步工具执行批量执行本轮所有工具调用。第九步附件注入工具执行完后注入 memory attachments、skill discovery 结果、排队中的命令。第十步决定下一轮把结果组装成新的消息列表回到循环开头。我试过把这套结构搬到自己的 Agent 项目里最直接的收益是错误恢复逻辑变得可维护。以前用递归时每种恢复策略都要在层间传递异常代码越写越乱改成状态机后恢复逻辑就在循环内自然实现不需要跨层传递。这不是什么高深技术就是工程成熟度的体现。很多成熟方案事后看都不难难的是在一开始就意识到需要这样设计。3. Streaming Tool Execution 并发控制canExecuteTool 与 isConcurrencySafe 配置实战传统做法是等模型完整输出收齐所有 tool_use block再批量执行工具。Claude Code 做了一个明显优化StreamingToolExecutor模型还在输出时已经完成的 tool_use block 就开始执行了。看核心并发控制逻辑src/services/tools/StreamingToolExecutor.ts/** * Executes tools as they stream in with concurrency control. * - Concurrent-safe tools can execute in parallel with other concurrent-safe tools * - Non-concurrent tools must execute alone (exclusive access) * - Results are buffered and emitted in the order tools were received */ export class StreamingToolExecutor { private tools: TrackedTool[] [] private hasErrored false private siblingAbortController: AbortController /** * Check if a tool can execute based on current concurrency state */ private canExecuteTool(isConcurrencySafe: boolean): boolean { const executingTools this.tools.filter(t t.status executing) return ( executingTools.length 0 || (isConcurrencySafe executingTools.every(t t.isConcurrencySafe)) ) } // ... }canExecuteTool的判断逻辑是只有当所有正在执行的工具都是并发安全的并且新工具也是并发安全的才允许并行执行否则排队等。这个判断依赖 Tool.ts 里的isConcurrencySafe标记。为什么这很重要想象模型在一次响应里决定调用 5 个工具——读 3 个文件、搜索代码、看 git log。传统做法要等模型生成完所有 5 个 tool_use可能 5 到 30 秒然后串行或并行执行。Streaming 做法是模型生成完第 1 个 tool_use 就立刻执行读文件模型还在生成第 2 个时第 1 个文件已经读完了模型生成完第 3 个时前 2 个工具可能都跑完了。总体延迟可以减少一半以上。如果你要在自己的 Agent 里复刻这套机制工具定义需要显式声明并发安全性。下面是一个可复制的工具配置片段路径对应src/tools/下的工具定义文件// src/tools/ReadFileTool.ts export const ReadFileTool: Tool { name: ReadFile, description: Read the contents of a file, inputSchema: { type: object, properties: { path: { type: string, description: Absolute file path }, offset: { type: number, description: Line offset to start reading }, limit: { type: number, description: Max lines to read }, }, required: [path], }, // 只读操作多个 ReadFile 可以并行 isConcurrencySafe: true, async execute(input, context) { // ... 读取逻辑 }, } // src/tools/BashTool.ts export const BashTool: Tool { name: Bash, description: Execute a shell command, inputSchema: { type: object, properties: { command: { type: string }, timeout: { type: number }, }, required: [command], }, // 写操作必须独占执行 isConcurrencySafe: false, async execute(input, context) { // ... 执行逻辑 }, }这里的关键判断标准是工具是否会产生副作用。读文件、搜索、查 git log 这类只读操作标记为true写文件、执行命令、改配置这类有副作用的标记为false。标记错了会导致数据竞争——比如两个写操作并行执行结果不可预测。实现 Streaming Tool Execution 的难点不在并发控制本身而在于你需要流式解析模型输出来判断一个 tool_use block 是否已经完整需要管理并发执行的工具生命周期需要处理工具执行出错时的回滚还要确保结果按正确顺序拼回消息列表。很多团队做 Agent 时精力全花在“让模型更聪明”上但用户感知到的“快”很多时候不是模型推理快而是工程层面的并发优化做得好。4. 用 TaoToken 验证 queryLoop 请求链路Base URL、Key、Model ID 三件套配置理解了 queryLoop 的调度逻辑后你需要一个稳定的 API 通道来实际验证请求行为。TaoToken 提供统一的 Key/API 通道把模型调用收敛到一个入口方便你对照源码观察每一轮循环实际发出的请求。先拿 Key。访问 https://taotoken.net/api-keys 创建 API Key然后在控制台 https://taotoken.net/console 确认账户状态。拿到 Key 后配置三件套Base URL、Key、Model ID。如果你用 Claude Code 或兼容 Anthropic 协议的客户端配置片段如下对应~/.claude/settings.json或项目级.claude/settings.json{ env: { ANTHROPIC_BASE_URL: https://taotoken.net/api, ANTHROPIC_API_KEY: sk-你的TaoToken密钥, ANTHROPIC_MODEL: claude-sonnet-4-20250514 } }如果你用 Codex 或兼容 OpenAI 协议的客户端配置~/.codex/auth.json{ OPENAI_BASE_URL: https://taotoken.net/api, OPENAI_API_KEY: sk-你的TaoToken密钥, model: gpt-4o }如果你用 Cline 或支持 MCP 的编辑器插件在 MCP 配置里写{ mcpServers: { taotoken: { url: https://taotoken.net/api, headers: { Authorization: Bearer sk-你的TaoToken密钥 } } } }三件套缺一不可Base URL 指向https://taotoken.net/apiKey 用你创建的密钥Model ID 填你要验证的模型。配置完成后你可以用 curl 直接验证请求链路是否通curl -X POST https://taotoken.net/api/v1/messages \ -H Content-Type: application/json \ -H x-api-key: sk-你的TaoToken密钥 \ -H anthropic-version: 2023-06-01 \ -d { model: claude-sonnet-4-20250514, max_tokens: 1024, messages: [ {role: user, content: 帮我重构这个函数function add(a,b){return ab}} ] }这个请求对应 queryLoop 第四步“调用模型 API”。如果你在客户端里开启流式就能观察到第五步“流式处理响应”的行为——tool_use block 出现时客户端会立刻开始执行工具而不是等整个响应结束。对于需要长期跑编码任务或 Agent 的场景建议用 Coding Planhttps://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding_planutm_campaignrewrite它针对高频调用做了配额优化。如果你只是想验证模型行为用模型对话https://taotoken.net/chat?utm_sourcetaotoken_aicg_blog_endutm_contentmodel_chatutm_campaignrewrite快速试一轮即可。接入文档在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite里面有完整的协议说明。配置好之后你可以对照 queryLoop 的九步流程在客户端日志里逐条核对请求发出时 system prompt 是否包含静态区和动态区、流式响应里 tool_use 是否触发了提前执行、错误恢复时是否走了 fallback model。这样源码和实际行为就能对上。5. 常见报错排查401、local proxy failed、reading choices、OAuth 对照表配置和验证过程中最容易撞上的是下面几类报错。我按真实错误信息对照排查路径整理成表方便你快速定位。报错信息根因排查动作401 UnauthorizedKey 无效或未正确传递检查ANTHROPIC_API_KEY/OPENAI_API_KEY是否填了完整 Key确认请求头是x-api-keyAnthropic 协议还是Authorization: BearerOpenAI 协议local proxy failed本地代理配置冲突检查客户端是否残留了旧的代理设置确认 Base URL 直接指向https://taotoken.net/api不要经过额外转发层reading choices响应格式与客户端预期不符确认你用的协议和客户端匹配Anthropic 协议返回content数组OpenAI 协议返回choices数组协议选错会导致解析失败OAuth token expired认证方式不匹配如果你用的是 API Key 模式不要同时启用 OAuth 流程在客户端里明确选择 API Key 认证重点说reading choices这个报错。它通常出现在你用 OpenAI 协议的客户端去请求 Anthropic 协议的端点或者反过来。Claude Code 用的是 Anthropic Messages API返回结构是{content: [{type: text, text: ...}]}而 OpenAI Chat Completions 返回的是{choices: [{message: {content: ...}}]}。客户端解析代码如果写死了读choices遇到 Anthropic 格式就会报这个错。解决办法是确认你的客户端协议和 Base URL 路径匹配Anthropic 协议走/v1/messagesOpenAI 协议走/v1/chat/completions。local proxy failed这个报错多数情况是客户端里配置了本地代理端口但代理服务没启动或者代理规则把taotoken.net也拦截了。排查时先把客户端代理设置清空让请求直连 Base URL确认能通之后再逐步加回必要配置。401最常见的原因是 Key 复制时带了空格或换行符。建议用echo -n sk-xxx | wc -c确认 Key 长度或者直接在客户端里重新粘贴一次。另外注意Anthropic 协议用x-api-key头OpenAI 协议用Authorization: Bearer头混用会直接 401。OAuth token expired一般出现在你同时配置了 OAuth 登录和 API Key 的场景。Claude Code 支持 OAuth 登录但如果你要走 TaoToken 的 API Key 通道需要在客户端里明确切换到 API Key 模式避免两套认证逻辑打架。排查完这些如果请求还是不通用第 4 节的 curl 命令做最小化验证。curl 能通说明 Key 和 Base URL 没问题问题在客户端配置curl 不通说明 Key 或网络层有问题先解决这一层。6. 把 Runtime 调用链路跑通之后从源码理解到实际接入把 queryLoop 的 state 结构、9 个 continue 分支、Streaming Tool Execution 的并发控制这三块拼起来一个 Agent Runtime 的引擎就清晰了。每一轮循环都可能因为 9 种不同原因继续每一轮都有四道压缩保护上下文不溢出每一次模型调用都有 prompt 缓存优化控制成本。如果你要自己实现类似的 Runtime三个点值得记住。第一Runtime 稳定性决定 Agent 上限。很多 Agent 产品失败不是模型不够强是 runtime 不够稳定——长会话爆栈、上下文溢出没兜底、工具执行超时没处理。9 种 continue reason 和多层错误恢复就是在解决这类问题。第二Streaming 不只是体验优化。它改变了工具执行的调度模型从“批量串行”变成“流式并发”带动了工具并发安全标记、消息乱序组装、部分失败处理一整条链路的升级。第三Prompt 缓存命中率值得认真对待。百万级请求量下命中率每提升 1% 都意味着可观的成本节省。实际接入时用 TaoToken 的 API Keyshttps://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi_keysutm_campaignrewrite创建 Key按第 4 节的三件套配置好 Base URL、Key、Model ID然后用 curl 或客户端跑一轮真实请求。对照 queryLoop 的九步流程在日志里逐条核对请求行为。接入文档https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite里有完整的协议说明和示例。如果你要长期跑编码任务Coding Planhttps://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding_planutm_campaignrewrite的配额更适合高频调用场景。下一篇会拆工具系统从模型说“我要调用 BashTool”到 BashTool 真正执行中间有一条 14 步的治理流水线输入校验、权限检查、风险预判、Hook 策略、用户交互每一步都可能拦住这次调用。42 个工具、1745 行执行逻辑不是“给模型暴露几个函数”那么简单。