# LlmProvider Trait 设计 > 本文档从 `9-llm-provider-unified-interface.md` 拆分而来,包含 §4 LlmProvider Trait 设计。 > > **相关文件:** > - [9b-ir-type-system.md](9b-ir-type-system.md) — IR 类型定义(MessageRequest、MessageResponse、StreamEvent 等) > - [9d-provider-implementations.md](9d-provider-implementations.md) — 各 Provider 的具体实现策略 > - [9f-edge-cases.md](9f-edge-cases.md) — Thinking signature 等边界情况(与 StreamEvent 设计关联) ## 4. LlmProvider Trait 设计 ### 4.1 核心接口 ```rust #[async_trait] pub trait LlmProvider: Send + Sync { /// 发送消息请求,获取完整响应。 async fn chat(&self, request: MessageRequest) -> Result; /// 流式消息请求,返回语义化事件流。 async fn chat_stream( &self, request: MessageRequest, ) -> Result< Pin> + Send>>, LlmError, >; /// 返回 Provider 的能力描述。 fn capabilities(&self) -> ProviderCapabilities; } ``` ### 4.2 ProviderCapabilities ```rust #[derive(Debug, Clone)] pub struct ProviderCapabilities { /// Provider 标识名称 pub provider_name: &'static str, /// 支持的模型列表(None = 不限制) pub supported_models: Option>, /// 特性标记 pub features: ProviderFeatures, } #[derive(Debug, Clone, Default)] pub struct ProviderFeatures { pub streaming: bool, pub thinking: bool, pub vision: bool, pub audio_input: bool, pub tool_use: bool, pub parallel_tool_calls: bool, /// true=OpenAI风格(system in messages), false=Anthropic风格(system as param) pub system_prompt_in_messages: bool, pub max_context_window: u32, } ``` #### capabilities() 的使用场景 1. **LlmCycle**:根据 `features.thinking` 决定是否启用 thinking 模式 2. **AgentBuilder**:在构建时校验模型是否支持所需特性 3. **UI/CLI**:展示 Provider 的能力矩阵 4. **智能路由**:根据能力自动选择最佳 Provider ### 4.3 流式事件 StreamEvent ```rust /// 流式事件 —— 流式响应的语义化增量构建过程。 /// 一组 StreamEvent 最终可汇聚为一个完整的 MessageResponse。 #[derive(Debug, Clone)] pub enum StreamEvent { // ── Meta ── /// 消息开始 MessageStart { id: String, model: String }, // ── Content 增量 ── /// 文本增量 TextDelta { text: String }, /// 思考增量(Anthropic thinking block) ThinkingDelta { text: String }, /// 拒绝增量(OpenAI refusal) RefusalDelta { text: String }, // ── Tool Call ── /// 工具调用开始 ToolCallStart { index: u32, id: String, name: String }, /// 工具参数增量 ToolCallArgumentsDelta { index: u32, arguments: String }, /// 工具调用结束 ToolCallEnd { index: u32 }, // ── 汇总 ── /// Token 用量 CostUpdate { usage: Usage }, /// 消息完成 MessageComplete { stop_reason: StopReason }, // ── 错误 ── Error { message: String }, } ``` > **🔄 待深入推演:StreamEvent 汇聚为 MessageResponse 的算法** > 文档提到"一组 StreamEvent 最终可汇聚为一个完整的 MessageResponse",但没有给出具体实现。 > 这是上层做"既流式又完整"双模处理的关键基础设施。 > **需要推演:** > 1. `PartialMessageResponse` 结构体设计(累积的状态:id, model, text_buffer, tool_calls 字典, usage, stop_reason 等) > 2. `StreamEvent::apply_to(&self, state: &mut PartialMessageResponse) -> bool` 算法——每个事件类型如何更新状态 > 3. `PartialMessageResponse::finalize(self) -> MessageResponse` 的完成逻辑 > 4. 边界情况:MessageStart 到达前就收到 TextDelta 怎么办?ToolCallArgumentsDelta 到达顺序乱序? > 多次 CostUpdate 是叠加还是覆盖? > 优先级:高(Phase 3 流式功能依赖此设计) > **🔄 待深入推演:ToolCallStart 的 index 来源与归一化** > `ToolCallStart.index` 在不同 Provider 流式协议中的含义不同: > - OpenAI SSE chunk 中 `delta.tool_calls[i].index` 是**该次 delta 的局部索引**(只含变动的 tool call), > 需要调用方累积状态来跟踪每个 tool call 的完整索引 > - Anthropic 的 `content_block_start` 中的 `index` 是**全局的 content block 索引**(直接映射到最终 content 数组位置) > **需要推演:** > 1. IR 的 index 使用哪种语义?建议用"全局 tool call 序号"(OpenAI Provider 内部从局部 index 映射到全局序号) > 2. 当 OpenAI 并行返回多个 tool_calls(同一 chunk 含多个 `delta.tool_calls` 条目),如何分配 index? > 优先级:中(Phase 2 实现 OpenAI 流式转换时需解决) #### 与当前 StreamEvent 的对比 | 当前 StreamEvent | 新 StreamEvent | 理由 | |-----------------|---------------|------| | `AssistantTextDelta { text }` | `TextDelta { text }` | 简洁化 | | — | `ThinkingDelta { text }` | Anthropic 需要 | | — | `RefusalDelta { text }` | OpenAI 需要 | | `ToolExecutionStarted { tool_name, input, tool_call_id }` | `ToolCallStart { index, id, name }` + `ToolCallArgumentsDelta { index, arguments }` | 支持增量参数(Anthropic 的 tool_use 流式需要) | | `ToolExecutionCompleted` | **移除** | 这是 LlmCycle 层的事件,非 Provider 层 | | `CostUpdate { usage }` | 保留 | ✅ | | `TurnComplete { reason }` | `MessageComplete { stop_reason }` | 语义更准确 | | `Error { message }` | 保留 | ✅ | | — | `MessageStart { id, model }` | Anthropic 需要 | | — | `ToolCallEnd { index }` | 明确参数完整时间点 |