模型适配层

统一入口不等于统一参数

做 AI 网关时,容易先追求一个漂亮的入口:所有请求走 /v1/generate,客户端只传 modelinputparameters

设计看起来干净,前端也好接。

但模型能力不统一:

  • 文本、图像、视频模型的输入结构不同。
  • 同样是图像模型,有的支持 size,有的支持 aspect_ratio
  • 网关如果参数原样透传,上游很快返回」不支持字段」。

统一 API 的价值不是假装所有模型一样,而是把差异收束在网关内部。

适配层要做两件事:对外保持稳定,对内承认差异。

flowchart LR
    A[统一请求] --> B[适配层]
    B -->|转换| C[模型 A 的参数表/调用路径]
    B -->|转换| D[模型 B 的参数表/调用路径]
    C --> E[统一响应]
    D --> E

客户端看到同一个任务 API。服务端看到每个模型自己的参数表、调用路径和响应格式。

参数声明要前置

最怕把 parameters 当成万能袋子。短期省事,长期把错误推迟到运行时:

  • 前端不知道能填什么。
  • 后端不知道该拒绝什么。
  • 上游报错很难转成清晰的业务错误。

更稳的做法:让每个模型声明自己的参数边界。

{
"id": "gpt-image-2",
"capability": "image",
"input": ["text", "image"],
"output": ["image"],
"stream": false,
"parameters": {
"prompt": { "type": "string", "required": true },
"size": { "type": "string", "required": true },
"quality": { "type": "string", "required": false },
"output_format": { "type": "string", "required": false }
}
}

这份声明不只是文档。它参与校验、前端表单生成、错误提示、模型路由。

调用前能拒绝的参数,就不要交给上游拒绝。

新增模型时,改动集中在适配器和模型定义里,不用到处写 if model == ...。模型差异被保留,但不会扩散。

调用路径也要适配

参数不同只是第一层差异,调用路径不同更麻烦。

  • 文本模型可能走 OpenAI 兼容端点。
  • 图像模型可能必须用运行时绑定调用。
  • 推理模型可能用另一套响应格式。

适配层把一次调用拆成三个明确动作:

  1. 把统一请求转成上游请求。
  2. 调用对应上游入口。
  3. 把上游响应转成统一响应。
type Adapter = {
toProviderRequest(input: GenerateRequest): ProviderRequest;
call(request: ProviderRequest): Promise<ProviderResponse>;
fromProviderResponse(response: ProviderResponse): GenerateResult;
};

接口不用复杂,边界要清楚。转换逻辑在适配器里,业务 API 只关心任务状态、输出和错误码。

适配层是在保护产品

模型会变,供应商会变,参数也会变。

没有适配层,这些变化会直接打到前端、文档和调用方。

好的 AI 网关不是把所有供应商包装成一个巨大透传代理,而是把不稳定的部分关在后端。

外部调用方只需要知道:这个模型能做什么,需要哪些参数,失败时得到什么错误。

这就是模型适配层的意义。不是为了写更多代码,而是为了让 API 长期稳定。