大模型 Tool 调用底层拆解:手写原生调用逻辑
不依赖 Agent 框架的自动调度,用最小可运行示例手写 Tool Calling:定义工具、模型决策、外部执行、结果回填,并弄清四类消息如何流转。
单纯的大模型本质是文本生成器。它的知识来自训练数据,拿不到实时信息,也做不了可靠计算,更谈不上操作本地设备或调用外部服务。纯对话模式下容易幻觉、难落地,根因就在这里。
Tool 调用的本质,是一套标准化通信协议。
它让模型先判断任务要不要外部协助,再输出规范的函数调用指令;真正执行则交给外部程序——本地函数、远程 API、云服务都可以。换句话说:模型负责思考和下指令,工具负责干活。Tool Calling 就是两者之间的共同语言。
完整链路可以概括为:
用户提问 → 携带工具定义传入模型 → 模型决定是否调用 → 输出工具名与参数 → 外部程序执行 → 结果回传模型 → 模型整合后给出最终回答
环境准备
新建空项目并安装依赖:
npm init -y
npm install @langchain/openai @langchain/core dotenv zod在项目根目录创建 .env:
OPENAI_API_KEY=你的密钥
OPENAI_BASE_URL=https://api.openai.com/v1
OPENAI_MODEL_NAME=gpt-3.5-turbo最简案例:手写加法工具调用
要吃透底层,最好从最小例子开始。下面用一个加法计算器,全程手动调度,不走框架自动编排。
新建 raw-tool-demo.mjs:
import 'dotenv/config';
import { ChatOpenAI } from "@langchain/openai";
import { tool } from "@langchain/core/tools";
import { z } from "zod";
// 1. 初始化大模型
const llm = new ChatOpenAI({
apiKey: process.env.OPENAI_API_KEY,
baseURL: process.env.OPENAI_BASE_URL,
modelName: process.env.OPENAI_MODEL_NAME,
temperature: 0
});
// 2. 定义工具:加法计算器
const addTool = tool(
async ({ a, b }) => {
return `计算结果:${a + b}`;
},
{
name: "add_calculator",
description: "用于计算两个数字的加法,精准数学计算使用",
schema: z.object({
a: z.number().describe("第一个数字"),
b: z.number().describe("第二个数字")
})
}
);
const tools = [addTool];
// 3. 手写完整原生工具调用流程
async function main() {
const messages = [
{
role: "system",
content: "遇到数学加法计算,必须调用计算器工具,不要自己计算。"
},
{
role: "user",
content: "请计算 9876 + 1234"
}
];
// 第一步:传入问题和工具列表,让模型自主决策
const response = await llm.invoke(messages, { tools });
// 第二步:判断是否需要调用工具
if (response.tool_calls && response.tool_calls.length > 0) {
const toolCall = response.tool_calls[0];
const toolName = toolCall.name;
const toolArgs = toolCall.args;
console.log("模型选择工具:", toolName);
console.log("模型传入参数:", toolArgs);
// 第三步:手动执行本地工具
let toolResult = "";
if (toolName === "add_calculator") {
toolResult = await addTool.invoke(toolArgs);
}
console.log("工具执行结果:", toolResult);
// 第四步:回填消息,让模型整合生成最终答案
messages.push(response);
messages.push({
role: "tool",
tool_call_id: toolCall.id,
name: toolName,
content: toolResult
});
const finalRes = await llm.invoke(messages);
console.log("\n最终回答:", finalRes.content);
} else {
console.log("最终回答:", response.content);
}
}
main().catch(console.error);流程拆解
这段代码是工具调用的最小原型。常见 Agent / 自动调度能力,大多是在这套逻辑上叠加循环、规划与错误处理。核心四步:
- 标准化定义工具:为函数提供唯一名称、功能描述和参数 schema,让模型知道「能做什么、怎么传参」。
- 模型自主决策:结合用户需求与工具列表,判断是否调用、调用哪个,并生成合规参数。
- 外部手动执行:程序读取
tool_calls,匹配工具并执行,拿到真实结果。 - 消息回填闭环:把执行结果写成标准 tool 消息塞回上下文,再让模型生成最终回答。
不变的分工是:模型负责决策,工具负责落地;标准化消息把两边串成闭环。
能力拓展:本地文件与终端工具
掌握单工具调用后,可以把能力扩到常见本地操作:读文件、写文件、建目录、执行命令。同样不做自动 Agent 封装,只保留原生循环。
新建 raw-mini-cursor.mjs:
import 'dotenv/config';
import { ChatOpenAI } from "@langchain/openai";
import { tool } from "@langchain/core/tools";
import { z } from "zod";
import fs from 'fs/promises';
import path from 'path';
import { execSync } from 'child_process';
const llm = new ChatOpenAI({
apiKey: process.env.OPENAI_API_KEY,
baseURL: process.env.OPENAI_BASE_URL,
modelName: process.env.OPENAI_MODEL_NAME,
temperature: 0
});
const readFileTool = tool(
async ({ filePath }) => {
try {
return await fs.readFile(path.resolve(filePath), "utf8");
} catch (e) {
return "读取失败:" + e.message;
}
},
{
name: "read_local_file",
description: "读取本地文本文件",
schema: z.object({ filePath: z.string() })
}
);
const writeFileTool = tool(
async ({ filePath, content }) => {
await fs.writeFile(path.resolve(filePath), content, "utf8");
return "文件写入成功";
},
{
name: "write_local_file",
description: "创建或写入本地文本文件",
schema: z.object({ filePath: z.string(), content: z.string() })
}
);
const mkdirTool = tool(
async ({ dirPath }) => {
await fs.mkdir(path.resolve(dirPath), { recursive: true });
return "目录创建成功";
},
{
name: "mkdir_dir",
description: "创建本地文件夹,支持多级目录",
schema: z.object({ dirPath: z.string() })
}
);
const execTool = tool(
async ({ cmd }) => {
return execSync(cmd, { encoding: "utf8" });
},
{
name: "exec_command",
description: "执行本地终端命令",
schema: z.object({ cmd: z.string() })
}
);
const tools = [readFileTool, writeFileTool, mkdirTool, execTool];
const toolMap = {
read_local_file: readFileTool,
write_local_file: writeFileTool,
mkdir_dir: mkdirTool,
exec_command: execTool
};
async function main() {
const messages = [
{
role: "system",
content: "你可以调用本地工具完成用户需求,根据需求选择对应工具,不要编造内容。"
},
{
role: "user",
content: "新建文件夹 demo,在里面创建 intro.txt 并写入 hello tool calling,然后列出当前目录文件"
}
];
// 手动循环:连续工具调用,直到模型不再请求工具
while (true) {
const res = await llm.invoke(messages, { tools });
if (!res.tool_calls?.length) {
console.log("\n最终回答:", res.content);
break;
}
for (const call of res.tool_calls) {
const selected = toolMap[call.name];
const toolResult = await selected.invoke(call.args);
messages.push(res);
messages.push({
role: "tool",
tool_call_id: call.id,
name: call.name,
content: toolResult
});
}
}
}
main().catch(console.error);注意:
exec_command会直接执行本机命令,演示环境请谨慎使用;生产场景应做白名单、沙箱与权限隔离。
四类消息:Tool Calling 的骨架
整条链路,其实是四类消息在上下文里不断拼接与回环。理清它们的职责,比背某个框架 API 更重要。
各自定位
- SystemMessage(系统消息):全局规则。设定角色、约束行为、规定何时必须用工具。通常放在消息数组首位,整段对话保持不变。
- HumanMessage(人类消息):用户真实需求,是任务起点。后续推理与调用都围绕它展开。
- AIMessage(AI 消息):模型的唯一响应载体。要么直接给文本答案,要么携带
tool_calls下发工具指令。 - ToolMessage(工具消息):工具执行结果的回填载体。没有它,模型看不到真实执行数据,多轮任务无法继续。
流转关系
flowchart LR A["SystemMessage 全局规则"] --> B["HumanMessage 用户需求"] B --> C["LLM 推理决策"] C --> D["AIMessage 模型响应"] D -->|需要调用工具| E["工具执行"] E --> F["ToolMessage 回填结果"] F --> C D -->|无需工具| G["输出最终答案"]
使用时机
SystemMessage 在初始化时写一次即可,用来统一行为:优先用工具、禁止瞎编、约束输出格式等。
HumanMessage 对应每一次用户输入;多轮工具调用最终都是为了完成这条需求。
AIMessage 既是答案出口,也是指令出口:是否调用、调用谁、传什么参数,都经它交给程序。
ToolMessage 是闭环关键一步。工具结果必须封装后并入上下文;跳过回填,模型就无法继续二次推理。
小结
可以收成四点:
- Tool Calling 是模型与外部程序之间的标准协议;执行体不限于本地,也可是远程 API 或云服务。
- 工具就是可被模型调度的外部函数:靠名称、描述和参数 schema,获得「识别—选择—传参」能力。
- 原生链路可以完全手写:需求 → 决策 → 执行 → 回填 → 最终输出,不必先依赖 Agent 框架。
- System / Human / AI / Tool 四类消息各司其职、循环流转,构成多轮工具任务的底层骨架。
把这层原生逻辑吃透之后,再去看自动化 Agent、编程助手或复杂工具串联,会更容易看清它们在循环、规划和容错上多做了什么,而不是只停留在套模板。