AI Agent 与工具调用

普通聊天机器人只能"说"——输出文本。Agent 能"做"——查数据库、调 API、操作文件系统。这个能力的核心是 Tool Calling:AI 决定调用什么工具、传什么参数,你的代码执行工具,再把结果返回给 AI 继续推理。本章从原理到实战讲清工具调用和 Agent 循环。

1. Tool Calling 原理

用户: "帮我查一下上海今天的天气"

AI 思考: 这需要查天气 API → 调用 getWeather 工具
    ↓
Tool Call: getWeather({ city: "上海" })
    ↓
你的代码执行: fetch('https://api.weather.com/...') → { temp: 28, desc: "多云" }
    ↓
AI 收到结果: "上海今天 28°C,多云"
    ↓
AI 最终回复: "上海今天天气多云,气温 28°C,建议带把伞。"

关键点:AI 不执行代码——它只输出一个 JSON(工具名 + 参数),你的服务端代码执行实际操作,再把结果喂回 AI。这确保了安全性和可控性。

底层原理:Function Calling 协议

Tool Calling 的底层是 AI 模型的 Function Calling 能力。当你把工具定义(名称、描述、参数 Schema)发给模型时,模型在生成回复时会判断是否需要调用工具。如果需要,它不输出文本,而是输出一个特殊的 tool_calls JSON 结构,包含工具名和参数。

AI 是怎么“决定”调用哪个工具的?它根据工具的 description 和用户的问题进行语义匹配。模型在训练时学习了“什么意图应该用什么工具”的模式。所以工具的 description 非常关键——它直接影响 AI 能否正确选择工具。

2. AI SDK 中的工具定义

2.1 基础工具

tool() 函数定义工具。一个工具由三部分组成:

  • description:告诉 AI 这个工具做什么、什么时候该用它。AI 根据这个描述来决定是否调用
  • parameters:用 zod Schema 定义参数结构。AI SDK 把它转换成 JSON Schema 发给模型,模型按 Schema 生成参数
  • execute:实际执行逻辑,接收 AI 生成的参数,返回结果
// lib/ai-tools.ts
import { tool } from 'ai'
import { z } from 'zod'
 
export const weatherTool = tool({
  description: '查询指定城市的天气信息',
  parameters: z.object({
    city: z.string().describe('城市名称,如"上海"、"北京"'),
  }),
  execute: async ({ city }) => {
    const response = await fetch(
      `https://api.weatherapi.com/v1/current.json?key=${process.env.WEATHER_API_KEY}&q=${city}`,
    )
    const data = await response.json()
    return {
      city,
      temperature: data.current.temp_c,
      condition: data.current.condition.text,
      humidity: data.current.humidity,
    }
  },
})

2.2 在 streamText 中使用

// app/api/chat/route.ts
import { streamText } from 'ai'
import { openai } from '@ai-sdk/openai'
import { weatherTool } from '@/lib/ai-tools'
 
export async function POST(request: Request) {
  const { messages } = await request.json()
 
  const result = streamText({
    model: openai('gpt-4o'),
    messages,
    tools: {
      getWeather: weatherTool,
    },
    maxSteps: 5,  // 允许最多 5 轮工具调用
  })
 
  return result.toDataStreamResponse()
}

maxSteps 是关键参数——它决定 AI 可以连续调用多少轮工具。设为 1 表示只能调用一次,设为 5 表示可以连续调用 5 次(多步推理)。

2.3 多工具协作

定义多个工具,AI 自己决定用哪个:

import { tool } from 'ai'
import { z } from 'zod'
 
// 搜索知识库
const searchKnowledge = tool({
  description: '在知识库中搜索相关文档,当用户问到公司内部信息时使用',
  parameters: z.object({
    query: z.string().describe('搜索关键词'),
  }),
  execute: async ({ query }) => {
    const chunks = await searchSimilarChunks(query, workspaceId)
    return chunks.map((c) => ({ content: c.content, source: c.documentName }))
  },
})
 
// 查询数据库
const queryDatabase = tool({
  description: '查询业务数据,如用户数、收入、Token 用量等统计信息',
  parameters: z.object({
    metric: z.enum(['user_count', 'revenue', 'token_usage', 'active_users']),
    timeRange: z.enum(['today', 'this_week', 'this_month', 'this_year']),
  }),
  execute: async ({ metric, timeRange }) => {
    const result = await getMetric(metric, timeRange)
    return { metric, timeRange, value: result.value, unit: result.unit }
  },
})
 
// 发送邮件
const sendEmail = tool({
  description: '发送邮件给团队成员',
  parameters: z.object({
    to: z.string().email().describe('收件人邮箱'),
    subject: z.string().describe('邮件主题'),
    body: z.string().describe('邮件正文'),
  }),
  execute: async ({ to, subject, body }) => {
    await resend.emails.send({ from: '[email protected]', to, subject, html: body })
    return { success: true, message: `邮件已发送给 ${to}` }
  },
})
 
