章节02 / 14
本文目录12 节
- 隔离模型差异:打通第一个多 Provider 请求
- 为什么不应该把原生大模型 SDK 直接引入业务逻辑
- 拆解主流大模型 API 的请求与响应结构差异
- 1. 系统提示词(System Prompt)的传递位置
- 2. 消息内容(Message Content)的多模态表达
- 3. 结束标识(Stop Reason)的归一化
- 动手实现:用原生 TypeScript 构建多 Provider 统一适配层
- 使用 Vercel AI SDK 简化多模型路由抽象
- 统一的调用示例
- 错误处理与边界情况:识别不同 API 的失败模式
- 1. 限流(Rate Limit / HTTP 429)
- 2. 鉴权失败(Unauthorized / HTTP 401)
打通第一个多 Provider 模型请求
掌握主流 AI API (OpenAI、Claude、Gemini) 的请求与响应结构差异,并通过手写适配层与引入 Vercel AI SDK 两种方式,实现多 Provider 的调用归一化,将平台差异隔离在业务逻辑之外。
- 理解基础的 HTTP 请求与 TypeScript 类型定义
- 已获取至少一个大模型平台的 API Key
- 能独立分析并归一化 OpenAI、Anthropic 和 Gemini 的 API 响应差异
- 构建一个可插拔、防污染的 TypeScript 多 Provider 调用适配层
- 使用 Vercel AI SDK 快速切换底层模型并处理流式输出
隔离模型差异:打通第一个多 Provider 请求
在开发上线 AI 应用时,最容易犯的直觉性错误是:直接在业务逻辑里导入特定厂商的官方 SDK。这种做法会迅速产生技术债。当业务需要因成本、速度或生成质量在 OpenAI、Claude 和 Gemini 之间切换,或者需要设计多模型灾备降级方案时,你会发现业务代码里散落着大量的平台特异性字段(例如 response.choices[0].message.content 或 response.content[0].text)。
本指南将带你拆解主流大模型 API 的请求与响应结构,并通过手写适配层与引入 Vercel AI SDK 两种方式,实现模型请求的最小闭环与归一化设计。
为什么不应该把原生大模型 SDK 直接引入业务逻辑
本节操作锚点:围绕“为什么不应该把原生大模型SDK直接引入业务逻辑”记录步骤、样例、诊断、风险、检查清单和验收结果。
在多模型共存的业务场景中,强耦合官方 SDK 会带来三大核心痛点:
- 数据结构不兼容:不同厂商对“消息”和“响应”的定义存在命名冲突。系统提示词、工具调用、结束状态等核心实体的表述各不相同。
- 异常捕获困难:每个 SDK 都有自己定义的 Error 类型与状态码,业务层很难用一套标准捕获诸如“限流(Rate Limit)”或“上下文超限(Context Window Exceeded)”等标准异常。
- 阻碍自动化评测与监控:如果请求调用散落在各处,你就无法在统一的入口拦截输入和输出,从而难以接入可观测性工具或进行离线评测。
如果要在生产环境中支持多模型灾备,应该在底层适配器中完成响应格式的归一化,而不是在业务层写满 if-else 分支,因为这会导致后续添加新模型时业务逻辑异常脆弱。
拆解主流大模型 API 的请求与响应结构差异
本节操作锚点:围绕“拆解主流大模型API的请求与响应结构差异”记录步骤、样例、诊断、风险、检查清单和验收结果。
要实现适配层,首先需要看清 OpenAI、Anthropic 和 Google Gemini 在 API 设计上的核心分歧。
1. 系统提示词(System Prompt)的传递位置
- OpenAI:根据 OpenAI Responses API(
openai-responses-api)及模型的典型设计,系统提示词是作为一个普通的消息对象,置于messages数组的首位,角色为system。 - Anthropic:根据 Anthropic Messages API(
anthropic-messages-api)规范,Claude 的消息系统将system提示词作为顶级参数,而非混入messages数组中。如果向 Anthropic 发送包含首个system角色的消息数组,API 会直接抛出 400 错误。 - Gemini:根据 Google Gemini API 规范,系统提示词被封装在
systemInstruction参数中,同样作为顶级的配置项。
2. 消息内容(Message Content)的多模态表达
- OpenAI:支持纯文本字符串,也支持由类型对象(
type: 'text' | 'image_url')组成的数组。 - Anthropic:消息内容必须是块级数组(Content Blocks),即使只有纯文本,其底层也表现为
[{ "type": "text", "text": "..." }]。
3. 结束标识(Stop Reason)的归一化
当模型生成结束时,各家返回的“结束原因”字段与枚举值完全不同:
| 平台 | 结束原因字段 | 正常结束值 | 达到 Token 上限值 | 工具调用触发值 |
|---|---|---|---|---|
| OpenAI | finish_reason | stop | length | tool_calls |
| Anthropic | stop_reason | end_turn | max_tokens | tool_use |
| Gemini | finishReason | STOP | MAX_TOKENS | TOOL_CALL |
动手实现:用原生 TypeScript 构建多 Provider 统一适配层
本节操作锚点:围绕“动手实现:用原生TypeScript构建多Pro”记录步骤、样例、诊断、风险、检查清单和验收结果。
下面我们通过编写一段轻量级的 TypeScript 代码,手动抹平 OpenAI 与 Anthropic 的 API 差异。这种做法不依赖任何复杂的第三方框架,适合对包体积有极致要求的轻量级后端服务。
首先定义统一的接口契约:
// types.ts
export type Role = 'system' | 'user' | 'assistant';
export interface ChatMessage {
role: Role;
content: string;
}
export interface ChatOptions {
model: string;
messages: ChatMessage[];
temperature?: number;
maxTokens?: number;
}
export interface NormalizedResponse {
text: string;
usage: {
promptTokens: number;
completionTokens: number;
totalTokens: number;
};
stopReason: 'stop' | 'length' | 'tool_calls' | 'unknown';
raw: any; // 保留原始响应以备特殊调试
}
接着实现统一的客户端适配器:
// client.ts
import { ChatOptions, NormalizedResponse, ChatMessage } from './types';
export interface ModelProvider {
chatComplete(options: ChatOptions): Promise<NormalizedResponse>;
}
// OpenAI 适配器
export class OpenAIProvider implements ModelProvider {
private apiKey: string;
private baseUrl: string;
constructor(apiKey: string, baseUrl = 'https://api.openai.com/v1') {
this.apiKey = apiKey;
this.baseUrl = baseUrl;
}
async chatComplete(options: ChatOptions): Promise<NormalizedResponse> {
// 根据 OpenAI Responses API 设计,直接将所有消息投递给 messages 数组
const response = await fetch(`${this.baseUrl}/chat/completions`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${this.apiKey}`
},
body: JSON.stringify({
model: options.model,
messages: options.messages,
temperature: options.temperature ?? 0.7,
max_tokens: options.maxTokens
})
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(`OpenAI API Error: [${response.status}] ${JSON.stringify(errorData)}`);
}
const data = await response.json();
const choice = data.choices[0];
// 映射 Stop Reason
let stopReason: NormalizedResponse['stopReason'] = 'unknown';
if (choice.finish_reason === 'stop') stopReason = 'stop';
else if (choice.finish_reason === 'length') stopReason = 'length';
else if (choice.finish_reason === 'tool_calls') stopReason = 'tool_calls';
return {
text: choice.message.content || '',
usage: {
promptTokens: data.usage?.prompt_tokens || 0,
completionTokens: data.usage?.completion_tokens || 0,
totalTokens: data.usage?.total_tokens || 0
},
stopReason,
raw: data
};
}
}
// Anthropic Claude 适配器
export class AnthropicProvider implements ModelProvider {
private apiKey: string;
private baseUrl: string;
constructor(apiKey: string, baseUrl = 'https://api.anthropic.com/v1') {
this.apiKey = apiKey;
this.baseUrl = baseUrl;
}
async chatComplete(options: ChatOptions): Promise<NormalizedResponse> {
// 教学判断:参考 Anthropic Messages API 规范,system 必须作为顶级参数提取,不能存在于 messages 数组内
const systemMessage = options.messages.find(msg => msg.role === 'system');
const filteredMessages = options.messages.filter(msg => msg.role !== 'system');
const response = await fetch(`${this.baseUrl}/messages`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-api-key': this.apiKey,
'anthropic-version': '2023-06-01' // 截至 2026-05-28 的标准 API 版本
},
body: JSON.stringify({
model: options.model,
system: systemMessage ? systemMessage.content : undefined,
messages: filteredMessages.map(msg => ({
role: msg.role === 'system' ? 'user' : msg.role, // 防御性转换
content: msg.content
})),
max_tokens: options.maxTokens ?? 1024,
temperature: options.temperature ?? 0.7
})
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(`Anthropic API Error: [${response.status}] ${JSON.stringify(errorData)}`);
}
const data = await response.json();
let stopReason: NormalizedResponse['stopReason'] = 'unknown';
if (data.stop_reason === 'end_turn') stopReason = 'stop';
else if (data.stop_reason === 'max_tokens') stopReason = 'length';
else if (data.stop_reason === 'tool_use') stopReason = 'tool_calls';
return {
text: data.content[0]?.text || '',
usage: {
promptTokens: data.usage?.input_tokens || 0,
completionTokens: data.usage?.output_tokens || 0,
totalTokens: (data.usage?.input_tokens || 0) + (data.usage?.output_tokens || 0)
},
stopReason,
raw: data
};
}
}
使用 Vercel AI SDK 简化多模型路由抽象
本节操作锚点:围绕“使用VercelAISDK简化多模型路由抽象”记录步骤、样例、诊断、风险、检查清单和验收结果。
手写适配器虽然能让我们彻底看清底层差异,但在实际的中大型项目中,随着 Tool Calling、流式渲染(Streaming)和结构化输出(Object Generation)的加入,手写维护成本会呈指数级上升。
根据 Vercel AI SDK Core 概述(vercel-ai-sdk-overview),该 SDK 专门为了抹平 TypeScript 应用中的 Provider 差异而设计。如果项目需要频繁在不同云厂商的底座间切换,可以优先采用 Vercel AI SDK Core 作为核心抽象,因为其统一了 generateText 接口,抹平了底层的差异。
统一的调用示例
首先,安装核心依赖:
npm install ai @ai-sdk/openai @ai-sdk/anthropic
编写统一的模型生成服务:
import { generateText, LanguageModel } from 'ai';
import { openai } from '@ai-sdk/openai';
import { anthropic } from '@ai-sdk/anthropic';
export async function askModel(
provider: 'openai' | 'anthropic',
modelName: string,
prompt: string
) {
// 1. 根据传入参数动态选择 Model Instance
let modelInstance: LanguageModel;
if (provider === 'openai') {
modelInstance = openai(modelName);
} else {
modelInstance = anthropic(modelName);
}
// 2. 统一调用 generateText
const { text, finishReason, usage } = await generateText({
model: modelInstance,
system: '你是一个专业的资深技术架构师。',
prompt: prompt,
temperature: 0.3,
});
return {
text,
finishReason, // 自动归一化为 'stop' | 'length' | 'content-filter' | 'tool-calls' | 'error' | 'other'
tokens: usage.totalTokens
};
}
错误处理与边界情况:识别不同 API 的失败模式
本节操作锚点:围绕“错误处理与边界情况:识别不同API的失败模式”记录步骤、样例、诊断、风险、检查清单和验收结果。
当 API 请求失败时,不要只做通用的 try...catch(e) { throw e }。必须针对高频发生的边界状态进行精确识别并编写健壮的代码。
1. 限流(Rate Limit / HTTP 429)
- 表现:各家 API 在达到 QPM (Requests Per Minute) 或 TPM (Tokens Per Minute) 限制时,均会返回 429 状态码。
- 处理手段:除非遇到特定的超低延迟场景,否则不要跳过 SDK 的内置重试机制而自己手写复杂的轮询,因为不规范的并发请求极易触发各大 Provider 的 Rate Limit。如果使用 Vercel AI SDK,其内置了基于指数退避算法的自动重试机制;若是手写 Fetch,必须在拦截到 429 状态码时读取
Retry-After响应头,并延迟重试。
2. 鉴权失败(Unauthorized / HTTP 401)
- 表现:API Key 填错、过期或余额不足。
- 处理手段:在适配层捕获到 401 时,必须立即熔断对该通道的请求,并抛出明确的 fatal 异常,切勿进行无意义的自动重试,因为这只会浪费计算资源并阻塞后续的任务路由。
3. 上下文超限(Context Window Exceeded / HTTP 400)
- 表现:单次请求的输入(含 System Prompt、历史 Message、Tool 定义)加上最大生成长度超过了模型所支持的最大 Token 数量限制。
- 处理手段:通过精确的 Token 计数器(如
tiktoken库)在客户端预估大小,并在发送前对历史对话进行截断或滑动窗口裁剪。
本课交付物:可插拔的模型调用客户端与响应归一化参考表
本节操作锚点:围绕“本课交付物:可插拔的模型调用客户端与响应归一化参”记录步骤、样例、诊断、风险、检查清单和验收结果。
1. 响应字段归一化对照表
在实现多 Provider 结构时,以下表格是开发适配层的重要依据:
| 统一属性 | OpenAI (Chat Completions) | Anthropic (Messages) | Vercel AI SDK Core |
|---|---|---|---|
| 主体内容 | choices[0].message.content | content[0].text | text |
| 提示词 Token | usage.prompt_tokens | usage.input_tokens | usage.promptTokens |
| 输出 Token | usage.completion_tokens | usage.output_tokens | usage.completionTokens |
| 结束标识 | choices[0].finish_reason | stop_reason | finishReason |
| 顶层 System Prompt | 置于 messages 数组首位 | 作为顶层参数 system | 作为 generateText 的 system 参数 |
2. 自动化自测脚本(TypeScript)
为了验证你的适配器是否工作正常,请将以下代码保存为 test-client.ts。在配置好环境变量后,通过 ts-node test-client.ts 运行,检查是否输出了标准的归一化结构。
import { OpenAIProvider, AnthropicProvider } from './client'; // 引入上面手写的类
import { ChatMessage } from './types';
async function runDiagnostic() {
const testMessages: ChatMessage[] = [
{ role: 'system', content: '你是一个测试助手。请只输出数字 1' },
{ role: 'user', content: '请输出对应的测试数字。' }
];
const openAIKey = process.env.OPENAI_API_KEY;
const anthropicKey = process.env.ANTHROPIC_API_KEY;
console.log('--- 开始多 Provider 调用诊断 ---');
if (openAIKey) {
try {
const openaiClient = new OpenAIProvider(openAIKey);
// 根据 OpenAI Models 官方文档,选用标准的 gpt-4o-mini 进行低成本测试
const res = await openaiClient.chatComplete({
model: 'gpt-4o-mini',
messages: testMessages,
maxTokens: 10
});
console.log('✅ OpenAI 适配器测试通过:');
console.log(` 输出: "${res.text}" | 消耗 Token: ${res.usage.totalTokens} | 结束原因: ${res.stopReason}`);
} catch (e: any) {
console.error('❌ OpenAI 适配器测试失败:', e.message);
}
} else {
console.log('⚠️ 未检测到 OPENAI_API_KEY,跳过 OpenAI 测试。');
}
if (anthropicKey) {
try {
const anthropicClient = new AnthropicProvider(anthropicKey);
const res = await anthropicClient.chatComplete({
model: 'claude-3-5-sonnet-20241022',
messages: testMessages,
maxTokens: 10
});
console.log('✅ Anthropic 适配器测试通过:');
console.log(` 输出: "${res.text}" | 消耗 Token: ${res.usage.totalTokens} | 结束原因: ${res.stopReason}`);
} catch (e: any) {
console.error('❌ Anthropic 适配器测试失败:', e.message);
}
} else {
console.log('⚠️ 未检测到 ANTHROPIC_API_KEY,跳过 Anthropic 测试。');
}
}
runDiagnostic();
验收标准:
- 无论调用哪个 Provider,终端输出的
usage.totalTokens必须是一个明确的数字,不能为undefined。 - 结束原因必须映射为统一的
'stop' | 'length' | 'tool_calls' | 'unknown'枚举。
时效性评估与 API 变更复核指南
本节操作锚点:围绕“时效性评估与API变更复核指南”记录步骤、样例、诊断、风险、检查清单和验收结果。
关键来源与访问依据
本指南内容设计基于以下各官方平台截至 2026-05-28 的最新技术文档:
- OpenAI (Responses API & Models): 依据官方最新
Responses API的规范,该规范优化了模型的请求处理,确立了模型族和 token 计费标准的最新基线。 - Anthropic (Messages API): 确立了 Claude 3/3.5 消息结构的顶级
system字段标准,以及特定的stop_reason枚举。 - Google (Gemini Models): 确立了多模态模型命名(如 Flash 家族)和系统指令顶级封装标准。
- Vercel AI SDK Core: 确立了主流 TypeScript 抽象框架在处理多模型接口解耦、统一
generateText签名时的具体行为。
未来需要复核的触发条件
大模型厂商的 API 变化极快,如遇到以下事件,开发团队需要重新评估并复核本单元的代码逻辑:
- OpenAI 废弃或彻底修改底层 Chat Completions 终结点:例如其推荐全面转向更具编排能力的
Responses API后,需确认参数字典是否发生字段重构。 - Anthropic 变更 API 强制请求头:例如
anthropic-version强制升级至更高版本,导致旧版 Message 格式不被受理。 - Vercel AI SDK 发生破坏性(Major)版本更新:导致其导出的
generateText、LanguageModel或 Provider 插件包初始化签名失效。
打通第一个多Provider模型请求:把判断写成可复查证据
这一课容易被读成原则,但真正要交付的是一份能被别人复做的记录。本课交付物是 一个可替换 provider 的最小 TypeScript 调用结构和响应归一化表,它要服务于 AI 应用 的下一步,而不是只证明你读过这一节。
如果你现在还没有真实输入,先用一个最小样例完成 打通第一个多Provider模型请求,因为没有输入就无法判断产物是否可用。 如果本课产物会影响后续交付,应该写明适用条件和不适用条件,否则下一课会把不确定性误当成基础。 如果检查结果只剩一句“看起来可以”,必须补一条反例或失败样例,只有能解释失败原因时,这个判断才站得住。
排查顺序建议从最可观察的症状开始:结果是否复现、记录是否完整、失败是否能定位到一步动作。围绕 打通第一个多Provider模型请求 做检查时,至少保留步骤、样例、风险、修复和验收五项。
OpenAI 的《Create a model response》说明:支撑 Responses API 当前响应创建入口、输入输出、工具、流式和应用侧编排边界。;这意味着 打通第一个多Provider模型 不能只写经验结论,要把来源变成检查动作。OpenAI 的《Models》提醒:支撑模型能力、适用场景、模型族、上下文和模型选择。;因此本课方案必须写清边界。Anthropic 的《Anthropic Messages API reference》提供的证据是:支撑 Claude 消息结构、system、tool_use、stop_reason 和多模型接口差异。;所以当前结论按 2026-05-28 的来源状态使用。
复核触发条件要写具体:如果官方 API/SDK、软件工具版本、版权政策、安全合规要求、行业规范、平台发布规则或关键来源页面发生变化,需要重新检查 打通第一个多Provider模型 的步骤、样例、风险和验收清单。
练习验收:把 打通第一个多Provider模型请求 应用到一个自己的任务,交付一份包含输入、步骤、样例、失败诊断、来源证据和复测结果的记录。缺少其中任何一项,都先标记为 needs_review。