Files

283 lines
12 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
tags: [多轮对话, QueryEngine, 会话管理, 成本追踪, 模型切换]
create time: 2026-06-09 22:15
---
# 多轮对话管理 - QueryEngine 会话编排与持久化
## 概述
Claude Code 的多轮对话由 `QueryEngine` 类统一编排,管理会话状态机、JSONL transcript 持久化、成本追踪和模型热切换。REPL、SDK、ACP 三种交互方式共享同一套会话管理基础设施。
## 正文
### 交互方式对比
首先要区分 Claude Code 的多种交互方式: REPL 关注交互形态,SDK 关注接入方式,ACP 则关注通信协议。
| 维度 | REPL(交互形态) | SDK(接入方式) | ACP(通信协议) |
| :--- | :--- | :--- | :--- |
| **是什么** | 供开发者直接在终端使用的**交互式对话环境** | 面向开发者的**程序化调用库**,供集成到其他应用 | 一种**开放式的通信标准**,连接不同 AI Agent 与编辑器 |
| **使用方式** | 直接在终端输入 `claude` 命令,进入专用界面 | 在 Node.js/Python 项目中安装 SDK 包,通过 API 发送查询 | 通过 ACP 适配器启动 Claude Code,供编辑器通信 |
| **典型场景** | 日常编写代码时提问、修改代码或执行任务 | 集成到自动化脚本、CI/CD 流程或其他应用后台 | 集成到 JetBrains IDE、Zed 等第三方编辑器 |
| **主要特点** | 面向人,交互式,功能完整 | 面向程序,编程化,轻量级 | 标准化,双向通信,与编辑器深度整合 |
其中 SDK 与 ACP 采用 `QueryEngine` 实现会话管理。REPL 交互形态则通过 `onQueryImpl` 在 `src/screens/REPL.tsx` 中调用 `query()` 函数。
#### REPL 模式的调用链路
```mermaid
flowchart TD
A["用户输入"] --> B["onSubmit REPL.tsx"]
B --> C["handlePromptSubmit"]
C --> D["executeUserInput"]
D --> E["onQuery REPL.tsx"]
E --> F["onQueryImpl REPL.tsx"]
F --> G["query query.ts"]
```
其中 `query` 函数是 Agentic Loop 的核心实现,包含 `while(true)` 循环处理对话回合(`query.ts:460-522`)。
`onQueryImpl` 是 REPL 中与 AI 模型交互的核心控制器,负责:
1. 环境准备(IDE、诊断、权限)
2. 会话标题的首次生成
3. 构建动态系统提示和用户上下文
4. 执行流式查询并实时更新 UI
5. 收集性能指标和最终清理
### onQueryImpl 方法详解
该方法是一个 React `useCallback` 包装的异步函数,负责处理用户消息到 AI 模型的**完整查询流程**。
#### 函数签名与参数
```typescript
const onQueryImpl = useCallback(
async (
messagesIncludingNewMessages: MessageType[],
newMessages: MessageType[],
abortController: AbortController,
shouldQuery: boolean,
additionalAllowedTools: string[],
mainLoopModelParam: string,
effort?: EffortValue,
) => { /* ... */ },
[ /* ...dependencies */ ]
)
```
| 参数 | 说明 |
|------|------|
| `messagesIncludingNewMessages` | 包含新增消息的完整消息列表,用于构建模型输入 |
| `newMessages` | 本次新增的消息(例如用户刚输入的文本或附件) |
| `abortController` | 用于取消当前查询的控制器 |
| `shouldQuery` | 是否真正执行查询;若为 `false` 则跳过模型调用 |
| `additionalAllowedTools` | 本轮查询额外允许的工具列表(通常来自 Skill 的 frontmatter) |
| `mainLoopModelParam` | 指定本次使用的主模型参数 |
| `effort` | 可选,覆盖全局的"努力程度"值 |
#### 总体执行流程
```mermaid
flowchart TD
A["开始"] --> B{"shouldQuery?"}
B -- true --> C["IDE集成: 刷新MCP客户端, 诊断追踪, 关闭差异视图"]
B -- false --> D["仅处理compact边界/重置状态并返回"]
C --> E["标记项目onboarding完成"]
E --> F["尝试生成会话标题 仅一次"]
F --> G["将additionalAllowedTools写入全局权限store"]
G --> H["获取ToolUseContext 含最新工具/MCP"]
H --> I["如有effort, 临时覆盖effortValue"]
I --> J["并行执行: 系统提示/用户上下文/系统上下文/自动模式检查"]
J --> K["构建有效系统提示"]
K --> L["重置各类耗时计时器"]
L --> M["执行query生成器, 流式处理事件"]
M --> N["后处理: BUDDY/UDS/指标收集"]
N --> O["重置加载状态, 输出性能报告"]
O --> P["结束"]
D --> P
```
#### 核心逻辑要点
**IDE 集成与诊断**: 从 store 中获取最新的 MCP 客户端,通知诊断追踪器查询开始,若存在已连接的 IDE 客户端则关闭所有打开的差异视图。
**会话标题生成**: 仅当全局标题未禁用、当前无任何标题且从未尝试过时执行。从新增消息中提取第一条非元用户消息的真实文本,异步调用 `generateSessionTitle`。
**权限工具覆盖写入**: 将本轮 `additionalAllowedTools` 写入全局 store 的 `toolPermissionContext.alwaysAllowRules.command`,通过浅比较避免不必要的状态更新。
**shouldQuery = false 分支**: 处理不需要实际调用模型的情况(如无效斜杠命令或手动 `/compact`)。若新消息中包含 compact 边界消息,则生成新的 `conversationId`。
**查询前置准备**: `getToolUseContext` 获取最新工具和 MCP 客户端配置;可选的 `effort` 参数临时覆盖 `getAppState` 返回的 `effortValue`(仅限本轮查询);并行获取系统提示、用户上下文和系统上下文。
**流式事件处理**: 重置本轮计时器,调用 `query` 生成器函数,遍历每个事件并调用 `onQueryEvent` 更新 UI。
**后处理与指标收集**: BUDDY 特性的 companion 反应、UDS_INBOX 中断处理、Ant 内部用户的 API 指标记录(TTFT、OTPS)、重置加载状态和输出性能报告。
### 单轮 vs 多轮: 架构层面的差异
- **单轮**(一次 Agentic Loop): `query()` 函数的一次完整执行——组装上下文 -> 调 API -> 处理工具调用 -> 循环直到结束
- **多轮**(一个 Session): `QueryEngine` 类管理的一次会话——跨越数十轮 `submitMessage()` 调用,持续数小时
`QueryEngine`(`src/QueryEngine.ts`)是单轮 Agentic Loop 之上的**会话编排器**,它管理的状态远不止消息列表:
```mermaid
mindmap
root(("QueryEngine 内部状态"))
mutableMessages
"Message[] 完整对话历史"
readFileState
"FileStateCache 已读文件缓存"
totalUsage
"NonNullableUsage 累计token消耗"
permissionDenials
"SDKPermissionDenial[] 权限拒绝记录"
discoveredSkillNames
"Set string 当前turn已发现的skill"
loadedNestedMemoryPaths
"Set string 已加载的嵌套memory路径"
hasHandledOrphanedPermission
"boolean 是否已处理孤立权限请求"
abortController
"AbortController 会话级中断控制"
```
### QueryEngine 的核心方法: submitMessage()
每次用户输入一条消息,SDK 调用 `submitMessage()`,它会执行完整的 turn 初始化链路:
```typescript
// src/QueryEngine.ts -- submitMessage() 简化流程
async *submitMessage(
prompt: string | ContentBlockParam[],
options?: { uuid?: string; isMeta?: boolean },
): AsyncGenerator<SDKMessage> {
// 1. 清除 turn 级追踪状态
this.discoveredSkillNames.clear()
// 2. 解析模型(用户可能中途通过 setModel() 切换了模型)
const mainLoopModel = this.config.userSpecifiedModel
? parseUserSpecifiedModel(this.config.userSpecifiedModel)
: getMainLoopModel()
// 3. 动态组装 System Prompt(每次 turn 都重新构建)
const { defaultSystemPrompt, userContext, systemContext } =
await fetchSystemPromptParts({ tools, mainLoopModel, mcpClients })
// 4. 包装权限检查(追踪每次拒绝)
const wrappedCanUseTool = async (tool, input, ...) => {
const result = await canUseTool(tool, input, ...)
if (result.behavior !== 'allow') {
this.permissionDenials.push({ /* ... */ })
}
return result
}
// 5. 调用核心 query() 函数执行 agentic loop
yield* query({
systemPrompt, messages: this.mutableMessages,
tools, model: mainLoopModel, ...
})
}
```
> [!tip] 关键设计
> `submitMessage()` 是 `async *Generator`——它逐步 yield `SDKMessage`,让调用方(REPL/SDK)能实时展示进度,而不是等整个 turn 结束。
### 会话持久化: JSONL Transcript
每次对话事件都被追加写入 transcript 文件(`src/utils/sessionStorage.ts`)。
**存储路径**: `~/.claude/projects/<sanitized-cwd>/<session-uuid>.jsonl`
- 路径由 `getProjectDir(originalCwd)` 生成,使用 `sanitizePath()` 将项目目录路径转换为安全的目录名
- 每条记录是一行 JSON(JSONL 格式),支持追加写入
- 读取上限为 50MB(`MAX_TRANSCRIPT_READ_BYTES`),防止超大会话导致 OOM
#### Transcript 写入流程
```mermaid
flowchart TD
A["recordTranscript sessionId, entry"] --> B["project.enqueueWrite filePath, entry"]
B --> C["scheduleDrain 设置定时器"]
C --> D["drainWriteQueue 按MAX_CHUNK_BYTES分批"]
D --> E["appendToFile 批量追加写入"]
E --> F{"配置了远程持久化?"}
F -- 是 --> G["persistToRemote"]
F -- 否 --> H["完成"]
G --> H
```
同步直写路径用于元数据重写等场景: `appendEntryToFile(fullPath, entry)` 使用 `appendFileSync`,失败时 `mkdir` + 重试。
#### 会话恢复链路
`--resume` 参数触发的恢复流程:
1. 解析 resume 参数: UUID 格式 -> `getTranscriptPathForSession(uuid)`;`.jsonl` 文件路径 -> 直接使用;boolean -> 最近一次会话的 picker
2. `loadTranscriptFromFile(path)`: 按 JSONL 行解析,过滤出消息类型记录,重建 `Message[]` 数组
3. 恢复上下文状态: `restoreCostStateForSession(sessionId)` 恢复累计费用、恢复 agentSetting、如有 `--rewind-files` 则恢复文件快照
4. 创建 `QueryEngine({ initialMessages: restoredMessages })` 从恢复的消息继续对话
### 成本追踪: 从 API Usage 到美元
成本追踪贯穿三个模块,形成完整的记录-累计-展示链路。
**记录层**: 每个 `message_delta` 事件携带 `usage` 字段(`input_tokens`、`output_tokens`、`cache_creation_input_tokens`、`cache_read_input_tokens`)。`accumulateUsage()` 将增量 usage 累加到会话总量。
**累计层** (`src/cost-tracker.ts`):
```typescript
type StoredCostState = {
totalCostUSD: number // 累计美元花费
totalAPIDuration: number // API 调用总时长(含重试)
totalAPIDurationWithoutRetries: number // 不含重试的纯推理时间
totalToolDuration: number // 工具执行总时长
totalLinesAdded: number // 代码增加行数
totalLinesRemoved: number // 代码删除行数
lastDuration: number | undefined // 最近一次会话时长
modelUsage: { [modelName: string]: ModelUsage } | undefined // 按模型分拆的用量
}
```
**持久化**: 每次会话结束时保存到项目配置(`saveCurrentSessionCosts`),跨重启保留。
**预算熔断**: `QueryEngineConfig.maxBudgetUsd` 提供会话级硬性预算上限。REPL 中累计费用超过 $5 时弹出费用提醒对话框(软提醒)。
### 模型热切换
在一个会话中切换模型不会丢失对话历史——因为 `mutableMessages` 与模型选择是解耦的:
```mermaid
sequenceDiagram
participant U as 用户
participant Q as QueryEngine
participant P as Parser
participant S as SystemPrompt
participant A as API
U->>Q: /model sonnet
Q->>Q: setModel("claude-sonnet-4-20250514")
Note over Q: config.userSpecifiedModel = model
U->>Q: submitMessage(下一条消息)
Q->>P: parseUserSpecifiedModel()
P-->>Q: 新模型配置
Q->>S: fetchSystemPromptParts(newModel)
S-->>Q: 重新组装的 System Prompt
Q->>A: query(model=newModel, messages=完整历史)
```
切换模型时,`contextWindowTokens` 和 `maxOutputTokens` 也会根据新模型的规格重新计算——例如从 Sonnet 切换到 Opus 时,上下文窗口可能从 200K 变为 1M。
### 文件快照与回滚
`fileHistoryMakeSnapshot()`(`src/utils/fileHistory.ts`)在 AI 每次修改文件前自动保存当前内容。快照绑定到具体的 `message.id`,使得 `--rewind-files <user-message-id>` 可以精确恢复到对话中任意时间点的文件状态——这比 git 更细粒度(git 只追踪已提交的内容)。
## 关联笔记
- [[the-loop]] - Agentic Loop 核心机制与状态机
- [[streaming]] - 流式响应机制与 SSE 事件处理
- [[../context/system-prompt]] - System Prompt 动态组装
- [[../context/compaction]] - 上下文压缩策略