章节02 / 14
  1. 01先把 AI 应用当成系统,而不是聊天框
  2. 02打通第一个多 Provider 模型请求
  3. 03把 Prompt 写成任务协议
  4. 04结构化输出不是格式化,而是业务门禁
  5. 05把等待时间拆成事件:流式响应实战
  6. 06Tool Calling:模型提出动作,应用执行动作
  7. 07有副作用的工具要先设计刹车
  8. 08RAG 的第一性问题:答案从哪里来
  9. 09让 RAG 回答经得起追问
  10. 10长上下文、会话状态与记忆压缩
  11. 11Agent 工作流要像状态机一样可恢复
  12. 12评测工程:用样例集防止应用退化
  13. 13可观测性与安全:线上问题要能被看见
  14. 14上线不是结束:路由、灰度、回滚和治理复盘
本文目录12
  1. 隔离模型差异:打通第一个多 Provider 请求
  2. 为什么不应该把原生大模型 SDK 直接引入业务逻辑
  3. 拆解主流大模型 API 的请求与响应结构差异
  4. 1. 系统提示词(System Prompt)的传递位置
  5. 2. 消息内容(Message Content)的多模态表达
  6. 3. 结束标识(Stop Reason)的归一化
  7. 动手实现:用原生 TypeScript 构建多 Provider 统一适配层
  8. 使用 Vercel AI SDK 简化多模型路由抽象
  9. 统一的调用示例
  10. 错误处理与边界情况:识别不同 API 的失败模式
  11. 1. 限流(Rate Limit / HTTP 429)
  12. 2. 鉴权失败(Unauthorized / HTTP 401)
02

打通第一个多 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.contentresponse.content[0].text)。

本指南将带你拆解主流大模型 API 的请求与响应结构,并通过手写适配层与引入 Vercel AI SDK 两种方式,实现模型请求的最小闭环与归一化设计。


为什么不应该把原生大模型 SDK 直接引入业务逻辑

本节操作锚点:围绕“为什么不应该把原生大模型SDK直接引入业务逻辑”记录步骤、样例、诊断、风险、检查清单和验收结果。

在多模型共存的业务场景中,强耦合官方 SDK 会带来三大核心痛点:

  1. 数据结构不兼容:不同厂商对“消息”和“响应”的定义存在命名冲突。系统提示词、工具调用、结束状态等核心实体的表述各不相同。
  2. 异常捕获困难:每个 SDK 都有自己定义的 Error 类型与状态码,业务层很难用一套标准捕获诸如“限流(Rate Limit)”或“上下文超限(Context Window Exceeded)”等标准异常。
  3. 阻碍自动化评测与监控:如果请求调用散落在各处,你就无法在统一的入口拦截输入和输出,从而难以接入可观测性工具或进行离线评测。

如果要在生产环境中支持多模型灾备,应该在底层适配器中完成响应格式的归一化,而不是在业务层写满 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 上限值工具调用触发值
OpenAIfinish_reasonstoplengthtool_calls
Anthropicstop_reasonend_turnmax_tokenstool_use
GeminifinishReasonSTOPMAX_TOKENSTOOL_CALL

动手实现:用原生 TypeScript 构建多 Provider 统一适配层

本节操作锚点:围绕“动手实现:用原生TypeScript构建多Pro”记录步骤、样例、诊断、风险、检查清单和验收结果。

下面我们通过编写一段轻量级的 TypeScript 代码,手动抹平 OpenAI 与 Anthropic 的 API 差异。这种做法不依赖任何复杂的第三方框架,适合对包体积有极致要求的轻量级后端服务。

首先定义统一的接口契约:

typescript
// 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; // 保留原始响应以备特殊调试
}

接着实现统一的客户端适配器:

typescript
// 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 接口,抹平了底层的差异。

统一的调用示例

首先,安装核心依赖:

bash
npm install ai @ai-sdk/openai @ai-sdk/anthropic

编写统一的模型生成服务:

typescript
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.contentcontent[0].texttext
提示词 Tokenusage.prompt_tokensusage.input_tokensusage.promptTokens
输出 Tokenusage.completion_tokensusage.output_tokensusage.completionTokens
结束标识choices[0].finish_reasonstop_reasonfinishReason
顶层 System Prompt置于 messages 数组首位作为顶层参数 system作为 generateTextsystem 参数

2. 自动化自测脚本(TypeScript)

为了验证你的适配器是否工作正常,请将以下代码保存为 test-client.ts。在配置好环境变量后,通过 ts-node test-client.ts 运行,检查是否输出了标准的归一化结构。

typescript
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();

验收标准

  1. 无论调用哪个 Provider,终端输出的 usage.totalTokens 必须是一个明确的数字,不能为 undefined
  2. 结束原因必须映射为统一的 '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 变化极快,如遇到以下事件,开发团队需要重新评估并复核本单元的代码逻辑:

  1. OpenAI 废弃或彻底修改底层 Chat Completions 终结点:例如其推荐全面转向更具编排能力的 Responses API 后,需确认参数字典是否发生字段重构。
  2. Anthropic 变更 API 强制请求头:例如 anthropic-version 强制升级至更高版本,导致旧版 Message 格式不被受理。
  3. Vercel AI SDK 发生破坏性(Major)版本更新:导致其导出的 generateTextLanguageModel 或 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。