BoxAgnts介绍(7)——OpenAI-API与Anthropic-API

2025 年的 AI 模型市场百花齐放。但每个提供商都有自己的 API 格式、认证方式、流式协议。BoxAgnts 的设计目标是:用户切换模型只需改一个参数,所有内部逻辑保持不变

本文从四个层面拆解这套抽象:

  1. 统一接口LlmProvider trait 如何定义"一个模型提供商"

  2. 三大 API 格式对比:Anthropic、OpenAI、Google Gemini 的格式差异

  3. 格式转换:如何在三种截然不同的消息格式之间互译

  4. 工程实践:Think 配置、错误处理、ProviderQuirks、API Key 管理


统一接口:LlmProvider trait

一切从接口定义开始:

rust 复制代码
// boxagnts-api/src/provider.rs

#[async_trait]

pub trait LlmProvider: Send + Sync {

    fn id(&self) -> &ProviderId;                              // 唯一标识

    fn name(&self) -> &str;                                   // 人类可读名称

  


    async fn create_message(                                  // 非流式请求

        &self,

        request: ProviderRequest,

    ) -> Result<ProviderResponse, ProviderError>;

  


    async fn create_message_stream(                           // 流式请求

        &self,

        request: ProviderRequest,

    ) -> Result<

        Pin<Box<dyn Stream<Item = Result<StreamEvent, ProviderError>> + Send>>,

        ProviderError,

    >;

  


    async fn list_models(&self) -> Result<Vec<ModelInfo>, ProviderError>;  // 模型列表

    async fn check_connectivity(&self) -> Result<ProviderStatus, ProviderError>; // 健康检查

    fn capabilities(&self) -> ProviderCapabilities;           // 能力声明

}

输入和输出都是与提供商无关的统一类型:

rust 复制代码
pub struct ProviderRequest {

    pub model: String,

    pub messages: Vec<Message>,          // 统一对话格式

    pub system_prompt: Option<SystemPrompt>,

    pub tools: Vec<ToolDefinition>,      // 统一工具定义

    pub max_tokens: u32,

    pub temperature: Option<f64>,

    pub thinking: Option<ThinkingConfig>, // 深度思考配置

    pub provider_options: Value,          // 提供商特有参数

}

  


pub struct ProviderResponse {

    pub id: String,

    pub content: Vec<ContentBlock>,      // 统一内容块

    pub stop_reason: StopReason,         // 统一停止原因

    pub usage: UsageInfo,                // Token 用量

    pub model: String,

}

归一化层的核心价值:无论底层是 Claude、GPT 还是 Gemini,上层代码只看到 ProviderRequestProviderResponse


ProviderRegistry:40+ 模型的统一入口

rust 复制代码
// boxagnts-api/src/registry.rs

pub struct ProviderRegistry {

    providers: HashMap<ProviderId, Arc<dyn LlmProvider>>,

    default_provider_id: ProviderId,

}

  


fn provider_from_key(provider_id: &str, key: String) -> Option<Arc<dyn LlmProvider>> {

    match provider_id {

        // 原生实现------各有各的 API 格式

        "anthropic" => Some(Arc::new(AnthropicProvider::from_config(...))),

        "openai"    => Some(Arc::new(OpenAiProvider::new(key))),

        "google"    => Some(Arc::new(GoogleProvider::new(key))),

        "github-copilot" => Some(Arc::new(CopilotProvider::new(key))),

        "cohere"    => Some(Arc::new(CohereProvider::new(key))),

  


        // OpenAI 兼容提供商------共享同一套转换逻辑,只换 base_url

        "deepseek", "groq", "ollama", "mistral", "xai",

        "perplexity", "openrouter", "siliconflow", "moonshot",

        "zhipu", "stepfun", "fireworks", "llamacpp",

        "sambanova", "huggingface", "nvidia", "cerebras",

        // ... 总计 30+ 个 OpenAI 兼容提供商

        _ => None,

    }

}

三种实现策略:

| 类型 | 代表 | 转换策略 | 数量 |

|------|------|----------|------|

| 原生 Anthropic | claude-sonnet-4-5 | 几乎零转换(内部格式即 Anthropic 格式) | 1 |

| 原生 OpenAI | gpt-4o, o3 | ProviderRequest → Chat Completions | 1 |

| 原生 Google | gemini-2.5-flash | ProviderRequest → generateContent | 1 |

| OpenAI 兼容 | deepseek, groq, ollama 等 | 与 OpenAI 相同逻辑,只换 URL | 30+ |

| 其他原生 | github-copilot, cohere | 独立的格式转换 | 3+ |


