# LLM Provider 重构改进方案(最终确认) > 本文档记录 2026-06-25 设计评审后确认的方案决策,是对 9 系文档(`9-llm-provider-unified-interface.md` 及 `9a`-`9g` 子文档)中已有设计的**精炼与修订**。 > > **阅读前提**:本文档假设读者已熟悉现有 9 系文档中的背景、架构总览和类型体系概念。 > > **与 9 系的关系**: > - 9 系文档中的 `ContentBlock`、`MessageRequest`、`MessageResponse`、`StopReason`、`ThinkingConfig`、`ToolDefinition`、`PartialUsage` 等核心类型定义**继续有效**,本文档不再重复 > - `ProviderCapabilities`、`LlmProvider trait` 签名、`PartialMessageResponse` 汇聚算法等 design intent **继续有效**(trait 签名由 9c 定义,切换时点由本文档 §4 Phase 0 执行) > - 本文档仅记录**本次确认的修订内容和执行计划** --- ## 修订记录 | 日期 | 版本 | 修订摘要 | |------|------|---------| | 2026-06-25 | v1 | 初版,记录 5 项设计决策 | | 2026-06-26 | v2 | 初审修订:修正 §1 表描述(Assistant → UserImage);消除 §2.3 MessageComplete 冗余字段;明确 Phase 0 add-only 策略;补充 9e 依赖审查说明;补充 HTTP mock 策略;修正 §5 兼容性验证标准;增加 §7 开放事项 | | 2026-06-26 | v3 | 复审修订:Phase 0 改为"add + trait 签名切换"消除结构性缺口;补充 OpenAI Response API 范围和 OpenAI-compatible 复用策略说明;调整 Phase 2 范围(聚焦逻辑简化) | --- ## 1. 修订摘要 | 设计维度 | 9 系文档 | 本次修订 | 修订原因 | |----------|---------|---------|---------| | Message 模型 | 结构化层次(`System/User/Assistant/Tool`,每项含 `content: Vec`) | **扁平大枚举**(User 拆出 `UserImage` 独立变体,Assistant 保持整体,ToolUse 仍在 content 中) | 编译器能检查约束,`UserImage` 消费方 match 可直接区分文本和图片输入,无需检查 Vec 内容 | | StreamEvent 终端事件 | `MessageComplete { stop_reason, thinking_signature }` | **精简为 `MessageComplete { full_response: MessageResponse }`**,移除冗余顶层字段 | 消除冗余和消费方疑惑,唯一信源 | | Provider 发现 | 未明确 | **Enum-based**(`ProviderType` enum + exhaustive match),不做动态注册 | 当前协议数量可控,编译期安全,无运行时查表开销 | | 项目阶段 | 9 系是"推演中" | **可直接执行**,无历史包袱,一步到位 | 项目尚未 release,没有 breaking change 顾虑 | --- ## 2. 本次修订的 5 项设计决策 ### 2.1 Decision-01:Message 采用扁平大枚举 #### 定义 ```rust /// 跨 Provider 统一的消息类型(扁平大枚举)。 /// /// 设计原则:每个变体直接承载完整语义, /// 消费方 match 即可获得所有信息,无需在嵌套的 Vec 中搜索。 #[derive(Debug, Clone)] pub enum Message { /// 系统提示(User & Assistant 之外的引导指令) System { content: Vec, }, /// 用户输入 User { content: Vec, }, /// 用户的图片输入(快捷构造,免去构造 ContentBlock 的 boilerplate) UserImage { data: String, mime_type: String, detail: ImageDetail, }, /// Assistant 回复内容块(可能包含 text、thinking、tool_use 等多种 block 的混合) /// /// 注意:Assistant 的一次回复可以同时包含文本、思考过程、工具调用。 /// 扁平大枚举并未将 ToolUse 提升为独立变体,而是保留在 content 中, /// 因为在一次 Assistant turn 中 text 和 tool_use 的**顺序关系**是有意义的。 /// (例如:先输出推理过程,再调用工具) Assistant { content: Vec, }, /// 工具调用结果 ToolResult { tool_call_id: String, content: Vec, is_error: bool, }, } ``` #### 与 9 系结构化层次的差异 | 维度 | 9 系(结构化层次) | 本次(扁平大枚举) | |------|-------------------|-------------------| | Assistant 消息结构 | `Assistant { content: Vec }`,ToolUse 在 content 中 | 同上,保持 ToolUse 在 content 中 | | "独立 Assistant 消息"的含义 | 一次 LLM 响应 = 一个 `Assistant { content: [...] }` | 同上 | | Thinking / ToolCall 作为独立变体 | ❌ 无独立变体 | **Thinking、ToolCall 不作为独立 Message 变体**,仍在 `Assistant.content` 中 | | UserImage 独立变体 | `User { content: [Image{...}] }` | `UserImage { data, mime, detail }` | | 为什么不把 ToolUse 提到 Message 层 | — | 因为 text ↔ tool_use 的**交错顺序**是 Assistant 响应的语义组成部分,拆散后会丢失顺序信息 | | 实际的参与方差异 | `User` + `UserImage` 合并为同一变体 | `User` 和 `UserImage` **拆开**,方便消费方 match(无需检查 Vec 内容来区分文字和图片) | > **与 9b 文档的关系**:9b 的 `Message::System`、`Message::User`、`Message::Assistant`、`Message::Tool` 四个变体分类保留, > 但 `User` 的图片输入场景通过新增 `UserImage` 变体提供便捷路径,减少 boilerplate。 > `Message::Assistant` 的 `content: Vec` 保持不变——ToolUse 仍在 content 中。 #### 便捷构造函数 ```rust impl Message { pub fn user_text(text: impl Into) -> Self; pub fn user_image(data: impl Into, mime_type: impl Into, detail: ImageDetail) -> Self; pub fn assistant(text: impl Into) -> Self; pub fn system(text: impl Into) -> Self; pub fn tool_result(tool_call_id: impl Into, text: impl Into, is_error: bool) -> Self; } ``` ### 2.2 Decision-02:LlmProvider 感知消息类型 沿用 9c 文档中的 trait 设计,无修订。 ```rust #[async_trait] pub trait LlmProvider: Send + Sync { async fn chat(&self, request: MessageRequest) -> Result; async fn chat_stream( &self, request: MessageRequest, ) -> Result> + Send>>, LlmError>; fn capabilities(&self) -> ProviderCapabilities; } ``` 每个 Provider 实现内部自行处理 `MessageRequest` ↔ 原生协议格式的映射。无外部转换层。 ### 2.3 Decision-03:StreamEvent 高精度 + 终端事件携带完整响应 沿用 9c 文档中定义的 `StreamEvent`,但**在终端事件中增加完整响应快照**。 #### 修订后的 MessageComplete 事件 ```rust pub enum StreamEvent { // ── Meta ── MessageStart { id: String, model: String }, // ── Content Block 边界 ── ContentBlockStart { index: u32, block_type: ContentBlockType }, ContentBlockEnd { index: u32 }, // ── 块内增量 ── TextDelta { text: String }, ThinkingDelta { text: String }, RefusalDelta { text: String }, ToolCallArgumentsDelta { index: u32, arguments: String }, ToolCallEnd { index: u32 }, // ── 汇总 ── CostUpdate { usage: PartialUsage }, // ═══════════════════════════════════════════════════════ // 修订:MessageComplete 携带完整响应快照(移除冗余的 stop_reason / thinking_signature) // ═══════════════════════════════════════════════════════ /// 消息完成 —— 唯一可靠的完整响应来源。 /// /// `full_response` 携带完整的 MessageResponse(含已拼接完毕的 content / usage / stop_reason), /// 消费方**无需自行累积 delta**,直接使用此快照继续后续流程。 /// /// 设计说明: /// - 9c 原有设计在 `MessageComplete` 中同时携带 `stop_reason` 和 `thinking_signature` 顶层字段, /// 但这些信息已包含在 `full_response` 中,造成冗余和消费方的疑惑(到底读顶层字段还是 full_response)。 /// - 本次修订全部移除顶层冗余字段,`full_response` 是唯一信源。 /// - Anthropic 的 thinking signature(message_delta 中下发,晚于 content_block_stop)由 Provider /// 的流处理循环直接调用 `PartialMessageResponse::set_thinking_signature()` 写入内部状态, /// 再通过 `finalize()` 回填到 Thinking block 中,最终出现在 `full_response` 的 content 里。 /// 消费方不需要感知 signature 的存在。 MessageComplete { /// 完整的响应快照。 /// /// 与 `PartialMessageResponse` 内部累积的状态**最终一致**, /// 提供此快照是为了让消费方(如 LlmCycle)在流结束后可以直接拿到 /// 完整的 MessageResponse,无需自己实现汇聚算法。 full_response: MessageResponse, }, // ── 错误 ── Error { message: String }, } ``` #### 设计理由 1. **简化消费方**:`LlmCycle::submit_stream()` 目前需要在 `while let` 循环中逐个处理 delta 并维护一个会话状态来判断"响应是否完整"。有了 `full_response`,`LlmCycle` 或 `AgentSession` 只需要监听 `MessageComplete` 事件,拿到快照后直接继续 tool 循环或返回给调用方。 2. **与 PartialMessageResponse 保持一致**:`PartialMessageResponse::finalize()` 产生的 `MessageResponse` 就是 `full_response` 的值。Provider 内部的汇聚逻辑不变,只是在发出 `MessageComplete` 时多传一个已完成构建的最终结果。 3. **零额外开销**:`MessageResponse` 在 Provider 内部已经构造好了(作为汇聚算法的最终产物),只是多 clone/arc 一次给事件携带。 4. **消除冗余**:9c 原有设计同时保留了顶层 `stop_reason`、`thinking_signature` 和 `full_response` 中的相同信息,造成消费方疑惑。本次修订只保留 `full_response` 为唯一信源。 #### 对 PartialMessageResponse 的影响 ```rust // finalize 在原有逻辑末尾增加一步: // 将 finalize 的结果提前缓存,由 MessageComplete 事件携带 impl PartialMessageResponse { pub fn finalize(mut self) -> Result { // ... 原有代码(按 index 升序遍历 blocks) ... let response = MessageResponse { ... }; // 新增:self. 中缓存 finalize 结果 // (实际由 Provider 的流处理循环在发出 MessageComplete 前调用 // finalize 并填充到事件中) Ok(response) } } ``` Provider 的流处理循环 - `thinking_signature` 不再经过事件层,由 Provider 直接写入 `PartialMessageResponse` 内部状态: ```rust // 伪代码:Provider 流处理循环(以 Anthropic 为例) let mut partial = PartialMessageResponse::new(); while let Some(event) = anthropic_stream.next().await { match event { // Anthropic 的 message_delta 携带 thinking.signature // → Provider 直接写入 PartialMessageResponse 内部状态 AnthropicEvent::MessageDelta { delta, usage } => { if let Some(thinking) = &delta.thinking { if let Some(sig) = &thinking.signature { partial.set_thinking_signature(sig.clone()); } } yield StreamEvent::CostUpdate { usage: map_usage(usage) }; } // 其他 Anthropic 事件 → 映射为 StreamEvent 并 apply_to other => { let ir_event = map_to_ir_event(other); ir_event.apply_to(&mut partial); } // message_stop → 调用 finalize 并发出完成事件 AnthropicEvent::MessageStop => { let full = partial.finalize()?; yield StreamEvent::MessageComplete { full_response: full }; break; } } } ``` > **变更追溯**:9c 的原有设计中,`MessageComplete` 事件携带顶层 `stop_reason` 和 `thinking_signature` 字段, > 供 `apply_to()` 设置 `PartialMessageResponse` 的内部状态。本次修订移除这些冗余字段后, > `thinking_signature` 改为由 Provider 直接调用 `partial.set_thinking_signature()` 写入内部状态, > `stop_reason` 则在 `finalize()` 中统一定于 `full_response.stop_reason`。 ### 2.4 Decision-04:LlmCycle 简化 沿用 9e 文档的改造方向,核心变化是内部消息类型从 `Vec` 改为 `Vec`。 > **⚠️ 依赖验证**:9e 文档写于结构化层次设计阶段(`Message::System` / `User` / `Assistant` / `Tool`), > 其中的代码片段(如 `build_request()` 中 match System 消息的分支、插入 System prompt 的判断逻辑) > 基于旧 Message 定义。扁平大枚举后—— > - `User` 拆出 `UserImage` → match 分支需增加 `UserImage` 的处理 > - `Message::Tool` 更名为 `Message::ToolResult` → 所有引用需改名 > - 其余 match 分支(`System`、`User`、`Assistant`)的基本逻辑不变 > > **实施 Phase 2 时**:从 9e 中摘取实现思路,代码手动编写,不直接复制 9e 中的代码片段。 > 修改 9e 文档中过时的代码片段不在本方案范围内,Phase 2 实施时自然淘汰。 关键变化要点(9e 已有详述): | 当前 | 改进后 | |------|--------| | `messages: Vec` | `messages: Vec` | | `build_request()` 中手动拼接 system prompt | system prompt 通过 `Message::System` 在 messages 中表达,Provider 映射层自行处理差异 | | `submit_stream()` 中自建 delta 聚合逻辑 | 监听 `MessageComplete.full_response`,直接拿到完整响应 | | tool 循环需自行解析 `ChatResponse` 中的 tool_calls | 从 `MessageResponse.message`(Assistant 变体)的 content 中提取 ContentBlock::ToolUse | ### 2.5 Decision-05:Provider 发现使用 Enum 不使用动态注册表,保留当前 `ProviderType` enum 模式,但扩展其覆盖范围。 ```rust #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum ProviderType { OpenaiChat, OpenaiResponse, Anthropic, DeepSeek, Qwen, } ``` 工厂函数 `create_provider()` 做 exhaustive match: ```rust pub fn create_provider( provider_type: ProviderType, config: ProviderConfig, ) -> Result, LlmError> { match provider_type { ProviderType::OpenaiChat => Ok(Box::new(providers::OpenaiChatProvider::new(...))), ProviderType::OpenaiResponse => Ok(Box::new(providers::OpenaiResponseProvider::new(...))), ProviderType::Anthropic => Ok(Box::new(providers::AnthropicProvider::new(...))), ProviderType::DeepSeek => Ok(Box::new(providers::DeepSeekProvider::new( config.base_url, config.api_key, config.model, ))), ProviderType::Qwen => Ok(Box::new(providers::QwenProvider::new(...))), } } ``` 新增 Provider 时,编译器通过 exhaustiveness check 强制要求 `match` 更新。 > **理由**:当前目标协议数量(4-5 种)完全可控,enum 的编译期安全检查优于运行时的 `HashMap::get()`。 > 未来如果扩展到 15+ 种以上,再改为注册表模式。 --- ## 3. 对 9 系文档的更新映射 | 9 系文档 | 变更类型 | 操作 | |---------|---------|------| | `9b-ir-type-system.md` §3.2 Message | 修订 | `UserImage` 变体新增;其余部分继续有效 | | `9c-llm-provider-trait.md` §4.1 LlmProvider trait | 切换时点修订 | trait 签名切换由"推迟到 Phase 2"改为 Phase 0 内完成。trait 定义本身不变。 | | `9c-llm-provider-trait.md` §4.3 StreamEvent | 修订 | `MessageComplete` 增加 `full_response: MessageResponse` 字段 | | `9c-llm-provider-trait.md` §4.4 PartialMessageResponse | 追加 | `finalize()` 返回结果需在 Provider 发出 `MessageComplete` 前已可用 | | `9d-provider-implementations.md` | 继续有效 | 实现策略不变 | | `9e-llm-cycle-and-upstream.md` | 需重新审查 | 方向不变,但其中的 match 分支和 System prompt 插入逻辑基于旧 Message 定义。Phase 2 实施时参考思路而非照搬代码(见 §2.4 ⚠️ 依赖验证) | | `9f-edge-cases.md` | 继续有效 | 边界情况处理不变 | | `9g-risk-and-migration.md` | 继续有效 | 风险评估不变 | | 本文档 `10-...` | **新增** | 记录最终决策和修订 | --- ## 4. 实施步骤 ### Phase 0:类型层落地 + trait 签名切换 **目标**:新增新的类型系统 + 切换 `LlmProvider` trait 签名,使全链路使用新类型。Phase 0 结束时 `cargo test` 全部通过。 **原则**: - 新类型定义放入**新文件**(`message.rs`、`request_v2.rs`、`response_v2.rs`),不堆积到已有类型文件 - 已有的 `request.rs`(`OpenaiChatRequest`)、`response.rs`(`OpenaiChatResponse`)**保留原样**,后续 Provider 实现可能作为内部转换目标继续引用 - `LlmProvider` trait 签名由 `chat(ChatRequest) → ChatResponse` 切换为 `chat(MessageRequest) → MessageResponse`,**在同一个 Phase 内完成**(见下方任务 6‒8) - trait 签名变更导致的编译错误(`StubProvider`、`LlmCycle` 调用点)**在 Phase 0 内全部修复**,不留到 Phase 1 - `AgentSession` 等上游中对 `LlmCycle.submit()` 返回值的引用同步适配 **`StreamEvent` 命名冲突处理**: 新 `StreamEvent`(高精度版)定义在 `src/llm/types/response_v2.rs` 中。 旧 `StreamEvent`(`src/llm/stream.rs` 中定义)的变体(`AssistantTextDelta`、`ToolExecutionStarted`、`TurnComplete` 等)与新类型冲突。 处理方式(任务 9 执行): 1. `response_v2.rs` 中的新 `StreamEvent` 是唯一的 `StreamEvent` 定义 2. `src/llm/stream.rs` 中的旧 `StreamEvent` 枚举**替换为**重新导出语句:`pub use super::types::response_v2::StreamEvent;` 3. 旧 `StreamEvent` 的变体(`AssistantTextDelta`、`ToolExecutionStarted`、`TurnComplete`)**暂时保留**为一个独立的枚举(命名为 `LegacyStreamEvent`)放在 `src/llm/types/old_stream.rs` 新文件中,供 `stream.rs` 中的 `parse_chunk_stream()` 内部使用 4. 这样 `stream.rs` 的辅助函数继续编译,`LlmCycle` 和 `AgentSession` 看到的是新 `StreamEvent` **涉及文件**: | 类型 | 文件 | 操作 | |------|------|------| | 新增 | `src/llm/types/message.rs` | 新文件 | | 新增 | `src/llm/types/request_v2.rs` | 新文件 | | 新增 | `src/llm/types/response_v2.rs` | 新文件 | | 新增 | `src/llm/types/old_stream.rs` | 新文件(从 `stream.rs` 迁移旧 `StreamEvent` 变体) | | 追加 | `src/llm/types/mod.rs` | 追加 `pub mod` 声明 | | 修改 | `src/llm/provider.rs` | 改 `LlmProvider` trait 签名 | | 修改 | `src/agent/builder.rs` | 更新 `StubProvider` 实现 | | 修改 | `src/llm/stream.rs` | 将旧 `StreamEvent` 枚举替换为对 `response_v2::StreamEvent` 的重新导出 | | 修改 | `src/llm/provider/openai.rs` | 修改:添加临时桥接实现(`MessageRequest → ChatRequest` 转换 + `ChatResponse → MessageResponse` 转换),Phase 1 重写时移除 | | 修改 | `src/llm/hooks.rs` | 更新 `HookContext.request` 类型为 `&'a MessageRequest` | | 修改 | `src/llm/cycle.rs` | 更新调用点(`build_request`、`submit`、`submit_stream`、`submit_messages`、`submit_request` 的类型引用和返回值) | | 修改 | `src/llm/cycle/retry.rs` | 如有对新 `LlmError` 类型的引用,同步适配 | | 修改 | `src/agent/error.rs`、`src/agent/runtime.rs`、`src/agent/session.rs` 等 | 如有对 `LlmCycle` 返回值或 `ChatResponse` 的引用,同步适配(具体文件由编译错误定位) | **具体任务**: 1. 新增 `src/llm/types/message.rs`,定义 `Message` 扁平大枚举 + `ContentBlock` + `ContentBlockType` 2. 将 9b 中的 `ContentBlock` 变体(`Text`, `Image`, `Audio`, `File`, `ToolUse`, `ToolResult`, `Thinking`, `Extension`)及其辅助类型(`ImageSource`、`AudioSource`、`FileSource`)定义到 `message.rs` 中 3. 新增 `src/llm/types/request_v2.rs`,定义 `MessageRequest`(从 9b 移植)+ `ExtraError` + extra 访问方法(`get_extra`、`get_extra_opt`、`get_extra_as`、`set_extra`) 4. 新增 `src/llm/types/response_v2.rs`,定义 `MessageResponse` + `StreamEvent`(高精度版,`MessageComplete` 只含 `full_response: MessageResponse`)+ `PartialUsage` + `PartialMessageResponse` + `apply_to` + `finalize` 5. 新类型侧单元测试:构造、序列化/反序列化(JSON roundtrip)、match 穷举性验证、`PartialMessageResponse.apply_to + finalize` 汇聚一致性测试 6. 修改 `src/llm/provider.rs`:`LlmProvider` trait 签名改为 `chat(MessageRequest) → Result`、`chat_stream(MessageRequest) → Result> + Send>>, LlmError>` 7. 修改 `src/agent/builder.rs`:更新 `StubProvider` 实现以匹配新 trait 签名 8. 修改 `src/llm/cycle.rs`: - `build_request()`:将已有的 `Vec` 转换为 `Vec`(通过 `chat_message → message` 映射函数),构造 `MessageRequest` - `submit()` / `submit_messages()`:返回 `Result` - `submit_stream()`:返回 `Result + Send>>, LlmError>` - 流处理循环:由消费 `OpenaiChatChunk` 改为消费 `StreamEvent`。流结束处的 `full_response` 暂不使用(Phase 2 才启用简化逻辑),先提取 `stop_reason` 和 `message` 构建传统返回 9. 新增文件 `src/llm/types/old_stream.rs`(从 `src/llm/stream.rs` 迁移旧 `StreamEvent` 定义),同时将 `src/llm/stream.rs` 中的旧 `StreamEvent` 枚举替换为对 `response_v2.rs` 中新 `StreamEvent` 的重新导出(`pub use super::types::response_v2::StreamEvent;`),确保 `stream.rs` 的 `parse_chunk_stream()` 和 `ChunkToEventStream` 继续编译通过 10. 修改 `src/llm/provider/openai.rs`:添加 `LlmProvider` trait 临时桥接实现—— - `chat()`:`MessageRequest → ChatRequest`(利用现有 `OpenaiChatMessage` 转换)→ 调用已有 `chat_inner()` → `ChatResponse → MessageResponse`(使用 `finalize()` 算法或直接映射) - `chat_stream()`:`MessageRequest → ChatRequest` → 调用已有 `chat_stream_inner()` → 将 `OpenaiChatChunk` 流映射为 `StreamEvent` 流(利用已有的 `parse_chunk_stream`) - 桥接实现标记 `// ponytail: Phase 0 临时桥接,Phase 1 重写时移除` 11. 测试适配(编译驱动,涉及文件不限于以下列表,由编译器报错定位): - `src/llm/cycle.rs` 测试模块(`MockProvider`、`assistant_text_response()`、`assistant_tool_call_response()`、各测试用例中的断言类型) - `src/agent/session.rs` 测试模块(`MockProvider`、响应构造 helper 等) - `src/agent/builder.rs` 测试模块(`StubProvider` 已单独由任务 7 处理) - `src/agent/session_memory.rs`、`src/agent/runtime.rs` 等 12. 编译驱动适配:对上游(`agent/session.rs`、`agent/runtime.rs`、`agent/error.rs` 等)中引用旧类型的地方,逐一按编译错误修复 **验证**:`cargo test` 全部通过。`git diff` 确认新增和修改文件范围符合预期。确认 `OpenaiProvider` 的临时桥接代码带有 `// ponytail: Phase 0 临时桥接` 注释,Phase 1 移除时易于定位。 ### Phase 1:Provider 适配 > **前置条件**:Phase 0 已完成,`LlmProvider` trait 签名已切换为 `chat(MessageRequest) → MessageResponse`。本 Phase 直接实现新 Provider,无需再处理 trait 兼容性。 **目标**:重写 `OpenaiProvider`(使用新类型),新增 `AnthropicProvider`。DeepSeek/Qwen 作为 OpenAI-compatible 协议实现一并纳入。 **涉及文件**: - `src/llm/provider.rs` — 修改 `create_provider` 工厂函数,匹配新的 `ProviderType` enum - `src/llm/provider/registry.rs` — 适配新 `LlmProvider` trait(改动极小,只是类型变化) - `src/llm/provider/openai.rs` — 重写:内部实现 `MessageRequest ↔ OpenaiChatRequest` 转换 - `src/llm/provider/anthropic.rs` — 新文件:`MessageRequest ↔ Anthropic Messages API` 映射 - `src/llm/provider/deepseek.rs` — 新文件(与 `OpenaiChatProvider` 共享 `/chat/completions` 协议) - `src/llm/provider/qwen.rs` — 新文件(同上) **具体任务**: 1. `OpenaiProvider` 内部 `chat()`:`MessageRequest` → `OpenaiChatRequest`(serde 序列化)→ HTTP POST → 解析 `OpenaiChatResponse` → `MessageResponse`(通过 `finalize()` 算法或直接映射) 2. `OpenaiProvider` 内部 `chat_stream()`:同样的转换路径,但响应解析改为 SSE 流式 → 逐 chunk 输出 `StreamEvent` 3. `AnthropicProvider`:实现 Anthropic Messages API 的请求/响应映射,包括: - Messages API 请求体构建(`system` 参数 + `messages[]` + `tools` 等) - SSE 流解析(`message_start`, `content_block_start`, `content_block_delta`, `content_block_stop`, `message_delta`, `message_stop`, `ping`) - 将 Anthropic SSE 事件映射为 IR `StreamEvent` 4. `DeepSeekProvider` / `QwenProvider`(OpenAI-compatible): - 共享 `OpenaiChatProvider` 的 `/chat/completions` 协议 - **代码复用策略实施时决定**(推荐:`OpenaiChatProvider` 参数化为 `GenericOpenaiProvider { base_url, api_key, model, provider_name }`,DeepSeek/Qwen 共用同一实现,仅配置不同;备选:trait 组合提取 HTTP 请求逻辑为可复用组件) - 差异化处理:`max_tokens` 字段名(部分兼容端点使用 `max_tokens` 而非 `max_completion_tokens`)、错误格式(非标准 error body 解析) 5. `ProviderRegistry` 的 `register_with_config()` 和 `create_provider()` 适配新 enum 6. **OpenAI Response API(`ProviderType::OpenaiResponse`)实现范围说明**:本 Phase 的 `OpenaiResponseProvider` 只覆盖核心对话能力(models response 创建、流式)、工具调用。内置工具(`web_search`、`file_search`)、`previous_response_id` 续写、`store` 等 Response API 独有特性通过 `MessageRequest.extra` 传递(参考 9b 的 extra key 约定表),内置工具的完整支持延后。如果资源有限,`OpenaiResponseProvider` 可延迟到 Phase 2 之后开发,不影响其他 Provider。 **验证**: - 每个 Provider 的 `chat()` 和 `chat_stream()` 基本路径集成测试(mock HTTP 层) - 消息类型双向映射测试(`Message → OpenaiChatRequest`, `OpenaiChatResponse → MessageResponse`) - 错误路径测试(HTTP 400/401/429/500 → `LlmError` 映射) **HTTP mock 策略**: - 推荐使用 [`wiremock`](https://crates.io/crates/wiremock) crate(项目尚无 HTTP mock 依赖) - 每个 Provider 的测试模块中,用 `MockServer` 启动 mock 服务端,返回预定义请求/流式响应 - `OpenaiProvider` 的 mock 端点为 `/chat/completions`(SSE 流或 JSON 响应) - `AnthropicProvider` 的 mock 端点为 `/v1/messages`(SSE 事件序列) - 测试不依赖真实网络,`base_url` 指向 `mock_server.uri()` ### Phase 2:LlmCycle 简化(逻辑重构) > **说明**:Phase 0 已完成 `LlmCycle` 的"类型迁移"(trait 签名、`build_request` 转换层、返回值类型)。Phase 2 聚焦**逻辑简化**——去掉 Phase 0 遗留的临时转换层,利用新类型的表达能力重写 LlmCycle 核心逻辑。 **目标**: - 将 `LlmCycle` 内部消息存储从 `Vec` 切换为 `Vec`,**移除 Phase 0 引入的 `OpenaiChatMessage → Message` 转换层** - 流处理循环重构:利用 `MessageComplete.full_response` 直接拿到完整响应,去掉手动 delta 累积 - 工具循环清洗:从 `MessageResponse.message` 的 content 中直接提取 `ContentBlock::ToolUse` - `compact.rs` 适配新 `Message` 类型 **涉及文件**: - `src/llm/cycle.rs` — 主要修改 - `src/llm/cycle/usage.rs` — 保持兼容(`Usage` 类型不变) - `src/llm/cycle/retry.rs` — 保持兼容 - `src/llm/compact.rs` — 适配 `Message` 类型 **具体任务**: 1. `self.messages` 从 `Vec` 改为 `Vec`,移除 `build_request()` 中的类型转换步骤 2. `build_request()` 直接构建 `MessageRequest`(`messages` 直接传入 `self.messages`),不再手动插入 system prompt(从 messages 中取 `Message::System`) 3. `submit()` / `submit_messages()`:已返回 `MessageResponse`,无需改签名。检查调用方是否直接解构 `MessageResponse` 是正确的 4. `submit_stream()`:流处理循环中锚定 `MessageComplete.full_response`,拿到完整的 `MessageResponse` 后直接继续 tool 循环或结束。去掉中间状态的维护 5. tool 循环:从 `MessageResponse.message` 的 `Assistant { content }` 中提取 `ContentBlock::ToolUse` 变体 6. `compact.rs` 适配:`microcompact()` / `should_compact()` 的操作对象从 `OpenaiChatMessage` 改为 `Message`,按 text block 长度计算 token 数 7. 清理 Phase 0 引入的临时转换函数(`chat_message_to_message`、`message_to_chat_message` 等),确认不再被引用后删除 **验证**: - `LlmCycle` 集成测试全部通过 - 多轮对话 + 工具调用的端到端流程正常 - `git diff` 确认 Phase 0 引入的临时转换函数已被删除 --- ## 5. 验证标准 | 维度 | 验证方法 | 通过条件 | |------|---------|---------| | 类型正确性 | `cargo test` | 所有测试通过 | | JSON 双向映射 | 单元测试 | `Message → JSON → Message` 往返不变 | | Provider 基本路径 | 集成测试(mock HTTP) | 每个 Provider 的 chat + chat_stream 成功 | | Provider 错误路径 | 集成测试(mock HTTP 4xx/5xx) | 错误映射为正确的 `LlmError` 变体 | | StreamEvent 完整快照 | 集成测试 | `MessageComplete.full_response` 与 PartialMessageResponse 聚合结果一致 | | LlmCycle 多轮对话 | 集成测试(mock Provider) | 多轮对话 + 工具循环正常 | | compact | 集成测试 | 超过 token 阈值后消息被正确压缩 | | 向后兼容(已有代码) | 编译检查 | Phase 0 修改 `LlmProvider` trait + `LlmCycle` 调用点 + `StubProvider` 后,`cargo test` 全部通过。`git diff` 只涉及预期变更的文件,无意外修改 | --- ## 6. 回滚方案 由于项目尚无外部消费者,回滚策略比较简单。每个 Phase 结束时打 tag 作为 checkpoint,允许跳跃回退。 | 阶段 | 触发条件 | 操作 | |------|---------|------| | Phase 0(类型层) | 新类型设计发现重大缺陷 | 回退 git,保留 9 系文档作为参照,重启设计评审 | | Phase 0 完成时 | 类型定义通过评审和测试 | 打 tag `types-v2-prototype` | | Phase 1(Provider 适配) | 某个 Provider 实现不合理 | 将该 Provider 回退为 `unimplemented!()`(当前状态),不影响其他 Provider | | Phase 1 完成时 | Provider 测试全部通过 | 打 tag `providers-v2-prototype` | | Phase 2(LlmCycle 简化) | 循环逻辑或 compact 出现问题 | 保留旧 `LlmCycle` 实现(不改文件名),通过 feature flag 切换 | | **跨阶段回退** | Phase 2 发现 Phase 0 类型设计有误 | 回退至 Phase 0 checkpoint(`types-v2-prototype`),在不动已有文件的前提下直接原地修改新类型文件重新迭代,不需要整个回退到 Phase 0 之前 | **风险储备**: - 如果 `OpenaiProvider` 的重写复杂度过高,可以保留旧的 `OpenaiProvider` 不变,在旁边新增一个 `OpenaiProviderV2` 并行开发 - `ChatRequest` / `ChatResponse` / `Message` / `ContentBlock` / `ToolDefinition` / `StopReason` 等类型别名和旧类型结构体的弃用路径: - **Phase 0 完成时**:旧别名**保留**(作为编译桥接),新类型通过不同路径(`request_v2::MessageRequest`、`response_v2::MessageResponse`)访问,两者同时存在于类型模块中 - **Phase 1 完成时**:Provider 实现切换到新类型,旧 `OpenaiProvider` 的临时桥接代码被 Phase 1 的真实实现替换。旧别名仍由 `src/llm/types/mod.rs` 导出,不影响其他模块 - **Phase 2 完成时**:`LlmCycle` 内部消息存储从 `Vec` 切换到 `Vec`,所有 `ChatRequest`/`ChatResponse` 引用被替换。此时对 `ChatRequest`、`ChatResponse`、`Message`、`ContentBlock`、`ToolDefinition`、`StopReason` 等旧别名和 `ChatResponse` 结构体加 `#[deprecated]` 标记 - **下一个版本(v0.2.0 或 v1.0.0)**:运行 `cargo check` 确认无外部引用后,删除所有 deprecated 别名和 `ChatResponse` 结构体 --- ## 7. 开放事项 以下事项已在 9 系文档中充分讨论,本次无修订,但列出以供跟踪: - [ ] `ContentBlock::Extension` 作为逃生舱的具体使用场景(OpenAI Response 内置工具、未知 block 类型) - [ ] Anthropic 的 `/v1/messages` 流式 SSE 解析状态机细节(9d Provider 实现文档) - [ ] `MessageRequest.extra` 中每个 Provider 实际需要的 key 清单(9b 已有草案,Phase 1 实现时细化和验证) - [ ] Thinking signature 的端到端测试(9f 已有处理策略,Phase 2 时分配合并完成) - [ ] cost 计算逻辑适配新类型(当前 `CostTracker` 在 `Usage` 上工作,类型不变、无需修改,但在集成测试中验证) - [ ] `Message::ToolResult` 命名 — 9b 中叫 `Tool`(对应 OpenAI 的 `tool` role),本设计改为 `ToolResult`。Anthropic 没有独立的 `tool` role(tool_result 是 content block),实施时需验证此命名与所有 Provider 映射的一致性