Files
agcore/design/pdd/10-llm-provider-refinement.md
T
徐涛 28ca43ccb2 chore(docs): 将设计文档从 docs 移至 design 目录
将 note、pdd、prd、roadmap 四类文档分别归入 `design/` 下对应子目录中,并新增 `.gitkeep` 占位文件
2026-07-23 05:45:53 +08:00

516 lines
33 KiB
Markdown
Raw 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.
# 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<ContentBlock>` | **扁平大枚举**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-01Message 采用扁平大枚举
#### 定义
```rust
/// 跨 Provider 统一的消息类型(扁平大枚举)。
///
/// 设计原则:每个变体直接承载完整语义,
/// 消费方 match 即可获得所有信息,无需在嵌套的 Vec 中搜索。
#[derive(Debug, Clone)]
pub enum Message {
/// 系统提示(User & Assistant 之外的引导指令)
System {
content: Vec<ContentBlock>,
},
/// 用户输入
User {
content: Vec<ContentBlock>,
},
/// 用户的图片输入(快捷构造,免去构造 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<ContentBlock>,
},
/// 工具调用结果
ToolResult {
tool_call_id: String,
content: Vec<ContentBlock>,
is_error: bool,
},
}
```
#### 与 9 系结构化层次的差异
| 维度 | 9 系(结构化层次) | 本次(扁平大枚举) |
|------|-------------------|-------------------|
| Assistant 消息结构 | `Assistant { content: Vec<ContentBlock> }`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<ContentBlock>` 保持不变——ToolUse 仍在 content 中。
#### 便捷构造函数
```rust
impl Message {
pub fn user_text(text: impl Into<String>) -> Self;
pub fn user_image(data: impl Into<String>, mime_type: impl Into<String>, detail: ImageDetail) -> Self;
pub fn assistant(text: impl Into<String>) -> Self;
pub fn system(text: impl Into<String>) -> Self;
pub fn tool_result(tool_call_id: impl Into<String>, text: impl Into<String>, is_error: bool) -> Self;
}
```
### 2.2 Decision-02LlmProvider 感知消息类型
沿用 9c 文档中的 trait 设计,无修订。
```rust
#[async_trait]
pub trait LlmProvider: Send + Sync {
async fn chat(&self, request: MessageRequest) -> Result<MessageResponse, LlmError>;
async fn chat_stream(
&self,
request: MessageRequest,
) -> Result<Pin<Box<dyn Stream<Item = Result<StreamEvent, LlmError>> + Send>>, LlmError>;
fn capabilities(&self) -> ProviderCapabilities;
}
```
每个 Provider 实现内部自行处理 `MessageRequest` ↔ 原生协议格式的映射。无外部转换层。
### 2.3 Decision-03StreamEvent 高精度 + 终端事件携带完整响应
沿用 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 signaturemessage_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<MessageResponse, LlmError> {
// ... 原有代码(按 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-04LlmCycle 简化
沿用 9e 文档的改造方向,核心变化是内部消息类型从 `Vec<OpenaiChatMessage>` 改为 `Vec<Message>`
> **⚠️ 依赖验证**: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<OpenaiChatMessage>` | `messages: Vec<Message>` |
| `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-05Provider 发现使用 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<Box<dyn LlmProvider>, 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<MessageResponse, LlmError>``chat_stream(MessageRequest) → Result<Pin<Box<dyn Stream<Item = Result<StreamEvent, LlmError>> + Send>>, LlmError>`
7. 修改 `src/agent/builder.rs`:更新 `StubProvider` 实现以匹配新 trait 签名
8. 修改 `src/llm/cycle.rs`
- `build_request()`:将已有的 `Vec<OpenaiChatMessage>` 转换为 `Vec<Message>`(通过 `chat_message → message` 映射函数),构造 `MessageRequest`
- `submit()` / `submit_messages()`:返回 `Result<MessageResponse, LlmError>`
- `submit_stream()`:返回 `Result<Pin<Box<dyn Stream<Item = StreamEvent> + 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 1Provider 适配
> **前置条件**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 2LlmCycle 简化(逻辑重构)
> **说明**Phase 0 已完成 `LlmCycle` 的"类型迁移"trait 签名、`build_request` 转换层、返回值类型)。Phase 2 聚焦**逻辑简化**——去掉 Phase 0 遗留的临时转换层,利用新类型的表达能力重写 LlmCycle 核心逻辑。
**目标**
-`LlmCycle` 内部消息存储从 `Vec<OpenaiChatMessage>` 切换为 `Vec<Message>`**移除 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<OpenaiChatMessage>` 改为 `Vec<Message>`,移除 `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 1Provider 适配) | 某个 Provider 实现不合理 | 将该 Provider 回退为 `unimplemented!()`(当前状态),不影响其他 Provider |
| Phase 1 完成时 | Provider 测试全部通过 | 打 tag `providers-v2-prototype` |
| Phase 2LlmCycle 简化) | 循环逻辑或 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<OpenaiChatMessage>` 切换到 `Vec<Message>`,所有 `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` roletool_result 是 content block),实施时需验证此命名与所有 Provider 映射的一致性