Tool Calling 不是让模型执行代码:而是在建立行动闭环
一旦你真的把 LLM 接进业务系统,很快就会碰到一个分界点:用户不再只问“解释一下”,而开始要求系统“帮我查一下”“帮我算一下”“帮我看看这个文件”。这时候,模型光会组织自然语言已经不够了,系统必须开始接触外部能力。
但这里有个特别危险的误解:很多人说“让模型调用函数”,脑子里想象的是模型直接进入运行时、拿到执行权限、自己把代码跑了。不是。模型不会执行你的函数。它只会根据你暴露的工具描述,生成一份结构化调用请求。真正执行动作的始终是你的程序。
这条边界如果不先说清楚,后面一谈 Agent,很容易滑向一种模糊又不安全的幻觉:好像只要给模型接几个函数,它就天然会做事。实际上,它只是开始会“发指令”,而不是获得了“执行权”。
为什么 Tool Calling 是 Agent 架构里的第一道硬分界
没有工具时,模型基本只能停留在文本世界:
- 解释问题
- 改写内容
- 做一些封闭式推理
- 猜一个大概率正确的答案
有了工具以后,系统才第一次形成真正的行动闭环:
- 模型判断要不要借助外部能力
- 模型产出工具名和参数
- 应用决定是否执行,以及怎么执行
- 工具返回真实结果
- 模型基于结果组织最终回答
这和“更会聊天”完全不是一回事。前者是在建系统闭环,后者只是把话说漂亮。
为什么这件事重要
因为只要 Tool Calling 这条边界不清楚,后面几乎所有治理问题都会失控:
- 你不知道权限该放在哪一层
- 你不知道参数校验该由谁做
- 你不知道为什么工具返回值还要再喂回模型
- 你不知道多步调用里到底哪一步出了错
下面这个例子虽然只演示天气查询和加法,但它已经把 Agent 最小行动闭环完整摊开了。
完整代码
// 03-tool-calling-basic.js
// 目标:让 AI 调用函数 (Function Calling)
// 这是 Agent 能够与外部世界交互的基础。
import { GoogleGenerativeAI } from '@google/generative-ai';
import dotenv from 'dotenv';
dotenv.config();
const genAI = new GoogleGenerativeAI(process.env.GEMINI_API_KEY);
// 1. 定义我们的工具函数 (真正的逻辑)
const tools = {
// 模拟一个天气查询 API
getWeather: ({ city }) => {
console.log(`[System] 正在查询 ${city} 的天气...`);
const weatherData = {
"Shanghai": "Sunny, 25°C",
"Beijing": "Cloudy, 18°C",
"London": "Rainy, 12°C"
};
return weatherData[city] || "Unknown weather data for this city.";
},
// 模拟一个简单的计算器
add: ({ a, b }) => {
console.log(`[System] 计算 ${a} + ${b}...`);
return a + b;
}
};
// 2. 定义工具的 Schema (告诉 LLM 这些工具长什么样)
// Gemini 使用标准的 OpenAPI Schema 格式
const toolsSchema = [
{
functionDeclarations: [
{
name: "getWeather",
description: "Get the current weather in a given city.",
parameters: {
type: "OBJECT",
properties: {
city: {
type: "STRING",
description: "The city name, e.g. 'Shanghai', 'New York'",
},
},
required: ["city"],
},
},
{
name: "add",
description: "Add two numbers together.",
parameters: {
type: "OBJECT",
properties: {
a: { type: "NUMBER", description: "First number" },
b: { type: "NUMBER", description: "Second number" },
},
required: ["a", "b"],
},
},
],
},
];
async function main() {
// 3. 初始化带有工具的模型
const model = genAI.getGenerativeModel({
model: "gemini-flash-latest", // 还是用这个最稳的模型
tools: toolsSchema,
});
const chat = model.startChat();
console.log("🤖 Agent with Tools 在线。");
console.log("试试问它:'上海今天天气怎么样?' 或者 '33 加 44 等于多少?'\n");
// 4. 发送一个 Query
const prompt = "上海今天天气怎么样?";
console.log(`User: ${prompt}`);
const result = await chat.sendMessage(prompt);
const response = result.response;
// 5. 检查 LLM 是否想要调用工具
// Gemini 的 response.functionCalls() 会返回调用请求
const functionCalls = response.functionCalls();
if (functionCalls && functionCalls.length > 0) {
const call = functionCalls[0];
const { name, args } = call;
console.log(`\n👉 LLM 决定调用工具: ${name}(${JSON.stringify(args)})`);
// 6. 执行工具代码
const toolResult = tools[name](args);
console.log(`✅ 工具执行结果: ${toolResult}`);
// 7. 把结果喂回给 LLM (这一步很关键)
// 我们必须告诉 LLM: "你刚才调用的 get_weather 结果是 'Sunny, 25°C'"
const result2 = await chat.sendMessage([
{
functionResponse: {
name: name,
response: { result: toolResult } // 格式必须是 { result: ... }
}
}
]);
// 8. 获取最终的自然语言回答
console.log(`\nAI: ${result2.response.text()}`);
} else {
// 如果 LLM 不需要调用工具,直接打印回答
console.log(`\nAI: ${response.text()}`);
}
}
main();先看一个真实运行结果
当前版本运行 node 03-tool-calling-basic.js,你会看到类似下面的输出:
🤖 Agent with Tools 在线。
试试问它:'上海今天天气怎么样?' 或者 '33 加 44 等于多少?'
User: 上海今天天气怎么样?
👉 LLM 决定调用工具: getWeather({"city":"Shanghai"})
[System] 正在查询 Shanghai 的天气...
✅ 工具执行结果: Sunny, 25°C
AI: 上海今天晴,气温 25°C。这个输出很适合放在代码前面看,因为它把整条调用链直接摊开了:模型先决定调用哪个工具,你的程序执行工具,再把结果回给模型生成最终答案。
先看这段代码真正建立了什么
它表面上是个天气 + 计算器 demo,实际上把 Agent 的核心闭环拆成了四层:
- 真实工具实现:你程序里真能执行的能力
- 工具 schema:暴露给模型看的能力说明书
- 模型调用请求:模型根据 schema 产出的结构化指令
- 结果回流:工具执行后的结果再喂给模型生成最终回答
只要这四层在脑子里足够清楚,后面你无论换成哪个框架,基本都不会迷路。
按责任边界拆代码
1. tools:这里才是真正能动手的系统能力
const tools = {
getWeather: ({ city }) => {
console.log(`[System] 正在查询 ${city} 的天气...`);
const weatherData = {
"Shanghai": "Sunny, 25°C",
"Beijing": "Cloudy, 18°C",
"London": "Rainy, 12°C"
};
return weatherData[city] || "Unknown weather data for this city.";
},
add: ({ a, b }) => {
console.log(`[System] 计算 ${a} + ${b}...`);
return a + b;
}
};这一层运行在你的程序里,是真实执行层。
天气数据虽然是 mock 的,但不影响它表达清楚工程边界:真正触达外部世界的,是这些工具函数,而不是模型本身。换成生产场景,这里完全可能是:
- 调公司内部 API
- 查数据库
- 读文件系统
- 创建工单
- 发消息或执行命令
2. toolsSchema:模型只能看见能力描述,看不见实现细节
const toolsSchema = [
{
functionDeclarations: [
{
name: "getWeather",
description: "Get the current weather in a given city.",
parameters: {
type: "OBJECT",
properties: {
city: {
type: "STRING",
description: "The city name, e.g. 'Shanghai', 'New York'",
},
},
required: ["city"],
},
},
{
name: "add",
description: "Add two numbers together.",
parameters: {
type: "OBJECT",
properties: {
a: { type: "NUMBER", description: "First number" },
b: { type: "NUMBER", description: "Second number" },
},
required: ["a", "b"],
},
},
],
},
];模型不知道你的 JavaScript 代码长什么样。它看到的只有这份 schema。也就是说,从模型视角看,它只是收到一份当前可用能力说明:
- 工具叫什么
- 适合解决什么问题
- 需要哪些参数
- 参数类型和限制是什么
这也是为什么 Tool Calling 的稳定性,往往更依赖 schema 设计质量,而不是 prompt 写得多花。
3. 初始化模型:注册的是“可请求工具”,不是“执行权限”
const model = genAI.getGenerativeModel({
model: "gemini-flash-latest",
tools: toolsSchema,
});
const chat = model.startChat();这里很多人会误会成“把函数注册给模型了”。更准确的描述应该是:
你告诉模型,在这一轮对话里,你可以请求这些能力。
注意,是“可以请求”,不是“可以执行”。执行权仍然在应用手里,这一点对安全控制至关重要。
4. 第一轮推理:模型负责判断要不要借工具
const result = await chat.sendMessage(prompt);
const response = result.response;
const functionCalls = response.functionCalls();这里是系统的第一个关键分叉:
- 如果模型觉得自己可以直接回答,就直接输出自然语言
- 如果模型判断需要外部能力,就返回
functionCalls()
也就是说,模型在这里做的是决策,不是执行。
5. 执行层:你的程序收到指令后才真正开始做事
const call = functionCalls[0];
const { name, args } = call;
console.log(`\n👉 LLM 决定调用工具: ${name}(${JSON.stringify(args)})`);
const toolResult = tools[name](args);
console.log(`✅ 工具执行结果: ${toolResult}`);这一段把责任划分得非常干净:
- 模型给出
name + args - 应用检查并执行对应工具
- 工具返回真实结果
而且这里也是你做治理的核心入口。真实系统里,通常不会直接 tools[name](args) 就完事,而是会在这一层补上:
- 参数校验
- 权限校验
- 超时控制
- 重试策略
- 幂等保护
- 审计日志
6. 结果回流:不把工具结果喂回模型,闭环就没完成
const result2 = await chat.sendMessage([
{
functionResponse: {
name: name,
response: { result: toolResult }
}
}
]);很多人第一次写 Tool Calling,会停在“模型已经给了我要调用的函数名”。但这只完成了一半。
后面必须有第二段推理:
- 告诉模型它刚才请求的工具已经执行完了
- 告诉它工具返回了什么
- 让它基于真实结果组织最终回答
如果没有这一步,系统只是学会了“生成指令格式”,还没有真正形成用户可见的闭环。
7. 最终回答其实来自第二次推理
console.log(`\nAI: ${result2.response.text()}`);这句看起来简单,但它提醒你一个非常重要的事实:一条完整的 Tool Calling 往往至少包含两个推理阶段。
- 判断要不要调工具,以及怎么调
- 基于工具返回值生成最终回答
所以 Agent 的复杂度,不只是“模型会不会调用函数”,而是整个调用-执行-回流链条能不能稳定工作。
这段代码背后的三层系统分工
模型负责
- 理解用户意图
- 判断是否需要工具
- 生成工具名与参数
- 消化工具结果并组织最终回复
应用负责
- 提供可用工具清单
- 校验模型输出是否合法
- 执行工具
- 记录调用链路
- 控制权限、错误和重试
- 把结果回传给模型
工具负责
- 和真实环境交互
- 完成具体动作
- 返回结构化结果或错误信息
这三层别混。Agent 稳不稳定,很多时候不是看模型有多聪明,而是看这三层有没有分清楚。
工程边界和常见坑
1. schema 写得太模糊,或者本地数据映射不完整
工具描述不清、参数定义过松,模型就更容易选错工具或传错参数。即使工具调用链本身是通的,如果你的本地 mock 数据没有覆盖模型常见输出形式,结果也可能出错。
这节课当前版本就是一个很典型的例子:用户问“上海今天天气怎么样”,模型有时会传 Shanghai,有时也可能传 上海。如果天气数据只覆盖其中一种写法,系统就会出现“调用成功,但返回 Unknown”的假阴性结果。
2. 把结构化输出当成可信输入
模型生成的 JSON 比自然语言更可解析,但不代表一定正确。执行层必须兜底。
3. 把模型当执行器
它只是一个决策器。执行权必须始终掌握在系统手里,否则权限和安全根本无从谈起。
4. 只考虑单步调用
真实任务通常不是一步:先搜、再读、再总结;先查状态、再下指令、再确认结果。单步 Tool Calling 只是最小起点。
收尾
Tool Calling 真正改变的,不是模型突然能运行代码了,而是系统终于建立起一套“模型决策、程序执行、结果回流”的行动闭环。
从这一刻开始,Agent 不再只是一个更会聊天的模型,而是一个开始具备外部动作能力的系统。接下来问题会继续升级:有些时候,系统不是不会做,而是不知道。那当模型原本就没有某些私有知识时,应该怎样把这些知识按需接进来?这就是 RAG 要补上的那条链路。

