01 · Provider 抽象层
本章讲什么: AI SDK 凭什么"换模型只改一行"?答案全在这条最底层的分界线——
LanguageModelV4接口。本章拆三件事:接口长什么样、model 字符串怎么变成真实模型、中间件怎么在不改模型的前提下改它的行为。
1.1 核心契约:LanguageModelV4
整个库的"provider 无关"建立在一个纯类型接口上。它在 @ai-sdk/provider 包里,这个包几乎没有运行时代码,只有类型——它定义"一个模型必须长什么样",但不实现任何模型。
接口本体小得惊人——核心就两个方法:
// packages/provider/src/language-model/v4/language-model-v4.ts —— 简化
export type LanguageModelV4 = {
readonly specificationVersion: 'v4'; // 版本标记
readonly provider: string; // 'openai' / 'anthropic' …
readonly modelId: string; // 'gpt-5.4' …
supportedUrls: /* 哪些 URL 模型能原生吃,不必下载 */;
doGenerate(options): PromiseLike<LanguageModelV4GenerateResult>; // 一次性
doStream(options): PromiseLike<LanguageModelV4StreamResult>; // 流式
};
见 LanguageModelV4(packages/provider/src/language-model/v4/language-model-v4.ts:8)。
几个设计要点:
do前缀是故意的。 注释明说:加do前缀是为了防止用户直接调用这两个方法(language-model-v4.ts:43-45)。用户应该走generateText这类高层函数,而不是手动model.doGenerate。specificationVersion: 'v4'是个字面量标记。编排层靠它判断模型实现的是哪一版接口——库同时兼容 v2/v3/v4(见下文resolveLanguageModel)。- 两个方法对应两种模式: 非流式
doGenerate一把返回完整结果;流式doStream返回一个 part 流。generateText用前者,streamText用后者。
统一的 prompt 格式
模型接口收到的不是你写的 prompt: 'hello',而是一个规范化的消息数组 LanguageModelV4Prompt。它有四种角色:
| role | content 能放什么 |
|---|---|
system | 一段字符串 |
user | 文本片段 + 文件片段 |
assistant | 文本 / 文件 / reasoning / 工具调用 / 工具结果 |
tool | 工具结果 / 工具审批响应 |
见 LanguageModelV4Message(packages/provider/src/language-model/v4/language-model-v4-prompt.ts:19)。注释强调这"不是面向用户的 prompt"——是 SDK 把用户写的 chat/instruction prompt 映射成的内部中立格式。这层映射(convertToLanguageModelPrompt)正是"各家方言→统一格式"的翻译器。
同理,Provider 是模型的工厂
比 model 高一级的是 ProviderV4——一个 provider 能按 id 返回各类模型:
// packages/provider/src/provider/v4/provider-v4.ts —— 简化
export interface ProviderV4 {
readonly specificationVersion: 'v4';
languageModel(modelId: string): LanguageModelV4;
embeddingModel(modelId: string): EmbeddingModelV4;
imageModel(modelId: string): ImageModelV4;
transcriptionModel?(modelId: string): TranscriptionModelV4;
// … speech / reranking / files / skills(可选)
}
见 ProviderV4(packages/provider/src/provider/v4/provider-v4.ts:13)。openai('gpt-5.4') 本质就是"调某个 ProviderV4 的 languageModel('gpt-5.4')"。
1.2 model 怎么变成真实模型:resolveLanguageModel
你给 generateText 的 model 可以是字符串也可以是对象。统一在入口处被 resolveLanguageModel 规整:
// packages/ai/src/model/resolve-model.ts —— 简化
export function resolveLanguageModel(model: LanguageModel): LanguageModelV4 {
if (typeof model === 'string') {
return getGlobalProvider().languageModel(model); // 字符串 → 全局 provider
}
if (!['v4', 'v3', 'v2'].includes(model.specificationVersion)) {
throw new UnsupportedModelVersionError(/* … */);
}
return asLanguageModelV4(model); // 老版本对象适配成 v4
}
见 resolveLanguageModel(packages/ai/src/model/resolve-model.ts:28)。两条路径:
- 传字符串(如
'openai/gpt-5.4')→ 交给"全局 provider"的languageModel(...)。默认全局 provider 是 Vercel AI Gateway(resolve-model.ts顶部import { gateway } from '@ai-sdk/gateway'),所以裸字符串会被路由到网关,由网关再转发给真实厂商。这就是 README 里"不装任何 provider 包也能跑"的原因。 - 传对象(如
anthropic('claude-opus-4-6'))→ 校验版本号、必要时把 v2/v3 适配成 v4。asLanguageModelV4让老 provider 包在新编排层里继续可用——这是库能平滑升级大版本的关键缝合点。
精华: "换模型只改字符串"不是魔法,是"字符串→全局 Gateway provider"这一条默认路由。直连厂商时,provider 对象自己实现了 LanguageModelV4,编排层一视同仁。
1.3 中间件:在不改模型的前提下改它的行为
有时你想给任意模型加点横切能力:打日志、改默认参数、模拟流式、从输出里抽 reasoning。wrapLanguageModel 用"装饰器"思路实现:包进去一个 LanguageModelV4,返回另一个 LanguageModelV4——对调用方完全透明。
中间件 能挂什么钩子
// packages/provider/src/language-model-middleware/v4/language-model-v4-middleware.ts —— 简化
export type LanguageModelV4Middleware = {
readonly specificationVersion: 'v4';
overrideProvider?(...): string; // 改 provider 名
overrideModelId?(...): string; // 改 modelId
transformParams?(...): Promise<LanguageModelV4CallOptions>; // 调用前 改参数
wrapGenerate?(...): Promise<...>; // 包住 doGenerate
wrapStream?(...): Promise<...>; // 包住 doStream
};
见 LanguageModelV4Middleware(packages/provider/src/language-model-middleware/v4/language-model-v4-middleware.ts:10)。三个层次:改身份(override*)、改输入(transformParams)、包执行(wrapGenerate/wrapStream)。
它怎么"包"
wrapLanguageModel 返回一个新的 v4 对象,其 doGenerate/doStream 先跑 transformParams 再交给中间件的 wrapGenerate/wrapStream:
// packages/ai/src/middleware/wrap-language-model.ts —— 简化自 doWrap
return {
specificationVersion: 'v4',
provider: providerId ?? overrideProvider?.({ model }) ?? model.provider,
modelId: modelId ?? overrideModelId?.({ model }) ?? model.modelId,
async doGenerate(params) {
const transformed = await doTransform({ params, type: 'generate' });
const doGenerate = () => model.doGenerate(transformed); // 原始能力
return wrapGenerate
? await wrapGenerate({ doGenerate, doStream, params: transformed, model })
: await doGenerate();
},
// doStream 同理
};
见 doWrap(packages/ai/src/middleware/wrap-language-model.ts:43)。两个巧妙点:
- 多个中间件按数组顺序组合。
wrapLanguageModel把中间件数组.reverse().reduce(...)逐个包上(wrap-language-model.ts:36-41),于是数组里第一个中间件在最外层、最先看到调用。 wrapGenerate同时拿到doGenerate和doStream。 这让"模拟流式"中间件成为可能:用户调doStream,中间件却去调底层doGenerate、再把整段结果切成假的流(就是simulate-streaming-middleware.ts干的事)。
库自带一批中间件(packages/ai/src/middleware/):defaultSettingsMiddleware(注入默认参数)、extractReasoningMiddleware(从文本里抠出 <think> 推理)、extractJsonMiddleware、simulateStreamingMiddleware 等。
1.4 巧妙之处
- 规格包零实现。
@ai-sdk/provider只有类型。编排层 import 的是"形状"不是"代码",所以厂商包能独立发版、独立演进,核心包不必跟着改。 - 版本号当适配开关。
specificationVersion: 'v4'+asLanguageModelV4让一个新编排层同时吃 v2/v3/v4 的老 provider(resolve-model.ts:32)。大版本升级不必一次性重写所有厂商包。 - 中间件和模型同构。 包出来的还是
LanguageModelV4,所以可以层层嵌套,也能塞回任何接受 model 的地方——这是"装饰器模式"的教科书级应用。
1.5 边界与局限
- 接口只规定
doGenerate/doStream的形状,不规定语义细节(如某参数对某模型是否生效)。不支持的能力靠运行时warnings上报,而非编译期报错。 - 裸字符串默认走 Gateway,意味着默认有一个网络中间商;要纯直连得显式传 provider 对象。
1.6 代码地图
| 主题 | 文件 | 符号 |
|---|---|---|
| 模型接口 | packages/provider/src/language-model/v4/language-model-v4.ts | LanguageModelV4 |
| 调用参数 | packages/provider/src/language-model/v4/language-model-v4-call-options.ts | LanguageModelV4CallOptions |
| 统一 prompt | packages/provider/src/language-model/v4/language-model-v4-prompt.ts | LanguageModelV4Message |
| Provider 接口 | packages/provider/src/provider/v4/provider-v4.ts | ProviderV4 |
| 模型路由 | packages/ai/src/model/resolve-model.ts | resolveLanguageModel |
| 中间件类型 | packages/provider/src/language-model-middleware/v4/language-model-v4-middleware.ts | LanguageModelV4Middleware |
| 包模型 | packages/ai/src/middleware/wrap-language-model.ts | wrapLanguageModel, doWrap |
| 抽 reasoning 中间件 | packages/ai/src/middleware/extract-reasoning-middleware.ts | extractReasoningMiddleware |
下一章:02 · generateText 工具循环,全库的心脏。