free-code-dotnet/docs/核心模块设计/核心模块设计-查询引擎-QueryEngine.md
应文浩wenhao.ying@xiaobao100.com e25ac591a7 init easy-code
2026-04-06 07:24:24 +08:00

302 lines
11 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.

# 核心模块设计 — 查询引擎 (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
/// <summary>
/// LLM查询引擎 - 核心消息处理管道
/// 对应原始 ../../src/QueryEngine.ts
/// </summary>
public interface IQueryEngine
{
/// <summary>提交用户消息并返回流式响应</summary>
IAsyncEnumerable<SDKMessage> SubmitMessageAsync(
string content,
SubmitMessageOptions? options = null,
CancellationToken ct = default);
/// <summary>取消当前查询</summary>
Task CancelAsync();
/// <summary>获取消息历史</summary>
IReadOnlyList<Message> GetMessages();
/// <summary>获取当前token使用量</summary>
TokenUsage GetCurrentUsage();
}
public record SubmitMessageOptions(
string? Model = null,
ToolPermissionContext? PermissionContext = null,
string? QuerySource = null,
bool IsSpeculation = false
);
```
`IAsyncEnumerable<SDKMessage>` 返回类型让调用方(终端 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<QueryEngine> _logger;
private readonly List<Message> _messages = new();
private CancellationTokenSource? _activeCts;
public async IAsyncEnumerable<SDKMessage> 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<Message> 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<string> BuildAsync(
IReadOnlyList<Message> 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($"<session_memory>\n{memory.Content}\n</session_memory>");
// 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)