4038 字
约 13 分钟
7
OpenAI 兼容协议和 ChatResponse 标准化有什么区别?

OpenAI 兼容协议和 ChatResponse 标准化有什么区别?

在做 AI Agent 或工作流平台时,经常会遇到一个很现实的问题:

我们不可能只接一家大模型。

可能有的节点想用 OpenAI,有的节点想用 DeepSeek,有的节点想用通义千问,还有的节点想用智谱。

这时候问题就来了:

不同厂商的 API 不一样,后端难道要给每一家都单独写一套调用代码吗?

老王就问我:

“你们接入了 OpenAI、DeepSeek、通义千问好几家模型,这些厂商的 API 不一样吧?怎么统一的?”

我说:

“主要靠两层统一。”

第一层:OpenAI 兼容协议,统一请求格式
第二层:ChatResponse 标准化,统一返回结果

这篇文章就把这两个概念讲清楚。


一、为什么需要 OpenAI 兼容协议?

最早接大模型时,不同厂商的接口格式差异比较明显。

比如 A 厂商叫 prompt,B 厂商叫 messages

A 厂商的接口地址是 /chat,B 厂商的接口地址是 /generate

A 厂商的温度参数叫 temperature,B 厂商可能叫 top_p 或别的名字。

如果每家都单独适配,代码很快就会变成这样:

if 是 OpenAI,就走 OpenAIClient
if 是 DeepSeek,就走 DeepSeekClient
if 是通义千问,就走 QwenClient
if 是智谱,就走 ZhipuClient

一开始看起来没问题。

但模型厂商越来越多之后,代码会越来越难维护。

所以现在很多模型厂商都会提供一种能力:

OpenAI 兼容接口

意思是:

虽然底层模型不是 OpenAI 的,但接口格式尽量按照 OpenAI 的格式来。

这样调用方就可以用一套 OpenAI 风格的请求结构,去调用多家模型。


二、OpenAI 兼容到底兼容了什么?

所谓 OpenAI 兼容,主要兼容的是请求协议

比如大家都尽量支持类似这样的接口:

/v1/chat/completions

请求体结构也差不多:

{
  "model": "deepseek-chat",
  "messages": [
    {
      "role": "user",
      "content": "帮我总结一下这段文字"
    }
  ],
  "temperature": 0.7
}

这几个字段是大模型调用里最常见的:

model:使用哪个模型
messages:对话上下文
temperature:生成随机性
stream:是否流式输出
tools:是否开启工具调用

所以,只要厂商支持 OpenAI 兼容协议,我们的后端就不需要为每一家重新设计一套请求结构。

大部分时候,只需要换三个东西:

base_url
api_key
model

比如:

OpenAI:
https://api.openai.com/v1

DeepSeek:
https://api.deepseek.com/v1

通义千问兼容模式:
https://dashscope.aliyuncs.com/compatible-mode/v1

虽然地址不一样,但请求格式基本一致。

这就是 OpenAI 兼容协议最大的价值:

用一套调用方式,接入多家模型厂商。

三、在代码里怎么统一创建模型客户端?

在 PaiAgent 里,可以通过一个 ChatClientFactory 来统一创建模型客户端。

核心代码类似这样:

private ChatModel createOpenAICompatibleModel(
        String apiUrl,
        String apiKey,
        String model,
        Double temperature
) {
    OpenAiApi openAiApi = new OpenAiApi(apiUrl, apiKey);

    OpenAiChatOptions options = OpenAiChatOptions.builder()
            .model(model)
            .temperature(temperature)
            .build();

    return new OpenAiChatModel(openAiApi, options);
}

这段代码的意思很简单:

1. 用 apiUrl 和 apiKey 创建 OpenAiApi
2. 用 model 和 temperature 创建模型参数
3. 最后创建 OpenAiChatModel

重点在这里:

new OpenAiApi(apiUrl, apiKey)

apiUrl 是动态传进来的。

所以它不一定非得是 OpenAI 官方地址,也可以是 DeepSeek,也可以是通义千问的兼容地址。

也就是说,不管外部传进来的是:

https://api.openai.com/v1

