工具调用机制
要点
- 工具调用(Tool Calling)让大语言模型从「只能生成文本」变成「能调用外部工具执行操作」
- 模型本身并不执行工具,它只负责输出「我想调用哪个工具、参数是什么」,真正的执行由你的代码完成
- 每个工具需要一份 JSON Schema 格式的描述,包含
name、description、parameters三个字段 - OpenAI 和 Claude 的工具调用 API 在结构上高度一致,区别主要在参数命名和响应格式的细节上
- 工具调用的完整流程是一个循环:用户提问 → 模型决策 → 参数提取 → 执行工具 → 结果返回 → 模型生成最终回答
- Function Calling 是工具调用的早期形式,两者在概念上可以视为同一件事的不同叫法
- 在 Hono 后端中实现工具调用,核心是写好工具注册表和调用循环
1. 什么是工具调用
如果你只用过大语言模型的 Chat 接口,你对模型的能力认知可能是:给它一段文字,它返回一段文字。
假设你做了一个客服机器人,用户问「北京今天天气怎么样」。模型的训练数据有截止日期,它不可能知道今天的天气。没有工具调用的话,你只能让它回答「我无法获取实时信息」。
工具调用要解决的问题:让模型能够调用你预先定义好的函数,拿到真实世界的执行结果,再基于这个结果生成回答。
以天气场景为例,流程是这样的:
- 你预先定义一个
get_weather函数,参数是城市名 - 用户问「北京今天天气怎么样」
- 模型不直接回答,而是输出:「我需要调用
get_weather,参数是{ city: "北京" }」 - 你的代码执行这个函数,拿到
{ temperature: 28, condition: "晴" } - 把这个结果喂回给模型
- 模型基于这个数据生成:「北京今天 28 度,晴天」
模型自己并不去请求天气 API。它只是告诉你「我该调用哪个函数、传什么参数」。真正发 HTTP 请求、拿数据、把结果传回去的,是你写的代码。
模型是一个决策引擎,不是一个执行引擎。
2. 为什么需要工具调用
也许你会先想到:直接在 Prompt 里告诉模型答案不就行了?
但这种方式有几个局限:
- 知识有截止日期。模型训练数据有时间边界,无法回答训练之后的事情
- 无法执行操作。模型不能帮你查数据库、发邮件、创建订单
- 计算不可靠。让模型算
387 * 492,它可能给你一个看似合理但错误的结果;调用计算器函数,结果是确定的 - 无法访问私有数据。用户的具体订单信息、账户余额不会出现在训练数据里
工具调用把模型的角色从「全能回答者」调整成了「任务调度者」。它负责理解用户意图、选择合适的工具、组织参数;数据获取和操作执行交给外部工具。
3. 工具的三要素:name、description、parameters
不管是 OpenAI 还是 Claude,一个工具的定义都需要三个字段。
name
工具的唯一标识。命名建议用 snake_case,一眼能看出功能:get_weather、search_orders、calculate_total。不要写成 tool1、func_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里列出所有参数,每个参数有type和descriptionrequired数组声明哪些参数是必填的- 给参数写
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_reason 为 tool_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) }]
}
)
}关键差异汇总:
| 维度 | OpenAI | Claude |
|---|---|---|
| 参数 Schema 字段名 | parameters | input_schema |
| 工具定义结构 | 嵌套在 type: 'function' 里 | 直接写工具对象 |
| 模型参数解析结果 | JSON 字符串,需 JSON.parse | 已经是 JS 对象 |
| 工具结果消息角色 | role: 'tool' | role: 'user' + tool_result block |
| 关联 ID 字段 | tool_call_id | tool_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 这层嵌套。
对于写代码来说,不需要纠结用哪个名字。底层机制是一样的:
- 你用 JSON Schema 定义一组工具
- 模型根据用户输入决定要不要调用、调用哪个
- 你的代码执行工具,把结果传回去
- 模型基于结果生成最终回答
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 路由,把多个工具串起来处理复杂任务。