本文面向:想用一套代码支持多个 LLM / Embedding 服务商的 AI 应用开发者。
预计阅读时间:12 分钟
最终效果:理解 Vercel AI SDK 的统一 API 层与 ChatCrystal 的 Provider 工厂模式,掌握 generateText / generateObject / embed 与多 Provider 切换的完整方案。
一个真实的困境
开发 AI 应用时,你迟早会遇到这个问题:换模型。
最初用 Ollama 本地跑通原型,成本为零。上线后发现本地模型效果不够,切到 OpenAI。用了一阵子想试试 Claude 的长上下文能力,又想换 Anthropic。每次切换,你都要:
- 学习新的 API 格式(OpenAI 的
/v1/chat/completions、Anthropic 的/v1/messages、Google 的generateContent) - 处理不同的认证方式(API Key、OAuth、Bearer Token)
- 适配不同的响应结构(choices[0].message vs content[0].text)
- 处理不同的流式协议(SSE 格式差异、chunk 结构差异)
这不是技术难度的问题,而是重复劳动的问题。每个 Provider 的 API 本质上做同一件事——发送 prompt、接收回复——但接口设计各不相同。
Vercel AI SDK(ai 包)解决了这个问题。它提供了一层统一抽象,让你的业务代码只写一次,底层 Provider 随时可切换。
ChatCrystal 的 Provider 矩阵
ChatCrystal 支持 6 种 Provider,覆盖了从本地到云端的完整场景:
| Provider | 类型 | 语言模型 | Embedding 模型 | 认证 |
|---|---|---|---|---|
| Ollama | 本地推理 | 支持 | 支持 | 无需 API Key |
| OpenAI | 云端 | 支持 | 支持 | API Key |
| Anthropic | 云端 | 支持 | 不支持 | API Key |
| 云端 | 支持 | 支持 | API Key | |
| Azure | 云端 | 支持 | 支持 | API Key + Endpoint |
| Custom | OpenAI 兼容 | 支持 | 支持 | API Key + Base URL |
注意 Anthropic 不支持 Embedding 模型——这是由模型服务商的能力决定的,不是 SDK 的限制。
Provider 工厂模式
ChatCrystal 的 Provider 注册集中在 server/src/services/providers.ts。核心设计是一个 Map<string, ProviderEntry>,每个 Provider 实现统一的接口:
interface ProviderEntry {
name: string;
displayName: string;
supportsEmbedding: boolean;
requiresApiKey: boolean;
requiresBaseURL: boolean;
createLanguageModel(config: ProviderConfig): LanguageModel;
createEmbeddingModel?(config: ProviderConfig): EmbeddingModel;
}
每个 Provider 注册时提供两个工厂函数:createLanguageModel 和 createEmbeddingModel。调用方不需要知道底层用的是哪个 SDK——只需要拿到一个 LanguageModel 或 EmbeddingModel 实例。
以 Ollama 为例:
providers.set('ollama', {
name: 'ollama',
displayName: 'Ollama',
supportsEmbedding: true,
requiresApiKey: false,
requiresBaseURL: true,
createLanguageModel({ baseURL, model }) {
const url = baseURL || 'http://localhost:11434';
const ollama = createOpenAI({
baseURL: `${url}/v1`,
apiKey: 'ollama',
name: 'ollama'
});
return ollama(model);
},
createEmbeddingModel({ baseURL, model }) {
const url = baseURL || 'http://localhost:11434';
const ollama = createOpenAI({
baseURL: `${url}/v1`,
apiKey: 'ollama',
name: 'ollama'
});
return ollama.textEmbeddingModel(model);
},
});
关键细节:Ollama 虽然不是 OpenAI,但它实现了 OpenAI 兼容的 /v1 端点。所以 ChatCrystal 直接用 @ai-sdk/openai 适配器对接 Ollama,只需把 baseURL 指向 Ollama 的地址。apiKey 传一个占位值 'ollama',因为 Ollama 不需要认证。
Custom Provider 也用同样的方式工作——任何实现了 OpenAI 兼容 API 的服务(比如 vLLM、LocalAI、LiteLLM)都可以通过 Custom Provider 接入。
统一 API 层
Provider 工厂解决了"怎么创建模型"的问题,AI SDK 的上层 API 解决了"怎么使用模型"的问题。
generateText:文本生成
最基础的调用方式。给一个 prompt,返回文本:
import { generateText } from 'ai';
const { text } = await generateText({
model: getLanguageModel(),
system: '你是一个技术助手',
prompt: '解释什么是 HNSW 算法',
});
ChatCrystal 在对话摘要流程中使用 generateText 的变体 generateObject,因为需要结构化输出。
generateObject:结构化输出
这是 AI SDK 最强大的能力。你用 Zod 定义一个 schema,LLM 返回严格符合该结构的 JSON:
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 个关键结论'),
code_snippets: z.array(z.object({
language: z.string(),
code: z.string(),
description: z.string(),
})).describe('0-3 个关键代码片段'),
tags: z.array(z.string()).describe('3-6 个小写英文标签'),
});
const { object } = await generateObject({
model: getLanguageModel(),
schema,
system: SYSTEM_PROMPT,
prompt: transcript,
});
generateObject 内部做了几件事:
- 将 Zod schema 转换为 JSON Schema
- 将 JSON Schema 注入到 prompt 中,引导 LLM 按结构输出
- 解析 LLM 返回的 JSON,用 Zod 校验
- 如果校验失败或请求出错,按
maxRetries自动重试(ChatCrystal 显式设为 3 次)
这意味着你的代码拿到的 object 已经是类型安全的 TypeScript 对象,不需要手动 JSON.parse 和类型断言。
embed:向量嵌入
用于生成文本的向量表示:
import { embed } from 'ai';
const { embedding } = await embed({
model: getEmbeddingModel(),
value: 'Fastify 插件注册机制',
});
// embedding: number[] (长度取决于模型,如 1536)
ChatCrystal 在两个场景使用 embed:
- 生成笔记向量:将笔记文本分块,每块调用
embed生成向量,存入 vectra 索引 - 搜索查询向量:用户输入搜索词,调用
embed生成查询向量,在 vectra 中做相似度搜索
模型创建的统一入口
ChatCrystal 通过两个函数封装了模型创建,分别位于不同的模块中:
// 语言模型(server/src/services/llm.ts — 导出)
export function getLanguageModel(): LanguageModel {
const { provider, ...config } = appConfig.llm;
return getProvider(provider).createLanguageModel(config);
}
// Embedding 模型(server/src/services/embedding.ts — 模块内部私有函数)
function getEmbeddingModel() {
const { provider, ...config } = appConfig.embedding;
const entry = getProvider(provider);
if (!entry.createEmbeddingModel) {
throw new Error(`Provider "${provider}" does not support embeddings. Use ollama, openai, google, azure, or custom.`);
}
return entry.createEmbeddingModel(config);
}
getLanguageModel() 是公开导出的,供摘要生成等模块调用。getEmbeddingModel() 是 embedding.ts 的内部函数,不对外导出——外部模块通过调用 generateEmbeddings() 或 semanticSearch() 等公开 API 间接使用它。
配置来自 config.json,结构如下:
{
"llm": {
"provider": "openai",
"model": "gpt-4o-mini",
"apiKey": "sk-..."
},
"embedding": {
"provider": "ollama",
"model": "nomic-embed-text",
"baseURL": "http://localhost:11434"
}
}
LLM 和 Embedding 可以使用不同的 Provider。这是有意为之的——很多人用 Ollama 本地跑 Embedding(免费、低延迟),同时用 OpenAI 的 GPT-4 做摘要生成(效果更好)。
OpenAI 兼容:事实标准
在 6 个 Provider 中,Ollama、Custom 和 OpenAI 本身都使用 @ai-sdk/openai 适配器。这不是巧合,而是因为 OpenAI 的 API 格式已经成为事实标准。
几乎所有本地推理框架(Ollama、vLLM、LocalAI、LiteLLM、llama.cpp server)都实现了 OpenAI 兼容的 /v1/chat/completions 和 /v1/embeddings 端点。这意味着 @ai-sdk/openai 适配器不仅能对接 OpenAI 官方 API,还能对接整个生态。
AI SDK 的 Provider 设计也反映了这个现实:
@ai-sdk/openai— 最通用,覆盖 OpenAI + 所有兼容实现@ai-sdk/anthropic— Anthropic 独有的消息格式(system prompt 分离、tool_use 结构)@ai-sdk/google— Google 独有的多模态格式(parts 数组)@ai-sdk/azure— Azure OpenAI 的端点格式差异(部署名 vs 模型名)
对于大多数应用,@ai-sdk/openai 一个适配器就能覆盖 80% 的场景。ChatCrystal 选择支持全部 6 个,是为了让用户有最大的灵活性。
错误处理与边界情况
AI SDK 统一了 API 格式,但不能统一所有行为。几个需要注意的差异:
速率限制:OpenAI 和 Anthropic 有不同的速率限制策略。AI SDK 不内置重试,ChatCrystal 在服务层通过 p-queue 控制并发和请求频率。
上下文窗口:每个模型的上下文窗口不同(GPT-4o 128K、Claude 200K、Ollama 取决于模型)。AI SDK 不做截断,ChatCrystal 在 prepareTranscript() 中主动截断到 32000 字符(通过 config.ts 中的 maxInputChars 配置)。
Embedding 维度:不同 Embedding 模型输出的向量维度不同(nomic-embed-text 是 768,text-embedding-3-small 是 1536)。vectra 索引在创建时确定维度,切换 Embedding 模型需要重建索引。
认证方式:Ollama 不需要 API Key,Azure 需要 Endpoint URL 而不是 Base URL。Provider 工厂函数封装了这些差异,但配置界面需要根据 Provider 动态显示/隐藏字段。
小结
Vercel AI SDK 的价值不在于它做了什么复杂的事,而在于它把一件简单的事标准化了。generateText、generateObject、embed 三个函数覆盖了 90% 的 AI 调用场景,Provider 适配器覆盖了 90% 的模型服务。
ChatCrystal 的 Provider 工厂模式在此基础上又加了一层:它让切换 Provider 变成改一行配置的事,而不是改一行代码的事。用户在设置页面选择 Provider、填入 API Key、选择模型,系统自动组装出正确的 LanguageModel 或 EmbeddingModel 实例。
这种分层抽象——AI SDK 统一 API 格式,Provider 工厂统一配置方式——让 ChatCrystal 能用最少的代码支持最多的模型服务。新增一个 Provider 只需要实现 ProviderEntry 接口并注册到 Map 中,不超过 30 行代码。
如有疑问欢迎在 GitHub Issues 或私信交流,很乐意解答。