章节06 / 14
  1. 01先把 AI 应用当成系统,而不是聊天框
  2. 02打通第一个多 Provider 模型请求
  3. 03把 Prompt 写成任务协议
  4. 04结构化输出不是格式化,而是业务门禁
  5. 05把等待时间拆成事件:流式响应实战
  6. 06Tool Calling:模型提出动作,应用执行动作
  7. 07有副作用的工具要先设计刹车
  8. 08RAG 的第一性问题:答案从哪里来
  9. 09让 RAG 回答经得起追问
  10. 10长上下文、会话状态与记忆压缩
  11. 11Agent 工作流要像状态机一样可恢复
  12. 12评测工程:用样例集防止应用退化
  13. 13可观测性与安全:线上问题要能被看见
  14. 14上线不是结束:路由、灰度、回滚和治理复盘
本文目录12
  1. 精准控权的 Tool Calling:如何把 AI 变成安全的行动派
  2. 执行权留给应用层:打破“模型在执行代码”的迷思
  3. JSON Schema 合约:如何教模型认清你的 API
  4. 解析不同厂商的工具通信循环
  5. 统一抽象层:基于 Vercel AI SDK 与 Zod 的工具声明
  6. 防御性编程:失败诊断与模型自我纠错机制
  7. 权限控制与安全边界:高危工具的人工确认垫片
  8. 练习与交付:实现带可观测日志的任务创建闭环
  9. 交付规范与预期输出
  10. 验收自测路径
  11. 异常情况诊断
  12. 来源、复核与时效说明
06

Tool Calling:模型提出动作,应用执行动作

掌握 Tool Calling 的底层逻辑与安全边界。本课将剖析 JSON Schema 的编写、各大模型厂商的工具调用通信循环、参数防御性校验以及应用层的安全执行权控制,并交付一个带完整可观测日志的任务创建闭环。

前置基础
  • 理解大语言模型(LLM)的 Chat Completion 基础 API 调用
  • 具备基本的 TypeScript 或 JavaScript 异步编程基础
  • 了解 JSON 数据的结构特征
学习结果
  • 理解 Tool Calling 的双向通信循环,明晰“模型提出动作、应用执行动作”的安全隔离边界
  • 能够使用 Zod 和 JSON Schema 编写可被模型精准识别的工具定义
  • 掌握参数校验失败时的自动纠错重试机制与高危操作的“人工确认”拦截设计
  • 交付一个包含完整执行日志、入参校验和错误恢复的本地工具调用闭环程序

精准控权的 Tool Calling:如何把 AI 变成安全的行动派

很多人在初学 Agent 或是工具调用时,容易产生一种直观的误解,认为大语言模型可以直接连接数据库、发送邮件或是在你的服务器上执行 Shell 脚本。这种误解很容易导致系统设计出现严重的安全漏洞。

本课将带你厘清 Tool Calling(工具调用)的本质,让你看清模型与应用程序之间的真实边界,并通过规范的代码与严密的流程控制,写出一个能够安全可控地执行外部动作的 AI 应用。


执行权留给应用层:打破“模型在执行代码”的迷思

本节操作锚点:围绕“执行权留给应用层:打破“模型在执行代码”的迷思”记录步骤、样例、诊断、风险、检查清单和验收结果。

不要误以为模型拥有魔法般的执行力。无论是 OpenAI 的 GPT、Anthropic 的 Claude,还是 Google 的 Gemini,它们本质上都只是“概率预测器”和“决策提出者”,而非“代码执行器”。

根据 OpenAI 的 Function calling 官方文档说明,模型在识别到用户意图符合工具定义时,并不会直接去调用外部系统的 API,它只是返回了一段结构化的 JSON 数据,其中包含了模型想要调用的工具名称以及模型推导出来的参数。此时,网络请求挂起,模型陷入“等待(Suspended)”状态。

真正去解析这段 JSON、校验参数、执行 fetch 请求、读写数据库、并把结果拼接成特定格式再喂回给模型的,全部是你的应用程序(运行在 Node.js、Python 或 Go 环境下的后端代码)

