Vercel AI SDK 入门:一行代码切换 LLM Provider

本文面向:想了解如何用统一接口对接多个 LLM Provider 的开发者。 预计阅读时间:10 分钟 最终效果:理解 Vercel AI SDK 的 generateText / generateObject / embed 核心 API,掌握 Provider 工厂模式和 Zod Schema 结构化输出。


为什么需要统一抽象?

假设你正在开发一个 AI 应用,最开始用 Ollama 本地模型做原型,后来想切到 OpenAI 获得更好的效果,再后来又想试试 Anthropic 的 Claude。如果没有统一抽象,你需要:

  • 为每个 Provider 写不同的 API 调用代码
  • 处理每个 Provider 不同的请求/响应格式
  • 在切换时修改大量业务代码

Vercel AI SDK(ai 包)解决了这个问题。它提供了一套统一的 generateTextgenerateObjectembed 等 API,底层通过 Provider 适配器对接不同的模型服务。你的业务代码只需要写一次,切换 Provider 只需改配置。

ChatCrystal 项目支持 6 种 Provider------Ollama、OpenAI、Anthropic、Google、Azure、Custom------全部通过 Vercel AI SDK 统一调用。本文以 ChatCrystal 的实际代码为例,讲解 SDK 的核心用法。

核心 API 速览

Vercel AI SDK 提供三个核心函数,覆盖了最常见的 AI 调用场景:

1. generateText --- 生成文本

最基本的调用方式,给一个 prompt,返回文本结果:

typescript 复制代码
import { generateText } from 'ai';

const { text } = await generateText({
  model: getLanguageModel(),
  prompt: '用一句话解释什么是向量数据库',
});

2. generateObject --- 结构化输出

这是 AI SDK 最强大的能力之一。你用 Zod 定义一个 schema,LLM 会返回严格符合该结构的 JSON 对象,无需手动解析:

typescript 复制代码
import { generateObject } from 'ai';
import { z } from 'zod';

const schema = z.object({
  title: z.string().describe('简洁的标题,20字以内'),
  summary: z.string().describe('2-4 段 markdown 摘要'),
  key_conclusions: z.array(z.string()).describe('3-5 个关键结论'),
  tags: z.array(z.string()).describe('3-6 个小写英文标签'),
});

const { object } = await generateObject({
  model: getLanguageModel(),
  schema,
  system: '你是一个技术对话分析专家',
  prompt: transcript,
});
// object 的类型完全匹配 schema,TypeScript 自动推导

ChatCrystal 的对话摘要功能就是这么实现的。generateObject 内部会将 Zod schema 转换为 JSON Schema,注入到 prompt 中引导 LLM 输出结构化数据,然后自动校验返回结果。如果校验失败,SDK 会自动重试(默认 3 次)。

3. embed --- 生成向量嵌入

用于语义搜索的文本向量化:

typescript 复制代码
import { embed } from 'ai';

const { embedding } = await embed({ model, value: query });
// embedding 是 number[],维度取决于模型

ChatCrystal 在两个地方用到 embed

  • 笔记入库时:将笔记文本分块后逐块生成向量,存入 vectra 索引
  • 搜索查询时:将用户查询转为向量,在索引中做相似度检索

Provider 工厂模式

ChatCrystal 用工厂模式管理 6 种 Provider。核心是一个 ProviderEntry 接口:

typescript 复制代码
interface ProviderEntry {
  name: string;
  displayName: string;
  supportsEmbedding: boolean;
  requiresApiKey: boolean;
  requiresBaseURL: boolean;
  createLanguageModel(config: ProviderConfig): LanguageModel;
  createEmbeddingModel?(config: ProviderConfig): EmbeddingModel;
}

每个 Provider 注册到一个 Map 中:

typescript 复制代码
const providers = new Map<string, ProviderEntry>();

providers.set('ollama', { /* ... */ });
providers.set('openai', { /* ... */ });
providers.set('anthropic', { /* ... */ });
// ...

获取模型实例只需两步:

typescript 复制代码
function getLanguageModel(): LanguageModel {
  const { provider, ...config } = appConfig.llm;
  return getProvider(provider).createLanguageModel(config);
}