三大 API 格式的差异

Anthropic、OpenAI、Google Gemini------三种 API 在消息格式上差异巨大。理解这些差异才能理解转换层的价值。

3.1 System Prompt

| 特性 | Anthropic | OpenAI | Google Gemini |

|------|-----------|--------|---------------|

| 位置 | 顶层 "system" 字段 | messages0role:"system" | 顶层 "systemInstruction" 字段 |

| 类型 | string 或 ContentBlock 数组 | 仅 string | 仅 content parts 数组 |

javascript 复制代码
// Anthropic --- 顶层独立字段

{"model": "claude-sonnet-4-5", "system": "You are helpful.", "messages": [...]}

  


// OpenAI --- 嵌入在 messages 数组里

{"model": "gpt-4o", "messages": [{"role":"system","content":"You are helpful."}, ...]}

  


// Google --- 使用 systemInstruction 字段,结构不同于 messages

{

  "systemInstruction": {"parts": [{"text": "You are helpful."}]},

  "contents": [{"role": "user", "parts": [{"text": "Hello"}]}]

}

3.2 Tool 定义

| 特性 | Anthropic | OpenAI | Google |

|------|-----------|--------|--------|

| 字段 | "tools": [{name, description, input_schema}] | "tools": [{type:"function", function:{...}}] | "tools": [{functionDeclarations: [{name, description, parameters}]}] |

| 包装层数 | 0 | 1 | 1,且用不同的嵌套名 |

3.3 Tool Call 响应

javascript 复制代码
// Anthropic --- content 数组中的 native block

{"content": [{"type":"tool_use", "id":"toolu_01A", "name":"read", "input": {...}}]}

  


// OpenAI --- 独立的 tool_calls 数组,arguments 是 JSON string

{"tool_calls": [{"id":"call_abc", "function": {"name":"read", "arguments": "{\"path\":\"...\"}"}}]}

  


// Google --- functionCall 嵌入在 parts 中,args 是 JSON object

{"candidates": [{"content": {"parts": [{"functionCall": {"name":"read", "args": {...}}}]}}]}

3.4 Tool Result 格式

javascript 复制代码
// Anthropic --- tool_result 是 user 消息 content 数组中的一个 block

{"role":"user", "content": [{"type":"tool_result", "tool_use_id":"toolu_01A", "content":"..."}]}

  


// OpenAI --- 需要单独的 role: "tool" 消息

{"role":"tool", "tool_call_id":"call_abc", "content":"..."}

  


// Google --- functionResponse 嵌入在 user content 的 parts 中

{"role":"user", "parts": [{"functionResponse": {"name":"read", "response": {...}}}]}

3.5 角色命名

| Anthropic | OpenAI | Google |

|-----------|--------|--------|

| user | user | user |

| assistant | assistant | model |

Google 用 model 而不是 assistant------这是最容易被忽略但最容易出错的差异。


转换层实现:以 OpenAI Provider 为例

OpenAiProvider 是转换层最完整的例子:

rust 复制代码
// boxagnts-api/src/providers/openai.rs

impl OpenAiProvider {

    fn to_openai_messages(

        messages: &[Message],

        system_prompt: Option<&SystemPrompt>,

    ) -> Vec<Value> {

        let mut result: Vec<Value> = Vec::new();

  


        // 第 1 步:system prompt → role: "system" 消息

        if let Some(sys) = system_prompt {

            result.push(json!({"role": "system", "content": sys_text}));

        }

  


        for msg in messages {

            match msg.role {

                Role::User => {

                    // user 消息中可能混合 text 和 tool_result blocks

                    // tool_result 需要拆分为独立的 role: "tool" 消息

                    Self::append_user_messages(&mut result, &msg.content);

                }

                Role::Assistant => {

                    let (text, tool_calls) = Self::assistant_content_to_openai(&msg.content);

                    result.push(json!({

                        "role": "assistant",

                        "content": text,

                        "tool_calls": tool_calls

                    }));

                }

            }

        }

        result

    }

  


    fn to_openai_tools(tools: &[ToolDefinition]) -> Vec<Value> {

        tools.iter().map(|td| {

            json!({

                "type": "function",

                "function": {

                    "name": td.name,

                    "description": td.description,

                    "parameters": td.input_schema

                }

            })

        }).collect()

    }

}

最复杂的部分是 tool_use_id 的 sanitize------Anthropic 的 tool ID(如 toolu_01Bx...)可能包含 OpenAI 不接受的字符。


Google Gemini Provider:第三种格式的完整适配

