本文面向:想了解如何用统一接口对接多个 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 包)解决了这个问题。它提供了一套统一的 generateText、generateObject、embed 等 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) - 探索
streamText和streamObject实现流式输出 - 了解 AI SDK 的 Tool Calling 能力,让 LLM 调用外部函数