Files
agcore/docs/9d-provider-implementations.md
T

12 KiB
Raw Blame History

Provider 实现策略

本文档从 9-llm-provider-unified-interface.md 拆分而来,包含 §5 Provider 实现策略。

相关文件:

5. Provider 实现策略

5.1 OpenaiProvider(兼容 Chat API

MessageRequest
  │
  ├── model             → model
  ├── messages          → messages (逐条映射,见下方)
  ├── system            → 插入为首条 System message(如无 System message 时)
  ├── tools             → tools (OpenaiTool::Function)
  ├── tool_choice       → tool_choice
  ├── max_tokens        → max_tokens
  ├── temperature       → temperature
  ├── top_p             → top_p
  ├── stop_sequences    → stop (StopSequence::Multiple)
  ├── thinking          → 忽略(OpenAI 不支持)
  └── extra.*           → 对应字段 / extra_body

Message → OpenaiChatMessage:
  System { content }    → System { content: to_openai_content(content) }
  User { content }      → User { content: to_openai_content(content) }
  Assistant { content } → Assistant {
                            content: to_openai_content(text_blocks),
                            tool_calls: extract_tool_calls(content)
                          }
  Tool { content, tool_call_id } → Tool { content, tool_call_id }

ContentBlock → OpenaiContentPart:
  Text { text }       → Text { text }
  Image { source }    → Image { image_url: { url, detail } }
  Audio { source }    → InputAudio { input_audio }
  File { source }     → File { file }
  ToolUse { id, name, input } → 转为 OpenaiToolCall 放入 tool_calls 字段
  ToolResult { .. }   → 通过 tool_call_id 关联到 Tool 消息
  Thinking { .. }     → 忽略
  Extension { .. }    → 忽略

OpenaiChatResponse → MessageResponse:
  id               → id
  model            → model
  usage            → usage
  choices[0].finish_reason → stop_reason
  choices[0].message → Message::Assistant {
    content: [
      ContentBlock::Text { text },
      ... (msg.tool_calls → ContentBlock::ToolUse)
    ]
  }

🔄 待深入推演:OpenAI 流式转换的具体实现 当前只有一行从 OpenAI SSE chunk 到 StreamEvent 的映射,没有实现细节。 实际实现中需要解决以下问题:

  1. SSE 字节流解析器:当前 SseChunkStream 从字节流解析 SSE 行(data: ...),新设计中需要改写为 直接产出 StreamEvent 的转换器。是否可以复用现有 SseChunkStream 的字节流解析逻辑?
  2. ContentBlockStart/End 合成OpenAI 的 SSE 流中没有原生 block 边界标记,只有一个 delta.contentdelta.tool_calls。转换器需自行合成:在第一次出现 delta.content 时发出 ContentBlockStart(0, Text),在 delta.tool_calls 出现时发出 ContentBlockStart(n, ToolUse { id, name })。注意 content 和 tool_calls 在同一 chunk 互斥 (需验证),合成规则为"来什么类型,合什么边界"。
  3. Tool call 全局序号映射OpenAI 的 delta.tool_calls[i].index该 chunk 内的局部索引 需要累积状态(HashMap<局部index, 全局index>)来推导全局 content block 序号。 对比:已推演的 IR 设计将 ToolCallStart 合并到 ContentBlockStartindex 在 IR 层面统一为 全局序号,差异完全封装在 Provider 层。
  4. 多 Choice 的处理:当前代码只处理 choices[0]。新设计中是继续忽略其他 choice 还是 通过某种机制暴露(如 extra 中携带)?
  5. usage 的时机OpenAI 的 usage 通常在最后一个 chunk 中携带,与 finish_reason 在同一 chunk。 是先发 CostUpdate(PartialUsage{...}) 再发 MessageComplete,还是反过来? 需要推演:
  • OpenaiStreamToEvents<S> 转换器的状态机设计(跟踪的局部状态、事件产出规则)
  • 边界情况:SSE 行乱序、[DONE] 标记的处理、网络断开重连 优先级:高(Phase 2 OpenAI Provider 重构的核心任务)

5.2 AnthropicProviderMessages API

MessageRequest → Anthropic Messages Request:
  model               → "anthropic-xxx"
  messages            → [只包含 User / Assistant / Tool role]
  system              → system (顶层参数)
  tools               → tools (Anthropic 原生格式)
  tool_choice         → tool_choice
  max_tokens          → max_tokens
  temperature         → temperature
  top_p               → top_p
  stop_sequences      → stop_sequences
  thinking            → thinking (原生支持)
  extra.*             → 忽略或不支持

Message → Anthropic Message:
  System { }          → 跳过(已在 system 参数中)
  User { content }    → { role: "user", content: to_anthropic_content(content) }
  Assistant { content } → { role: "assistant", content: to_anthropic_content(content) }
  Tool { content, tool_call_id } → { role: "user", content: [tool_result block] }

ContentBlock → Anthropic Content Block:
  Text { text }       → { type: "text", text }
  Image { source }    → { type: "image", source: { type: "base64", ... } }
  ToolUse { id, name, input } → { type: "tool_use", id, name, input }
  ToolResult { tool_use_id, content, is_error } → { type: "tool_result", ... }
  Thinking { text, signature } → { type: "thinking", thinking: text, signature }
  Audio / File / Extension → 忽略或不支持

Anthropic Response → MessageResponse:
  id               → id
  model            → model
  content          → Message::Assistant { content: map_blocks(content) }
  usage.input_tokens  → usage.prompt_tokens
  usage.output_tokens → usage.completion_tokens
  stop_reason      → stop_reason

🔄 待深入推演:Anthropic 流式状态机设计 Anthropic 的流式比 OpenAI 复杂得多——7 种事件类型、需要维护 block index 状态、 thinking 有 signature 晚于 content_block 下发。 需要推演:

  1. 状态机状态设计
    • PendingStart → 等待 message_start
    • InBlock { block_index, block_type, tool_acc: Option<ToolAccumulator> } → 在某个 content block 中
    • BetweenBlocks → 等待下一个 content_block_startmessage_delta
    • Completed → 收到 message_stop
    • Errored
  2. block index 追踪content_block_start 中的 index 是全局 content block 序号, 直接对应最终 content 数组中的位置。ToolUse { id, name } 嵌入在 ContentBlockStartblock_type 中(已推演:ToolCallStart 已移除,合并到 ContentBlockType::ToolUse)。 而 input 通过后续的 content_block_delta(含 input_json_delta)增量到达。 实现时发出 ContentBlockStart(index, ToolUse { id, name }) 后, 通过后续的 ToolCallArgumentsDelta { index, arguments } 累积参数。
  3. Thinking signature 的附着时机 已推演(方案 CMessageComplete 兜底) Anthropic 中,thinking block 的 signature 不在 content_block_startcontent_block_delta 中下发,而是在最后的 message_delta(与 stop_reason 一起)下发。 推演结论:
    • StreamEvent::MessageComplete { stop_reason, thinking_signature: Option<String> } 携带 signature,不在 ThinkingDelta 或 ContentBlockEnd 中传递
    • Anthropic 映射层在收到 message_delta 时,将 message_delta.thinking.signature 填入 MessageComplete.thinking_signature
    • 汇聚算法(PartialMessageResponse::finalize())在遍历 Thinking block 时, 如果 builder 中的 signature 为 None,用 self.thinking_signature 回填
    • 该方案不需新增独立事件,统一在 finalize 时处理,语义清晰
  4. block 嵌套的边界情况Anthropic 的响应中 block 不会嵌套,但允许多个 block 连续出现。 需要确保状态机正确处理 block 间切换(前一个 content_block_stop → 后一个 content_block_start)。 需要推演:
  • 完整的状态转移图(当前状态 + 输入事件 → 新状态 + 产出的 StreamEvent
  • SSE 字节流解析器(与 OpenAI 共享字节流层,差异化事件解析层)
  • 错误恢复:收到 error 事件时的状态机行为 优先级:高(Phase 4 AnthropicProvider 实现的前提条件)

5.3 OpenAI Response API(草案)

// 核心思路:Response API 的 "input as messages" 模式映射到 IR

// MessageRequest → Response API Request:
//   model                         → model
//   messages                      → input (作为 multi-turn conversation)
//   tools                         → tools (tool 定义)
//   extra.previous_response_id    → previous_response_id
//   extra.built_in_tools          → tools (内置工具配置)
//   extra.instructions            → instructions

// Response → MessageResponse:
//   output[0] (type="message")    → message
//   output[1..n]                  → ContentBlock::Extension

// 内置工具(web_search 等)需要额外的能力 trait:
#[async_trait]
pub trait BuiltInToolsCapable: LlmProvider {
    fn available_builtin_tools(&self) -> Vec<(&'static str, serde_json::Value)>;
    async fn execute_builtin_tool(&self, name: &str, input: Value) -> Result<Value, LlmError>;
}

🔄 待深入推演:OpenAI Response API 完整映射表 当前 §5.3 只有注释级别的草案,缺乏完整映射。 需要推演:

  1. input 字段支持三种模式:字符串、Vec<Message>IR 消息列表)、response_id(前序响应)。 IR 的 MessageRequest.messages 能否同时覆盖这三种?previous_response_id 通过 extra 传递后, messages 是否还需要存在?
  2. output 中的每种类型(message, web_search_call, file_search_call, code_interpreter_call, computer_call, reasoning)如何映射到 ContentBlock 当前 Extension 逃生舱能否承载?是否需要新增 ContentBlock variant
  3. Response API 的 tools 参数除了定义 function 工具外,还支持配置内置工具的参数 (如 web_searchsearch_context_size)。ToolDefinition 能否表达?
  4. Streaming 差异:Response API 的流式事件类型(response.output_items.added 等)与 Chat API 完全不同,如何映射到 StreamEvent? 优先级:低(Phase 4 之后的远期计划)

5.4 DeepSeek / Qwen 等兼容 Provider 的落地策略

当前 ProviderType 枚举中已列出 DeepSeek 和 Qwen,但实现均标记为 unimplemented!()

🔄 待深入推演:DeepSeek/Qwen Provider 的落地策略 这些 Provider 通常兼容 OpenAI Chat API 格式。在新 IR 设计下,有两种落地路径: 路径 A — 复用 OpenaiProvider(推荐):ProviderRegistry 中注册时直接使用 OpenaiProvider::new(base_url, api_key, model) 仅需换 base_url。适合 DeepSeek、Qwen、Groq、Azure 等 API 格式与 OpenAI 完全一致的场景。 此时 ProviderType 枚举可能不再需要(OpenaiProvider 通过 capabilities().provider_name 标识自身为 "openai-compatible" 或具体名称)。 路径 B — 独立 Provider 实现: 如果某 Provider 在 OpenAI 格式基础上做了扩展/修改(如自定义参数、不同的错误格式), 可独立实现 LlmProvider trait,复用 IR 类型,仅在 IR ↔ 原生格式映射层做差异处理。 需要推演:

  1. ProviderType 枚举在新的注册体系中是否还有存在的必要(工厂函数模式 vs 直接 new Provider
  2. OpenaiProvider 是否要重命名为更通用的 OpenaiCompatibleProvider? 优先级:低(Phase 4 后梳理)