// 在 API 中使用
const result = streamText({
  model: openai('gpt-4o'),
  messages,
  tools: { searchKnowledge, queryDatabase, sendEmail },
  maxSteps: 5,
})

当用户说"帮我查一下这个月的 Token 用量,然后发邮件给 [email protected]"时,AI 会:

  1. 调用 queryDatabase({ metric: 'token_usage', timeRange: 'this_month' })
  2. 收到结果后,调用 sendEmail({ to: '[email protected]', subject: '本月 Token 用量报告', body: '...' })
  3. 最终回复:"已查询到本月 Token 用量为 xxx,并已发送邮件给 [email protected]"

3. 工具参数设计

3.1 description 是关键

AI 根据 description 决定是否调用工具。描述要具体、明确

// ❌ 差的描述 — AI 不知道什么时候用
tool({ description: '搜索', ... })
 
// ✅ 好的描述 — AI 明确知道使用场景
tool({ description: '在知识库中搜索公司内部文档和产品文档,当用户提问与公司产品、政策、流程相关的问题时使用', ... })

3.2 参数的 describe

每个参数的 .describe() 帮助 AI 正确填充参数值:

parameters: z.object({
  // ❌ AI 不知道格式
  date: z.string(),
 
  // ✅ AI 知道该传什么格式
  date: z.string().describe('日期,格式为 YYYY-MM-DD,如 2024-03-15'),
})

3.3 枚举约束

z.enum 限制参数取值范围,避免 AI 传入无效值:

parameters: z.object({
  // ❌ AI 可能传入任意字符串
  priority: z.string(),
 
  // ✅ 限定取值范围
  priority: z.enum(['low', 'medium', 'high', 'urgent']).describe('任务优先级'),
})

4. Agent 循环(多步推理)

4.1 什么是 Agent

Agent = LLM + 工具 + 循环。普通聊天机器人只能做一次“问答”,而 Agent 能够自主规划和执行多步任务。这基于 ReAct 模式(Reasoning + Acting)——AI 先思考需要什么信息,然后调用工具获取,观察结果后再决定下一步。这个“观察-思考-行动”循环持续进行,直到任务完成:

观察: 用户想知道竞品分析报告
思考: 需要先搜索知识库
行动: 调用 searchKnowledge("竞品分析")
观察: 找到了 3 段相关内容,但信息不够完整
思考: 需要查询最新的市场数据
行动: 调用 queryDatabase("market_share", "this_quarter")
观察: 获得了市场份额数据
思考: 现在信息足够了,可以生成报告
行动: 输出最终回复

4.2 AI SDK 的 maxSteps

maxSteps 控制 Agent 的推理深度——即最多允许多少轮“调用工具 → 获取结果 → 再次思考”的循环。它既是能力参数(设太小 Agent 不能完成复杂任务),也是安全护栏(防止无限循环消耗 Token)。建议从 5 开始,根据任务复杂度调整:

const result = streamText({
  model: openai('gpt-4o'),
  messages,
  tools: { searchKnowledge, queryDatabase, createChart },
  maxSteps: 10,  // 允许最多 10 轮推理
})

AI SDK 内部的循环逻辑(每一步都是一次完整的 LLM 调用):

  1. AI 输出 → 如果包含 tool_call → 执行工具 → 把工具结果作为新的 tool 角色消息追加到历史
  2. 再次调用 AI(带上完整历史 + 工具结果)→ AI 看到工具返回的数据,决定是继续调用其他工具还是生成最终回复
  3. 直到 AI 输出纯文本(finishReason = 'stop')或达到 maxSteps

注意:每一步都是一次完整的 API 调用,会产生 Token 消耗。maxSteps=5 意味着一次用户提问可能触发 5 次 LLM 调用,成本是普通对话的 5 倍。这也是为什么 maxSteps 需要限制。

4.3 服务端 Agent(generateText)

有些 Agent 任务不需要流式输出——在后台完成即可:

// lib/agents/research-agent.ts
import { generateText } from 'ai'
import { openai } from '@ai-sdk/openai'
 
export async function runResearchAgent(topic: string) {
  const { text, steps } = await generateText({
    model: openai('gpt-4o'),
    system: `你是一个研究助手。使用提供的工具收集信息,然后写一份简洁的研究报告。`,
    prompt: `研究主题:${topic}`,
    tools: {
      webSearch: tool({
        description: '搜索互联网获取最新信息',
        parameters: z.object({ query: z.string() }),
        execute: async ({ query }) => {
          // 调用搜索 API
          return await searchWeb(query)
        },
      }),
      readUrl: tool({
        description: '读取网页内容',
        parameters: z.object({ url: z.string().url() }),
        execute: async ({ url }) => {
          const response = await fetch(url)
          const html = await response.text()
          return extractMainContent(html)
        },
      }),
    },
    maxSteps: 10,
  })
 
  // steps 包含完整的推理过程
  console.log(`Agent 完成,共 ${steps.length} 步`)
  return text
}