GoogleProvider 展示了当 API 格式与 Anthropic 和 OpenAI 都不同时的处理方式:

rust 复制代码
// boxagnts-api/src/providers/google.rs

// URL 模式完全不同于 OpenAI 的 /v1/chat/completions

fn generate_url(&self, model: &str) -> String {

    format!(

        "{}/v1beta/models/{}:generateContent?key={}",

        self.base_url, model, self.api_key  // API Key 在 URL 查询参数中!

    )

}

与 OpenAI 的关键差异:

| 差异点 | Google Gemini | OpenAI |

|--------|--------------|--------|

| API Key 位置 | URL 查询参数 ?key= | HTTP Header Authorization: Bearer |

| 端点格式 | /v1beta/models/{model}:generateContent | /v1/chat/completions |

| 流式端点 | /v1beta/models/{model}:streamGenerateContent?alt=sse | /v1/chat/completions + stream:true |

| 消息角色 | user / model(不是 assistant) | user / assistant |

| Tool 结果 | functionResponse in parts | 独立 role: tool 消息 |

| 图片输入 | inlineData base64 | image_url 或 content parts |


Thinking 配置:深度推理的模型差异

ThinkingConfig 是归一化的深度思考配置------但不同提供商的处理方式完全不同:

rust 复制代码
// 归一化的配置

pub struct ThinkingConfig {

    pub budget_tokens: u32,   // 思考 token 预算

}

  


// 在构建 ProviderRequest 时,根据 provider capabilities 决定是否传递

let provider_request = ProviderRequest {

    // ...

    thinking: if caps.thinking {

        effective_thinking_budget

            .map(|b| ThinkingConfig::enabled(b))

    } else {

        None  // 这个提供商不支持 thinking,不传

    },

};

| 提供商 | Thinking 支持 | 传递方式 |

|--------|--------------|----------|

| Anthropic(Claude 3.5+) | ✓ | "thinking": {"type": "enabled", "budget_tokens": N} |

| Google(Gemini 2.5+) | ✓ | "thinkingConfig": {"thinkingBudget": N} |

| OpenAI(o1/o3 系列) | 部分 | 通过 reasoning_effort 参数 |

| 其他 OpenAI 兼容 | 大部分不支持 | 不传递 |

在请求构建阶段,ProviderCapabilities 声明了每个提供商的能力:

rust 复制代码
pub struct ProviderCapabilities {

    pub thinking: bool,              // 是否支持深度思考

    pub prompt_caching: bool,        // 是否支持 prompt 缓存

    pub image_input: bool,           // 是否支持图片输入

    pub native_tool_use: bool,       // 是否有原生 tool calling

    pub supports_streaming: bool,    // 是否支持流式响应

    // ...

}

ProviderQuirks:每个提供商的"小脾气"

各 OpenAI 兼容提供商的 API 大致兼容,但都有细微差异。ProviderQuirks 用来处理这些:

rust 复制代码
pub struct ProviderQuirks {

    /// 上下文溢出时的特定错误消息模式

    pub overflow_patterns: Vec<String>,

    /// 本地服务无需 API Key(如 Ollama、LM Studio)

    pub no_api_key_required: bool,

    /// 流式响应中是否包含 usage 信息

    pub include_usage_in_stream: bool,

    /// DeepSeek 等提供商需要 reasoning_content 字段

    pub reasoning_field: Option<String>,

}

例如 DeepSeek 在流式响应中返回的 reasoning content 字段名与 OpenAI 不同------通过 reasoning_field 适配。Ollama 的上下文溢出错误消息是 "exceeds the available context size",而 LM Studio 的是 "greater than the context length"------通过 overflow_patterns 适配。


流式处理的统一

流式响应在三种 API 中也完全不同:

| 特性 | Anthropic (SSE) | OpenAI (SSE) | Google (SSE) |

|------|-----------------|--------------|--------------|

| 事件粒度 | 高粒度:6 种事件类型(start/delta/stop ×2) | 低粒度:每个 chunk 是一个完整 delta | 中等:按 chunk 推送,但结构扁平 |

| Tool call 增量 | 分块发送 input_json_delta | 一次性发送完整 arguments 字符串 | 一次性发送完整 functionCall |

| 终止信号 | message_stop 事件 | data: [DONE] 标记 | 流自然结束 |

| 是否需要按 index 重组 | 是(多个 tool_use 时按 index 分块) | 是 | 是 |

三种格式都被归一化到同样的 StreamEvent 枚举:

rust 复制代码
pub enum StreamEvent {

    MessageStart { id, model, usage },

