工具调用机制

要点

  • 工具调用(Tool Calling)让大语言模型从「只能生成文本」变成「能调用外部工具执行操作」
  • 模型本身并不执行工具,它只负责输出「我想调用哪个工具、参数是什么」,真正的执行由你的代码完成
  • 每个工具需要一份 JSON Schema 格式的描述,包含 namedescriptionparameters 三个字段
  • OpenAI 和 Claude 的工具调用 API 在结构上高度一致,区别主要在参数命名和响应格式的细节上
  • 工具调用的完整流程是一个循环:用户提问 → 模型决策 → 参数提取 → 执行工具 → 结果返回 → 模型生成最终回答
  • Function Calling 是工具调用的早期形式,两者在概念上可以视为同一件事的不同叫法
  • 在 Hono 后端中实现工具调用,核心是写好工具注册表和调用循环

1. 什么是工具调用

如果你只用过大语言模型的 Chat 接口,你对模型的能力认知可能是:给它一段文字,它返回一段文字。

假设你做了一个客服机器人,用户问「北京今天天气怎么样」。模型的训练数据有截止日期,它不可能知道今天的天气。没有工具调用的话,你只能让它回答「我无法获取实时信息」。

工具调用要解决的问题:让模型能够调用你预先定义好的函数,拿到真实世界的执行结果,再基于这个结果生成回答。

以天气场景为例,流程是这样的:

  1. 你预先定义一个 get_weather 函数,参数是城市名
  2. 用户问「北京今天天气怎么样」
  3. 模型不直接回答,而是输出:「我需要调用 get_weather,参数是 { city: "北京" }
  4. 你的代码执行这个函数,拿到 { temperature: 28, condition: "晴" }
  5. 把这个结果喂回给模型
  6. 模型基于这个数据生成:「北京今天 28 度,晴天」

模型自己并不去请求天气 API。它只是告诉你「我该调用哪个函数、传什么参数」。真正发 HTTP 请求、拿数据、把结果传回去的,是你写的代码。

模型是一个决策引擎,不是一个执行引擎。

2. 为什么需要工具调用

也许你会先想到:直接在 Prompt 里告诉模型答案不就行了?

但这种方式有几个局限:

  1. 知识有截止日期。模型训练数据有时间边界,无法回答训练之后的事情
  2. 无法执行操作。模型不能帮你查数据库、发邮件、创建订单
  3. 计算不可靠。让模型算 387 * 492,它可能给你一个看似合理但错误的结果;调用计算器函数,结果是确定的
  4. 无法访问私有数据。用户的具体订单信息、账户余额不会出现在训练数据里

工具调用把模型的角色从「全能回答者」调整成了「任务调度者」。它负责理解用户意图、选择合适的工具、组织参数;数据获取和操作执行交给外部工具。

3. 工具的三要素:name、description、parameters

不管是 OpenAI 还是 Claude,一个工具的定义都需要三个字段。

name

工具的唯一标识。命名建议用 snake_case,一眼能看出功能:get_weathersearch_orderscalculate_total。不要写成 tool1func_a——模型选择工具时依赖名字和描述,名字越清晰,模型选对的概率越高。

description

用自然语言告诉模型「这个工具能做什么、什么时候该用它」:

// src/tools/get-weather.ts
// 好的 description
description: '获取指定城市的当前天气信息,包括温度、天气状况、湿度。当用户询问某个地方的天气时使用。'
 
// 差的 description
description: '查天气的'

好的 description 应该包含:工具做什么、在什么场景下使用、如果有限制条件也要说明。

parameters

用 JSON Schema 描述函数接受的参数:

// src/tools/get-weather.ts
parameters: {
  type: 'object',
  properties: {
    city: {
      type: 'string',
      description: '城市名称,例如「北京」「上海」'
    },
    unit: {
      type: 'string',
      enum: ['celsius', 'fahrenheit'],
      description: '温度单位,默认 celsius'
    }
  },
  required: ['city']
}

几个要点:

  • properties 里列出所有参数,每个参数有 typedescription
  • required 数组声明哪些参数是必填的
  • 给参数写 description 是有用的——模型会参考这段描述来决定填什么值
  • enum 限制参数的可选值,防止模型生成不合法的值

三个字段组合在一起就是一个完整的工具定义:

// src/tools/get-weather.ts
export const getWeatherTool = {
  name: 'get_weather',
  description: '获取指定城市的当前天气信息,包括温度、天气状况、湿度。当用户询问某个地方的天气时使用。',
  parameters: {
    type: 'object',
    properties: {
      city: { type: 'string', description: '城市名称,例如「北京」' },
      unit: { type: 'string', enum: ['celsius', 'fahrenheit'], description: '温度单位,默认 celsius' }
    },
    required: ['city']
  }
}

4. OpenAI 与 Claude 的工具调用 API

两家的概念一致,差异在包装方式。直接对比同一个天气工具在两边的写法。

注册工具

OpenAI 的工具定义需要包一层 type: 'function',参数字段叫 parameters

