Claude Code - Query Loop
Claude Code - Query Loop
Query Loop
Overview 伪代码调用关系
- ts语法糖, 异步生成器, 边执行边抛输出 ```ts // 第一层 ask:SDK 入口,一次性调用 async function* ask(params) { const engine = new QueryEngine(config) yield* engine.submitMessage(params.prompt) }
// 第二层 QueryEngine:管这次对话的会话状态 class QueryEngine { async submitMessage(prompt) { // 处理 /斜杠命令、组装系统提示、注入上下文 …… yield query({ messages, systemPrompt, tools, … }) } }
// 第三层 query:流式包装层 asyncfunction* query(params) { yield* queryLoop(params) }
// 第四层 queryLoop:核心循环本体 async function* queryLoop(params) { while (true) { // 准备消息 → 调模型 → 判断 → 执行工具 → 塞回结果 } }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
### queryLoop
```ts
async function* queryLoop(params) {
let state = { messages: [...], turnCount: 1, ... }
while (true) {
// 1. 准备消息:必要时压缩
const messagesForQuery = maybeCompact(state.messages)
// 2. 流式调大模型,边收边处理
let toolUseBlocks = []
let needsFollowUp = false
for await (const chunk of callModel(messagesForQuery)) {
yield chunk // 文字块即时抛给用户
if (chunk 是 tool_use 块) {
toolUseBlocks.push(chunk)
needsFollowUp = true
}
}
// 3. 判断继续还是结束
if (!needsFollowUp) {
return { reason: 'completed' }
}
// 4. 执行所有 tool_use(可并行/串行)
const toolResults = await runTools(toolUseBlocks)
// 5. 塞回结果进下一轮
state = {
...state,
messages: [...messagesForQuery, ...assistantMessages, ...toolResults],
turnCount: state.turnCount + 1,
}
// continue 回到 while 头部
}
}
- Anthropic response
- text block 和 tool_use block
- 和OpenAI response有差别的
退出原因
- 模型resp没有tool_use就completed
- 正常结束
- max_turns: 达到最大turns
- aborted_streaming: 用户终止了模型输出
- aborted_tools: 用户终止了工具执行
- prompt_too_long: 消息太长, 压缩也进不了上下文窗口
- max_output_tokens_recovery: 模型输出被截断, 多次尝试续写还是失败
- stop_hook_prevented: 自定义了stop hook(例如执行git push前要先执行lint, 可是没有执行, 于是在stop hook被拦下)
- image_error
- 细分一共17种
异常退出处理
- 识别错误状态
- 做必要的清理(例如取消正在执行的工具, 解锁资源等)
- 返回结构化reason, 让调用者知道结束原因
执行工具
- 只读工具流式执行 ```ts // 模型流式输出时,每识别到一个工具调用就立刻丢到后台开跑 streamingToolExecutor.addTool(toolBlock, message)
// 模型流完后,把所有已完成的工具结果一次性收回来 const toolUpdates = streamingToolExecutor.getRemainingResults()
1
2
3
4
5
6
7
8
9
10
11
12
- 会改状态的工具串行
- 工具定义时声明属性(只读还是会改状态), 为指定默认会改状态兜底
### State
- 一个loop中跨turn传递
```ts
type State = {
messages: Message[] // 累积的对话消息历史
turnCount: number // 当前是第几轮
maxOutputTokensRecoveryCount: number // 输出截断已经恢复了几次
hasAttemptedReactiveCompact: boolean // 本轮是不是已经触发过压缩了, 避免反复压缩
}
tool_use block
- 每个tool_use block都必须要有一个tool_result block对应
- 如果输出了tool_use block但是tool被终止了, 没有tool_result, 会创建一个假的结果(yieldMissingToolResultBlocks)
1 2 3 4 5 6
yield 一条用户角色消息({ 类型: 'tool_result', // 标明这是个工具结果 内容: '这个工具没执行成功', // 解释为啥失败 is_error: true, // 打个错误标记 tool_use_id: 对应的工具调用ID, // 让 API 能配对上 })
模型output max token处理
- 静默升档
- 触发8K截断时, 将上限调成64K
- 模型续写
- 64K还是截断了, 在下一轮消息中插入「上次输出超长被截断了,请直接从断点续写,不要道歉、不要回顾你刚才在干什么、把剩余工作拆成更小的块」提示
- 进行nudge(轻推续写), 续写尝试次数达到上限就返回错误
This post is licensed under CC BY 4.0 by the author.