本文面向想用一套代码支持多个 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 SDKai包解决了这个问题。它提供了一层统一抽象让你的业务代码只写一次底层 Provider 随时可切换。ChatCrystal 的 Provider 矩阵ChatCrystal 支持 6 种 Provider覆盖了从本地到云端的完整场景Provider类型语言模型Embedding 模型认证Ollama本地推理支持支持无需 API KeyOpenAI云端支持支持API KeyAnthropic云端支持不支持API KeyGoogle云端支持支持API KeyAzure云端支持支持API Key EndpointCustomOpenAI 兼容支持支持API Key Base URL注意 Anthropic 不支持 Embedding 模型——这是由模型服务商的能力决定的不是 SDK 的限制。Provider 工厂模式ChatCrystal 的 Provider 注册集中在server/src/services/providers.ts。核心设计是一个Mapstring, ProviderEntry每个 Provider 实现统一的接口interfaceProviderEntry{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}){consturlbaseURL||http://localhost:11434;constollamacreateOpenAI({baseURL:${url}/v1,apiKey:ollama,name:ollama});returnollama(model);},createEmbeddingModel({baseURL,model}){consturlbaseURL||http://localhost:11434;constollamacreateOpenAI({baseURL:${url}/v1,apiKey:ollama,name:ollama});returnollama.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}fromai;const{text}awaitgenerateText({model:getLanguageModel(),system:你是一个技术助手,prompt:解释什么是 HNSW 算法,});ChatCrystal 在对话摘要流程中使用generateText的变体generateObject因为需要结构化输出。generateObject结构化输出这是 AI SDK 最强大的能力。你用 Zod 定义一个 schemaLLM 返回严格符合该结构的 JSONimport{generateObject}fromai;import{z}fromzod;constschemaz.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}awaitgenerateObject({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}fromai;const{embedding}awaitembed({model:getEmbeddingModel(),value:Fastify 插件注册机制,});// embedding: number[] (长度取决于模型如 1536)ChatCrystal 在两个场景使用embed生成笔记向量将笔记文本分块每块调用embed生成向量存入 vectra 索引搜索查询向量用户输入搜索词调用embed生成查询向量在 vectra 中做相似度搜索模型创建的统一入口ChatCrystal 通过两个函数封装了模型创建分别位于不同的模块中// 语言模型server/src/services/llm.ts — 导出exportfunctiongetLanguageModel():LanguageModel{const{provider,...config}appConfig.llm;returngetProvider(provider).createLanguageModel(config);}// Embedding 模型server/src/services/embedding.ts — 模块内部私有函数functiongetEmbeddingModel(){const{provider,...config}appConfig.embedding;constentrygetProvider(provider);if(!entry.createEmbeddingModel){thrownewError(Provider ${provider} does not support embeddings. Use ollama, openai, google, azure, or custom.);}returnentry.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 是 768text-embedding-3-small 是 1536。vectra 索引在创建时确定维度切换 Embedding 模型需要重建索引。认证方式Ollama 不需要 API KeyAzure 需要 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.com/ZengLiangYi/ChatCrystal如有疑问欢迎在 GitHub Issues 或私信交流很乐意解答。