getProvider(name) 从 Map 中查找 Provider,调用其 createLanguageModel 方法返回一个 LanguageModel 实例。之后所有业务代码都通过这个实例调用 generateText / generateObject,完全不关心底层是哪个 Provider。

6 种 Provider 的实现细节

Ollama --- 本地推理

Ollama 没有官方的 Vercel AI SDK 适配器,但它提供 OpenAI 兼容的 /v1/ 端点。所以 ChatCrystal 用 @ai-sdk/openai 来对接:

typescript 复制代码
providers.set('ollama', {
  createLanguageModel({ baseURL, model }) {
    const url = baseURL || 'http://localhost:11434';
    const ollama = createOpenAI({
      baseURL: `${url}/v1`,
      apiKey: 'ollama',    // Ollama 不需要真实 key,填个占位值
      name: 'ollama',
    });
    return ollama(model);
  },
});

这个技巧非常实用:任何提供 OpenAI 兼容 API 的服务(比如 vLLM、LiteLLM、LocalAI)都可以用 createOpenAI 适配。apiKey 填一个占位字符串即可,因为本地服务不校验它。

OpenAI / Anthropic / Google --- 原生 SDK

这三个 Provider 各有官方适配器,用法几乎一致:

typescript 复制代码
// OpenAI
const openai = createOpenAI({ baseURL, apiKey });
return openai(model);

// Anthropic
const anthropic = createAnthropic({ apiKey });
return anthropic(model);

// Google
const google = createGoogleGenerativeAI({ apiKey });
return google(model);

注意 Anthropic 不支持 embedding(supportsEmbedding: false),所以如果你用 Claude 做摘要,embedding 还需要单独配一个支持的 Provider。

Azure --- 企业部署

Azure OpenAI 通过 @ai-sdk/azure 适配,需要提供 Azure 专属的 endpoint URL 和 API key:

typescript 复制代码
const azure = createAzure({ baseURL, apiKey });
return azure(model);

Custom --- 万能兜底

Custom Provider 和 Ollama 的思路一样,用 createOpenAI 对接任何 OpenAI 兼容的 API:

typescript 复制代码
providers.set('custom', {
  createLanguageModel({ baseURL, apiKey, model }) {
    const custom = createOpenAI({ baseURL, apiKey, name: 'custom' });
    return custom(model);
  },
});

这是最具扩展性的设计------用户不需要等我们新增 Provider 适配,只要服务兼容 OpenAI API 格式,填个 URL 和 key 就能用。

generateObject 的 Zod Schema 验证

generateObject 的结构化输出依赖 Zod schema。ChatCrystal 在两个场景中重度使用它:

场景一:对话摘要

typescript 复制代码
const SummarizeSchema = z.object({
  title: z.string(),
  summary: z.string(),
  key_conclusions: z.array(z.string()),
  code_snippets: z.array(z.object({
    language: z.string(),
    code: z.string(),
    description: z.string(),
  })),
  tags: z.array(z.string()),
});

const { object } = await generateObject({
  model: getLanguageModel(),
  schema: SummarizeSchema,
  system: SYSTEM_PROMPT,
  prompt: transcript,
  maxOutputTokens: 4096,
  maxRetries: 3,
});

.describe() 方法可以给每个字段加上自然语言描述,这些描述会被注入到 prompt 中,帮助 LLM 理解每个字段的期望内容。

场景二:笔记关系发现

typescript 复制代码
const RelationElementSchema = z.object({
  target_note_id: z.number().describe('目标笔记的 ID'),
  relation_type: z.enum([
    'CAUSED_BY', 'LEADS_TO', 'RESOLVED_BY', 'SIMILAR_TO',
    'CONTRADICTS', 'DEPENDS_ON', 'EXTENDS', 'REFERENCES',
  ]).describe('关系类型'),
  confidence: z.number().min(0).max(1).describe('置信度,0.0-1.0'),
  description: z.string().describe('简短说明关系,20字以内'),
});