这意味着:

  1. 安全边界完全由你掌控:模型只是写了一份“申请书”,批不批准、怎么执行、执行时的权限控制,全部由你的后端代码说了算。
  2. 应用层必须承担校验职责:永远不要直接将模型生成的参数盲目传入敏感系统,因为模型随时可能发生幻觉,生成不存在的 ID 或越权的指令。

JSON Schema 合约:如何教模型认清你的 API

本节操作锚点:围绕“JSONSchema合约:如何教模型认清你的AP”记录步骤、样例、诊断、风险、检查清单和验收结果。

为了让模型在正确的时候提出调用工具的申请,你需要给它提供一份“说明书”。这份说明书遵循 JSON Schema 规范。

根据 JSON Schema 官方文档的定义,Schema 是一种用于描述 JSON 数据结构的声明式语言。模型在进行推理时,会阅读你传入的 Schema,以此来理解工具的作用、需要的参数类型以及哪些参数是必填项。

下面是一份符合规范的 JSON Schema 声明,用于定义一个“创建任务”的工具:

json
{
  "name": "create_task",
  "description": "在项目管理系统中创建一个新任务。仅当用户明确要求创建、添加或记录待办事项时使用。",
  "parameters": {
    "type": "object",
    "properties": {
      "title": {
        "type": "string",
        "description": "任务的简短标题,例如:‘撰写下周周报’"
      },
      "priority": {
        "type": "string",
        "enum": ["high", "medium", "low"],
        "description": "任务紧急程度,默认为 medium"
      },
      "due_date": {
        "type": "string",
        "format": "date",
        "description": "截止日期,格式必须为 YYYY-MM-DD"
      }
    },
    "required": ["title"]
  }
}

编写这段“说明书”时,有两个不容忽视的细节:

  • 描述(description)是给模型读的 Prompt:这里的描述需要写得极其具体。如果你的描述写得含糊不清,模型就可能在不需要的时候滥用工具,或者无法正确推导出参数。
  • 严格限制枚举(enum):如果你的系统只支持特定几个选项,务必在 Schema 中限制 enum。这可以大幅度降低模型信口开河的几率。

解析不同厂商的工具通信循环

本节操作锚点:围绕“解析不同厂商的工具通信循环”记录步骤、样例、诊断、风险、检查清单和验收结果。

当你在底层直接对接不同的模型厂商时,你会发现它们在协议命名和消息结构上存在细微的差别,但其核心的“双向通信循环”完全一致。

我们来看一下三大主流厂商在协议设计上的对应关系:

阶段OpenAI (Function calling)Anthropic (Tool use)Gemini (Function calling)
1. 声明工具传入 tools 数组,每个元素包含 type: "function"function 描述。传入 tools 数组,直接定义每个 tool 的 name, description, input_schema传入 tools 参数,包含 functionDeclarations 列表。
2. 模型提出调用响应中包含 tool_calls 数组,每个元素有独一无二的 id,以及 function.arguments响应中包含 content 数组,其中类型为 tool_use 的节点含有 idinput响应中包含 functionCalls 数组,每个元素有 nameargs
3. 应用回传结果发送一条 role: "tool" 的消息,必须带上对应的 tool_call_id发送一条 role: "user"content 类型为 tool_result 的消息,必须对应 tool_use_id发送一条 role: "function" 的消息,在 parts 中回传包含 response 的结构体。

Anthropic Tool use 官方文档明确指出,在模型返回 tool_use 之后,应用层必须向模型回传一个 tool_result 类型的消息。这就推导出一个重要的设计原则:工具调用不是单向的单次请求,而是一个严格互锁的多阶段对话循环。你不能跳过结果回传,否则对话上下文将会断裂,模型将无法继续生成后续答复。

这个循环的完整链路如下图所示:

text
用户: "帮我建一个明天截止的写代码任务"
       │
       ▼
[应用层] 发送用户 Prompt + 工具 Schema
       │
       ▼ (网络请求)
