Skip to content

Agent 循环:心脏

QueryEngine —— 一切的中心

如果 Claude Code 是一个人,那 QueryEngine 就是它的心脏。这个类管理着一整场对话——记住你说过什么、模型回复了什么、调用了哪些工具、花了多少 token。

每次你在终端里输入一句话,就是 QueryEngine 的一个 turn(轮次)。

System Prompt:不是写出来的,是"编译"出来的

在每次调用 API 之前,QueryEngine 会先组装一个 System Prompt——这是一段"背景说明",告诉模型它身处什么环境、有什么能力、要遵守什么规则。

System Prompt 里包含这些信息:

内容作用
可用工具列表让模型知道它能用 Read、Bash、Edit 等工具
当前工作目录让模型知道它在哪个项目里
项目上下文git 状态、项目结构等
CLAUDE.md 内容项目级的自定义指令(如果有的话)
权限规则哪些操作需要确认,哪些可以直接做
MCP 服务器信息有哪些外部工具可用

Prompt 缓存:把提示词当编译器输出来优化

源码中有一个关键概念:SYSTEM_PROMPT_DYNAMIC_BOUNDARY(动态边界标记)。

这个标记把 System Prompt 分成两半:

区域内容缓存策略
边界线上方(静态)工具描述、基础规则、行为准则Claude API 缓存,不重复计费
边界线下方(动态)Git 分支、CLAUDE.md、用户偏好、记忆每次对话都不同,不缓存

Anthropic 把提示词当成了编译器的输出来优化——静态部分是"编译后的二进制",动态部分是"运行时参数"。好处是:

  • 省钱:静态部分走缓存,不重复计费
  • :缓存命中直接跳过这些 token 的处理
  • 灵活:动态部分让每次对话都能感知当前环境

而且工具的排序是固定的,也是为了保持缓存稳定性——如果工具顺序每次不同,缓存就会失效。

每个工具都有独立的"AI 使用手册"

更值得注意的是:每个工具目录下都有一个 prompt.ts 文件——这是专门写给 LLM 看的行为准则

以 BashTool 为例,它的 prompt.ts 约有 370 行,详细规定了:

  • 什么命令可以直接执行,什么必须先确认
  • 绝对不能做的事(如 git push --force
  • 输出太长时的处理策略
  • 与其他工具协作的规则

这不是写给人看的文档,是写给 AI 看的操作规范。每次 Claude Code 启动时,这些规则都会被注入到 System Prompt 中。这就是为什么 Claude Code 从不会擅自 git push --force,而某些工具会——不是模型更聪明,是提示词里已经把规矩讲清楚了。

Anthropic 内部版本和你用的不一样

代码里大量出现类似 isAnthropicEmployee 的判断分支。内部员工(ant)的版本有:

  • 更详细的代码风格指引("不写注释除非 WHY 不明显")
  • 更激进的输出策略("倒金字塔写作法")
  • 仍在 A/B 测试的实验功能(如 Verification Agent、Explore & Plan Agent)

这说明 Anthropic 自己就是 Claude Code 最大的用户——他们在用自己的产品来开发自己的产品。

你可能好奇的

CLAUDE.md 就像项目的"说明书"。你可以在里面写类似"这个项目用 Python 3.12,测试用 pytest,数据库用 PostgreSQL"这样的信息,Claude Code 每次都会读取它,这样它就"了解"你的项目了。

流式响应:为什么感觉很快?

Claude Code 用的是 Server-Sent Events (SSE) 流式 API。模型的回复不是等全部生成完再返回,而是一边生成一边往回推

这意味着:

  • 你能看到模型"正在打字"的效果
  • 工具调用在响应还没完全结束时就能检测到
  • 整体感知速度比等全部生成完快很多

Token 预算:不能无限循环

模型的上下文窗口有 200K tokens 的限制。QueryEngine 需要管理这个预算:

200K tokens
├── System Prompt    ~ 5-15K(取决于工具数量和项目信息)
├── 对话历史         ~ 大部分空间在这
├── 当前工具结果     ~ 可变
└── 预留生成空间     ~ 模型需要空间写回复

当对话太长、占用达到 75-92% 时,会触发自动压缩(compaction)。我们在第 7 章详细讲。

多轮执行的安全阀

除了 token 预算,还有其他停止条件:

  • maxTurns:最大循环轮次(防止无限循环)
  • maxBudgetUsd:花费上限(防止 API 费用失控)
  • 用户中断:你可以随时 Ctrl+C
  • 模型主动停止:模型判断任务完成了,返回纯文字

AsyncGenerator:优雅的流式架构

QueryEngine 的 submitMessage 是一个 AsyncGenerator(异步生成器)。如果你不熟悉这个概念,可以这样理解:

什么是 AsyncGenerator?

普通函数:调一次,返回一个结果。 Generator:调一次,可以返回很多个结果,每次返回一个,调用方需要时再给下一个。 AsyncGenerator:和 Generator 一样,但每个结果的产生可以是异步的(比如等网络请求)。

Claude Code 用 AsyncGenerator 是因为:

  • API 响应是流式的(异步一点点到达)
  • UI 需要实时更新(每到一点就渲染一点)
  • 工具执行是穿插在中间的(收到工具调用 → 暂停 → 执行 → 继续接收)

这样一来,UI 层只需要 for await (const msg of engine.submitMessage(input)) 就能实时收到所有更新。

完整的一次交互

把以上所有部分串起来,一次完整的交互是这样的:

  1. 你在终端输入 "帮我修复 tests 里的失败用例"
  2. QueryEngine 组装 System Prompt(工具列表 + 项目信息 + 规则)
  3. 处理你的输入(解析 slash 命令、模型切换等)
  4. 调用 Anthropic API,流式接收响应
  5. 模型决定先读测试文件 → 执行 Read 工具 → 结果返回
  6. 模型分析失败原因,决定修改代码 → 权限检查 → 执行 Edit 工具
  7. 模型决定跑一下测试 → 执行 Bash 工具 (npm test)
  8. 测试通过了,模型返回纯文字总结
  9. QueryEngine 保存会话历史,这个 turn 结束

这一切都在那个 while 循环里完成。

接下来我们深入看看这些"工具"到底是什么——工具系统:给 AI 双手