steps 数组记录了 Agent 的每一步操作——可以用于审计、调试和展示推理过程。

5. 确认型工具

有些工具有副作用(发邮件、删除数据、支付),不能让 AI 自动执行。设计原则:只读操作可以自动执行,写操作必须用户确认。实现方式是定义工具时不提供 execute 函数——AI 仍然会生成工具调用,但服务端不执行,而是把调用信息转发到客户端,由客户端展示确认对话框:

// 服务端:定义工具但不自动执行
const deleteUser = tool({
  description: '删除用户账号(需要用户确认)',
  parameters: z.object({
    userId: z.string(),
    reason: z.string(),
  }),
  // 不提供 execute — 这是一个"确认型工具"
})
 
// 服务端 API
const result = streamText({
  model: openai('gpt-4o'),
  messages,
  tools: { deleteUser },
})
// 客户端:拦截工具调用,展示确认对话框
'use client'
 
import { useChat } from 'ai/react'
 
export function ChatWithConfirmation() {
  const { messages, ... } = useChat({
    api: '/api/chat',
    onToolCall: async ({ toolCall }) => {
      if (toolCall.toolName === 'deleteUser') {
        // 展示确认对话框
        const confirmed = await showConfirmDialog(
          `确认删除用户 ${toolCall.args.userId}?原因:${toolCall.args.reason}`,
        )
 
        if (confirmed) {
          // 用户确认 → 执行操作
          await deleteUserAction(toolCall.args.userId)
          return { success: true }
        } else {
          return { success: false, message: '用户取消了操作' }
        }
      }
    },
  })
}

6. 工具结果在 UI 中展示

工具调用的结果可以用自定义组件渲染(而非纯文本)——这是 Generative UI 的基础(下一章详讲):

// 消息渲染:检测工具调用并展示特殊 UI
{messages.map((msg) => (
  <div key={msg.id}>
    {msg.content && <Markdown content={msg.content} />}
 
    {/* 渲染工具调用结果 */}
    {msg.toolInvocations?.map((invocation) => {
      if (invocation.state === 'result') {
        switch (invocation.toolName) {
          case 'getWeather':
            return <WeatherCard key={invocation.toolCallId} data={invocation.result} />
          case 'queryDatabase':
            return <MetricCard key={invocation.toolCallId} data={invocation.result} />
          default:
            return <pre key={invocation.toolCallId}>{JSON.stringify(invocation.result, null, 2)}</pre>
        }
      }
      // 工具正在执行
      return <div key={invocation.toolCallId} className="animate-pulse">正在查询...</div>
    })}
  </div>
))}

7. 安全与限制

7.1 工具权限控制

不同用户/角色能使用的工具不同:

function getToolsForUser(role: 'admin' | 'member' | 'viewer') {
  const baseTools = { searchKnowledge, getWeather }
 
  if (role === 'admin') {
    return { ...baseTools, queryDatabase, sendEmail, deleteUser }
  }
  if (role === 'member') {
    return { ...baseTools, queryDatabase }
  }
  return baseTools // viewer 只有基础工具
}
 
// API 中
const result = streamText({
  model: openai('gpt-4o'),
  messages,
  tools: getToolsForUser(session.user.role),
  maxSteps: 5,
})

7.2 工具执行限制

防止 Agent 无限循环或滥用工具:

const result = streamText({
  model: openai('gpt-4o'),
  messages,
  tools,
  maxSteps: 5,          // 限制最大步数
  toolChoice: 'auto',   // 'auto' | 'required' | 'none' | { type: 'tool', toolName: '...' }
})
  • auto(默认):AI 自己根据上下文决定是否调用工具。大多数场景用这个
  • required:强制 AI 至少调用一次工具。适合“每次都需要查询数据”的场景
  • none:禁止调用工具。用于 Agent 最后一步强制生成文本回复
  • 指定工具:强制调用某个特定工具。适合工作流中明确知道该调哪个工具的场景

7.3 输入验证

工具的 execute 函数中始终验证输入——不要信任 AI 传入的参数:

execute: async ({ userId }) => {
  // 验证 userId 格式
  if (!isValidUUID(userId)) {
    return { error: '无效的用户 ID' }
  }
 
  // 验证权限
  const hasPermission = await checkPermission(currentUser, 'delete', userId)
  if (!hasPermission) {
    return { error: '没有权限执行此操作' }
  }
 
  // 执行操作
  await deleteUser(userId)
  return { success: true }
}

本章小结

  • Tool Calling:AI 输出 JSON(工具名 + 参数)→ 你的代码执行 → 结果返回 AI
  • 工具定义description 描述使用场景,parameters 用 zod 约束参数,execute 执行逻辑
  • 多步推理maxSteps 控制 Agent 循环深度,AI 反复调用工具直到完成任务
  • 确认型工具:不提供 execute,在客户端拦截并展示确认对话框
  • 安全:按角色分配工具、限制最大步数、在 execute 中验证输入和权限

下一章讲流式 UI 与生成式界面。