// src/routes/chat-openai.ts
const openaiTools = [
  {
    type: 'function' as const,
    function: {
      name: 'get_weather',
      description: '获取指定城市的当前天气信息',
      parameters: {
        type: 'object',
        properties: { city: { type: 'string', description: '城市名称' } },
        required: ['city']
      }
    }
  }
]
 
const response = await openai.chat.completions.create({
  model: 'gpt-4o',
  messages: [{ role: 'user', content: '北京今天天气怎么样?' }],
  tools: openaiTools
})

Claude 直接写工具对象,参数字段叫 input_schema,且必须传 max_tokens

// src/routes/chat-claude.ts
const claudeTools = [
  {
    name: 'get_weather',
    description: '获取指定城市的当前天气信息',
    input_schema: {
      type: 'object',
      properties: { city: { type: 'string', description: '城市名称' } },
      required: ['city']
    }
  }
]
 
const response = await anthropic.messages.create({
  model: 'claude-sonnet-4-20250514',
  max_tokens: 1024,
  messages: [{ role: 'user', content: '北京今天天气怎么样?' }],
  tools: claudeTools
})

解析模型的决策与返回结果

OpenAI 的响应里,finish_reasontool_calls 时,message.tool_calls 包含调用详情。arguments 是 JSON 字符串,需要手动 JSON.parse。执行完工具后,用 role: 'tool' 的消息把结果传回去:

// src/routes/chat-openai.ts
if (message.tool_calls) {
  for (const call of message.tool_calls) {
    const args = JSON.parse(call.function.arguments)
    const result = await executeGetWeather(args)
    messages.push({
      role: 'tool',
      tool_call_id: call.id,
      content: JSON.stringify(result)
    })
  }
}

Claude 的响应里,content 数组中会出现 tool_use 类型的 block。input 已经是 JS 对象,不需要 JSON.parse。执行完工具后,用 role: 'user' + tool_result block 把结果传回去:

// src/routes/chat-claude.ts
const toolUseBlock = response.content.find((b) => b.type === 'tool_use')
if (toolUseBlock && toolUseBlock.type === 'tool_use') {
  const result = await executeGetWeather(toolUseBlock.input)
  messages.push(
    { role: 'assistant', content: response.content },
    {
      role: 'user',
      content: [{ type: 'tool_result', tool_use_id: toolUseBlock.id, content: JSON.stringify(result) }]
    }
  )
}

关键差异汇总:

维度OpenAIClaude
参数 Schema 字段名parametersinput_schema
工具定义结构嵌套在 type: 'function'直接写工具对象
模型参数解析结果JSON 字符串,需 JSON.parse已经是 JS 对象
工具结果消息角色role: 'tool'role: 'user' + tool_result block
关联 ID 字段tool_call_idtool_use_id

两家 API 的概念完全一致,如果你需要同时对接两家,可以抽一层适配器抹平差异。

5. 完整流程:Hono 路由中的工具调用循环

工具调用通常不止一轮——模型可能在拿到第一个工具结果后发现还需要调用另一个工具。所以需要写一个循环,而不是一次 if/else:

用户提问 → 模型判断是否需要工具
  → 是:输出工具名 + 参数 → 执行工具 → 结果传回模型 → 再次调用模型
  → 否:直接回答

工具注册表

把所有工具集中管理。每个工具文件导出两样东西:工具定义(给模型看的)和执行函数(给代码用的)。

// src/tools/registry.ts
import { getWeatherTool, executeGetWeather } from './get-weather'
import { searchOrdersTool, executeSearchOrders } from './search-orders'
 
export const toolDefinitions = [getWeatherTool, searchOrdersTool]
 
export const toolExecutors: Record<string, (args: any) => Promise<any>> = {
  get_weather: executeGetWeather,
  search_orders: executeSearchOrders
}
// src/tools/get-weather.ts
export const getWeatherTool = {
  type: 'function' as const,
  function: {
    name: 'get_weather',
    description: '获取指定城市的当前天气信息,包括温度、天气状况、湿度',
    parameters: {
      type: 'object',
      properties: { city: { type: 'string', description: '城市名称,例如「北京」' } },
      required: ['city']
    }
  }
}
 
export async function executeGetWeather(args: { city: string }) {
  const url = new URL('https://api.weather.example.com/v1/current')
  url.searchParams.set('city', args.city)
  const res = await fetch(url.toString())
  if (!res.ok) throw new Error(`天气查询失败: ${res.status}`)
  return res.json()
}

调用循环

// src/routes/agent.ts
import { Hono } from 'hono'
import OpenAI from 'openai'
import { toolDefinitions, toolExecutors } from '../tools/registry'
 
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY })
const agentApp = new Hono()
 
