feat(llm): 实现 Provider 重构方案并更新文档
根据复核反馈修正以下内容: - 补充 §2.4 中扁平大枚举对 9e 依赖验证的说明 - 消除 §2.3 MessageComplete 冗余字段 - 明确 Phase 0 add-only 策略和 trait 签名切换时点 - 补充 HTTP mock 策略和兼容性验证标准
This commit is contained in:
@@ -6,17 +6,27 @@
|
||||
>
|
||||
> **与 9 系的关系**:
|
||||
> - 9 系文档中的 `ContentBlock`、`MessageRequest`、`MessageResponse`、`StopReason`、`ThinkingConfig`、`ToolDefinition`、`PartialUsage` 等核心类型定义**继续有效**,本文档不再重复
|
||||
> - `ProviderCapabilities`、`LlmProvider trait` 签名、`PartialMessageResponse` 汇聚算法**继续有效**
|
||||
> - `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>`) | **扁平大枚举**(Assistant 拆散为多个独立的消息变体) | 编译器能检查约束,消费方 match 清晰,无需在 Vec 中搜索特定 block 类型 |
|
||||
| StreamEvent 终端事件 | `MessageComplete { stop_reason, thinking_signature }` | **增加 `full_response: LlmResponse`**,终端事件携带完整快照 | 消费方无需自己拼接 delta,直接拿到完整响应 |
|
||||
| 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 顾虑 |
|
||||
|
||||
@@ -140,16 +150,22 @@ pub enum StreamEvent {
|
||||
CostUpdate { usage: PartialUsage },
|
||||
|
||||
// ═══════════════════════════════════════════════════════
|
||||
// 修订:MessageComplete 携带完整响应快照
|
||||
// 修订:MessageComplete 携带完整响应快照(移除冗余的 stop_reason / thinking_signature)
|
||||
// ═══════════════════════════════════════════════════════
|
||||
/// 消息完成。
|
||||
/// 消息完成 —— 唯一可靠的完整响应来源。
|
||||
///
|
||||
/// `thinking_signature` 仅 Anthropic 场景使用,回填到最后的 Thinking block。
|
||||
/// `full_response` 携带完整的 MessageResponse(含已拼接完毕的 content + usage + stop_reason),
|
||||
/// `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 {
|
||||
stop_reason: StopReason,
|
||||
thinking_signature: Option<String>,
|
||||
/// 完整的响应快照。
|
||||
///
|
||||
/// 与 `PartialMessageResponse` 内部累积的状态**最终一致**,
|
||||
@@ -168,6 +184,7 @@ pub enum StreamEvent {
|
||||
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 的影响
|
||||
|
||||
@@ -188,33 +205,56 @@ impl PartialMessageResponse {
|
||||
}
|
||||
```
|
||||
|
||||
Provider 的流处理循环在发出 `MessageComplete` 时,提前调用 `finalize()` 取得 `MessageResponse` 并填入事件:
|
||||
Provider 的流处理循环 - `thinking_signature` 不再经过事件层,由 Provider 直接写入 `PartialMessageResponse` 内部状态:
|
||||
|
||||
```rust
|
||||
// 伪代码:Provider 流处理循环
|
||||
// 伪代码:Provider 流处理循环(以 Anthropic 为例)
|
||||
let mut partial = PartialMessageResponse::new();
|
||||
|
||||
while let Some(anthropic_event) = anthropic_stream.next().await {
|
||||
match map_to_ir_event(anthropic_event) {
|
||||
StreamEvent::MessageComplete { stop_reason, thinking_signature } => {
|
||||
// 在此处调用 finalize 并将结果携带到事件中
|
||||
while let Some(event) = anthropic_stream.next().await {
|
||||
match event {
|
||||
// Anthropic 的 message_delta 携带 thinking.signature
|
||||
// → Provider 直接写入 PartialMessageResponse 内部状态
|
||||
AnthropicEvent::MessageDelta { delta, usage } => {
|
||||
if let Some(sig) = delta.thinking?.signature {
|
||||
partial.set_thinking_signature(sig);
|
||||
}
|
||||
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 {
|
||||
stop_reason,
|
||||
thinking_signature,
|
||||
full_response: full,
|
||||
};
|
||||
yield StreamEvent::MessageComplete { full_response: full };
|
||||
break;
|
||||
}
|
||||
other_event => { other_event.apply_to(&mut partial); }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> **变更追溯**: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<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 已有详述):
|
||||
|
||||
| 当前 | 改进后 |
|
||||
@@ -272,10 +312,11 @@ pub fn create_provider(
|
||||
| 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` | 继续有效 | 改造方向不变 |
|
||||
| `9e-llm-cycle-and-upstream.md` | 需重新审查 | 方向不变,但其中的 match 分支和 System prompt 插入逻辑基于旧 Message 定义。Phase 2 实施时参考思路而非照搬代码(见 §2.4 ⚠️ 依赖验证) |
|
||||
| `9f-edge-cases.md` | 继续有效 | 边界情况处理不变 |
|
||||
| `9g-risk-and-migration.md` | 继续有效 | 风险评估不变 |
|
||||
| 本文档 `10-...` | **新增** | 记录最终决策和修订 |
|
||||
@@ -284,37 +325,60 @@ pub fn create_provider(
|
||||
|
||||
## 4. 实施步骤
|
||||
|
||||
### Phase 0:类型层落地
|
||||
### Phase 0:类型层落地 + trait 签名切换
|
||||
|
||||
**目标**:定义并测试新的类型系统。
|
||||
**目标**:新增新的类型系统 + 切换 `LlmProvider` trait 签名,使全链路使用新类型。Phase 0 结束时 `cargo test` 全部通过。
|
||||
|
||||
**原则**:
|
||||
- 新类型定义放入**新文件**(`message.rs`、`request_v2.rs`、`response_v2.rs`),不堆积到已有类型文件
|
||||
- 已有的 `request.rs`(`OpenaiChatRequest`)、`response.rs`(`OpenaiChatResponse`)、`stream.rs`(旧 `StreamEvent`)**保留原样**,后续 Provider 实现可能作为内部转换目标继续引用
|
||||
- `LlmProvider` trait 签名由 `chat(ChatRequest) → ChatResponse` 切换为 `chat(MessageRequest) → MessageResponse`,**在同一个 Phase 内完成**(见下方任务 6‒8)
|
||||
- trait 签名变更导致的编译错误(`StubProvider`、`LlmCycle` 调用点)**在 Phase 0 内全部修复**,不留到 Phase 1
|
||||
- `AgentSession` 等上游中对 `LlmCycle.submit()` 返回值的引用同步适配
|
||||
|
||||
**涉及文件**:
|
||||
- `src/llm/types/mod.rs` — 新增消息类型模块,保持向后兼容导出
|
||||
- `src/llm/types/message.rs` — 新文件,定义 `Message`、`ContentBlock`、`ContentBlockType`
|
||||
- `src/llm/types/request.rs` — 修改 `ChatRequest = OpenaiChatRequest` 为 `type ChatRequest = MessageRequest`(过渡期同时保留 `OpenaiChatRequest` 作为 Provider 内部类型)
|
||||
- `src/llm/types/response.rs` — 修改 `ChatResponse` 为指向新类型
|
||||
- `src/llm/stream.rs` — 扩展 `StreamEvent`,增加 `MessageComplete.full_response`
|
||||
- `src/llm/compact.rs` — 适配新 `Message` 类型(compact 逻辑只关心 text 长度,变化小)
|
||||
|
||||
| 类型 | 文件 | 操作 |
|
||||
|------|------|------|
|
||||
| 新增 | `src/llm/types/message.rs` | 新文件 |
|
||||
| 新增 | `src/llm/types/request_v2.rs` | 新文件 |
|
||||
| 新增 | `src/llm/types/response_v2.rs` | 新文件 |
|
||||
| 追加 | `src/llm/types/mod.rs` | 追加 `pub mod` 声明 |
|
||||
| 修改 | `src/llm/provider.rs` | 改 `LlmProvider` trait 签名 |
|
||||
| 修改 | `src/agent/builder.rs` | 更新 `StubProvider` 实现 |
|
||||
| 修改 | `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` + 便捷构造函数
|
||||
2. 将 `ContentBlock` 的现有定义(`Text`, `Image`, `ToolUse`, `ToolResult`, `Thinking`, `Extension`)从 9b 移植过来
|
||||
3. 扩展 `StreamEvent`:`MessageComplete` 增加 `full_response: MessageResponse`
|
||||
4. 确认 `ContentBlock`、`ImageSource`、`ToolDefinition`、`PartialUsage` 等辅助类型在 9b 中的定义,视需要移动或引用
|
||||
5. 类型侧单元测试:构造、序列化/反序列化(JSON roundtrip)、match 穷举性验证
|
||||
1. 新增 `src/llm/types/message.rs`,定义 `Message` 扁平大枚举 + `ContentBlock` + `ContentBlockType`
|
||||
2. 将 9b 中的 `ContentBlock` 变体(`Text`, `Image`, `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. 编译驱动适配:对上游(`agent/session.rs`、`agent/runtime.rs`、`agent/error.rs` 等)中引用旧类型的地方,逐一按编译错误修复
|
||||
|
||||
**验证**:`cargo test` 通过,新类型可独立编译且 match 是 exhaustive 的。
|
||||
**验证**:`cargo test` 全部通过。`git diff` 确认新增和修改文件范围符合预期。
|
||||
|
||||
### Phase 1:Provider 适配
|
||||
|
||||
**目标**:重写 `OpenaiProvider`,新增 `AnthropicProvider`。
|
||||
> **前置条件**:Phase 0 已完成,`LlmProvider` trait 签名已切换为 `chat(MessageRequest) → MessageResponse`。本 Phase 直接实现新 Provider,无需再处理 trait 兼容性。
|
||||
|
||||
**目标**:重写 `OpenaiProvider`(使用新类型),新增 `AnthropicProvider`。DeepSeek/Qwen 作为 OpenAI-compatible 协议实现一并纳入。
|
||||
|
||||
**涉及文件**:
|
||||
- `src/llm/provider.rs` — 修改 `create_provider` 工厂函数签名
|
||||
- `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 协议,只需处理 base_url + 差异)
|
||||
- `src/llm/provider/deepseek.rs` — 新文件(与 `OpenaiChatProvider` 共享 `/chat/completions` 协议)
|
||||
- `src/llm/provider/qwen.rs` — 新文件(同上)
|
||||
|
||||
**具体任务**:
|
||||
@@ -324,34 +388,54 @@ pub fn create_provider(
|
||||
- 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`:与 `OpenaiChatProvider` 共享相同的 `/chat/completions` 协议,通过参数化或 trait 组合复用代码
|
||||
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` 映射)
|
||||
|
||||
### Phase 2:LlmCycle 简化
|
||||
**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()`
|
||||
|
||||
**目标**:将 `LlmCycle` 内部消息存储从 `Vec<OpenaiChatMessage>` 切换到 `Vec<Message>`,利用 `MessageComplete.full_response` 简化流处理。
|
||||
### Phase 2:LlmCycle 简化(逻辑重构)
|
||||
|
||||
> **说明**: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. `messages: Vec<Message>` 替换 `messages: Vec<OpenaiChatMessage>`
|
||||
2. `build_request()` 改为直接构建 `MessageRequest`(不再手动拼接 system prompt)
|
||||
3. `submit()` / `submit_messages()`:调用 `provider.chat()` 后,响应类型从 `ChatResponse` 改为 `MessageResponse`
|
||||
4. `submit_stream()`:流处理循环改为监听 `MessageComplete.full_response`
|
||||
5. tool 循环:从 `MessageResponse.message`(Assistant)的 content 中提取 `ContentBlock::ToolUse`
|
||||
6. `compact.rs` 适配:`microcompact()` 和 `should_compact()` 的操作对象从 `OpenaiChatMessage` 改为 `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 引入的临时转换函数已被删除
|
||||
|
||||
---
|
||||
|
||||
@@ -366,13 +450,13 @@ pub fn create_provider(
|
||||
| StreamEvent 完整快照 | 集成测试 | `MessageComplete.full_response` 与 PartialMessageResponse 聚合结果一致 |
|
||||
| LlmCycle 多轮对话 | 集成测试(mock Provider) | 多轮对话 + 工具循环正常 |
|
||||
| compact | 集成测试 | 超过 token 阈值后消息被正确压缩 |
|
||||
| 向后兼容(已存在的 pub API) | 编译检查 | 外部 crate 使用 `agcore::llm::types::*` 的功能不受影响(类型别名过渡) |
|
||||
| 向后兼容(已有代码) | 编译检查 | Phase 0 修改 `LlmProvider` trait + `LlmCycle` 调用点 + `StubProvider` 后,`cargo test` 全部通过。`git diff` 只涉及预期变更的文件,无意外修改 |
|
||||
|
||||
---
|
||||
|
||||
## 6. 回滚方案
|
||||
|
||||
由于项目尚无外部消费者,回滚策略比较简单:
|
||||
由于项目尚无外部消费者,回滚策略比较简单。每个 Phase 结束时打 tag 作为 checkpoint,允许跳跃回退。
|
||||
|
||||
| 阶段 | 触发条件 | 操作 |
|
||||
|------|---------|------|
|
||||
@@ -381,10 +465,11 @@ pub fn create_provider(
|
||||
| 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` 等类型别名在第 3 个 minor release 前不需要移除,给外部消费者留出迁移时间
|
||||
- `ChatRequest` / `ChatResponse` 等类型别名不主动移除——旧别名在 Phase 2 切换后通过 `#[deprecated]` 标记引导迁移,确认无外部使用者后再删除
|
||||
|
||||
---
|
||||
|
||||
@@ -397,3 +482,4 @@ pub fn create_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 映射的一致性
|
||||
|
||||
Reference in New Issue
Block a user