很多人第一次看到 AI 自动查天气、读 Excel、搜索网页、操作电脑时,会下意识觉得:大模型已经能直接使用外部世界了。
但从工程实现看,这更像一个精心设计的协作流程:大模型负责判断和生成调用意图,真正执行工具的是我们写的运行时程序。
本文基于一个最小 Node.js Demo,拆开讲清楚 Tool Use 背后的技术逻辑:
- 工具为什么要写成 JSON Schema?
- 大模型返回的
tool_calls到底是什么? - 为什么说
LLM + Tools才是 Agent 的基础形态? - 开发者在其中真正要负责什么?
1. 先打破一个错觉:LLM 本身不会调用 API
大模型的底层能力仍然是语言建模,也就是根据上下文预测下一个 token。它不会天然拥有这些能力:
- 知道实时天气
- 查询数据库
- 读取本地 Excel
- 调用股票接口
- 点击屏幕按钮
- 操作文件系统
如果只看大模型本身,它更像一个被放在服务器里的"缸中大脑":能理解语言、生成语言、推理语言,但不能直接触碰外部世界。
那为什么我们看到的 AI 可以查天气、搜网页、分析表格?
答案是:外部系统把工具能力包装成模型能理解的语言说明,模型再根据说明生成一次"函数调用请求",最后由运行时真正执行。
可以简单理解为:
txt
用户问题
↓
大模型判断是否需要工具
↓
大模型生成 tool_calls
↓
开发者运行时执行真实函数/API
↓
工具结果返回给大模型
↓
大模型组织最终答案
这就是 Tool Use 的核心。
2. 工具的本质:把函数降维成语言
在传统软件里,工具就是一个函数。
比如一个查天气函数:
js
function get_weather(city) {
if (city === "北京") return "晴,25°C,湿度40%";
if (city === "上海") return "多云,28°C,湿度65%";
if (city === "深圳") return "阵雨,30°C,湿度80%";
return "未能找到该城市的天气信息";
}
对普通程序来说,调用它很简单:
js
get_weather("北京");
但大模型不能直接读取你的代码,也不会自动知道:
- 这个函数叫什么
- 它能做什么
- 它需要哪些参数
- 参数是什么类型
- 什么时候应该调用它
所以我们需要把函数描述成模型能理解的形式。常见做法就是使用 JSON Schema:
js
const tools = [
{
type: "function",
function: {
name: "get_weather",
description: "获取指定城市的天气信息",
parameters: {
type: "object",
properties: {
city: {
type: "string",
description: "城市名称"
}
},
required: ["city"]
}
}
}
];
这段配置不是给 JavaScript 引擎看的,而是给大模型看的。
它相当于告诉模型:
你现在有一个叫
get_weather的工具。当用户想知道某个城市天气时,可以调用它。
调用时必须传一个
city字段,类型是字符串。
这一步可以叫"认知植入":我们把软件世界里的函数,翻译成自然语言和结构化参数描述,让大模型知道自己"可以请求使用哪些工具"。
3. 大模型做的不是执行,而是生成调用意图
假设用户问:
txt
今天北京的天气怎么样?
如果没有工具,大模型只能根据训练语料猜一个答案,这显然不可靠。
有了 get_weather 工具描述后,模型会判断:
- 用户问的是天气
- 我自己没有实时天气数据
- 当前上下文里有一个
get_weather工具 - 需要传入城市名
北京
于是模型不会直接回答用户,而是返回类似这样的结构:
json
{
"role": "assistant",
"content": null,
"tool_calls": [
{
"id": "call_xxx",
"type": "function",
"function": {
"name": "get_weather",
"arguments": "{\"city\":\"北京\"}"
}
}
]
}
注意这里的关键点:这不是函数执行结果,而是一次函数调用请求。
大模型只是生成了:
- 要调用哪个函数:
get_weather - 参数是什么:
{"city":"北京"} - 这次调用的 ID:
call_xxx
真正的函数并没有被模型执行。
4. Runtime 介入:开发者代码真正执行工具
接下来轮到运行时程序工作。
在 Node.js 里,我们要检查模型返回里有没有 tool_calls:
js
const response = await sendMessage(messages);
const message = response.choices[0].message;
messages.push({
role: message.role,
content: message.content,
tool_calls: message.tool_calls
});
if (message.tool_calls) {
const toolCall = message.tool_calls[0];
const args = JSON.parse(toolCall.function.arguments);
if (toolCall.function.name === "get_weather") {
const weather = get_weather(args.city);
messages.push({
role: "tool",
content: weather,
tool_call_id: toolCall.id
});
}
}
这里有三个工程细节非常重要。
第一,arguments 通常是字符串,需要 JSON.parse:
js
const args = JSON.parse(toolCall.function.arguments);
第二,工具执行是普通代码完成的:
js
const weather = get_weather(args.city);
这里的 get_weather 可以是本地函数,也可以进一步封装真实的天气 API、数据库查询、RPC 调用、浏览器自动化脚本等。
第三,工具结果不是直接返回给用户,而是作为一条 role: "tool" 的消息重新放回上下文:
js
messages.push({
role: "tool",
content: weather,
tool_call_id: toolCall.id
});
tool_call_id 用来把工具结果和模型之前发起的那次调用关联起来。尤其当一次回复里有多个工具调用时,这个 ID 非常关键。
5. 最后一步:把工具结果交还给大模型组织答案
工具执行完后,我们还要再次请求模型:
js
const finalRes = await sendMessage(messages);
console.log("最终模型回答:", finalRes.choices[0].message.content);
为什么不直接把 天气信息:晴,25°C,湿度40% 返回给用户?
因为工具结果通常只是原始数据,真正面向用户的回答还需要:
- 补全自然语言表达
- 结合用户问题组织上下文
- 过滤无关字段
- 处理多个工具结果
- 给出更符合对话语境的答案
比如工具返回:
txt
晴,25°C,湿度40%
模型可以组织成:
txt
今天北京天气晴,气温约 25°C,湿度 40%,整体比较舒适,适合外出。
这就是 Tool Use 的完整闭环。
6. 一个更完整的最小示例
下面是根据 Demo 整理后的版本,同时包含天气查询和股票收盘价查询两个工具。
为了让重点放在 Tool Use 流程上,下面的 get_weather 和 get_closing_price 使用的是本地 mock 数据。真实项目里,只需要把这两个函数替换成天气 API、行情 API、数据库查询或其他业务服务。
js
import { OpenAI } from "openai";
import dotenv from "dotenv";
dotenv.config();
const client = new OpenAI({
apiKey: process.env.DEEPSEEK_API_KEY,
baseURL: process.env.DEEPSEEK_BASE_URL
});
const tools = [
{
type: "function",
function: {
name: "get_closing_price",
description: "获取指定股票的收盘价",
parameters: {
type: "object",
properties: {
name: {
type: "string",
description: "股票名称"
}
},
required: ["name"]
}
}
},
{
type: "function",
function: {
name: "get_weather",
description: "获取指定城市的天气信息",
parameters: {
type: "object",
properties: {
city: {
type: "string",
description: "城市名称"
}
},
required: ["city"]
}
}
}
];
function get_closing_price(name) {
if (name === "青岛啤酒") return "67.92";
if (name === "贵州茅台") return "1488.21";
return "未能找到股票";
}
function get_weather(city) {
if (city === "北京") return "晴,25°C,湿度40%";
if (city === "上海") return "多云,28°C,湿度65%";
if (city === "深圳") return "阵雨,30°C,湿度80%";
return "未能找到该城市的天气信息";
}
async function sendMessage(messages) {
return client.chat.completions.create({
model: process.env.DEEPSEEK_MODEL,
messages,
tools,
tool_choice: "auto"
});
}
async function main() {
const messages = [
{
role: "user",
content: "今天北京的天气怎么样?"
}
];
const response = await sendMessage(messages);
const assistantMessage = response.choices[0].message;
messages.push({
role: assistantMessage.role,
content: assistantMessage.content,
tool_calls: assistantMessage.tool_calls
});
if (!assistantMessage.tool_calls) {
console.log(assistantMessage.content);
return;
}
for (const toolCall of assistantMessage.tool_calls) {
const args = JSON.parse(toolCall.function.arguments);
let result;
if (toolCall.function.name === "get_weather") {
result = get_weather(args.city);
} else if (toolCall.function.name === "get_closing_price") {
result = get_closing_price(args.name);
} else {
result = "未知工具";
}
messages.push({
role: "tool",
content: result,
tool_call_id: toolCall.id
});
}
const finalRes = await sendMessage(messages);
console.log(finalRes.choices[0].message.content);
}
main();
这个示例虽然很小,但已经包含了 Tool Use 的核心结构:
txt
定义工具 → 模型选择工具 → 程序执行工具 → 结果回填上下文 → 模型生成最终回复
7. 为什么 description 很关键?
很多人写工具调用时,只关注函数名和参数,却忽略了 description。
实际上,description 会直接影响模型是否能选对工具。
比如:
js
description: "获取指定城市的天气信息"
这比下面这种描述更清楚:
js
description: "查询信息"
工具描述越模糊,模型越容易出现这些问题:
- 该调用工具时没有调用
- 不该调用工具时乱调用
- 选错工具
- 生成错误参数
- 把同义词理解错
好的工具描述应该做到:
- 明确工具能做什么
- 明确工具不能做什么
- 参数描述具体
- 返回值最好也在描述里说明
- 多个工具之间的边界不要重叠得太严重
例如股票工具可以进一步写清楚:
js
description: "根据中文股票名称获取该股票的收盘价,返回价格字符串"
这样模型更容易理解它适合回答"青岛啤酒收盘价是多少"这类问题。
8. Tool Use 和 Agent 是什么关系?
一个常见说法是:
txt
LLM + Tools = Agent
这句话有一定道理,但还不完整。
更准确地说,Tool Use 是 Agent 的基础能力之一。一个更完整的 Agent 往往还需要:
- 任务规划:把复杂目标拆成多个步骤
- 记忆管理:保存用户偏好、历史状态、执行结果
- 工具选择:在多个工具中选择合适的一个或多个
- 结果校验:判断工具返回是否可信、是否需要重试
- 权限控制:限制模型能做什么,避免误操作
- 运行时编排:循环调用模型和工具,直到任务完成
所以,Tool Use 解决的是"模型如何接触外部能力"的问题。
Agent 解决的是"模型如何围绕目标持续行动"的问题。
9. 开发者真正要负责什么?
Tool Use 看起来像是模型能力,实际上工程质量很大程度取决于开发者。
你至少要负责这些事情。
1. 定义清晰的工具边界
不要把一个工具写成万能接口。
坏例子:
js
name: "do_something"
description: "处理用户请求"
好例子:
js
name: "get_weather"
description: "获取指定城市的天气信息"
工具越具体,模型越容易稳定调用。
2. 校验模型生成的参数
模型生成的参数不一定可信。
即使 JSON Schema 声明了 city 是字符串,运行时仍然应该做校验:
js
if (typeof args.city !== "string" || !args.city.trim()) {
throw new Error("city 参数非法");
}
在真实业务里,尤其要防止:
- 参数缺失
- 类型错误
- 枚举值非法
- SQL 注入
- 越权访问
- 高风险操作未确认
3. 控制工具权限
不是所有工具都应该无条件交给模型。
查询天气、查公开数据通常风险较低;但如果工具能发邮件、删文件、下单、转账,就必须加入权限控制和用户确认。
一个实用原则是:
txt
读操作可以相对自动化,写操作必须更谨慎。
4. 处理失败和重试
工具可能失败:
- API 超时
- 网络错误
- 参数不合法
- 数据不存在
- 第三方服务限流
不要假设工具总能返回正常结果。运行时应该把错误包装成模型能理解的信息,再交回上下文,让模型决定下一步怎么回复用户。
10. 总结
Tool Use 不是魔法,它是一套很清晰的工程协作机制:
txt
工具定义:开发者把函数能力描述给模型
意图识别:模型判断是否需要调用工具
调用生成:模型输出结构化 tool_calls
运行执行:开发者代码执行真实函数或 API
结果回填:工具结果作为上下文返回给模型
最终回复:模型基于工具结果生成自然语言答案
大模型本身不直接调用 API,也不真正操作数据库。它做的是理解问题、选择工具、生成调用请求。
真正连接外部世界的,是开发者写的 runtime。
这也是为什么 Tool Use 是理解 AI Agent 的第一块拼图:它让一个只会生成语言的模型,开始具备"借助外部工具完成任务"的能力。