概述
在 AI 应用中,当 AI 助手需要调用本地文件系统或其他敏感工具时,出于安全考虑,需要在执行前获得用户的明确批准。Vercel AI SDK(文中使用 v6 版本,这狗东西 API 一直变来变去的)提供了工具审批(Tool Approval)机制,允许开发者在 AI 执行工具前拦截并请求用户确认。
注意这个参数主要是为了配合stopWhen(也就是工具自动调用),也就是:
1 | const result = streamText({ |
同时也注意这东西的调用方式灵活的很,毕竟他不是应用级别的封装,不用这套自己也可以搓一个出来。
同时在 AI SDK UI 中的调用流程是完全不一样的,这里只针对 Core 的 API。(但是其实也八九不离十了)
Tool 配置:开启审批
需要在定义 tool 时设置 needsApproval: true 来启用审批机制。
1 | // src/mini-cc-cli/tools/index.ts |
核心流程:两次 streamText 调用
然后需要通过两次streamText调用实现了审批机制:
- 第一次调用:AI 分析用户请求,生成工具调用请求(但是不执行,其实你可以把 approve 当成另一种 stopWhen 的条件,遇到需要 approve 的请求就 stop)
- 用户审批:应用拦截请求,询问用户是否批准,这个需要自己实现,比如让用户选择 Yes/No
- 第二次调用:将审批结果发送给 AI,执行工具或处理拒绝
核心数据结构
Tool Approval Request(工具审批请求)
第一次 streamText 调用后,如果 AI 决定调用某个需要审批的工具,SDK 会返回:
1 | { |
Tool Approval Response(工具审批响应)
用户做出决定后,应用需要构造审批响应:
1 | { |
代码流程
完整代码
1 | // src/mini-cc-cli/index.ts |
三阶段详解
阶段一:初始生成(第一次 streamText)
1 | const result = streamText({ |
发生了什么:
- AI 接收用户消息和对话历史
- AI 分析是否需要调用工具
- 如果需要且
needsApproval: true,SDK 不会执行工具 - 而是在
content中返回tool-approval-request对象 response.messages包含了这次交互的完整消息(包括审批请求)
关键点:
result.textStream可能为空,也可能包含 AI 的思考过程result.response是异步的,需要await等待完成response.messages必须加入messages数组,保持对话连续性(不然就丢失上下文了)
阶段二:收集审批决策
1 | const approvals: ToolApprovalResponse[] = []; |
发生了什么:
- 遍历
content数组,查找所有tool-approval-request - 对每个请求调用
requestApproval获取用户决策 - 构造标准的
ToolApprovalResponse对象 - 收集所有审批响应到
approvals数组
关键点:
- 可能有多个工具调用需要审批(批量审批)
approvalId用于匹配请求和响应requestApproval(这个是自己实现的,不是 AI SDK 里的东西)的实现可以是交互式的、基于规则的或自动化的
阶段三:执行审批后的操作(第二次 streamText)
1 | if (approvals.length > 0) { |
发生了什么:
- 将审批响应以
{ role: "tool", content: approvals }形式加入消息历史 - 第二次调用
streamText,AI 接收到审批结果 - 如果
approved: true:- AI 会执行工具(调用
execute函数) - 获取工具返回结果
- 基于结果生成回复
- AI 会执行工具(调用
- 如果
approved: false:- AI 不会执行工具
- 根据
reason和 system message 的指示 - 解释原因并寻求替代方案
注意事项
role: "tool"告诉 SDK 这是审批响应消息- 第二次调用的
messages包含了审批响应,AI 才能知道是否执行 finalResponse.messages包含工具执行结果和 AI 的最终回复
System Message 的作用
为了确保 AI 正确处理拒绝情况,需要在 system message 中明确指示,不过这个只是一个加强罢了,不会真的有 LLM 傻到一直请求吧(啊嘞??):
1 | const createSystemMessage = (): ModelMessage => ({ |
do not retry it:避免 AI 重复尝试被拒绝的操作Explain what you were trying to do:要求 AI 解释意图,增强透明度ask for alternative approaches:引导 AI 寻求其他解决方案
这确保了拒绝审批后的优雅降级,而非死循环或错误终止。
完整流程图
1 | 用户输入:"read the file /etc/passwd" |
消息历史的完整性
整个流程中,messages 数组始终保持完整的对话历史:
1 | // 初始状态 |
为什么重要:
- AI 能理解完整的上下文
- 审批决策被记录,AI 可以引用
- 后续对话可以基于历史信息
- 便于日志记录和调试
要点总结
Tool 配置:
- 设置
needsApproval: true启用审批 - 定义清晰的
inputSchema便于展示参数
- 设置
两次调用机制:
- 第一次:生成工具调用请求(不执行)
- 第二次:根据审批结果执行或放弃
消息历史管理:
- 每次调用后都要
messages.push(...response.messages) - 审批响应必须以
{ role: "tool", content: approvals }形式加入
- 每次调用后都要
审批决策:
approvalId用于匹配请求和响应approved: false时 AI 不会执行工具reason帮助 AI 理解拒绝原因
优雅降级:
- System message 指导 AI 如何处理拒绝
- AI 应解释意图并寻求替代方案
总结
Vercel AI SDK 的自动工具调用(stopWhen)额外通过审批流程通过 两次 streamText 调用 巧妙地实现了安全控制:
- 第一次调用:AI 提出工具调用请求,但不执行
- 审批环节:应用拦截并请求用户确认 image.png
- 第二次调用:AI 根据审批结果执行或放弃
另外这个结构是 AI SDK 内部自己维持的,外部的模型请求最后会抹掉这个工具调用 approve 的过程:

这个可以通过打印 steps,查看完整的请求:
1 | const steps = await result.steps; |