跳到主要内容

vercel-ai-sdk — 本课题摘录

读了哪几篇: 02-generate-text-loop(多步工具循环)、03-tools(工具系统)。 其余三篇(provider 抽象、流式与 UI、结构化输出)本轮没读——属换厂商与界面圈。

这是我们最可能直接拿来用的那一套,而且是 TypeScript。

它对本课题回答了什么

决定四:什么时候停 —— 把停止条件抽成纯函数谓词

最值得抄的一条。 停止逻辑不是散在循环里的 if,而是一个只看"已经跑过哪些步"的函数,返回真就停。

停止条件 = (已跑过的步) => 真 / 假

内置两个现成的:"跑满 N 步就停"、"模型一旦调了某个指定工具就停"。多个条件里任一为真即停。 (依据:前沿库 · Vercel AI SDK · generateText 多步工具循环 —— StopCondition 是只接收 steps 数组返回布尔的纯函数,isStepCount 与 hasToolCall 是内置实现,多个条件由 isStopConditionMet 评估、任一为真即停)

好处: 想自定义"什么时候停",写个函数即可,不必碰循环正文。这跟 cline 的"完成是工具的一个属性"是同一种思路的两种落法——都在把停止从循环里挪出去。

决定四补充:循环的主出口是"模型不再要工具"

它的循环继续条件写得很直白:这一步确实调了客户端工具,而且全都执行完(或被拒绝)了,才继续。 模型这一步纯文本回答 → 条件为假 → 退出。 (依据:前沿库 · Vercel AI SDK · generateText 多步工具循环 —— do…while 的继续条件是「这一步有客户端工具调用且全部执行完或被拒」且未命中 stopWhen,模型不再调工具即自然终止)

一个默认值的坑: 裸用生成函数时,默认停止条件是"跑满 1 步就停"——默认只跑一步,不是多步 agent。是外面那层 agent 封装把默认拨到 20 步,才变成真正的多步循环。 (依据:前沿库 · Vercel AI SDK · generateText 多步工具循环 —— generateText 的 stopWhen 默认 isStepCount(1) 只跑一步,ToolLoopAgent 在 prepareCall 里把默认拨到 isStepCount(20))

"Agent" 在这个库里不是一个独立引擎,而是"生成函数 + 一组打包好的默认配置"。 心脏始终是那个循环。

决定二:怎么认出模型要调工具 —— 分四类,分类直接当开关

两条正交的轴:参数格式谁定义?谁执行? 交叉出四类:

类型参数格式谁定谁执行
普通函数工具我们我们的代码
动态工具我们(运行时才知道)我们的代码
厂商定义的工具厂商我们的代码
厂商执行的工具厂商厂商那边

(依据:前沿库 · Vercel AI SDK · 工具系统 —— 工具按「schema 谁定义 / 谁执行」两条正交轴分成 FunctionTool / DynamicTool / ProviderDefinedTool / ProviderExecutedTool 四类)

这个分类不是文档摆设,是控制流开关:循环只执行"不是厂商执行"的那些工具,厂商那边执行的结果由模型带回来,我们的代码不碰。 (依据:前沿库 · Vercel AI SDK · 工具系统 —— 循环只执行 !providerExecuted 的工具,providerExecuted 一个布尔直接决定 SDK 执不执行)

还有一条"不给执行函数的工具,不会被自动执行"——它把调用交还给调用方,常用于"模型说要调、实际由浏览器那边处理"。这是特性不是 bug,但很容易让人以为工具没跑。

决定三:结果怎么回填

解析失败不直接崩,给一次修复机会。 模型给的工具调用是不可信的——工具名可能不存在、参数可能不合格式、JSON 可能拼错。它把"坏的调用 + 错误信息"再丢给一个修复函数(常见做法是再调一次模型让它改)。 (依据:前沿库 · Vercel AI SDK · 工具系统 —— parseToolCall 校验工具名与输入 schema,配 repairToolCall 在解析失败时给一次修复机会而不是直接报错中断)

这一步的多个工具调用是并行跑的;执行完拼成"工具"角色的消息接到下一步输入里。 (依据:前沿库 · Vercel AI SDK · generateText 多步工具循环 —— executeTools 内部用 Promise.all 把这一步的多个工具调用并行跑完,结果经 toResponseMessages 拼成 tool 角色消息接进下一步输入)

它的做法(可以抄的部分)

每一步可以换参数,不只是"转圈"

循环每一步前留了一个回调,让你逐步换模型、改系统提示、裁剪消息、限制这一步能用哪些工具。比如"前三步用便宜模型,之后换贵的"。 (依据:前沿库 · Vercel AI SDK · generateText 多步工具循环 —— prepareStep 回调允许逐步换模型、改 system、裁剪消息、限制可用工具)

这条把"一轮的输入怎么拼"变成了循环的一个可插拔点,而不是循环外面拼好就不能动。

人的"同意"是一张要防伪的凭证,不是一个布尔

审批做成循环级机制:模型要调某工具 → 先问"这工具要不要审批" → 四种结果(不适用 / 自动批 / 自动拒 / 等人回复)。等人回复的那种会产出一条审批请求,工具进"被挡住"名单,不执行

真正值得记的是这一条:审批请求可以用密钥签名,回放时验签,防止客户端伪造"已批准"。 (依据:前沿库 · Vercel AI SDK · 工具系统 —— 传 toolApprovalSecret 后每个审批请求用 HMAC 签名,回放时验签防止客户端伪造已批准)

判断(无锚): 我们的最小原型不需要签名——本地跑,没有不可信的客户端。但这条要记在配方里,一旦把审批放到浏览器那边就必须补上。 如果错,会错在: 如果原型一开始就是前后端分离、审批在浏览器点,那"同意"就是从不可信端回来的,第一天就得签名。

而且审批是跨请求的:人类的批准/拒绝下次随消息带回,入口处先把上一轮待审的执行掉。这跟官方 SDK 的"把运行状态整个存下来"是同一问题的两种解法——一个存状态,一个存在消息里。

它没回答什么

  • 历史超长怎么办——这两篇没讲压缩与裁剪。要看 headroomopencodedexter
  • 不用原生工具调用怎么办——它假设模型支持。看 crewaismolagentsoh-my-pi
  • 循环的状态存哪、崩了怎么续——它没有"运行状态"这个概念(状态就是消息数组)。要看 openai-agents-jsriglanggraph

坑与代价

  • 默认只跑一步。 上面说过,这是最容易踩的一脚:以为在写 agent,实际只调了一次模型。
  • 工具级的"需要审批"标记已废弃,审批统一上移到调用层。混用老代码会出问题。
  • 厂商那边执行的工具,我们的代码无法在中途干预。 结果完全由厂商控制。 (依据:前沿库 · Vercel AI SDK · 工具系统 —— provider-executed 工具的结果完全由 provider 控制,SDK 无法在中途干预其执行)