一句话项目 slogan:把「调模型」变成「调接口」。
这是「一天一个开源项目」系列的一篇。今天讲 Vercel 的 AI SDK。
做 AI 应用的开发者大概率都写过这种代码:先 openai.chat.completions.create(...),某个功能需要 Anthropic 的长上下文,换成 anthropic.messages.create(...);想支持 Gemini 又得改第三遍;每次换模型,流式解析、工具调用、结构化输出的处理方式全都不一样。更糟的是你想做多供应商降级 —— 哪家挂了自动切另一家 —— 结果发现每家的错误格式、结束原因、工具调用参数结构都不一样。
更麻烦的是这些差异不在业务代码里该操心。模型会换、协议会变、用户会要求「顺便支持第 N 个供应商」,而每一次调整都要在十几个分支里同步改一遍。
AI SDK 的解法是:定义一层自己的规范,让所有 provider 都实现它。 业务代码只写 generateText({ model, prompt }),换模型就是换一个字符串。
项目速览
⭐ 27,514 Stars | 🍴 5,441 Forks | 📄 Apache-2.0 协议 | 🔧 9,278 次提交 | 🐛 1,539 个 Open Issues
截至 2026-10-26。项目创建于 2023-05-23,三年多涨到 2.75 万星。ai 这个包在 npm 上过去 30 天被下载 1.12 亿次,@ai-sdk/react 3,575 万次 —— 这个下载量级在 npm 生态里属于头部。贡献者 468 人,Top 5 贡献者贡献了 3,700+ 次提交,其中三个是 bot(github-actions、ai-sdk-factory、dependabot)—— 自动化发布流水线本身就是贡献大户。
阅读价值与前置说明
读完你应该能回答这几个问题:
- 它是怎么让 44 个 provider 包在行为上保持一致的?规范长什么样?
- 换模型这件事,到底哪些代码不用改,哪些还得改?
- 工具调用为什么要分「客户端执行」「provider 执行」两类?中间那道审批是怎么设计的?
Code Mode为什么要让模型写代码而不是直接调工具?
前置知识:
- 熟悉 TypeScript 和 ESM(v7 起是纯 ESM,Node 22+)
- 了解 LLM 的基本概念:prompt、token、流式输出、tool calling
- 知道各家模型 API 大致的差异(哪怕只是听说过 Chat Completions 和 Messages 两套协议)
- 可选:写过 Next.js Server Actions 或 RSC,会更容易理解
@ai-sdk/rsc的定位
项目背景与完整档案
官方定位在 README 里写得很直接:"a provider-agnostic TypeScript toolkit designed to help you build AI-powered applications and agents"。仓库描述是:"The AI Toolkit for TypeScript. From the creators of Next.js"。
它是一个 pnpm + Turborepo 管理的 monorepo,packages/ 下有 80+ 个包。粗分成五类:
| 类别 | 代表包 | 职责 |
|---|---|---|
| 核心 | ai、@ai-sdk/provider、@ai-sdk/provider-utils | 框架无关的生成逻辑与规范定义 |
| UI 适配 | @ai-sdk/react、vue、svelte、angular、rsc | 各框架的状态 hook 与组件 |
| Provider | openai、anthropic、google、bedrock 等 44 个 | 各家协议到内部规范的翻译层 |
| 桥接 | @ai-sdk/openai-compatible、@ai-sdk/langchain | 兼容协议与第三方框架适配 |
| 工具与沙箱 | @ai-sdk/code-mode、sandbox-vercel、tool-bash | 沙箱执行、编码代理工具集 |
基础信息:
- npm 包名:
ai,当前版本 7.0.153(@ai-sdk/provider为 4.0.34) - 协议:Apache-2.0(GitHub API 的
/license返回NOASSERTION,但仓库根目录的LICENSE文件明确是 Apache-2.0) - 运行时:Node.js >= 22,v7 起
"type": "module"纯 ESM - peerDependency:
zod(^3.25.76 || ^4.1.8)—— schema 定义的事实标准 - 构建工具:
tsup打包,vitest测 Node 与 Edge 两套运行时 - 发布方式:Changesets + GitHub Actions,npm 带 provenance 签名
项目数据(截至 2026-10-26):
- ⭐ Stars 27,514 | 🍴 Forks 5,441
- 🔧 提交 9,278 | 🐛 Open Issues 1,539
- 👥 贡献者 468 人(Top 5:lgrammel 2,075 次 / github-actions 1,119 / gr2m 654 / ai-sdk-factory 478 / aayush-kapoor 430)
- 🗓️ 创建于 2023-05-23,最近推送 2026-10-26
- 💻 代码构成:TypeScript 29.0MB / MDX 5.1MB / JavaScript 0.2MB
- 📦 npm 近 30 天下载:
ai1.12 亿次,@ai-sdk/react3,575 万次
它解决什么问题
直接调各家 API 的通病:
- 接口形状各不相同。OpenAI 是
choices[0].message.content,Anthropic 是content[]数组(要遍历找textblock),Gemini 又是另一套。每家还都有各自的流式格式(SSE chunk 里的delta字段名都不一样) - 工具调用的回填格式各不相同。有的要
tool_call_id,有的要tool_use_id,有的要求把结果包成特定 JSON 形状塞回 messages 数组 - 结构化输出各写各的。OpenAI 有
response_format,Anthropic 用 tool 模拟,Gemini 有responseSchema—— 三套完全不同的实现 - 换模型 = 改业务代码。每接一个新供应商,就是一次散弹枪式修改
AI SDK 的解法路径是递进的:
第一步是定义规范。@ai-sdk/provider 里定义 LanguageModelV4 接口,每个 provider 包实现它,把自家协议翻译成统一形状。
第二步是把差异收进 provider 包。业务代码调 generateText({ model: anthropic('...'), prompt }),返回值永远是 { text, usage, toolCalls } 这个形状,跟哪家模型无关。
第三步是处理模型不支持某个能力的情况。这是很多抽象库做得不好的地方 —— 直接抛错不够,用户会问「为什么 Gemini 不行」。AI SDK 的做法是降级 + 警告:比如某个 provider 不支持结构化输出,就退回用 tool 模拟,同时在 warnings 数组里说明原因,而不是让调用方去猜。
使用场景:
- 多供应商产品:主用 Claude、Claude 挂了降级到 GPT、两者都超时降级到本地模型 —— 换模型只改一个字符串
- 产品化 AI 功能:要支持用户自带 key(BYOK),每个人可能用不同 provider
- 自建 AI 应用平台:内部有十几个模型,团队想用统一接口调用
快速上手
前提条件:Node.js 22+。
最简安装,走 Vercel AI Gateway(一个 key 打通所有主流模型):
npm install aiimport { generateText } from 'ai'
const { text } = await generateText({
model: 'anthropic/claude-opus-5.5',
prompt: 'What is an agent?',
})注意 model 是字符串而不是模型对象 —— Gateway 帮你解析路由。直连各家则装对应的包:
npm install @ai-sdk/openai @ai-sdk/anthropic @ai-sdk/googleimport { anthropic } from '@ai-sdk/anthropic'
const { text } = await generateText({
model: anthropic('claude-opus-5.5'),
prompt: 'What is an agent?',
})接一个「OpenAI 兼容」的自建服务(很多国内模型和自建网关都提供这套接口):
npm install @ai-sdk/openai-compatibleimport { createOpenAICompatible } from '@ai-sdk/openai-compatible'
const { text } = await generateText({
model: createOpenAICompatible({
baseURL: 'https://api.example.com/v1',
name: 'example',
apiKey: process.env.MY_API_KEY,
}).chatModel('meta-llama/Llama-3-70b-chat-fh'),
prompt: 'Write a vegetarian lasagna recipe for 4 people.',
})流式输出:
import { streamText } from 'ai'
const result = streamText({ model, prompt: '...' })
for await (const textPart of result.textStream) {
process.stdout.write(textPart)
}结构化输出(用 zod 定义 schema,类型自动推导):
import { generateText, Output } from 'ai'
import { z } from 'zod'
const { output } = await generateText({
model,
output: Output.object({ recipe: z.object({ name: z.string() }) }),
prompt: 'Generate a lasagna recipe.',
})顺带一提:仓库里有个
skills/目录,可以用npx skills add vercel/ai把 AI SDK 的技能装进你的 Claude Code 或 Cursor 仓库。
核心特性拆解
统一 provider 规范 —— @ai-sdk/provider 定义 LanguageModelV4,方法是 doGenerate / doStream(do 前缀是为了防止用户直接调用)。44 个 provider 包实现同一接口。
四种模型类型统一 —— 除语言模型外,规范里还有 EmbeddingModelV4、ImageModelV4、VideoModelV4、SpeechModelV4、TranscriptionModelV4、RerankingModelV4、RealtimeModelV4。所有类型共享同一套 provider 包和中间件机制。
工具调用的三层分类 —— 客户端执行(SDK 跑你的函数)、provider 执行(模型厂商自己跑,如 OpenAI 的 web_search)、provider 定义但需审批(厂商侧工具但要用户点头)。这个分层是后面深度剖析的重点。
中间件系统 —— wrapLanguageModel 可以给模型套一层或多层中间件,用来改参数、包装调用、覆盖 provider 名和 model id。内置的就有 extractJsonMiddleware、simulateStreamingMiddleware、defaultSettingsMiddleware 等。
Agent 循环 —— ToolLoopAgent 把「调用模型 → 执行工具 → 把结果喂回去 → 再调用」这个循环封装了,默认 stopWhen: isStepCount(20),到步数上限会打警告日志提示你调高。
Code Mode —— 让模型写一段 TypeScript 来调用你的工具,而不是一次一个工具调用。代码在 QuickJS 沙箱里跑。
可观测性 —— @ai-sdk/otel 接 OpenTelemetry,另有 @ai-sdk/devtools 做本地调试。
多框架 UI —— React / Vue / Svelte / Angular 都有对应的 useChat 等 hook,另有 @ai-sdk/rsc 走 React Server Components 流式渲染。
深度设计剖析
设计亮点一:规范里明确区分「谁来执行工具」
常规方案:
大多数抽象层只区分「有 function call」和「没有 function call」。工具怎么执行、结果怎么回填,各家抽象库的做法都不一样 —— 有的统一在 SDK 里跑(你的 execute 函数),有的把整块逻辑交给 provider。开发者遇到不支持的情况只能自己 try-catch。
AI SDK 的做法:
它在规范层就把工具拆成了三类,每一类有独立的类型:
LanguageModelV4ProviderTool—— provider 侧工具,type: 'provider',id格式强制为`${string}.${string}`(即<provider-id>.<tool-name>),参数由 provider 定义,部分在厂商系统上执行- 客户端工具 —— 你的
execute函数,SDK 负责调用和回填 LanguageModelV4ProviderExecutedTool—— 带了supportsDeferredResults标志,表示结果可能不在同一轮返回
第三类还有个更细的机制:LanguageModelV4ToolApprovalRequest。规范里写得很清楚:
This is used for flows where the provider executes the tool (e.g. MCP tools) but requires an explicit user approval before continuing.
也就是说,工具逻辑跑在厂商那边,但执行前需要你点头。这条需求很实际 —— MCP server 提供的工具可能碰你的文件、你的数据库,模型直接调用就等于给了它钥匙。
为什么这么做:
因为「工具在谁那边跑」和「要不要人批」是两个正交的维度,硬塞进一个布尔值迟早要重构。拆成三个类型后,编译期就能把不该混用的组合挡掉。
工程价值与代价:
价值是换模型时工具行为完全一致。代价是类型体系明显变复杂 —— packages/ai/src/generate-text/ 目录下光处理工具相关逻辑的文件就有 40 多个:collect-tool-approvals.ts、resolve-tool-approval.ts、validate-tool-approvals.ts、tool-approval-signature.ts、approved-tool-call-concurrency.ts、tool-fingerprint.ts…… 想完全理解它的工具系统,读代码量不小。
设计亮点二:工具审批用 HMAC 签名,防止模型伪造批准
常规方案:
Agent 循环里的「人工审批」常见做法是:服务端把待批准的 tool call 丢给前端,用户点确认,前端把 toolCallId 传回服务端,服务端查库确认这个 id 确实在待批准列表里,执行。
问题在于,如果审批状态完全由客户端传回,或者服务端只做了简单的「id 在列表里」检查,那么一个被诱导的模型可以伪造一个 toolCallId 混过去。模型会调用工具、会生成参数,它当然也会生成看起来像批准回执的东西。
AI SDK 的做法:
tool-approval-signature.ts 用 HMAC-SHA256 给审批回执签名。签名载荷的构造方式很讲究:
function buildPayload(approvalId, toolCallId, toolName, inputDigest) {
return encoder.encode(
JSON.stringify(['ai-sdk-tool-approval-v1', approvalId, toolCallId, toolName, inputDigest])
)
}源码里的注释解释了为什么用 JSON 而不是拼接:
Serialize with JSON so the encoding is injective: fields may contain any character (including newlines), and escaping + array structure keeps field boundaries unambiguous. The version prefix provides domain separation.
三个细节值得注意:
- JSON 数组而非换行拼接 —— 旧版本用
\n拼接,如果某个字段本身含换行,边界就模糊了,签名会出现歧义。新格式是单射的 - 版本前缀
ai-sdk-tool-approval-v1—— 域分隔,防止别的场景的签名被误用来批准工具 - 包含
inputDigest—— 签的是「这次工具调用的参数」的摘要,参数被改过签名就对不上
密钥用 crypto.subtle.importKey('raw', ..., { name: 'HMAC', hash: 'SHA-256' }) 导入,extractable: false —— 密钥进了 WebCrypto 就不可再导出。
为什么这么做:
审批的本质是把「用户确实同意过」这件事变成可验证的密码学断言,而不是一个可以被模型伪造的字符串。
工程价值与代价:
工程价值是抵御了「模型自己批准自己」这一类攻击 —— 这在 agent 越权时是真实存在的风险。代码里还有个细节体现了这个设计的严谨性:旧版的换行拼接格式没有直接删掉,而是保留为 verify 时的 fallback,且只在字段不含分隔符时才用。
// TODO(#17494): remove in v8 when backwards compatibility with pre-JSON
// signatures (approvals signed before the injective format) is no longer
// needed.代价是:签名密钥必须安全地传到执行工具的那一侧。如果你的审批 UI 和工具执行不在同一个信任域,这套机制就落不了地。另外验签逻辑的分支(legacy 格式 fallback)也是额外的攻击面,代码里自己标了待删。
设计亮点三:Code Mode —— 让模型写代码调工具,而不是一个个调
常规方案:
模型要调 5 个工具,标准 tool calling 流程是:模型输出 tool call → SDK 执行 → 回填结果 → 模型再输出下一个 tool call → …… 来回 5 轮。每轮都有一次完整的模型推理(网络往返 + token 消耗),而且模型必须自己规划中间变量 —— 但它没有真正的变量,只有上下文窗口里越来越长的消息。
AI SDK 的做法:
@ai-sdk/code-mode 让模型生成一段完整的 TypeScript,工具以 tools.name(input) 的形式暴露给它。代码在 QuickJS 沙箱里执行(依赖 run 这个包),返回 JSON 可序列化的结果。
工具提示词是这么生成的:
const sections = [
'Execute code-mode TypeScript in an isolated sandbox.',
'Put the full program in `js`; top-level `await`/`return` work. Return a JSON-serializable result.',
'Call host tools only as async `tools.name(input)`; await each or use `Promise.all` for independent calls.',
...'Fetch: `fetch` is not available.',
]注意 Promise.all 那句 —— 独立工具可以并行调,这是标准 tool calling 做不到的。还有 fetch 不可用的明确声明。
代码里用 experimental_toolCallers 控制哪些工具可以被 code mode 调用、哪些只能直接调:
experimental_toolCallers: {
getInventory: ['code_mode', DIRECT_TOOL_CALL],
getDemand: ['code_mode'],
}工具目录有两种投放方式:toolDiscovery: 'description'(写进工具描述里)或 'conversation'(作为「能力更新」消息增量投放)。后者有个明确的协议:
This catalog replaces all previous code mode capability catalogs. Only the tools listed below are currently available through
tools.
为什么这么做:
标准 tool calling 的瓶颈不是工具本身,是轮次。而且模型在多步规划中很容易出错 —— 算错了、把参数传错了、中间步骤忘了。写成代码之后,中间变量是真变量,循环和分支由 TypeScript 表达式表达,模型只需要一次想清楚。
工程价值与代价:
工程价值是轮次从 N 降到 1,且并行调用成为可能。沙箱用 QuickJS 而不是 Node vm,隔离性强得多 —— run 包在受限的 QuickJS 上下文里跑,连 fetch 都不给。
代价有三个,都挺重:
- 模型必须能写对代码。写错了就是语法错误,调试成本比填错一个 JSON 参数高得多
- 沙箱与宿主之间的桥接有复杂度。
run-code-mode.ts里有大量中断/续跑逻辑:中断 id 由toolCallId加后缀拼成、interruptResolution、continuation-capability.ts—— 沙箱执行到一半需要宿主决策时,会中断并把控制权交回去 - JSON 序列化是硬约束。返回必须是 JSON 可序列化的,沙箱里没法直接传对象引用
项目细节补充
版本号策略值得一看。 ai 包当前 7.0.153,@ai-sdk/provider 是 4.0.34 —— 三个包各有各的大版本号,Changeset 每次发版会把依赖的子包版本一起带上。这说明它把「规范」和「实现」当成两个独立演进的生命周期:规范升大版本,所有 provider 包跟着升;规范只加新特性(比如 VideoModelV4),provider 包跟着升小版本。
规范 v2 / v3 / v4 三代共存。 packages/provider/src/language-model/ 下 v2、v3、v4 三个目录都在。asLanguageModelV4 用 Proxy 做适配:
export function asLanguageModelV4(
model: LanguageModelV2 | LanguageModelV3 | LanguageModelV4,
): LanguageModelV4 {
if (model.specificationVersion === 'v4') {
return model
}
// first convert v2 to v3, then proxy v3 as v4:
const v3Model =
model.specificationVersion === 'v2' ? asLanguageModelV3(model) : model
return new Proxy(v3Model, { ... })
}v2 先转 v3,再由 Proxy 冒充 v4。这样老 provider 包不用重写就能继续用。代价是仓库里塞了三代规范的全部类型定义,类型体操的部分相当难读。
小版本也能破坏类型。 CHANGELOG 里 7.0.149 的 interrupted finish reason 有一段明确的兼容性警告:
Compatibility notice: Adding
interruptedto the public finish-reason types can break type checking after upgrading, even though this is a patch release. Exhaustive switches (for example, using anassertNeverfallback) and mappings such asRecord<FinishReason, string>must handle the new value.
这是个诚实但会让人意外的取舍 —— 补丁版本加了个枚举值,穷尽式 switch 就编译不过了。
默认停止条件会主动喊你。 createDefaultStopCondition(20) 在触发时通过 logWarnings 打出一条明确的警告,告诉你循环是因为达到 isStepCount(20) 停的,以及怎么改。这比默默截断好得多 —— 静默截断会让人误以为模型自己完成了。
Harness 适配器阵容庞大。 packages/ 下有 harness-claude-code、harness-codex、harness-cursor、harness-copilot、harness-grok-build、harness-opencode 等十几个。这说明 SDK 的目标用户已经从「写 AI 应用的人」扩展到了「造 coding agent 的人」。
安全相关的工具链也在。 packages/policy-opa 看着像 OPA(Open Policy Agent)集成,packages/sandbox-vercel 和 sandbox-just-bash 是两种沙箱后端,tool-bash / tool-web-fetch 是给 agent 用的工具。GitHub 语言构成里出现了 Open Policy Agent 4,125 字节。
项目资源汇总
官方资源
- GitHub:vercel/ai
- 官网与文档:ai-sdk.dev
- API 参考:ai-sdk.dev/docs/reference
- 开源协议:Apache-2.0(仓库根目录
LICENSE) - 更新日志:仓库内
packages/ai/CHANGELOG.md - 贡献指南:仓库内
CONTRIBUTING.md - 迁移指南:仓库内
content/docs/08-migration-guides/ - npm 包:
ai(当前 7.0.153)
相关资源
- Zod:schema 定义的 peerDependency,AI SDK 的类型推导全靠它
- OpenTelemetry:可观测性通过
@ai-sdk/otel接入 - OpenAI Compatible 规范:自建模型服务最省事的接入路径
- Model Context Protocol:MCP 工具的来源,AI SDK 有专门的
mcp包
媒体报道与延伸阅读
- DeepWiki 架构导读(第三方自动生成,覆盖分层架构、Provider-V4 契约、消息架构与工具执行模型)
核心要点回顾
- AI SDK 的核心是一层 provider 规范(
LanguageModelV4),44 个 provider 包把各家协议翻译成统一形状,业务代码只认generateText/streamText - 工具按谁来执行分三类,provider 执行的工具还能要求人工审批,审批回执用 HMAC-SHA256 签名防模型伪造
- Code Mode 让模型写 TypeScript 调工具,在 QuickJS 沙箱执行,轮次从 N 降到 1,还能
Promise.all并行 - 规范 v2/v3/v4 三代并存用 Proxy 适配,但补丁版本也可能破坏类型 —— 穷尽式 switch 要注意
适合谁
- 要做多供应商 AI 产品、且希望换模型只改一个字符串的开发者
- 需要把 AI 能力产品化(BYOK、降级、可观测性)的团队
- 关注 LLM 抽象层设计本身的架构师 —— 三代规范共存、Proxy 适配、签名审批都是可复用的经验
一句话评价
它把「模型会变、协议会变」这件事的成本,从每个应用各扛一遍,变成了整个生态扛一遍。
