前言
在 AI 应用开发中,当你需要同时集成 OpenAI、Google、DeepSeek、Ollama 等多个厂商的模型时,是否常遇到这些问题:
- 不同提供商的接口格式、调用方式千差万别,代码里充斥着大量条件判断;
- 模型的工具调用能力、推理特性各不相同,适配逻辑散落在业务代码中;
- 配置文件改了就得重新部署,动态启用 / 禁用模型全靠硬编码;
- 新接入一个模型时,要从头写一套适配逻辑,重复劳动且容易出错。
这些问题的核心,在于缺乏一套统一的模型管理机制 ------ 而这正是本文要分享的「模型注册表系统」的设计初衷。
小nuo基于 Next.js 和 AI-SDK,从零构建了一套可复用的模型管理方案:通过 TypeScript 类型约束确保配置安全,用动态加载 + 降级策略实现配置灵活更新,以注册表模式屏蔽多厂商接口差异,再针对工具调用、推理模型等场景设计专用逻辑。
无论你是正在开发多模型 AI 应用的开发者,还是想了解如何在 Next.js 中规范化管理 AI 资源,小nuo带你从实际问题出发,拆解从类型定义到工具调用的全链路实现,最终获得一套可直接复用的模型管理方案。
第一部分:模型类型定义和配置格式
1.1 Model 接口设计原理 /lib/types/models.ts
这个接口设计的每个字段都有特定用途:
TypeScript
export interface Model {
id: string
name: string
provider: string
providerId: string
enabled: boolean
toolCallType: 'native' | 'manual'
toolCallModel?: string
}
id: 模型的唯一标识符,用于在代码中引用特定模型name: 用户界面显示的友好名称provider: 提供商的完整名称(如 "Google Generative AI")providerId: 提供商的简短标识符(如 "google"),用于代码逻辑判断enabled: 布尔值,控制模型是否在 UI 中可选toolCallType: 关键字段,区分模型的工具调用能力toolCallModel: 可选字段,为不支持原生工具调用的模型指定替代模型
1.2 models.json 格式设计原理 /lib/config/models.json
JSON格式选择原因:
- 可读性: JSON 格式便于人工编辑和维护
- 动态加载: 可以在运行时动态获取,无需重新编译
- 版本控制友好: 文本格式便于 Git 跟踪变更
- 类型安全: 通过 TypeScript 接口确保数据结构正确
JSON
{
"models": [
{
"id": "deepseek-reasoner",
"name": "DeepSeek R1",
"provider": "DeepSeek",
"providerId": "deepseek",
"enabled": true,
"toolCallType": "manual",
"toolCallModel": "deepseek-chat"
},
{
"id": "deepseek-chat",
"name": "DeepSeek V3",
"provider": "DeepSeek",
"providerId": "deepseek",
"enabled": true,
"toolCallType": "manual"
},
{
"id": "gpt-4.1",
"name": "GPT-4.1",
"provider": "OpenAI",
"providerId": "openai",
"enabled": true,
"toolCallType": "native"
},
{
"id": "gpt-4.1-mini",
"name": "GPT-4.1 mini",
"provider": "OpenAI",
"providerId": "openai",
"enabled": true,
"toolCallType": "native"
},
{
"id": "gpt-4.1-nano",
"name": "GPT-4.1 nano",
"provider": "OpenAI",
"providerId": "openai",
"enabled": true,
"toolCallType": "native"
},
{
"id": "o3-mini",
"name": "o3 mini",
"provider": "OpenAI",
"providerId": "openai",
"enabled": true,
"toolCallType": "native"
},
{
"id": "gpt-4o",
"name": "GPT-4o",
"provider": "OpenAI",
"providerId": "openai",
"enabled": true,
"toolCallType": "native"
},
{
"id": "gpt-4o-mini",
"name": "GPT-4o mini",
"provider": "OpenAI",
"providerId": "openai",
"enabled": true,
"toolCallType": "native"
}
]
}
每个模型对象的字段含义:
toolCallType为 "native" 表示模型原生支持工具调用toolCallType为 "manual" 表示需要通过其他模型处理工具调用toolCallModel字段只在 "manual" 类型时需要,指定用于工具调用的模型
第二部分:模型配置加载系统
2.1 validateModel 函数 /lib/config/models.ts
用于验证模型对象是否接口要求
TypeScript
// 验证模型
export function validateModel(model: Model) {
// 验证流程
return (
typeof model.id === "string" &&
typeof model.name === "string" &&
typeof model.provider === "string" &&
typeof model.providerId === "string" &&
typeof model.enabled === "boolean" &&
(model.toolCallType === "native" || model.toolCallType === "manual") &&
model.toolCallModel == "string"
);
}
2.2 getModels 函数详细实现 /lib/config/models.ts
TypeScript
import { Model } from '@/lib/types/models'
import { getBaseUrl } from '@/lib/utils/url'
import defaultModels from './default-models.json'
export function validateModel(model: any): model is Model {
return (
typeof model.id === 'string' &&
typeof model.name === 'string' &&
typeof model.provider === 'string' &&
typeof model.providerId === 'string' &&
typeof model.enabled === 'boolean' &&
(model.toolCallType === 'native' || model.toolCallType === 'manual') &&
(model.toolCallModel === undefined ||
typeof model.toolCallModel === 'string')
)
}
export async function getModels(): Promise<Model[]> {
try {
// Get the base URL using the centralized utility function
const baseUrlObj = await getBaseUrl()
/**
*Construct the models.json URL
*Beacuse Next.js rules
*The path /config/models.json = public/config/models.json
*/
const modelUrl = new URL('/config/models.json', baseUrlObj)
console.log('Attempting to fetch models from:', modelUrl.toString())
try {
const response = await fetch(modelUrl, {
cache: 'no-store',
headers: {
Accept: 'application/json'
}
})
if (!response.ok) {
console.warn(
`HTTP error when fetching models: ${response.status} ${response.statusText}`
)
throw new Error(`HTTP error! status: ${response.status}`)
}
const text = await response.text()
// Check if the response starts with HTML doctype
if (text.trim().toLowerCase().startsWith('<!doctype')) {
console.warn('Received HTML instead of JSON when fetching models')
throw new Error('Received HTML instead of JSON')
}
const config = JSON.parse(text)
if (Array.isArray(config.models) && config.models.every(validateModel)) {
console.log('Successfully loaded models from URL')
return config.models
}
} catch (error: any) {
// Fallback to default models if fetch fails
console.warn(
'Fetch failed, falling back to default models:',
error.message || 'Unknown error'
)
if (
Array.isArray(defaultModels.models) &&
defaultModels.models.every(validateModel)
) {
console.log('Successfully loaded default models')
return defaultModels.models
}
}
} catch (error) {
console.warn('Failed to load models:', error)
}
// Last resort: return empty array
console.warn('All attempts to load models failed, returning empty array')
return []
}
函数执行流程详解:
第20-21行: 获取基础URL,用于构建模型配置文件的完整路径
第24行 : 构建 /config/models.json 的完整URL
第25行: 记录尝试获取模型的URL,便于调试
第28-33行: 发起HTTP请求获取模型配置
cache: 'no-store': 禁用缓存,确保获取最新配置Accept: 'application/json': 明确请求JSON格式
第35-40行: 检查HTTP响应状态
- 如果响应不成功,记录警告并抛出错误
第42行: 获取响应文本内容
第45-48行: 检查响应是否为HTML而非JSON
- 这是为了处理某些部署环境可能返回404页面的情况
第50-54行: 解析JSON并验证模型数组
- 使用
validateModel确保每个模型对象都符合接口要求
第55-68行: 错误处理和回退逻辑
- 如果网络请求失败,回退到默认模型配置
- 同样使用
validateModel验证默认配置
第74-76行: 最后的安全网
- 如果所有尝试都失败,返回空数组而不是崩溃
其中用到的工具函数 /lib/utils/url.ts
TypeScript
import { headers } from 'next/headers'
/**
* Helper function to get base URL from headers
* Extracts URL information from Next.js request headers
*/
export async function getBaseUrlFromHeaders(): Promise<URL> {
const headersList = await headers()
const baseUrl = headersList.get('x-base-url')
const url = headersList.get('x-url')
const host = headersList.get('x-host')
const protocol = headersList.get('x-protocol') || 'http:'
try {
// Try to use the pre-constructed base URL if available
if (baseUrl) {
return new URL(baseUrl)
} else if (url) {
return new URL(url)
} else if (host) {
const constructedUrl = `${protocol}${
protocol.endsWith(':') ? '//' : '://'
}${host}`
return new URL(constructedUrl)
} else {
return new URL('<http://localhost:3000>')
}
} catch (urlError) {
// Fallback to default URL if any error occurs during URL construction
return new URL('<http://localhost:3000>')
}
}
/**
* Resolves the base URL using environment variables or headers
* Centralizes the base URL resolution logic used across the application
* @returns A URL object representing the base URL
*/
export async function getBaseUrl(): Promise<URL> {
// Check for environment variables first
const baseUrlEnv = process.env.NEXT_PUBLIC_BASE_URL || process.env.BASE_URL
if (baseUrlEnv) {
try {
const baseUrlObj = new URL(baseUrlEnv)
console.log('Using BASE_URL environment variable:', baseUrlEnv)
return baseUrlObj
} catch (error) {
console.warn(
'Invalid BASE_URL environment variable, falling back to headers'
)
// Fall back to headers if the environment variable is invalid
}
}
// If no valid environment variable is available, use headers
return await getBaseUrlFromHeaders()
}
/**
* Gets the base URL as a string
* Convenience wrapper around getBaseUrl that returns a string
* @returns A string representation of the base URL
*/
export async function getBaseUrlString(): Promise<string> {
const baseUrlObj = await getBaseUrl()
return baseUrlObj.toString()
}
第三部分:提供商注册表系统
3.1 registry 创建详解 /lib/utils/registry.ts
TypeScript
import { anthropic } from '@ai-sdk/anthropic'
import { createAzure } from '@ai-sdk/azure'
import { deepseek } from '@ai-sdk/deepseek'
import { createFireworks, fireworks } from '@ai-sdk/fireworks'
import { google } from '@ai-sdk/google'
import { groq } from '@ai-sdk/groq'
import { createOpenAI, openai } from '@ai-sdk/openai'
import { xai } from '@ai-sdk/xai'
import {
createProviderRegistry,
extractReasoningMiddleware,
wrapLanguageModel
} from 'ai'
import { createOllama } from 'ollama-ai-provider'
export const registry = createProviderRegistry({
openai,
anthropic,
google,
groq,
ollama: createOllama({
baseURL: `${process.env.OLLAMA_BASE_URL}/api`
}),
azure: createAzure({
apiKey: process.env.AZURE_API_KEY,
resourceName: process.env.AZURE_RESOURCE_NAME,
apiVersion: '2025-03-01-preview'
}),
deepseek,
fireworks: {
...createFireworks({
apiKey: process.env.FIREWORKS_API_KEY
}),
languageModel: fireworks
},
'openai-compatible': createOpenAI({
apiKey: process.env.OPENAI_COMPATIBLE_API_KEY,
baseURL: process.env.OPENAI_COMPATIBLE_API_BASE_URL
}),
xai
})
export function getModel(model: string) {
const [provider, ...modelNameParts] = model.split(':') ?? []
const modelName = modelNameParts.join(':')
if (model.includes('ollama')) {
const ollama = createOllama({
baseURL: `${process.env.OLLAMA_BASE_URL}/api`
})
// if model is deepseek-r1, add reasoning middleware
if (model.includes('deepseek-r1')) {
return wrapLanguageModel({
model: ollama(modelName),
middleware: extractReasoningMiddleware({
tagName: 'think'
})
})
}
// if ollama provider, set simulateStreaming to true
return ollama(modelName, {
simulateStreaming: true
})
}
// if model is groq and includes deepseek-r1, add reasoning middleware
if (model.includes('groq') && model.includes('deepseek-r1')) {
return wrapLanguageModel({
model: groq(modelName),
middleware: extractReasoningMiddleware({
tagName: 'think'
})
})
}
// if model is fireworks and includes deepseek-r1, add reasoning middleware
if (model.includes('fireworks') && model.includes('deepseek-r1')) {
return wrapLanguageModel({
model: fireworks(modelName),
middleware: extractReasoningMiddleware({
tagName: 'think'
})
})
}
return registry.languageModel(
model as Parameters<typeof registry.languageModel>[0]
)
}
export function isProviderEnabled(providerId: string): boolean {
switch (providerId) {
case 'openai':
return !!process.env.OPENAI_API_KEY
case 'anthropic':
return !!process.env.ANTHROPIC_API_KEY
case 'google':
return !!process.env.GOOGLE_GENERATIVE_AI_API_KEY
case 'groq':
return !!process.env.GROQ_API_KEY
case 'ollama':
return !!process.env.OLLAMA_BASE_URL
case 'azure':
return !!process.env.AZURE_API_KEY && !!process.env.AZURE_RESOURCE_NAME
case 'deepseek':
return !!process.env.DEEPSEEK_API_KEY
case 'fireworks':
return !!process.env.FIREWORKS_API_KEY
case 'xai':
return !!process.env.XAI_API_KEY
case 'openai-compatible':
return (
!!process.env.OPENAI_COMPATIBLE_API_KEY &&
!!process.env.OPENAI_COMPATIBLE_API_BASE_URL
)
default:
return false
}
}
export function getToolCallModel(model?: string) {
const [provider, ...modelNameParts] = model?.split(':') ?? []
const modelName = modelNameParts.join(':')
switch (provider) {
case 'deepseek':
return getModel('deepseek:deepseek-chat')
case 'fireworks':
return getModel(
'fireworks:accounts/fireworks/models/llama-v3p1-8b-instruct'
)
case 'groq':
return getModel('groq:llama-3.1-8b-instant')
case 'ollama':
const ollamaModel =
process.env.NEXT_PUBLIC_OLLAMA_TOOL_CALL_MODEL || modelName
return getModel(`ollama:${ollamaModel}`)
case 'google':
return getModel('google:gemini-2.0-flash')
default:
return getModel('openai:gpt-4o-mini')
}
}
export function isToolCallSupported(model?: string) {
const [provider, ...modelNameParts] = model?.split(':') ?? []
const modelName = modelNameParts.join(':')
if (provider === 'ollama') {
return false
}
if (provider === 'google') {
return false
}
// Deepseek R1 is not supported
// Deepseek v3's tool call is unstable, so we include it in the list
return !modelName?.includes('deepseek')
}
export function isReasoningModel(model: string): boolean {
if (typeof model !== 'string') {
return false
}
return (
model.includes('deepseek-r1') ||
model.includes('deepseek-reasoner') ||
model.includes('o3-mini')
)
}
每个提供商的配置解析:
第17行 : openai - 直接使用 AI SDK 的 OpenAI 提供商
第18行 : anthropic - 直接使用 AI SDK 的 Anthropic 提供商
第19行 : google - 直接使用 AI SDK 的 Google 提供商
第20行 : groq - 直接使用 AI SDK 的 Groq 提供商
第21-23行 : ollama 配置
- 使用
createOllama创建自定义配置 baseURL从环境变量OLLAMA_BASE_URL获取,添加/api后缀
第24-28行 : azure 配置
- 需要 API 密钥和资源名称
- 指定 API 版本为 '2025-03-01-preview'
第29行 : deepseek - 直接使用 AI SDK 的 DeepSeek 提供商
第30-35行 : fireworks 配置
- 使用展开运算符合并
createFireworks的结果 - 额外添加
languageModel: fireworks属性
第36-39行 : openai-compatible 配置
- 支持 OpenAI 兼容的 API
- 需要自定义 API 密钥和基础URL
第40行 : xai - 直接使用 AI SDK 的 xAI 提供商
3.2 getModel 函数详细解析 registry.ts:43-90
函数逐行解析:
TypeScript
// ...
// /lib/utils/registry.ts
export function getModel(model: string) {
const [provider, ...modelNameParts] = model.split(':') ?? []
const modelName = modelNameParts.join(':')
if (model.includes('ollama')) {
const ollama = createOllama({
baseURL: `${process.env.OLLAMA_BASE_URL}/api`
})
// if model is deepseek-r1, add reasoning middleware
if (model.includes('deepseek-r1')) {
return wrapLanguageModel({
model: ollama(modelName),
middleware: extractReasoningMiddleware({
tagName: 'think'
})
})
}
// if ollama provider, set simulateStreaming to true
return ollama(modelName, {
simulateStreaming: true
})
}
// if model is groq and includes deepseek-r1, add reasoning middleware
if (model.includes('groq') && model.includes('deepseek-r1')) {
return wrapLanguageModel({
model: groq(modelName),
middleware: extractReasoningMiddleware({
tagName: 'think'
})
})
}
// if model is fireworks and includes deepseek-r1, add reasoning middleware
if (model.includes('fireworks') && model.includes('deepseek-r1')) {
return wrapLanguageModel({
model: fireworks(modelName),
middleware: extractReasoningMiddleware({
tagName: 'think'
})
})
}
return registry.languageModel(
model as Parameters<typeof registry.languageModel>[0]
)
}
// ...
第44行: 使用解构赋值分割模型字符串
model.split(':')将 "provider:model-name" 分割[provider, ...modelNameParts]获取提供商和模型名部分
第45行: 重新组合模型名称
- 处理模型名称中可能包含冒号的情况
第46-65行: Ollama 特殊处理
-
第47-49行:创建 Ollama 实例,配置基础URL
-
第52-59行:DeepSeek R1 推理模型处理
- 使用
wrapLanguageModel包装模型 - 添加
extractReasoningMiddleware中间件 tagName: 'think'指定推理标签
- 使用
-
第62-64行:普通 Ollama 模型处理
- 设置
simulateStreaming: true模拟流式输出
- 设置
第67-75行: Groq + DeepSeek R1 处理
- 检查是否为 Groq 提供商的 DeepSeek R1 模型
- 同样应用推理中间件
第77-85行: Fireworks + DeepSeek R1 处理
- 检查是否为 Fireworks 提供商的 DeepSeek R1 模型
- 同样应用推理中间件
第87-89行: 默认处理
- 使用注册表的
languageModel方法获取模型实例 - 类型断言确保参数类型正确
为什么 DeepSeek 需要其他提供商模型应用推理中间件?
从模型注册表的实现来看,DeepSeek 提供商是直接注册的,没有特殊包装逻辑。
但是在 model.json 配置中,我们可以看到 DeepSeek 的模型配置:toolCallType: "manual"
说明 DeepSeek 原生提供商本身没有推理中间件的包装逻辑。
这个返回语句的具体作用 registry.ts:87-89
这行代码的意图是:
- 调用注册表的标准方法 :使用
registry.languageModel()方法获取模型实例 - 类型安全转换 :
model as Parameters<typeof registry.languageModel>[0]确保传入的模型字符串符合注册表方法期望的类型 - 处理标准提供商:对于 OpenAI、Anthropic、Google、DeepSeek 等不需要特殊处理的提供商,直接通过注册表获取模型
第四部分:提供商启用检查系统
4.1 isProviderEnabled 函数详解 registry.ts:92-120
每个 case 的环境变量检查:
TypeScript
// /lib/utils/registry.ts
// ...
export function isProviderEnabled(providerId: string): boolean {
switch (providerId) {
case 'openai':
return !!process.env.OPENAI_API_KEY
case 'anthropic':
return !!process.env.ANTHROPIC_API_KEY
case 'google':
return !!process.env.GOOGLE_GENERATIVE_AI_API_KEY
case 'groq':
return !!process.env.GROQ_API_KEY
case 'ollama':
return !!process.env.OLLAMA_BASE_URL
case 'azure':
return !!process.env.AZURE_API_KEY && !!process.env.AZURE_RESOURCE_NAME
case 'deepseek':
return !!process.env.DEEPSEEK_API_KEY
case 'fireworks':
return !!process.env.FIREWORKS_API_KEY
case 'xai':
return !!process.env.XAI_API_KEY
case 'openai-compatible':
return (
!!process.env.OPENAI_COMPATIBLE_API_KEY &&
!!process.env.OPENAI_COMPATIBLE_API_BASE_URL
)
default:
return false
}
}
// ...
第94-95行: OpenAI 检查
!!process.env.OPENAI_API_KEY双重否定转换为布尔值- 只需要 API 密钥
第96-97行: Anthropic 检查
- 只需要
ANTHROPIC_API_KEY
第98-99行: Google 检查
- 需要
GOOGLE_GENERATIVE_AI_API_KEY
第100-101行: Groq 检查
- 需要
GROQ_API_KEY
第102-103行: Ollama 检查
- 需要
OLLAMA_BASE_URL,不是 API 密钥而是服务地址
第104-105行: Azure 检查
- 需要两个环境变量:
AZURE_API_KEY和AZURE_RESOURCE_NAME - 使用
&&确保两个都存在
第106-107行: DeepSeek 检查
- 需要
DEEPSEEK_API_KEY
第108-109行: Fireworks 检查
- 需要
FIREWORKS_API_KEY
第110-111行: xAI 检查
- 需要
XAI_API_KEY
第112-116行: OpenAI 兼容检查
- 需要两个环境变量:API 密钥和基础URL
- 使用括号和
&&确保两个都存在
第117-118行: 默认情况
- 返回
false,未知提供商默认不启用
第五部分:工具调用支持系统
5.1 getToolCallModel 函数详解 registry.ts:122-143
函数逐行解析:
TypeScript
export function getToolCallModel(model?: string) {
const [provider, ...modelNameParts] = model?.split(':') ?? []
const modelName = modelNameParts.join(':')
switch (provider) {
case 'deepseek':
return getModel('deepseek:deepseek-chat')
case 'fireworks':
return getModel(
'fireworks:accounts/fireworks/models/llama-v3p1-8b-instruct'
)
case 'groq':
return getModel('groq:llama-3.1-8b-instant')
case 'ollama':
const ollamaModel =
process.env.NEXT_PUBLIC_OLLAMA_TOOL_CALL_MODEL || modelName
return getModel(`ollama:${ollamaModel}`)
case 'google':
return getModel('google:gemini-2.0-flash')
default:
return getModel('openai:gpt-4o-mini')
}
}
第123-124行 : 解析模型字符串,与 getModel 函数相同的逻辑
第125-142行: 根据提供商选择工具调用模型
-
第126-127行 : DeepSeek 使用
deepseek-chat作为工具调用模型 -
第128-131行 : Fireworks 使用
llama-v3p1-8b-instruct模型 -
第132-133行 : Groq 使用
llama-3.1-8b-instant模型 -
第134-137行: Ollama 特殊处理
- 优先使用环境变量
NEXT_PUBLIC_OLLAMA_TOOL_CALL_MODEL - 如果未设置,使用原模型名称
- 优先使用环境变量
-
第138-139行 : Google 使用
gemini-2.0-flash作为工具调用模型 -
第140-141行 : 默认情况使用
gpt-4o-mini
5.2 isToolCallSupported 函数详解 registry.ts:145-160
函数逐行解析:
TypeScript
export function isToolCallSupported(model?: string) {
const [provider, ...modelNameParts] = model?.split(':') ?? []
const modelName = modelNameParts.join(':')
if (provider === 'ollama') {
return false
}
if (provider === 'google') {
return false
}
// Deepseek R1 is not supported
// Deepseek v3's tool call is unstable, so we include it in the list
return !modelName?.includes('deepseek')
}
第146-147行: 解析模型字符串
第149-151行: Ollama 提供商检查
- Ollama 模型不支持原生工具调用,返回
false
第153-155行: Google 提供商检查
- Google 模型不支持原生工具调用,返回
false
第157-159行: DeepSeek 模型检查
- DeepSeek R1 不支持工具调用
- DeepSeek V3 的工具调用不稳定,也标记为不支持
- 使用
!modelName?.includes('deepseek')检查
第六部分:推理模型支持系统
6.1 isReasoningModel 函数详解 registry.ts:162-171
TypeScript
// registry.ts
// ...
export function isReasoningModel(model: string): boolean {
if (typeof model !== 'string') {
return false
}
return (
model.includes('deepseek-r1') ||
model.includes('deepseek-reasoner') ||
model.includes('o3-mini')
)
}
// ...
函数逐行解析:
第163-165行: 类型检查
- 确保输入是字符串类型
- 如果不是字符串,返回
false
第166-170行: 推理模型模式匹配
- 检查模型名称是否包含
deepseek-r1 - 检查模型名称是否包含
deepseek-reasoner - 检查模型名称是否包含
o3-mini - 使用
||操作符,任一条件满足即返回true
总结
这个模型注册表系统的设计体现了以下关键原则:
- 类型安全: 通过 TypeScript 接口和验证函数确保数据完整性
- 配置灵活性: 支持动态配置加载和回退机制
- 提供商抽象: 统一接口处理不同 AI 提供商的差异
- 特殊处理: 为推理模型和工具调用提供专门的逻辑
- 环境驱动: 基于环境变量的运行时配置
每个函数都有明确的职责,通过组合这些函数,系统能够灵活地管理多个 AI 提供商和模型,同时保持代码的可维护性和扩展性。
小nuo,目前大二还在努力学,对于AI-SDK也是刚刚上手,如果有更好的方法,欢迎在评论与我探讨,让我多多学习嘿嘿。