还是:

https://api.deepseek.com/v1

还是:

https://dashscope.aliyuncs.com/compatible-mode/v1

都可以走同一套创建逻辑。


四、工厂方法怎么屏蔽厂商差异?

一般在工厂类里,会有一个类似 switch 的逻辑。

比如:

public ChatModel createChatModel(ModelConfig config) {
    switch (config.getProvider()) {
        case "openai":
        case "deepseek":
        case "qwen":
            return createOpenAICompatibleModel(
                    config.getApiUrl(),
                    config.getApiKey(),
                    config.getModel(),
                    config.getTemperature()
            );

        default:
            throw new IllegalArgumentException("Unsupported provider");
    }
}

你会发现,openaideepseekqwen 虽然是不同厂商,但它们都指向了同一个方法:

createOpenAICompatibleModel(...)

这就说明,只要这些模型厂商都支持 OpenAI 兼容协议,我们在代码里就可以把它们当成同一类模型来处理。

区别只放在配置里:

{
  "provider": "deepseek",
  "apiUrl": "https://api.deepseek.com/v1",
  "apiKey": "sk-xxx",
  "model": "deepseek-chat",
  "temperature": 0.7
}

换成通义千问,也只是配置变化:

{
  "provider": "qwen",
  "apiUrl": "https://dashscope.aliyuncs.com/compatible-mode/v1",
  "apiKey": "sk-xxx",
  "model": "qwen-plus",
  "temperature": 0.7
}

业务代码不用改。

这就是工厂模式带来的好处:

把模型创建逻辑集中收口,把厂商差异放到配置里。

五、OpenAI 兼容是不是等于完全一样?

不是。

这一点非常重要。

OpenAI 兼容主要解决的是请求格式统一,不代表所有厂商的返回结果都百分百一样。

老王接着问:

“那 Response 呢?各家返回的格式也完全一致吗?”

我说:

“不完全一致。大部分字段差不多,但细节上还是会有差异。”

比如普通非流式返回里,很多厂商都会返回类似结构:

{
  "choices": [
    {
      "message": {
        "role": "assistant",
        "content": "这是模型生成的回答"
      },
      "finish_reason": "stop"
    }
  ],
  "usage": {
    "prompt_tokens": 100,
    "completion_tokens": 50,
    "total_tokens": 150
  }
}

我们最常用的内容一般在:

choices[0].message.content

所以从“拿模型回答”这个角度看,各家差异不大。

但细节字段可能不一样。

比如 token 统计字段,有的厂商可能是:

{
  "usage": {
    "prompt_tokens": 100,
    "completion_tokens": 50,
    "total_tokens": 150
  }
}

有的厂商可能更偏向:

{
  "usage": {
    "input_tokens": 100,
    "output_tokens": 50,
    "total_tokens": 150
  }
}

再比如流式输出时,大家虽然大多用 SSE,但 chunk 的细节也可能不同。

比如:

finish_reason 的枚举值可能不完全一样
usage 返回时机可能不一样
tool call 的 delta 结构可能略有差异
异常返回格式可能不一样

所以,千万不要以为“OpenAI 兼容”就等于“完全没有适配成本”。

更准确的说法是:

OpenAI 兼容降低了接入成本,但没有完全消灭厂商差异。

六、那 Response 怎么统一?

这时候就轮到框架层出场了。

在 Spring AI 里,我们不是直接拿各家厂商原始返回的 JSON 到处传,而是使用统一的 ChatResponse

也就是说,底层厂商返回的数据可能略有不同,但 Spring AI 会尽量帮我们转换成统一结构。

我们业务代码里拿到的是类似这样的对象:

ChatResponse response = chatModel.call(prompt);

然后从里面取结果:

String content = response.getResult()
        .getOutput()
        .getText();

或者取 token 用量:

Usage usage = response.getMetadata().getUsage();

这个时候,业务代码不需要关心底层厂商到底叫:

prompt_tokens

还是:

input_tokens

因为框架已经帮我们做了一层标准化。

所以这里要分清两个概念:

OpenAI 兼容协议:统一请求怎么发
ChatResponse:统一结果怎么拿