const { object: rawRelations } = await generateObject({
  model: getLanguageModel(),
  output: 'array',    // 输出是一个数组
  schema: RelationElementSchema,
  system: RELATION_SYSTEM_PROMPT,
  prompt,
});

这里用了 output: 'array' 参数,告诉 SDK 返回一个数组而非单个对象。配合 z.enum() 可以限制字段的合法取值范围------如果 LLM 返回了不在枚举中的关系类型,SDK 会自动重试。

配置驱动的 Provider 切换

ChatCrystal 的配置结构非常直观:

typescript 复制代码
// config.json
{
  "llm": {
    "provider": "openai",
    "baseURL": "",
    "apiKey": "sk-...",
    "model": "gpt-4o"
  },
  "embedding": {
    "provider": "ollama",
    "baseURL": "http://localhost:11434",
    "apiKey": "",
    "model": "nomic-embed-text"
  }
}

LLM 和 Embedding 是独立配置的。你可以用 OpenAI 做摘要、Ollama 做 embedding,反之亦然。切换 Provider 只需要改 provider 字段,业务代码零修改。

这就是抽象层的价值------generateObject({ model, ... }) 这行代码不关心 model 是 GPT-4o、Claude Sonnet 还是 Qwen2.5,SDK 帮你处理了所有差异。

错误处理:自动重试

AI SDK 内置了重试机制。maxRetries 参数控制失败后的重试次数:

typescript 复制代码
const { object } = await generateObject({
  model: getLanguageModel(),
  schema: SummarizeSchema,
  system: SYSTEM_PROMPT,
  prompt: transcript,
  maxRetries: 3,  // 失败后最多重试 3 次
});

ChatCrystal 在关系发现中设 maxRetries: 2,在摘要中设 maxRetries: 3。重试对处理 429(Rate Limit)和 5xx 错误特别有用。此外,ChatCrystal 在队列层还做了并发控制(concurrency=1)和 1 req/sec 的限速,与 SDK 的重试机制形成双层保护。

总结

Vercel AI SDK 的核心设计理念是抽象差异、统一接口

概念 作用
generateText 生成自由文本
generateObject 生成结构化 JSON,Zod schema 校验
embed 生成向量嵌入
Provider 适配器 createOpenAI / createAnthropic / createGoogleGenerativeAI
LanguageModel 统一的模型实例,所有 API 函数的第一个参数
maxRetries 自动重试,处理瞬时错误

对于学生来说,理解这个设计模式比记住具体 API 更重要:当你需要对接多个外部服务时,定义一个统一接口,让每个服务实现自己的适配器,业务代码只依赖接口而不依赖具体实现。这就是 Strategy 模式在 AI 领域的典型应用。

下一步

  • 阅读 Vercel AI SDK 官方文档 了解更多 API 细节
  • 试试用 createOpenAI 对接一个兼容 OpenAI 的本地服务(如 vLLM、LM Studio)
  • 探索 streamTextstreamObject 实现流式输出
  • 了解 AI SDK 的 Tool Calling 能力,让 LLM 调用外部函数

项目地址:github.com/ZengLiangYi...

相关推荐
三乐2282 小时前
原型链是什么?五分钟教会你
javascript
用户5191495848452 小时前
WordPress ACF City Selector 插件任意文件上传漏洞利用工具 (CVE-2024-56264)
人工智能·aigc
ZengLiangYi2 小时前
Electron 入门:Web 应用打包成桌面软件
前端·electron
前端环境观察室3 小时前
别再靠人工记浏览器环境了:用 TypeScript 设计一套可审计模型
前端
Delicate3 小时前
彻底搞懂JS原型:_ _ proto _ _与prototype的区别到底在哪?
javascript
鱼樱前端3 小时前
我做了一个不止有基础组件的 Vue 3 UI 库,还把 AI 组件也做进去了
前端·vue.js·ai编程
用户5191495848453 小时前
WordPress WPMasterToolkit 插件漏洞检测与利用工具
人工智能·aigc
泥秋哥3 小时前
微前端-Module Federation运行时工具
前端·架构
小黑蛋9123 小时前
Nacos 集群部署方案
前端