5.7 KiB
5.7 KiB
LlmProvider Trait 设计
本文档从
9-llm-provider-unified-interface.md拆分而来,包含 §4 LlmProvider Trait 设计。相关文件:
- 9b-ir-type-system.md — IR 类型定义(MessageRequest、MessageResponse、StreamEvent 等)
- 9d-provider-implementations.md — 各 Provider 的具体实现策略
- 9f-edge-cases.md — Thinking signature 等边界情况(与 StreamEvent 设计关联)
4. LlmProvider Trait 设计
4.1 核心接口
#[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
#[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() 的使用场景
- LlmCycle:根据
features.thinking决定是否启用 thinking 模式 - AgentBuilder:在构建时校验模型是否支持所需特性
- UI/CLI:展示 Provider 的能力矩阵
- 智能路由:根据能力自动选择最佳 Provider
4.3 流式事件 StreamEvent
/// 流式事件 —— 流式响应的语义化增量构建过程。
/// 一组 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",但没有给出具体实现。 这是上层做"既流式又完整"双模处理的关键基础设施。 需要推演:
PartialMessageResponse结构体设计(累积的状态:id, model, text_buffer, tool_calls 字典, usage, stop_reason 等)StreamEvent::apply_to(&self, state: &mut PartialMessageResponse) -> bool算法——每个事件类型如何更新状态PartialMessageResponse::finalize(self) -> MessageResponse的完成逻辑- 边界情况: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 数组位置) 需要推演:
- IR 的 index 使用哪种语义?建议用"全局 tool call 序号"(OpenAI Provider 内部从局部 index 映射到全局序号)
- 当 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 } |
明确参数完整时间点 |