[模型] 分析语义,匹配 Schema,输出 tool_call 请求:
       { name: "create_task", arguments: { title: "写代码", due_date: "2026-05-29" } }
       │
       ▼ (网络响应挂起)
[应用层] 拦截到 tool_call -> 执行参数校验 -> 执行本地代码(写入数据库)-> 拿到 result: { success: true, task_id: 1024 }
       │
       ▼ (新网络请求)
[应用层] 将工具执行结果 `{ role: "tool", tool_call_id: "...", content: "{...}" }` 回传给模型
       │
       ▼
[模型] 阅读执行结果,生成最终人类语言:“我已经为您创建好了任务,ID是1024。”

统一抽象层:基于 Vercel AI SDK 与 Zod 的工具声明

本节操作锚点:围绕“统一抽象层:基于VercelAISDK与Zod的”记录步骤、样例、诊断、风险、检查清单和验收结果。

如果开发多模型适配的应用,可以优先采用类似 Vercel AI SDK 的统一 Tool 抽象层,因为这样能规避不同厂商在 tool_use 协议格式上的微小差异,否则你需要为 OpenAI, Claude 和 Gemini 手写多套复杂的格式转换与消息追加逻辑。

Vercel AI SDK 允许我们使用 zod 来声明参数 Schema,并直接在工具定义中绑定 execute 函数。下面展示如何在 TypeScript 中声明一个查询外部知识库和创建任务的工具:

typescript
import { z } from 'zod';
import { tool } from 'ai';

// 模拟的外部系统接口
const db = {
  async createTask(title: string, priority: string, dueDate?: string) {
    return { id: Math.floor(Math.random() * 10000), title, priority, dueDate, status: 'created' };
  },
  async searchWiki(query: string) {
    if (query.includes('AI')) {
      return { content: 'AI应用开发中,Tool Calling 是实现 Agent 架构的核心。' };
    }
    return { content: '未找到相关资料。' };
  }
};

export const tools = {
  // 工具一:查询资料
  searchWiki: tool({
    description: '查询内部 Wiki 知识库,获取相关开发规范与背景资料。',
    parameters: z.object({
      query: z.string().describe('检索关键词,如 "AI 开发规范"')
    }),
    execute: async ({ query }) => {
      console.log(`[工具执行] 正在检索 Wiki: "${query}"`);
      const result = await db.searchWiki(query);
      return result;
    }
  }),

  // 工具二:创建任务
  createTask: tool({
    description: '在项目管理系统中创建一个新任务。',
    parameters: z.object({
      title: z.string().describe('任务标题'),
      priority: z.enum(['high', 'medium', 'low']).default('medium').describe('紧急程度'),
      dueDate: z.string().optional().describe('截止日期,格式为 YYYY-MM-DD')
    }),
    execute: async ({ title, priority, dueDate }) => {
      console.log(`[工具执行] 正在创建任务: "${title}", 优先级: ${priority}`);
      const result = await db.createTask(title, priority, dueDate);
      return result;
    }
  })
};

在这段代码中,Vercel AI SDK 的 tool 函数帮我们默默完成了两件事:

  1. 它利用 zod-to-json-schema 自动把 Zod 的 Schema 转换成了模型能读懂的 JSON Schema 标准格式。
  2. 当模型返回工具调用请求时,SDK 会自动提取参数、运行对应的 execute 异步函数,并将结果打包好发送给下一轮模型请求,消除了繁琐的手工拼接过程。

防御性编程:失败诊断与模型自我纠错机制

本节操作锚点:围绕“防御性编程:失败诊断与模型自我纠错机制”记录步骤、样例、诊断、风险、检查清单和验收结果。

模型并不总是完美的,有时候它会传递格式错误的日期(例如写成 'tomorrow' 而非 '2026-05-29'),或者漏掉必填字段。如果模型输出的参数无法通过 JSON Schema 校验,应该把校验错误作为新一轮对话的 User Message 喂给模型,让其自我修正,除非重试次数已经达到上限,此时必须直接报错打断流程。

