☰
Claude Code 源码架构深度解析(二):拆解 Agent Runtime 的 1729 行——queryLoop 与 Streaming Tool Execution 如何驱动 TaoToke
2026/10/12 9:58:43 网站建设 项目流程

1. 从一次“帮我重构这个函数”说起:queryLoop 到底在循环什么

你在 Claude Code 里敲下“帮我重构这个函数”,回车之后发生的事情,远比“拼 prompt → 调 API → 打印结果”复杂。真实链路是:cli.tsx 做 fast-path 分发,加载 main.tsx;main.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_source=taotoken_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: Promise<ToolUseSummaryMessage | 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/api,Key 用你创建的密钥,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 a+b}"} ] }'

这个请求对应 queryLoop 第四步“调用模型 API”。如果你在客户端里开启流式,就能观察到第五步“流式处理响应”的行为——tool_use block 出现时,客户端会立刻开始执行工具,而不是等整个响应结束。

对于需要长期跑编码任务或 Agent 的场景,建议用 Coding Plan(https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding_plan&utm_campaign=rewrite),它针对高频调用做了配额优化。如果你只是想验证模型行为,用模型对话(https://taotoken.net/chat?utm_source=taotoken_aicg_blog_end&utm_content=model_chat&utm_campaign=rewrite)快速试一轮即可。接入文档在 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite,里面有完整的协议说明。

配置好之后,你可以对照 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-key(Anthropic 协议)还是Authorization: Bearer(OpenAI 协议)
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/messages,OpenAI 协议走/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 Keys(https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api_keys&utm_campaign=rewrite)创建 Key,按第 4 节的三件套配置好 Base URL、Key、Model ID,然后用 curl 或客户端跑一轮真实请求。对照 queryLoop 的九步流程,在日志里逐条核对请求行为。接入文档(https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite)里有完整的协议说明和示例。如果你要长期跑编码任务,Coding Plan(https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding_plan&utm_campaign=rewrite)的配额更适合高频调用场景。

下一篇会拆工具系统:从模型说“我要调用 BashTool”到 BashTool 真正执行,中间有一条 14 步的治理流水线,输入校验、权限检查、风险预判、Hook 策略、用户交互,每一步都可能拦住这次调用。42 个工具、1745 行执行逻辑,不是“给模型暴露几个函数”那么简单。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询