# Phase 0 剩余模块 — 实施方案 > 定稿日期:2026-06-02 ## 背景与目标 AG Core Phase 0(Foundation)已完成核心数据类型、错误体系、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>, default_name: Option, } impl ProviderRegistry { pub fn new() -> Self; /// 注册一个已初始化的 Provider 实例。 pub fn register(&mut self, name: impl Into, provider: Box); /// 通过 ProviderType + ProviderConfig 创建并注册。 pub fn register_with_config( &mut self, name: impl Into, 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, } /// 生命周期钩子 trait。 #[async_trait] pub trait Hook: Send + Sync { async fn execute(&self, ctx: &HookContext<'_>) -> HookResult; } /// 钩子执行器——管理注册与触发。 pub struct HookExecutor { hooks: Vec<(HookEvent, Box)>, } impl HookExecutor { pub fn new() -> Self; pub fn register(&mut self, event: HookEvent, hook: Box); pub async fn execute(&self, event: HookEvent, ctx: &HookContext<'_>) -> Vec; } ``` **与 LlmCycle 集成**: - `LlmCycle` 新增字段 `hook_executor: Option` - 新增 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> + Send>>, ) -> Pin + Send>>; ``` **Provider 层扩展**(`src/llm/provider.rs`): ```rust #[async_trait] pub trait LlmProvider: Send + Sync { async fn chat(&self, request: ChatRequest) -> Result; /// 流式聊天请求——返回原始 SSE chunk 流。 /// 默认实现回退到非流式调用。 async fn chat_stream( &self, request: ChatRequest, ) -> Result< Pin> + 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, ) -> Result< Pin + 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, keep_recent: usize) -> u32; ``` **与 LlmCycle 集成**: - `LlmCycle` 新增字段 `compact_config: Option`, `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,不影响现有代码)