这就是在 Agent 开发中常用的**自我纠错(Self-Correction)**垫片机制。在 Vercel AI SDK 架构下,虽然底层在调用 execute 前会自动进行 Zod 校验,但我们可以通过自定义执行流,抓取到这些校验错误并让模型重试。以下是自定义防御性校验与纠错的流程设计:

typescript
import { generateText, ToolExecutionValidationError } from 'ai';
import { openai } from '@ai-sdk/openai'; // 需要提前配置好 OPENAI_API_KEY

async function runAgentWithRetry(prompt: string, maxRetries = 2) {
  let currentPrompt = prompt;
  let attempts = 0;

  while (attempts <= maxRetries) {
    try {
      const response = await generateText({
        model: openai('gpt-4o'),
        tools: tools,
        prompt: currentPrompt,
        maxSteps: 5, // 允许自动进行多轮工具调用循环
      });

      return response;
    } catch (error) {
      attempts++;
      if (error instanceof Error && error.name === 'ZodError') {
        // 捕捉到本地 Zod 校验失败
        console.warn(`[参数校验失败] 第 ${attempts} 次尝试失败,正在反馈给模型进行纠错。`);
        currentPrompt = `${currentPrompt}\n\n【系统提示】上一次工具调用参数未通过校验,错误信息为:"${error.message}"。请重新检查你的参数格式并再次调用。`;
      } else {
        // 遇到不可恢复的其他错误(如网络中断、API 凭证失效),不再重试,直接抛出
        throw error;
      }
    }
  }
  throw new Error('达到最大重试次数,模型未能成功纠正其生成的工具参数。');
}

这种防御性设计的精妙之处在于,我们没有因为一次参数格式错误就彻底崩溃中断,而是给模型一次“看清报错、重新做人”的机会。这大大提升了系统的鲁棒性。


权限控制与安全边界:高危工具的人工确认垫片

本节操作锚点:围绕“权限控制与安全边界:高危工具的人工确认垫片”记录步骤、样例、诊断、风险、检查清单和验收结果。

如果工具涉及写库或转账等高危操作,必须在应用层引入人工确认(Human-in-the-loop)机制,否则一旦模型发生幻觉或遭遇 Prompt 注入攻击,可能会对业务数据造成毁灭性的破坏。

如何优雅地在工具流中加入人工确认环节?我们需要在工具执行前,将执行状态挂起,将工具调用的上下文序列化后持久化到数据库中,等待管理员在前端页面点击“批准”后,再恢复执行。

以下是一个模拟人工确认拦截的伪代码逻辑,展示了如何在不破坏工具生命周期的前提下,实现动作拦截:

typescript
import { tool } from 'ai';
import { z } from 'zod';

// 模拟挂起和等待外部确认的信号器
const pendingApprovals = new Map<string, (approved: boolean) => void>();

export const secureTransferTool = tool({
  description: '将资金转入指定的银行账户。此操作敏感,必须经过人工审核。',
  parameters: z.object({
    amount: z.number().positive().describe('转账金额'),
    accountNumber: z.string().describe('目标账户')
  }),
  execute: async ({ amount, accountNumber }) => {
    const approvalId = `req_${Math.random().toString(36).substr(2, 9)}`;
    console.log(`\n⚠️ [高危操作拦截] 检测到转账请求!`);
    console.log(`- 目标账户: ${accountNumber}`);
    console.log(`- 转账金额: ¥${amount}`);
    console.log(`- 审批 ID : ${approvalId}`);
    console.log(`[系统提示] 请在控制台中输入 "yes" 同意转账,或输入 "no" 拒绝:`);

    // 挂起 execute 的异步流程,等待外部输入信号
    const approved = await new Promise<boolean>((resolve) => {
      pendingApprovals.set(approvalId, resolve);
      
      // 在控制台中模拟人工交互输入
      process.stdin.once('data', (data) => {
        const input = data.toString().trim().toLowerCase();
        if (input === 'yes' || input === 'y') {
          resolve(true);
        } else {
          resolve(false);
        }
      });
    });

    pendingApprovals.delete(approvalId);

    if (!approved) {
      throw new Error('用户拒绝了该转账操作,转账已被安全拦截。');
    }

    // 只有在获批后,才会真正调用底层写库/转账接口
    return { status: 'success', transactionId: `tx_${Date.now()}`, message: '转账成功' };
  }
});

