From 000cd2022dd381ba1c636bcd06a6d0226a0ecb3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=90=E6=B6=9B?= Date: Fri, 26 Jun 2026 08:12:12 +0800 Subject: [PATCH] =?UTF-8?q?feat(llm):=20=E5=AE=9E=E7=8E=B0=20Provider=20?= =?UTF-8?q?=E9=87=8D=E6=9E=84=E6=96=B9=E6=A1=88=E5=B9=B6=E6=9B=B4=E6=96=B0?= =?UTF-8?q?=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 根据复核反馈修正以下内容: - 补充 §2.4 中扁平大枚举对 9e 依赖验证的说明 - 消除 §2.3 MessageComplete 冗余字段 - 明确 Phase 0 add-only 策略和 trait 签名切换时点 - 补充 HTTP mock 策略和兼容性验证标准 --- docs/10-llm-provider-refinement.md | 188 +++++++++++++++++++++-------- 1 file changed, 137 insertions(+), 51 deletions(-) diff --git a/docs/10-llm-provider-refinement.md b/docs/10-llm-provider-refinement.md index d7f102a..2464d7e 100644 --- a/docs/10-llm-provider-refinement.md +++ b/docs/10-llm-provider-refinement.md @@ -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`) | **扁平大枚举**(Assistant 拆散为多个独立的消息变体) | 编译器能检查约束,消费方 match 清晰,无需在 Vec 中搜索特定 block 类型 | -| StreamEvent 终端事件 | `MessageComplete { stop_reason, thinking_signature }` | **增加 `full_response: LlmResponse`**,终端事件携带完整快照 | 消费方无需自己拼接 delta,直接拿到完整响应 | +| 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 顾虑 | @@ -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, /// 完整的响应快照。 /// /// 与 `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` 改为 `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 已有详述): | 当前 | 改进后 | @@ -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`、`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. 编译驱动适配:对上游(`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` 切换到 `Vec`,利用 `MessageComplete.full_response` 简化流处理。 +### 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. `messages: Vec` 替换 `messages: Vec` -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` 改为 `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 引入的临时转换函数已被删除 --- @@ -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 映射的一致性