agentApp.post('/chat', async (c) => {
  const { message } = await c.req.json<{ message: string }>()
 
  const messages: OpenAI.ChatCompletionMessageParam[] = [
    { role: 'system', content: '你是一个有用的助手,可以调用工具来获取信息。' },
    { role: 'user', content: message }
  ]
 
  const MAX_ROUNDS = 5
  for (let i = 0; i < MAX_ROUNDS; i++) {
    const response = await openai.chat.completions.create({
      model: 'gpt-4o',
      messages,
      tools: toolDefinitions
    })
 
    const choice = response.choices[0]
    messages.push(choice.message as OpenAI.ChatCompletionMessageParam)
 
    // 模型没有请求工具调用 → 最终回答已就绪
    if (choice.finish_reason !== 'tool_calls' || !choice.message.tool_calls) {
      return c.json({ reply: choice.message.content })
    }
 
    // 逐个执行工具,把结果追加到 messages
    for (const toolCall of choice.message.tool_calls) {
      const executor = toolExecutors[toolCall.function.name]
      if (!executor) {
        messages.push({
          role: 'tool',
          tool_call_id: toolCall.id,
          content: JSON.stringify({ error: `未知工具: ${toolCall.function.name}` })
        })
        continue
      }
      try {
        const args = JSON.parse(toolCall.function.arguments)
        const result = await executor(args)
        messages.push({
          role: 'tool',
          tool_call_id: toolCall.id,
          content: JSON.stringify(result)
        })
      } catch (error) {
        messages.push({
          role: 'tool',
          tool_call_id: toolCall.id,
          content: JSON.stringify({ error: `工具执行失败: ${(error as Error).message}` })
        })
      }
    }
  }
 
  return c.json({ reply: '工具调用轮次已达上限,请简化问题后重试。' })
})
 
export default agentApp

循环的退出条件有两种:模型不再请求工具调用(正常结束),或者达到最大轮次(兜底保护)。

6. 工具调用与 Function Calling 的关系

你可能在不同地方看到过这两个词。它们是两件事吗?

不是。它们是同一个概念在不同阶段的名字。

OpenAI 在 2023 年 6 月首次发布这个能力时,用的名字是 Function Calling。当时只有 OpenAI 一家提供这个功能,所以这个名字被广泛使用。后来 Anthropic、Google 等厂商也陆续支持了类似的能力,大家开始用更通用的「Tool Calling」或「工具调用」来指代这类机制。

在 OpenAI 的 API 里,你至今仍能看到 Function Calling 的痕迹——工具定义里的 type: 'function'、响应里的 message.tool_calls[].function.name。Claude 的 API 从一开始就用 Tool Use 来命名,没有 function 这层嵌套。

对于写代码来说,不需要纠结用哪个名字。底层机制是一样的:

  1. 你用 JSON Schema 定义一组工具
  2. 模型根据用户输入决定要不要调用、调用哪个
  3. 你的代码执行工具,把结果传回去
  4. 模型基于结果生成最终回答

7. 几个容易踩的坑

模型可能不调用工具

如果用户提问和工具描述关联度不高,模型可能直接回答而不调用工具。如果希望强制调用,可以设置 tool_choice

// src/routes/chat.ts
const response = await openai.chat.completions.create({
  model: 'gpt-4o',
  messages,
  tools: toolDefinitions,
  tool_choice: { type: 'function', function: { name: 'get_weather' } }
})

tool_choice 可选值:auto(默认,模型自己决定)、required(必须调用工具)、none(禁用工具调用)、指定某个工具(强制调用该工具)。

参数解析可能失败

模型输出的 arguments 不一定总是合法的 JSON。用 try/catch 包住 JSON.parse,把错误信息作为工具结果返回给模型让它自己修正,比直接报错中断体验更好。

工具执行要有超时控制

涉及外部 API 调用的工具需要设置超时。一个慢工具会卡住整个对话循环:

// src/tools/get-weather.ts
export async function executeGetWeather(args: { city: string }) {
  const controller = new AbortController()
  const timeout = setTimeout(() => controller.abort(), 5000)
  try {
    const res = await fetch(
      `https://api.weather.example.com/v1/current?city=${encodeURIComponent(args.city)}`,
      { signal: controller.signal }
    )
    return res.json()
  } finally {
    clearTimeout(timeout)
  }
}

工具描述要精确

模型选择工具主要依据工具名和描述。描述模糊会导致模型在相似工具之间犹豫:

// src/tools/registry.ts
// 模糊
{ name: 'search', description: '搜索功能' }
 
// 精确——模型能区分两个工具的适用场景
{ name: 'search_products', description: '根据关键词搜索商品,返回商品列表。当用户想查找某个商品时使用。' }
{ name: 'search_orders', description: '根据订单号或用户 ID 搜索历史订单。当用户询问订单状态或历史购买记录时使用。' }

总结

回顾一下这篇的要点:

  • 工具调用让模型从「生成文本」升级到「调度工具」,模型做决策,你的代码做执行
  • 每个工具需要三个字段:name(唯一标识)、description(告诉模型什么时候用)、parameters(JSON Schema 定义参数)
  • OpenAI 和 Claude 的工具调用 API 概念一致,差异在参数命名和响应格式的细节上
  • 完整的调用流程是一个循环:模型决策 → 参数提取 → 执行工具 → 结果返回 → 再次调用模型,直到模型不再请求工具
  • Function Calling 和工具调用是同一件事,前者是 OpenAI 早期的叫法
  • 工程落地时注意:参数解析要 try/catch、工具执行要超时控制、工具描述要精确、循环要有最大轮次兜底

下一篇会用这套机制搭一个完整的 Agent 路由,把多个工具串起来处理复杂任务。