Skip to content

课 1 · Tool Call 与 Agent 基础

本课目标

理解 Tool Call 工作机制,用 Vercel AI SDK 实现工具调用闭环。课后你会拿到一个能调用工具执行任务的 Agent。

这一课先掌握一条主线就够了:模型决定调用什么,代码真正执行动作。inputSchemastepCountIs(...) 这类术语,先理解它们在链路里的作用,不要求现在把底层细节一次学完。

前面四个模块做的都是"聊天"——模型只能说话,不能做事。

Agent 的关键区别:不只是聊天,而是能执行动作。 比如查询天气、搜索文档、操作数据库、调用 API。

从前端工程师视角看,这一课最重要的不是记住多少名词,而是看清动作闭环:用户提出目标 → 模型决定该调用什么 → 你的代码真的去做 → 再把结果组织成回复。

Chat 和 Agent 的区别

ChatAgent
能力回答问题回答问题 + 执行任务
输出文本文本 + 动作
例子"北京今天多少度?" → "大约 25 度吧""北京今天多少度?" → 调用天气 API → "北京今天 26°C"

Chat 依赖模型的训练知识回答,可能不准确。Agent 可以调用真实的数据源,答案是准确的。

Tool Call 的工作机制

Tool Call(也叫 Function Calling)是模型与外部工具交互的机制。核心流程四步:

关键理解:模型不执行工具。 模型只是"决定"调用哪个工具,实际执行是你的代码完成的。

你可以把它类比成前端里的“事件分发 + 业务处理”:模型像一个根据上下文决定“要触发哪个动作”的调度层,真正访问 API、读数据库、写文件的还是应用代码。

流程图

在 Vercel AI SDK 中定义工具

工具定义需要三个要素:

  1. description — 告诉模型这个工具能做什么(模型靠描述决定什么时候调用)
  2. inputSchema — 用 Zod Schema 定义输入参数
  3. execute — 实际执行函数

这里的 inputSchema 本质上是在告诉模型“这个工具需要什么参数”。课程示例用 Zod 来写,因为它和 TypeScript 搭配最好;你这一步先知道它是“工具入参说明书”就够了。

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

const weatherTool = tool({
  description: '查询指定城市的当前天气',
  inputSchema: z.object({
    city: z.string().describe('城市名称,如"北京"、"上海"'),
  }),
  execute: async ({ city }) => {
    // 这里调用真实的天气 API
    // 示例用模拟数据
    const data: Record<string, { temp: number; weather: string }> = {
      '北京': { temp: 26, weather: '晴' },
      '上海': { temp: 28, weather: '多云' },
      '广州': { temp: 32, weather: '雷阵雨' },
    }
    return data[city] ?? { temp: 0, weather: '未知城市' }
  },
})

工具描述的重要性

模型靠工具的 description 和参数的 describe 来决定调用哪个工具。描述不好,工具再强大也不会被调用。

typescript
// ❌ 不好的描述
const badTool = tool({
  description: '获取数据',  // 太模糊,模型不知道什么时候该用
  inputSchema: z.object({ id: z.string() }),  // 没有描述
  execute: async ({ id }) => { /* ... */ },
})

// ✅ 好的描述
const goodTool = tool({
  description: '根据用户 ID 查询用户的个人资料信息,包括姓名、邮箱、注册时间',
  inputSchema: z.object({
    userId: z.string().describe('用户唯一标识,格式如 user_abc123'),
  }),
  execute: async ({ userId }) => { /* ... */ },
})

使用 generateText 的 Tool Call

typescript
import 'dotenv/config'
import { generateText, stepCountIs, tool } from 'ai'
import { createOpenAI } from '@ai-sdk/openai'
import { z } from 'zod'

const model = createOpenAI({
  apiKey: process.env.CHAT_API_KEY,
  baseURL: 'https://dashscope.aliyuncs.com/compatible-mode/v1',
}).chat('qwen3.6-plus')

const result = await generateText({
  model,
  tools: {
    weather: tool({
      description: '查询指定城市的当前天气',
      inputSchema: z.object({
        city: z.string().describe('城市名称'),
      }),
      execute: async ({ city }) => {
        return { city, temp: 26, weather: '晴' }
      },
    }),
    calculator: tool({
      description: '计算数学表达式',
      inputSchema: z.object({
        expression: z.string().describe('数学表达式,如 2+3*4'),
      }),
      execute: async ({ expression }) => {
        return { result: Function(`return ${expression}`)() }
      },
    }),
  },
  stopWhen: stepCountIs(5), // 允许模型最多调用 5 步
  prompt: '北京今天天气怎么样?',
})

