feat(llm): 实现 Phase 0 剩余四个模块

实现 ProviderRegistry、HookExecutor、StreamEvents 和 Auto-compaction 模块,并集成到 LlmCycle 中
This commit is contained in:
徐涛
2026-06-02 08:51:42 +08:00
parent 69b6dd942b
commit 32f3edaf19
13 changed files with 1299 additions and 9 deletions
+375
View File
@@ -0,0 +1,375 @@
# Phase 0 剩余模块 — 实施方案
> 定稿日期:2026-06-02
## 背景与目标
AG Core Phase 0Foundation)已完成核心数据类型、错误体系、Provider 接口、OpenAI 实现、生命周期引擎等基础设施。剩余 4 个子项尚未实现:**ProviderRegistry**、**HookExecutor**、**StreamEvents**、**Auto-compaction**。它们均被 Roadmap 标记为 P0(Must Have),是本阶段不可或缺的底层能力。
**目标**:完成这 4 个模块的设计与实现,使 Phase 0 全面交付。
---
## 需求分析
### 功能需求
| 模块 | 需求 | 验收条件 |
|------|------|---------|
| ProviderRegistry | 支持注册命名 Provider、按名称查找、设置默认 | 3 个公开方法 + 工厂辅助 |
| HookExecutor | 4 个事件点:PreRequest / PostRequest / OnRetry / OnError | Hook trait + HookExecutor 触发 |
| StreamEvents | 流式事件枚举 + Provider 流式方法 + Cycle 流式入口 | 6 种事件类型 + SSE 解析 |
| Auto-compaction | Token 估算 + 微压缩 + 断路器 | 触发后释放 token 且不改变语义 |
### 非功能需求
- 所有公开 API 必须带 `///` 文档注释
- 无新增 `unwrap()` 调用
- 与现有 `LlmCycle` 集成时保持向后兼容(全为可选/增量添加)
- 错误统一使用 `LlmError` 枚举
---
## 方案设计
### 1. ProviderRegistry (`src/llm/provider/registry.rs`)
**职责**:管理多个 LLM Provider 实例,支持按名称注册、发现、切换。
```rust
// src/llm/provider/registry.rs
use std::collections::HashMap;
use crate::llm::error::LlmError;
use crate::llm::provider::{LlmProvider, ProviderConfig, ProviderType, create_provider};
/// Provider 注册表——管理多个 LLM Provider 实例。
pub struct ProviderRegistry {
providers: HashMap<String, Box<dyn LlmProvider>>,
default_name: Option<String>,
}
impl ProviderRegistry {
pub fn new() -> Self;
/// 注册一个已初始化的 Provider 实例。
pub fn register(&mut self, name: impl Into<String>, provider: Box<dyn LlmProvider>);
/// 通过 ProviderType + ProviderConfig 创建并注册。
pub fn register_with_config(
&mut self,
name: impl Into<String>,
provider_type: ProviderType,
config: ProviderConfig,
) -> Result<(), LlmError>;
/// 设置默认 Provider。
pub fn set_default(&mut self, name: &str) -> Result<(), LlmError>;
/// 按名称查找 Provider。
pub fn get(&self, name: &str) -> Option<&dyn LlmProvider>;
/// 获取默认 Provider。
pub fn get_default(&self) -> Option<&dyn LlmProvider>;
}
```
**无新增依赖**
---
### 2. HookExecutor (`src/llm/hooks.rs`)
**职责**:在 LLM 调用生命周期的关键节点插入自定义逻辑。
```rust
// src/llm/hooks.rs
use async_trait::async_trait;
use crate::llm::error::LlmError;
use crate::llm::types::ChatRequest;
/// 生命周期钩子事件点。
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum HookEvent {
/// LLM 请求发起之前(可阻断)。
PreRequest,
/// 成功响应之后。
PostRequest,
/// 重试之前(仅可重试错误时触发)。
OnRetry,
/// 不可恢复错误返回之前。
OnError,
}
/// 此次钩子调用的上下文。
#[derive(Debug, Clone)]
pub struct HookContext<'a> {
pub event: HookEvent,
pub request: Option<&'a ChatRequest>,
pub error: Option<&'a LlmError>,
pub attempt: u32,
}
/// 钩子执行结果。
#[derive(Debug, Clone)]
pub struct HookResult {
/// 是否阻断后续操作(仅 PreRequest 有效)。
pub should_block: bool,
/// 阻断/备注原因。
pub reason: Option<String>,
}
/// 生命周期钩子 trait。
#[async_trait]
pub trait Hook: Send + Sync {
async fn execute(&self, ctx: &HookContext<'_>) -> HookResult;
}
/// 钩子执行器——管理注册与触发。
pub struct HookExecutor {
hooks: Vec<(HookEvent, Box<dyn Hook>)>,
}
impl HookExecutor {
pub fn new() -> Self;
pub fn register(&mut self, event: HookEvent, hook: Box<dyn Hook>);
pub async fn execute(&self, event: HookEvent, ctx: &HookContext<'_>) -> Vec<HookResult>;
}
```
**与 LlmCycle 集成**
- `LlmCycle` 新增字段 `hook_executor: Option<HookExecutor>`
- 新增 builder 方法 `with_hook_executor()`
- `submit()` 中在 4 个点触发(PreRequest 若阻断则提前返回)
**无新增依赖**async-trait 已存在)。
---
### 3. StreamEvents (`src/llm/stream.rs`)
**职责**:提供流式 LLM 调用的事件抽象,将原始 SSE chunk 解析为语义化事件。
```rust
// src/llm/stream.rs
use std::pin::Pin;
use tokio_stream::Stream;
use crate::llm::error::LlmError;
use crate::llm::types::{FinishReason, Usage, OpenaiChatChunk, ToolDefinition};
use serde_json::Value;
/// 流式事件——LLM 调用全生命周期的语义化事件。
#[derive(Debug, Clone)]
pub enum StreamEvent {
/// 助手回复文本增量。
AssistantTextDelta { text: String },
/// 工具调用开始。
ToolExecutionStarted { tool_name: String, input: Value },
/// 工具调用完成。
ToolExecutionCompleted { tool_name: String, output: Value, is_error: bool },
/// Token 用量更新。
CostUpdate { usage: Usage },
/// 一轮会话完成。
TurnComplete { reason: FinishReason },
/// 可恢复的错误事件。
Error { message: String },
}
/// 将原始 OpenaiChatChunk 流解析为 StreamEvent 流。
pub fn parse_chunk_stream(
chunks: Pin<Box<dyn Stream<Item = Result<OpenaiChatChunk, LlmError>> + Send>>,
) -> Pin<Box<dyn Stream<Item = StreamEvent> + Send>>;
```
**Provider 层扩展**`src/llm/provider.rs`):
```rust
#[async_trait]
pub trait LlmProvider: Send + Sync {
async fn chat(&self, request: ChatRequest) -> Result<ChatResponse, LlmError>;
/// 流式聊天请求——返回原始 SSE chunk 流。
/// 默认实现回退到非流式调用。
async fn chat_stream(
&self,
request: ChatRequest,
) -> Result<
Pin<Box<dyn Stream<Item = Result<OpenaiChatChunk, LlmError>> + Send>>,
LlmError,
> {
let response = self.chat(request).await?;
let chunk = OpenaiChatChunk::from(response);
Ok(Box::pin(tokio_stream::once(Ok(chunk))))
}
}
```
**LlmCycle 扩展**
```rust
impl LlmCycle {
/// 提交请求并返回语义事件流。
pub async fn submit_stream(
&mut self,
prompt: String,
tools: Vec<ToolDefinition>,
) -> Result<
Pin<Box<dyn Stream<Item = StreamEvent> + Send>>,
LlmError,
>;
}
```
**新增依赖**
```toml
tokio-stream = "0.1"
```
---
### 4. Auto-compaction (`src/llm/compact.rs`)
**职责**:在上下文过长时自动压缩历史消息,避免 ContextLength 错误。
```rust
// src/llm/compact.rs
use crate::llm::types::{ContentField, Message, OpenaiChatMessage, OpenaiContentPart};
// === 常量 ===
const AUTOCOMPACT_BUFFER_TOKENS: u32 = 13_000;
const RESERVED_OUTPUT_TOKENS: u32 = 20_000;
const MAX_CONSECUTIVE_FAILURES: u32 = 3;
const KEEP_RECENT: usize = 6;
/// 上下文压缩配置。
#[derive(Debug, Clone)]
pub struct CompactConfig {
/// 模型上下文窗口大小(token 数)。
pub context_window: u32,
/// 为输出预留的 token 数。
pub reserved_tokens: u32,
/// 微压缩保留的最近消息数。
pub keep_recent: usize,
}
impl Default for CompactConfig {
fn default() -> Self {
Self {
context_window: 128_000,
reserved_tokens: RESERVED_OUTPUT_TOKENS,
keep_recent: KEEP_RECENT,
}
}
}
impl CompactConfig {
pub fn threshold(&self) -> u32 {
self.context_window
.saturating_sub(self.reserved_tokens)
.saturating_sub(AUTOCOMPACT_BUFFER_TOKENS)
}
}
/// 压缩状态——跟踪连续失败次数(断路器模式)。
#[derive(Debug, Clone)]
pub struct CompactState {
consecutive_failures: u32,
}
impl CompactState {
pub fn new() -> Self;
pub fn record_success(&mut self);
/// 记录失败,返回 true 表示已达断路器上限。
pub fn record_failure(&mut self) -> bool;
}
/// 粗略估计消息列表的 token 数(基于字符数,4 字符 ≈ 1 token)。
pub fn estimate_message_tokens(messages: &[Message]) -> u32;
/// 判断是否需要触发自动压缩。
pub fn should_compact(messages: &[Message], config: &CompactConfig, state: &CompactState) -> bool;
/// 执行微压缩——用占位符替换旧的 tool result 内容。
/// 返回释放的 token 数。
pub fn microcompact(messages: &mut Vec<Message>, keep_recent: usize) -> u32;
```
**与 LlmCycle 集成**
- `LlmCycle` 新增字段 `compact_config: Option<CompactConfig>`, `compact_state: CompactState`
- 新增 builder 方法 `with_compact_config()`
- `submit()` 开始时调用 `should_compact()``microcompact()`
**完整 LLM 摘要压缩**留占位,Phase 2 实现(需要循环内调用 LLM 的能力)。
**无新增依赖**
---
## 实现计划
### Step 1: 先写方案文档
创建 `docs/3-phase0-remaining.md`(即本文档)。
### Step 2: ProviderRegistry
- 创建 `src/llm/provider/registry.rs`
- `provider.rs` 添加 `pub mod registry;`
- `cargo check` 验证
### Step 3: HookExecutor
- 创建 `src/llm/hooks.rs`
- `llm.rs` 添加 `pub mod hooks;`
- `LlmCycle` 新增字段和方法
- `submit()` 中插入钩子触发点
- `cargo check` 验证
### Step 4: StreamEvents
- `Cargo.toml` 添加 `tokio-stream`
- 创建 `src/llm/stream.rs`
- `llm.rs` 添加 `pub mod stream;`
- `LlmProvider` 添加 `chat_stream()`(默认回退)
- `OpenaiProvider` 实现 SSE 解析
- `LlmCycle` 添加 `submit_stream()`
- `cargo check` 验证
### Step 5: Auto-compaction
- 创建 `src/llm/compact.rs`
- `llm.rs` 添加 `pub mod compact;`
- `LlmCycle` 新增字段和方法
- `submit()` 开头插入压缩检查
- `cargo check` 验证
### Step 6: 收尾
- `cargo clippy` — 无警告
- `cargo build --release` — 完整构建
- 检查所有新公开 API 有 `///` 注释
---
## 风险评估
| 风险 | 概率 | 影响 | 缓解措施 |
|------|------|------|---------|
| SSE 解析边界情况 | 中 | 中 | 参考 `reqwest` 的 chunked 响应处理;先用简单的 `lines()` 方式逐行读取 |
| token 估算不准 | 中 | 低 | 仅用于触发微压缩的阈值判断,保守估算即可;后续可接入 tiktoken-rs |
| 钩子阻断语义复杂 | 低 | 中 | PreRequest 阻断后返回明确错误消息;其他事件点只读不阻断 |
| 与后续 Phase 冲突 | 低 | 高 | 保持接口向后兼容,全用可选集成(Option) |
---
## 验收标准
1. `cargo check` 编译通过
2. `cargo clippy` 无警告
3. 4 个模块文件存在且路径正确
4. `ProviderRegistry` 支持注册/查找/默认 Provider
5. `HookExecutor` 支持 4 个事件点注册与触发
6. `StreamEvents` 支持从 SSE chunk 解析为语义事件
7. `Auto-compaction` 支持 token 估算与微压缩
8. `LlmCycle` 向后兼容(新增字段全为 Option,不影响现有代码)