# 核心模块设计 — 查询引擎 (QueryEngine)
> 所属项目: free-code .NET 10 重写
> 原始代码来源: `../../src/QueryEngine.ts`
> 原始设计意图: 协调 LLM 消息流转、工具调用循环和 System Prompt 构建,是整个 Agent 行为的核心管道
> 上级文档: [核心模块设计总览](核心模块设计.md)
> 交叉参考: [CLI 启动与解析](核心模块设计-CLI启动与解析.md) | [基础设施设计 — 状态管理](../基础设施设计/基础设施设计-状态管理.md)
---
## 概述
`QueryEngine` 是 free-code 的核心消息处理管道。每当用户提交一条消息,`IQueryEngine.SubmitMessageAsync` 便接管控制权,驱动一个"助手回复 → 工具调用 → 执行结果 → 继续"的循环,直到助手不再请求工具调用或用户取消。
原始 `QueryEngine.ts` 是一个约 1200 行的 TypeScript 文件,包含消息历史管理、System Prompt 构建、工具执行调度、上下文压缩判断、记忆提取等所有逻辑。.NET 重写将其中的 System Prompt 构建拆分为独立的 `SystemPromptBuilder`(实现 `IPromptBuilder`),其余核心循环保留在 `QueryEngine` 中。
---
## 6.1 IQueryEngine 接口
```csharp
///
/// LLM查询引擎 - 核心消息处理管道
/// 对应原始 ../../src/QueryEngine.ts
///
public interface IQueryEngine
{
/// 提交用户消息并返回流式响应
IAsyncEnumerable SubmitMessageAsync(
string content,
SubmitMessageOptions? options = null,
CancellationToken ct = default);
/// 取消当前查询
Task CancelAsync();
/// 获取消息历史
IReadOnlyList GetMessages();
/// 获取当前token使用量
TokenUsage GetCurrentUsage();
}
public record SubmitMessageOptions(
string? Model = null,
ToolPermissionContext? PermissionContext = null,
string? QuerySource = null,
bool IsSpeculation = false
);
```
`IAsyncEnumerable` 返回类型让调用方(终端 UI 层)可以在消息流抵达时即时渲染,无需等待整个响应完成。`SDKMessage` 是一个联合类型(discriminated union),涵盖流式文本增量、工具调用开始、工具执行结果等所有消息类型。
---
## 6.2 QueryEngine 实现
### 依赖关系
`QueryEngine` 通过构造函数注入以下依赖,每一项都有对应的原始 TypeScript 概念:
| .NET 依赖 | 对应原始概念 |
|-----------|-------------|
| `IApiProviderRouter` | `claude.ts` 的 API 调用函数 |
| `IToolRegistry` | `tools.ts` 的 `getTools()` |
| `IPermissionEngine` | `permission.ts` 的权限检查逻辑 |
| `IPromptBuilder` | `getSystemPrompt()` 内联函数 |
| `ISessionMemoryService` | `memory.ts` 相关逻辑 |
| `IFeatureFlagService` | `isFeatureEnabled()` |
### 流式循环实现
```csharp
public class QueryEngine : IQueryEngine
{
private readonly IApiProviderRouter _providerRouter;
private readonly IToolRegistry _toolRegistry;
private readonly IPermissionEngine _permissionEngine;
private readonly IPromptBuilder _promptBuilder;
private readonly ISessionMemoryService _memoryService;
private readonly IFeatureFlagService _featureFlags;
private readonly ILogger _logger;
private readonly List _messages = new();
private CancellationTokenSource? _activeCts;
public async IAsyncEnumerable SubmitMessageAsync(
string content,
SubmitMessageOptions? options = null,
[EnumeratorCancellation] CancellationToken ct = default)
{
_activeCts = CancellationTokenSource.CreateLinkedTokenSource(ct);
options ??= new SubmitMessageOptions();
// 1. 构建用户消息
var userMessage = new Message
{
Role = MessageRole.User,
Content = new UserContent(content),
Timestamp = DateTime.UtcNow,
MessageId = Guid.NewGuid().ToString()
};
_messages.Add(userMessage);
yield return new SDKMessage.UserMessage(userMessage);
// 2. 查询循环 (Assistant → ToolUse → Execute → Continue)
var shouldContinue = true;
while (shouldContinue && !_activeCts.Token.IsCancellationRequested)
{
// 2a. 构建System Prompt
var systemPrompt = await _promptBuilder.BuildAsync(
_messages, options.PermissionContext, options);
// 2b. 获取可用工具
var tools = await _toolRegistry.GetToolsAsync(options.PermissionContext);
// 2c. 调用API Provider
var provider = _providerRouter.GetActiveProvider();
var apiRequest = new ApiRequest
{
SystemPrompt = systemPrompt,
Messages = BuildApiMessages(_messages),
Tools = tools,
Model = options.Model
};
await foreach (var response in provider.StreamAsync(
apiRequest, _activeCts.Token))
{
switch (response)
{
case SDKMessage.AssistantMessage am:
_messages.Add(am.ToMessage());
yield return am;
break;
case SDKMessage.StreamingDelta sd:
yield return sd;
break;
case SDKMessage.ToolUseStart tus:
yield return tus;
break;
case SDKMessage.ToolUseResult tur:
shouldContinue = tur.ShouldContinue;
_messages.Add(tur.ToMessage());
yield return tur;
break;
case SDKMessage.CompactBoundary cb:
yield return cb;
break;
}
}
}
// 3. 后处理
await PostQueryProcessingAsync(options);
}
private async Task PostQueryProcessingAsync(SubmitMessageOptions options)
{
// 记忆提取 (EXTRACT_MEMORIES flag)
if (_featureFlags.IsEnabled(FeatureFlags.ExtractMemories))
{
_ = _memoryService.TryExtractAsync(_messages); // fire-and-forget
}
// Prompt建议
// 上下文压缩检查
// 统计更新
}
public Task CancelAsync()
{
_activeCts?.Cancel();
return Task.CompletedTask;
}
public IReadOnlyList GetMessages() => _messages.AsReadOnly();
public TokenUsage GetCurrentUsage() => /* 从最近API响应头提取 */;
}
```
### 循环流程图
```
用户提交消息
│
▼
构建 System Prompt ◄────────────────────┐
│ │
▼ │
获取工具列表 │
│ │
▼ │
调用 API Provider (流式) │
│ │
├── AssistantMessage → 存入历史 │
├── StreamingDelta → 直接 yield │
├── ToolUseStart → 直接 yield │
└── ToolUseResult ────────────────────┤
│ ShouldContinue = true │
└────────────────────────────►┘
│ ShouldContinue = false
▼
后处理(记忆提取等)
│
▼
结束
```
---
## 6.3 System Prompt Builder
`SystemPromptBuilder` 对应原始 `QueryEngine.ts` 中内联的 `getSystemPrompt()` 函数,负责将多个信息源组装为最终发送给 API 的 System Prompt。
**原始设计意图:** 原始代码将系统指令、工具描述、命令描述、记忆、上下文信息拼接在一个函数中,每次查询都重新计算。.NET 重写将其提取为独立接口 `IPromptBuilder`,便于测试和替换。
```csharp
public class SystemPromptBuilder : IPromptBuilder
{
private readonly IToolRegistry _toolRegistry;
private readonly ICommandRegistry _commandRegistry;
private readonly ISessionMemoryService _memoryService;
private readonly IFeatureFlagService _features;
private readonly ICompanionService? _companionService;
public async Task BuildAsync(
IReadOnlyList messages,
ToolPermissionContext? permissionContext,
SubmitMessageOptions options)
{
var sb = new StringBuilder();
// 1. 基础系统指令 (CLAUDE.md 等价物)
sb.AppendLine(await BuildBaseInstructionsAsync());
// 2. 工具描述 (JSON Schema)
var tools = await _toolRegistry.GetToolsAsync(permissionContext);
sb.AppendLine(BuildToolDescriptions(tools));
// 3. 命令描述
var commands = await _commandRegistry.GetEnabledCommandsAsync();
sb.AppendLine(BuildCommandDescriptions(commands));
// 4. 会话记忆
var memory = await _memoryService.GetCurrentMemoryAsync();
if (memory != null)
sb.AppendLine($"\n{memory.Content}\n");
// 5. 上下文信息 (工作目录、Git状态、环境)
sb.AppendLine(await BuildContextInfoAsync());
// 6. 同伴介绍 (BUDDY flag)
if (_features.IsEnabled(FeatureFlags.Buddy) && _companionService != null)
{
var companion = await _companionService.GetCompanionAsync();
if (companion != null)
sb.AppendLine(CompanionPromptBuilder.BuildIntro(companion));
}
return sb.ToString();
}
}
```
### 六段构建逻辑说明
| 段落 | 内容来源 | 条件 |
|------|----------|------|
| 1. 基础指令 | `BuildBaseInstructionsAsync()` — 读取内置指令文本 | 始终包含 |
| 2. 工具描述 | `IToolRegistry.GetToolsAsync()` — 当前可用工具的 JSON Schema | 始终包含 |
| 3. 命令描述 | `ICommandRegistry.GetEnabledCommandsAsync()` — 斜杠命令列表 | 始终包含 |
| 4. 会话记忆 | `ISessionMemoryService.GetCurrentMemoryAsync()` | 存在记忆时包含 |
| 5. 上下文信息 | 工作目录、Git 状态、环境变量摘要 | 始终包含 |
| 6. 同伴介绍 | `ICompanionService.GetCompanionAsync()` | `BUDDY` feature flag 开启时包含 |
工具描述(段落 2)的稳定性直接影响 Anthropic 的 prompt cache 命中率。`ToolRegistry` 对工具列表进行字典序排序(见[工具系统](核心模块设计-工具系统.md))正是为了保证这一段的内容在相同工具集下保持不变。
---
## 取消机制
`CancelAsync()` 通过 `CancellationTokenSource.Cancel()` 触发取消。`SubmitMessageAsync` 内的 `[EnumeratorCancellation]` 特性确保当外部 `CancellationToken` 被取消时,异步枚举自动停止。
两个取消来源通过 `CreateLinkedTokenSource` 合并:
- 用户调用 `CancelAsync()`(例如按 Ctrl+C)
- 外部传入的 `ct` 参数(例如超时或应用关闭)
---
## 参考资料
- [核心模块设计总览](核心模块设计.md)
- [CLI 启动与解析](核心模块设计-CLI启动与解析.md)
- [工具系统](核心模块设计-工具系统.md)
- [基础设施设计 — 状态管理](../基础设施设计/基础设施设计-状态管理.md)
- [原始代码映射 — 核心模块](reference/原始代码映射-核心模块.md)