每日壁纸

AI SDK

TypeScriptApache-2.0vercel/ai官网

/---

一句话项目 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 与组件
Provideropenai、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 天下载:ai 1.12 亿次,@ai-sdk/react 3,575 万次

它解决什么问题

直接调各家 API 的通病:

  • 接口形状各不相同。OpenAI 是 choices[0].message.content,Anthropic 是 content[] 数组(要遍历找 text block),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 ai
import { 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/google
import { 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-compatible
import { 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.

三个细节值得注意:

  1. JSON 数组而非换行拼接 —— 旧版本用 \n 拼接,如果某个字段本身含换行,边界就模糊了,签名会出现歧义。新格式是单射的
  2. 版本前缀 ai-sdk-tool-approval-v1 —— 域分隔,防止别的场景的签名被误用来批准工具
  3. 包含 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 interrupted to the public finish-reason types can break type checking after upgrading, even though this is a patch release. Exhaustive switches (for example, using an assertNever fallback) and mappings such as Record<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)

相关资源

媒体报道与延伸阅读

  • 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 适配、签名审批都是可复用的经验

一句话评价

它把「模型会变、协议会变」这件事的成本,从每个应用各扛一遍,变成了整个生态扛一遍。

← Previous projectDeepSeek Bot