统一 API 接口
本节定位
一旦你的系统不只接一个模型,问题很快就会冒出来:
- 不同 provider 的参数名不一样
- 返回结构不一样
- 错误处理不一样
这时真正有价值的就不是“再接一个模型”,而是:
先把模型调用入口统一起来。
学习目标
- 理解为什么多模型系统需要统一 API 层
- 理解统一 API 接口在工程上到底省了什么
- 看懂一个最小 provider 抽象示例
- 明白统一 API 不等于“所有模型都完全一样”
先建立一张地图
如果你已经学过本地模型运行和推理服务,这一节最自然的续接就是:
- 前面你已经知道模型怎样被加载和服务化
- 这一节开始回答:一旦系统接多个模型 / 多个 provider,怎样不让上层业务代码变乱
所以统一 API 这一节真正重要的不是“再包一层接口”,而是:
- 给多模型系统建立一个稳定入口层
统一 API 这节最适合新人的理解顺序不是“再包一层接口”,而是 先看清:
所以这节真正想解决的是:
- 为什么多模型系统一定会自然长出一层抽象
- 为什么业务代码不该到处知道 provider 差异
一个更适合新人的总类比
你可以把统一 API 理解成:
- 给很多不同品牌的插头做一个统一转接头
如果没有这层转接头,
上层业务代码就会变成:
- 这里适配 A 厂商
- 那里适配 B 厂商
- 再另一处适配本地模型
最后系统会越来越碎。
统一 API 最重要的价值,就是把这些差异收拢在一层里。
一、为什么统一 API 会变得重要?
1.1 当你只有一个模型时,还不明显
如果你的项目里只有一个模型,一个简单 client 往往就够了。
1.2 一旦开始多模型 / 多 provider
你就会面对这些问题:
- A 模型叫
messages - B 模型叫
prompt - 有的返回
content - 有的返回
output_text - 有的 token 统计字段也不同
这时业务代码会迅速变得很乱。
所以统一 API 的核心价值可以先记成:
把 provider 差异收拢到一层,而不是让业务层到处知道这些差异。
1.3 第一次学统一 API,最该先抓住什么?
最该先抓住的不是“抽象得多漂亮”,而是这句:
统一 API 的核心价值,是把模型差异隔离掉,让业务层面对稳定接口。
这句话一旦稳住,后面你看:
- provider 适配
- 路由
- fallback
- 统一日志
都会更自然地知道它们为什么会长在这一层。
二、统一 API 最常见的目标是什么?
通常至 少包括:
- 统一请求结构
- 统一响应结构
- 统一错误处理
- 统一日志与 trace
2.1 一个最小统一请求结构
request = {
"provider": "demo_provider",
"model": "demo-chat-model",
"query": "退款政策是什么?"
}
print(request)
2.2 一个最小统一响应结构
response = {
"provider": "demo_provider",
"model": "demo-chat-model",
"answer": "课程购买后 7 天内且学习进度低于 20% 可退款。",
"usage": {
"prompt_tokens": 24,
"completion_tokens": 18
}
}
print(response)
这样做的好处是:
- 上层业务逻辑只面对一套稳定结构
2.3 一个很适合初学者先记的统一表
| 层 | 这一层最该统一什么 |
|---|---|
| 请求 | query / model / provider / 参数格式 |
| 响应 | answer / usage / error |
| 日志 | trace_id / provider / latency / token |
| 错误 | error_code / message / retryable |
这个表很适合新人,因为它能把“统一 API”从一个抽象名词重新拉回成几类可见对象。
三、一个最小 provider 抽象示例
class ProviderA:
def chat(self, query, model):
return {
"text": f"A-provider reply: {query}",
"tokens": 30
}
class ProviderB:
def generate(self, prompt, model_name):
return {
"output_text": f"B-provider reply: {prompt}",
"usage": {"total_tokens": 28}
}
如果你直接让业务代码分别去调这两个 provider,代码会越来越碎。
四、统一适配层到底在做什么?
4.1 把不同 provider 翻译成同一种结构
class UnifiedClient:
def __init__(self):
self.providers = {
"provider_a": ProviderA(),
"provider_b": ProviderB()
}
def chat(self, provider, query, model):
if provider == "provider_a":
raw = self.providers[provider].chat(query=query, model=model)
return {
"provider": provider,
"model": model,
"answer": raw["text"],
"usage": {"total_tokens": raw["tokens"]}
}
if provider == "provider_b":
raw = self.providers[provider].generate(prompt=query, model_name=model)
return {
"provider": provider,
"model": model,
"answer": raw["output_text"],
"usage": raw["usage"]
}
return {"error": "unknown_provider"}
client = UnifiedClient()
print(client.chat("provider_a", "退款政策是什么?", "demo-1"))
print(client.chat("provider_b", "退款政策是什么?", "demo-2"))
4.2 这段代码真正重要的不是语法,而是分层
它在告诉你:
- provider 差异应该尽量收拢在统一适配层
- 上层业务代码最好只看到统一接口
这就是“统一 API”最实际的工程价值。
4.3 为什么这层特别适合承担日志、统计和路由?
因为它天然站在“所有请求都会经过”的入口位置。
所以像这些能力都很适合长在这里:
- token / 成本统计
- trace 和日志
- provider fallback
- 模型路由
4.4 再看一个最小“统一错误结构”示例
def normalize_error(provider, error_type, message):
return {
"provider": provider,
"ok": False,
"error": {
"type": error_type,
"message": message,
"retryable": error_type in {"timeout", "rate_limit"},
},
}
print(normalize_error("provider_a", "timeout", "request timed out"))
这个示例很适合初学者,因为它会帮助你意识到:
- 真正难维护的常常不是成功返回
- 而是不同 provider 出错时怎么还能对上层保持同一种契约