在构建 Web 应用时,你应该将这个控制台输入替换为 Webhook 或者是基于 WebSocket 的长连接通知。应用的核心逻辑是:工具调用的 execute 方法在 Promise 解决之前绝不返回,利用底层的等待机制天然卡住模型的推理回路,直到安全验证通过。


练习与交付:实现带可观测日志的任务创建闭环

本节操作锚点:围绕“练习与交付:实现带可观测日志的任务创建闭环”记录步骤、样例、诊断、风险、检查清单和验收结果。

现在,轮到你亲自动手实现一个可以在本地运行的工具调用闭环系统了。本次练习的交付目标是编写一个完整的 TypeScript 脚本,能够让模型根据用户的自然语言输入,自主决定调用“查询知识库(Wiki)”和“创建待办任务”工具,并输出清晰明了的可观测执行日志

交付规范与预期输出

  1. 依赖配置文件 (package.json)
json
{
  "name": "tool-calling-demo",
  "version": "1.0.0",
  "type": "module",
  "dependencies": {
    "@ai-sdk/openai": "^1.0.0",
    "ai": "^3.0.0",
    "zod": "^3.22.0"
  }
}
  1. 主程序 (index.ts)
typescript
import { generateText } from 'ai';
import { openai } from '@ai-sdk/openai';
import { tools } from './tools.js'; // 导入我们前面写好的 tools 定义

async function main() {
  // 模拟输入:一句话同时触发 Wiki 检索和任务创建
  const userInput = "根据 Wiki 里关于 AI 的开发规范,帮我建一个今天截止的‘编写 AI 模块代码’任务,优先级定为高。";

  console.log(`[User]: ${userInput}\n`);
  console.log(`--- [开始执行可观测 Trace] ---`);

  const response = await generateText({
    model: openai('gpt-4o'), // 确保已在环境变量中注入了 OPENAI_API_KEY
    tools: tools,
    prompt: userInput,
    maxSteps: 5, // 开启多步调用,允许模型先查 Wiki,拿到结果后再建任务
    onStepFinish({ text, toolCalls, toolResults, finishReason }) {
      // 每一步推理/执行结束时的钩子,这是可观测性的关键
      console.log(`\n[Step Finish Log]`);
      if (text) { 
        console.log(`> 模型输出: "${text}"`); 
      }
      if (toolCalls && toolCalls.length > 0) {
        console.log(`> 模型提议调用工具:`);
        toolCalls.forEach(call => {
          console.log(`  - 工具名: ${call.toolName}`);
          console.log(`  - 提取参数: ${JSON.stringify(call.args)}`);
        });
      }
      if (toolResults && toolResults.length > 0) {
        console.log(`> 应用程序执行工具结果:`);
        toolResults.forEach(res => {
          console.log(`  - 工具: ${res.toolName}`);
          console.log(`  - 结果: ${JSON.stringify(res.result)}`);
        });
      }
      console.log(`> 终止状态: ${finishReason}`);
    }
  });

  console.log(`\n--- [Trace 结束] ---`);
  console.log(`\n[AI 最终答复]: ${response.text}`);
}

main().catch(console.error);

验收自测路径

请在你的开发终端执行以下步骤以进行验收:

  1. 确保你的环境中配置了正确的环境变量:export OPENAI_API_KEY="your-key"
  2. 运行脚本:node --loader ts-node/esm index.ts
  3. 合格的日志输出格式(必须包含两个不同的工具调用步骤):
    • 步骤一:模型检测到需要“关于 AI 的开发规范”的信息,提议调用 searchWiki,拿到返回内容(“AI应用开发中,Tool Calling 是实现 Agent 架构的核心。”)。
    • 步骤二:模型基于 Wiki 返回内容,结合任务截止日期和优先级,提议调用 createTask(参数中 title'编写 AI 模块代码'priority'high')。
    • 最终状态:模型吐出最终人类友好的文本答复。

