141 lines
5.7 KiB
Markdown
141 lines
5.7 KiB
Markdown
# 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<MessageResponse, LlmError>;
|
||
|
||
/// 流式消息请求,返回语义化事件流。
|
||
async fn chat_stream(
|
||
&self,
|
||
request: MessageRequest,
|
||
) -> Result<
|
||
Pin<Box<dyn Stream<Item = Result<StreamEvent, LlmError>> + 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<Vec<String>>,
|
||
/// 特性标记
|
||
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 }` | 明确参数完整时间点 |
|