一个偏输入,一个偏输出。

一个解决“怎么调用模型”,一个解决“怎么消费结果”。


七、OpenAI 兼容和 ChatResponse 的区别

可以用一张表来理解:

OpenAI 兼容协议:
    解决请求格式统一
    关注 apiUrl、apiKey、model、messages、temperature
    作用在模型调用前

ChatResponse:
    解决返回结果统一
    关注 content、metadata、usage、finishReason
    作用在模型调用后

再通俗一点:

OpenAI 兼容协议,管的是“怎么问模型”。
ChatResponse 标准化,管的是“模型回答后怎么取结果”。

举个例子。

你要去不同餐厅点餐。

OpenAI 兼容协议像是大家都支持同一种点餐格式:

我要一份牛肉面,少辣,不要香菜。

不管你去 A 餐厅还是 B 餐厅,都能这么点。

但是每家餐厅端上来的小票格式可能不一样。

有的写:

主食:牛肉面
价格:28

有的写:

商品名:牛肉面
实付金额:28

这时候 ChatResponse 就像一个统一小票解析器。

它把不同餐厅的小票都整理成统一格式:

菜品名称
价格
备注

所以:

OpenAI 兼容负责统一点餐方式。
ChatResponse 负责统一小票格式。

八、为什么不能直接用原始 Response?

理论上可以,但不建议。

如果业务代码直接解析原始 JSON,就会变成这样:

if (provider.equals("openai")) {
    content = json.get("choices")
            .get(0)
            .get("message")
            .get("content");
} else if (provider.equals("deepseek")) {
    content = json.get("choices")
            .get(0)
            .get("message")
            .get("content");
} else if (provider.equals("qwen")) {
    content = json.get("output")
            .get("text");
}

一开始你可能觉得还能接受。

但是后面要处理更多东西:

流式输出
token 用量
工具调用
finish reason
异常信息
安全拦截
模型拒答
多模态内容

代码就会越来越乱。

更好的方式是:

底层适配不同厂商
上层只处理统一对象

也就是:

厂商原始 Response
        ↓
框架适配层
        ↓
统一 ChatResponse
        ↓
业务代码使用

这样业务层就干净很多。


九、运行时怎么动态切换模型?

老王继续问:

“你们是怎么实现运行时动态切换模型的?不重启服务就能换?”

答案是:

模型客户端不是写死的,也不是固定注入一个 Spring 单例,而是在运行时根据节点配置动态创建。

也就是说,每个节点都可以有自己的模型配置。

比如工作流 JSON 里可以这样定义:

{
  "id": "node_analysis",
  "type": "llm",
  "modelConfig": {
    "provider": "deepseek",
    "apiUrl": "https://api.deepseek.com/v1",
    "apiKey": "sk-xxx",
    "model": "deepseek-chat",
    "temperature": 0.5
  }
}

另一个节点可以这样定义:

{
  "id": "node_polish",
  "type": "llm",
  "modelConfig": {
    "provider": "openai",
    "apiUrl": "https://api.openai.com/v1",
    "apiKey": "sk-xxx",
    "model": "gpt-4o",
    "temperature": 0.8
  }
}

这样一个工作流里就可以出现:

第一个节点:用 DeepSeek 做初步分析
第二个节点:用 GPT 做精细加工
第三个节点:用 Qwen 做结果总结

执行时,系统读取当前节点的配置,然后调用 ChatClientFactory 创建对应的 ChatModel

流程大概是:

读取工作流配置
      ↓
执行到某个 LLM 节点
      ↓
读取该节点的 modelConfig
      ↓
ChatClientFactory 创建 ChatModel
      ↓
调用模型
      ↓
返回统一 ChatResponse

所以,前端拖拽编辑器里改了模型名称、apiUrl 或 temperature,下次执行工作流时就能生效。

不需要重启服务。


十、为什么不用 Spring 单例注入?

很多项目里,我们习惯这样写:

@Autowired
private ChatModel chatModel;

这种方式适合模型配置固定的场景。

比如整个系统就用一个模型,那当然可以。

但工作流平台不一样。

工作流平台的特点是:

不同用户可能用不同模型
不同工作流可能用不同模型
同一个工作流的不同节点也可能用不同模型
同一个节点下次执行时配置也可能变化

如果把 ChatModel 写成固定 Spring 单例,就不够灵活了。

因为单例对象在应用启动时就创建好了。

它的 apiUrlapiKeymodel 基本都是固定的。

而 PaiAgent 需要的是:

运行时读配置,运行时创建客户端,运行时决定调用哪个模型。

所以这里更适合使用工厂模式,而不是直接注入一个固定模型对象。


十一、动态创建 ChatClient 有什么好处?

第一,灵活。

每个节点都可以单独配置模型。

第二,扩展简单。

新增一个支持 OpenAI 兼容协议的厂商时,通常只需要加配置,不需要大改业务逻辑。

第三,适合工作流编排。

工作流本来就是配置驱动的。

既然节点、边、参数都可以配置,模型自然也应该可以配置。

第四,方便灰度和测试。

同一个流程,可以快速切换不同模型做效果对比。

比如:

DeepSeek 版本
GPT 版本
Qwen 版本

通过不同配置跑一遍,就可以比较输出质量、成本和耗时。


十二、动态创建有什么缺点?

缺点也很明显:

每次调用都 new OpenAiApi
每次调用都 new OpenAiChatModel
可能会有一定对象创建开销

对于低频工作流调用,这个问题不大。

因为一次工作流执行里,大模型调用本身才是最耗时的部分。

相比模型推理耗时,创建几个客户端对象的开销通常可以接受。

但如果是高并发在线推理场景,比如:

每秒几百次请求
每秒几千次请求
实时对话服务
在线客服系统

那就要进一步优化。

可以考虑:

1. 按 apiUrl + model 缓存 ChatModel
2. 复用底层 HTTP Client
3. 做连接池管理
4. 控制客户端对象数量
5. 给不同模型配置限流策略

也就是说:

工作流低频场景,可以动态创建;
高并发在线场景,最好做缓存和连接复用。

十三、最终架构可以怎么理解?

整个多模型接入链路可以总结成这样:

工作流节点配置
      ↓
读取 modelConfig
      ↓
ChatClientFactory
      ↓
OpenAI Compatible Client
      ↓
调用不同厂商模型
      ↓
厂商原始响应
      ↓
Spring AI 标准化
      ↓
ChatResponse
      ↓
业务节点继续处理

这里面有三个关键角色。

1. OpenAI 兼容协议

它解决的是:

不同厂商怎么用同一种请求格式调用。

重点是统一:

接口路径
请求字段
messages 格式
model 参数
temperature 参数
stream 参数

2. ChatClientFactory

它解决的是:

运行时根据配置创建不同模型客户端。

重点是统一:

apiUrl
apiKey
model
temperature
provider

3. ChatResponse

它解决的是:

不同厂商返回结果怎么用同一种方式读取。

重点是统一:

模型输出内容
token 用量
finish reason
metadata

所以这三者的关系是:

OpenAI 兼容协议负责统一请求;
ChatClientFactory 负责动态创建客户端;
ChatResponse 负责统一响应结果。

十四、一句话总结

如果只记一句话,可以这样说:

OpenAI 兼容协议解决“怎么用同一套格式请求不同模型”,ChatResponse 解决“怎么用同一套格式读取不同模型的返回结果”。

再进一步:

OpenAI 兼容偏调用协议;
ChatResponse 偏结果抽象;
ChatClientFactory 偏工程落地。

在工作流平台里,这套设计非常实用。

因为它让我们可以做到:

模型厂商可切换
节点模型可配置
请求协议可复用
返回结果可统一
服务无需重启
工作流运行时动态生效

表面上看,只是换了一个 apiUrlmodel

但底层真正体现的是一种架构思想:

把厂商差异收敛在适配层,把业务代码稳定在统一抽象上。

这也是多模型 Agent 平台最核心的设计之一。

OpenAI 兼容协议和 ChatResponse 标准化有什么区别?
http://clxhxhhr.top/posts/593/
作者
clxstart
发布于
2026-09-13
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。