console.log(result.text)
// "北京今天 26°C,天气晴。"

stopWhen 的作用

可以用 stopWhen: stepCountIs(n) 控制模型最多进行多少轮"思考 → 工具调用 → 拿结果"的循环。

现在先把它理解成一个执行步数上限开关就够了。它的价值不是术语本身,而是防止模型无限尝试、把一次简单任务跑得太长。

  • stopWhen: stepCountIs(1) — 模型最多调用一次工具
  • stopWhen: stepCountIs(5) — 模型可以连续调用多次工具(比如先查天气,再计算温差)

设太大有失控风险,设太小可能完不成任务。对于简单工具调用,3-5 足够。

查看工具调用过程

typescript
// result.steps 记录了每一步的详细信息
for (const step of result.steps) {
  if (step.toolCalls) {
    for (const tc of step.toolCalls) {
      console.log(`调用工具: ${tc.toolName}`)
      console.log(`参数: ${JSON.stringify(tc.args)}`)
    }
  }
  if (step.toolResults) {
    for (const tr of step.toolResults) {
      console.log(`结果: ${JSON.stringify(tr.result)}`)
    }
  }
}

这在调试和日志记录中非常有用——你能看到模型每一步的决策和工具返回。

多个工具协作

给模型提供多个工具,模型会自己判断用哪个:

这里先抓住“多个工具怎么串起来完成一个任务”这个结果,不要求现在就去研究复杂的工具路由策略。

typescript
const result = await generateText({
  model,
  tools: {
    searchDocs: tool({
      description: '在知识库中搜索相关文档',
      inputSchema: z.object({
        query: z.string().describe('搜索关键词'),
      }),
      execute: async ({ query }) => {
        return { results: [`关于 ${query} 的文档内容...`] }
      },
    }),
    createTodo: tool({
      description: '创建一个待办事项',
      inputSchema: z.object({
        title: z.string().describe('待办标题'),
        priority: z.enum(['low', 'medium', 'high']).describe('优先级'),
      }),
      execute: async ({ title, priority }) => {
        return { id: 'todo_1', title, priority, created: true }
      },
    }),
    sendEmail: tool({
      description: '发送邮件',
      inputSchema: z.object({
        to: z.string().describe('收件人邮箱'),
        subject: z.string().describe('邮件主题'),
        body: z.string().describe('邮件正文'),
      }),
      execute: async ({ to, subject, body }) => {
        return { sent: true, to, subject }
      },
    }),
  },
  stopWhen: stepCountIs(5),
  prompt: '帮我查一下 React Server Components 的文档,然后创建一个待办提醒我学习',
})

模型会先调用 searchDocs,拿到结果后再调用 createTodo,最后用自然语言总结执行结果。

本课产物

  • ✅ 理解 Tool Call 四步闭环
  • ✅ 用 Zod 定义工具参数
  • ✅ generateText + tools 的完整用法
  • ✅ 多工具协作
  • ✅ 工具执行过程的追踪

完整代码在 basic/examples/05-agent/01-tool-call/index.ts

试试看

bash
cd daqi-ai-agent
pnpm exec tsx basic/examples/05-agent/01-tool-call/index.ts
  1. 问一个需要工具的问题(如「现在几点了」或「帮我算 234 × 567」)——观察工具调用日志
  2. 问一个不需要工具的问题——确认模型会直接回答,而不是强制调用工具
  3. 故意问一个超出所有工具能力范围的问题,看模型如何应对

面试追问

Q:Tool Call 的执行者是模型还是应用?

应用。模型只决定"调用哪个工具、传什么参数",实际执行是应用层代码完成的。模型生成的是一个 JSON 格式的调用指令(工具名 + 参数),应用层解析后执行对应的函数,再把结果回传给模型。

Q:模型怎么知道该调用哪个工具?

靠工具的 description 和参数的 describe。这些描述会作为 System Prompt 的一部分传给模型,模型根据用户的问题和工具的描述做出选择。描述越清晰准确,模型选择的准确率越高。

Q:工具太多时模型会不会选错?

会。工具数量超过 10–15 个时,模型的选择准确率会下降。常见做法是用更精确的 description 区分工具,或者把工具分组,让不同场景只加载必要的工具子集。

面向前端工程师和独立开发者的 AI 应用工程课程