异常情况诊断

  • 问题一:模型直接给出答案,没有调用工具。
    • 排查路径:检查 userInput 里的措辞。如果你的 prompt 里没有包含让模型感到“未知”的信息(例如 Wiki 里的特定知识),模型可能会依靠自身的预训练知识直接做出回答。应该修改 prompt,确保其明确包含需要查询 Wiki 或对系统发起写操作的意图。
  • 问题二:报 ZodError 错误。
    • 排查路径:模型生成的某些参数(例如日期)没有通过你的 Zod 约束。请打印出 toolCalls 的原始参数,对比你的 Zod schema,适当放宽 schema 限制,或在描述(describe)中给出更具体的格式范例(如 'due_date 必须是 YYYY-MM-DD 格式,不允许填写 tomorrow')。

来源、复核与时效说明

本节操作锚点:围绕“来源、复核与时效说明”记录步骤、样例、诊断、风险、检查清单和验收结果。

本单元内容基于以下来源的官方技术文档进行编写:

  • OpenAI Function calling Guide (2026-05-28):规定了模型作为动作提议方,应用作为动作执行方的架构分工。
  • Anthropic Tool Use Documentation (2026-05-28):规定了 tool_usetool_result 消息配对的完备性合约。
  • Google Gemini API Function Calling (2026-05-28):确认了主流多模型在函数声明与回调生命周期上的一致性。
  • Vercel AI SDK Core - Tools (2026-05-28):提供了基于 TypeScript/Zod 的统一包装层与可观测性钩子(onStepFinish)。
  • JSON Schema Draft (2026-05-28):奠定了模型理解结构化输入的基础协议。

复核触发条件: 当 Vercel AI SDK 废弃 tool 声明函数或对 onStepFinish 签名进行不向下兼容的修改时,或者 OpenAI/Anthropic 发布了无需回传 tool_result 的全新单向异步执行协议时,本课内容需要进行复核与更新。

ToolCalling:模型提出动作,应用执行动作:把判断写成可复查证据

这一课的价值在于把模糊经验拆成可以检查的动作,而不是把清单背下来。本课交付物是 一个查询资料/创建任务的工具调用闭环和执行日志格式,它要服务于 AI 应用 的下一步,而不是只证明你读过这一节。

如果你现在还没有真实输入,先用一个最小样例完成 ToolCalling:模型提出动作,因为没有输入就无法判断产物是否可用。 如果本课产物会影响后续交付,应该写明适用条件和不适用条件,否则下一课会把不确定性误当成基础。 如果检查结果只剩一句“看起来可以”,必须补一条反例或失败样例,只有能解释失败原因时,这个判断才站得住。

验收失败时要区分三类原因:理解偏差、执行遗漏、外部条件变化。三类问题对应的修复方式不同。围绕 ToolCalling:模型提出动作 做检查时,至少保留步骤、样例、风险、修复和验收五项。

OpenAI 的《Function calling》说明:支撑工具定义、模型提出调用、应用执行工具和结果回传的边界。;这意味着 ToolCalling:模型提出 不能只写经验结论,要把来源变成检查动作。Anthropic 的《Anthropic Tool use overview》提醒:支撑 tool_use / tool_result 流程、工具执行权留在应用层和代理边界。;因此本课方案必须写清边界。Google AI 的《Gemini API function calling》提供的证据是:支撑 Gemini 函数声明、工具调用、多模型工具抽象和调用结果处理。;所以当前结论按 2026-05-28 的来源状态使用。

复核触发条件要写具体:如果官方 API/SDK、软件工具版本、版权政策、安全合规要求、行业规范、平台发布规则或关键来源页面发生变化,需要重新检查 ToolCalling:模型提出 的步骤、样例、风险和验收清单。

练习验收:把 ToolCalling:模型提出动作 应用到一个自己的任务,交付一份包含输入、步骤、样例、失败诊断、来源证据和复测结果的记录。缺少其中任何一项,都先标记为 needs_review。