Query Loop 模块对比分析
2026/4/27大约 5 分钟
Query Loop 模块对比分析
1. 模块边界
Query Loop 负责把一次用户请求推进到“模型回复结束”或“工具结果回填后继续下一轮”为止。这里关注的是主循环本身,以及它依赖的 prompt 组装、tool call 回填、重试、预算和压缩衔接。
2. Claude Code:以 query.ts 为中心的大循环
2.1 关键源码路径
claude-code-rev/src/query.ts:主循环与状态机,负责消息标准化、流式采样、tool use 检测、继续/终止决策。claude-code-rev/src/query/config.ts:在一次query()开始时冻结会话级配置与 feature gate。claude-code-rev/src/query/tokenBudget.ts:处理 turn budget 的续跑判断,不属于 API 调用本身,但直接影响循环是否继续。claude-code-rev/src/services/tools/toolOrchestration.ts:把同一轮里模型发出的多个工具调用切成并发批次或串行批次。claude-code-rev/src/services/tools/StreamingToolExecutor.ts、src/services/tools/toolExecution.ts:执行工具并把中间进度、结果、错误映射回消息流。claude-code-rev/src/services/compact/*、src/services/contextCollapse/*:在 token 压力或异常路径下压缩上下文,供主循环恢复。
2.2 运行方式
Claude Code 的主循环不是单个 while(true) 的薄壳,而是一个把很多恢复路径内联到同一层的“大循环”:
buildQueryConfig()在入口处冻结会话级 gate,避免一次 query 内出现配置漂移。- 主循环内部会对消息做 API 侧标准化,并在真正调用模型前拼接用户上下文、系统上下文、附件与记忆。
- 流式采样过程中直接识别
tool_use、max_output_tokens、prompt too long、stop hook 等事件。 - 一旦检测到工具调用,转入
runTools(),按并发安全性分组后执行,并把tool_result再写回消息链。 - 若触发预算、压缩或恢复路径,主循环不会立即结束,而是构造新的用户消息或压缩后的消息集继续下一轮。
2.3 设计重点
- 主循环承担了更多恢复语义。
max_output_tokens恢复、reactive compact、context collapse、tool result budget、stop hook 都直接嵌在query.ts。 - token budget 是显式的一等概念。
query/tokenBudget.ts用 continuation 计数和收益递减判断来控制是否继续“自动补完”当前回合。 - prompt 组装和工具执行都被视为主循环的组成部分,而不是完全独立的外围服务。
2.4 代价
- 单文件复杂度高,理解成本高。
- 但好处是所有“继续还是结束”的条件集中,出问题时更容易沿单条调用链追踪。
3. Opencode:以 SessionProcessor 为中心的分层循环
3.1 关键源码路径
opencode/packages/opencode/src/session/processor.ts:真正消费LLM.stream()事件流的核心循环。opencode/packages/opencode/src/session/llm.ts:组装 system prompt、provider 参数、插件注入、工具集合,然后调用streamText()。opencode/packages/opencode/src/session/prompt.ts:从用户输入进入 session loop,负责创建 user message、处理 noReply、拼接权限和触发loop()。opencode/packages/opencode/src/session/retry.ts:根据 provider 头和错误类型计算重试退避。opencode/packages/opencode/src/session/status.ts:把 session 暴露为idle / busy / retry三种可观察状态。opencode/packages/opencode/src/session/compaction.ts:overflow 后的压缩与 replay 逻辑,不直接写在处理循环里。
3.2 运行方式
Opencode 的 Query Loop 更像“processor + provider wrapper + compaction/retry sidecar”的组合:
session/prompt.ts把用户输入落成消息,再进入 session loop。session/llm.ts负责准备 provider、system prompt、headers、tool definitions、plugin hook 注入,然后返回stream.fullStream。SessionProcessor.create().process()用while (true)消费流事件,逐段更新 reasoning/text/tool part。tool-call事件到来时,先把 tool part 写成running,再由工具层执行并把结果回填为completed或error。- 若进入 retry、deny 或 overflow 等路径,则分别通过
SessionRetry、Permission、SessionCompaction处理,而不是把所有恢复逻辑塞进processor.ts。
3.3 设计重点
LLM.stream()是明显的抽象边界。模型接入、provider 选项、plugin hook 注入都在session/llm.ts处理,SessionProcessor主要关心事件消费与状态落盘。SessionStatus、SessionRetry、SessionCompaction都被拆成独立模块,主循环更薄。- 有显式的 doom loop 防护。
processor.ts会检查最近三次相同工具、相同输入的重复调用,并通过Permission.ask()触发人工确认。
3.4 代价
- 核心控制流比 Claude Code 更易读。
- 但恢复语义分散在多个 session 子模块里,排查复杂场景时需要跨
prompt -> llm -> processor -> compaction/retry/permission跳转。
4. 核心差异
| 维度 | Claude Code | Opencode |
|---|---|---|
| 主循环中心 | src/query.ts 单点集中 | session/processor.ts + session/llm.ts 分层 |
| Prompt 注入 | 主循环内拼接用户/系统上下文 | session/llm.ts 统一组装 system 与 provider 参数 |
| Tool 回填 | runTools() 后继续在同一 query 状态机推进 | processor.ts 按流事件更新 part,再由工具层回填 |
| Budget 控制 | query/tokenBudget.ts 明确控制续跑 | 以 provider 上下文限制和 compaction 为主,没有独立的 turn budget 模块 |
| 压缩衔接 | 与主循环强耦合,恢复路径多 | 作为独立 SessionCompaction 模块接入 |
| 可观察状态 | 更偏内部状态机 | SessionStatus 直接暴露 busy/retry/idle |
5. 设计取舍
5.1 Claude Code 的取舍
- 优先把“长会话能否继续跑下去”放在首位,所以 query loop 内建了大量预算和压缩恢复逻辑。
- 代价是主循环本身更厚、更难改,但复杂能力都集中在一个关键入口。
5.2 Opencode 的取舍
- 优先把模型流处理、状态落盘、权限、压缩拆开,保持每个模块职责清晰。
- 代价是跨模块联动更多,但新增 provider、plugin hook 或 compaction 机制时更容易插入。
6. 结论
Claude Code 的 Query Loop 更像一个“具备恢复能力的统一调度器”,重点是复杂场景下不断线;Opencode 的 Query Loop 更像“事件流处理器 + 独立侧模块”,重点是分层清晰和可替换性。前者偏集中式控制,后者偏组合式编排。