    ContentBlockStart { index, content_block },

    TextDelta { text },

    ThinkingDelta { thinking },

    InputJsonDelta { index, partial_json },

    ContentBlockStop { index },

    MessageDelta { stop_reason, usage },

    MessageStop,

}

错误处理:从提供商差异到统一语义

每个提供商的错误格式也不同:

rust 复制代码
// 错误类型统一化

pub enum ProviderError {

    Auth { ... },             // 认证失败

    RateLimited { ... },      // 速率限制

    ContextOverflow { ... },  // 上下文超窗口(通过 ProviderQuirks 匹配)

    InvalidRequest { ... },   // 请求参数错误

    ServerError { ... },      // 服务端错误

    StreamError { ... },      // 流中断

    Other { ... },            // 未知错误

}

在查询循环中,特定错误触发特定恢复策略:

java 复制代码
RateLimited / Overloaded → 切换到 fallback_model

ContextOverflow → 触发 auto_compact

StreamError (stall) → 重试(最多 2 次,45s 超时)

Auth → 不可恢复,返回错误

API Key 的分级管理

BoxAgnts 为每个提供商定义了环境变量名映射:

rust 复制代码
// boxagnts-workspace/src/config.rs

pub fn api_key_env_vars_for_provider(provider_id: &str) -> &'static [&'static str] {

    match provider_id {

        "anthropic" => &["ANTHROPIC_API_KEY"],

        "openai" => &["OPENAI_API_KEY"],

        "google" => &["GOOGLE_API_KEY", "GOOGLE_GENERATIVE_AI_API_KEY"],

        "deepseek" => &["DEEPSEEK_API_KEY"],

        "mistral" => &["MISTRAL_API_KEY"],

        "xai" => &["XAI_API_KEY"],

        "zhipu" => &["ZHIPU_API_KEY"],

        // ... 40+ 个提供商的环境变量

    }

}

三级优先级:环境变量 > 用户配置 JSON > 无默认值。这种设计支持多租户、CI/CD、本地开发等不同场景。


小结

BoxAgnts 的模型抽象层解决了"一套代码适配所有 API"这个本质问题:

scss 复制代码
┌──────────────────────────────────────────────┐

│  boxagnts-query (Agent 推理循环)              │

│  只使用 ProviderRequest / ProviderResponse   │

└────────────────────┬─────────────────────────┘

                     │

┌────────────────────▼─────────────────────────┐

│  LlmProvider trait                            │

│  + ProviderRegistry (40+ providers)           │

├──────────┬──────────┬──────────┬─────────────┤

│Anthropic │ OpenAI   │ Google   │ OpenAiCompat │

│Provider  │ Provider │ Provider │ (30+ 厂商)   │

│(几乎零    │(完整     │(独立     │(共享 OpenAI  │

│ 转换)    │ 格式转换) │ 格式转换) │ 转换+Quirks)  │

└──────────┴──────────┴──────────┴─────────────┘

三个关键能力:

  1. 用户自由 :切换模型只改 --model 参数

  2. 代码不受影响run_query_loop() 完全不知道底层是谁

  3. 扩展成本极低:新增 OpenAI 兼容提供商约 3 行代码

这不是一个简单的"适配器模式"------它是一个经过 40+ 个实际 API 验证的生产级抽象。

相关资源

相关推荐
喵个咪2 小时前
AI重构软件开发范式:框架与脚手架为何仍是生产级开发的刚需?
架构·go·ai编程
zzzzz3692 小时前
快速搭建SpringAi项目 集成智能问答,RAG,FUINCTION_CALLING等功能
java·ai编程
Cho1yon2 小时前
【AI Agent 第十期:Claude Code 完全配置指南:三系统一步到位,AI编程助手轻松上手】
人工智能·ai编程
AI闲聊的椰汁2 小时前
RAG技术深度解析:核心原理+全链路调优+主流开源框架选型
ai编程
阿里云大数据AI技术2 小时前
基于阿里云 DataWorks Data Agent 进行大模型热度分析
人工智能·agent·nvidia
刀法如飞3 小时前
AI时代:一文搞懂DDD领域驱动设计
后端·架构·ai编程
HjhIron4 小时前
从零开始掌握Prompt工程:大模型调教指南
api·agent
AI砖家4 小时前
Claude Code 跳过确认完全指南:让 AI 自己完成开发任务
前端·人工智能·python·ai编程·代码规范
搬石头的马农4 小时前
Claude Code SpringBoot开发:从0到1搭建企业级项目的6个核心Skill
java·人工智能·spring boot·后端·ai编程