大模型到底是怎么“调用工具”的?从一个 Node.js Demo 看懂 Tool Use

很多人第一次看到 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_weatherget_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 的第一块拼图:它让一个只会生成语言的模型,开始具备"借助外部工具完成任务"的能力。

相关推荐
烬羽1 小时前
中英文 token 数量差一倍?两段 JS 代码搞懂 LLM 底层是怎么"读"文字的
javascript·程序员·架构
MingXin1 小时前
Claude Code 对接 DeepSeek 完整使用教程(2026 最新版)
人工智能
山河木马1 小时前
矩阵专题1-怎么创建模型矩阵(uModelMatrix)
javascript·webgl·计算机图形学
Oo9201 小时前
LLM 分词与嵌入:从文本到向量,模型如何"读懂"你的输入
人工智能
Databend3 小时前
在 AWS 中国峰会逛了一天,我在 Databend 展台看到了 Agent 数据基础设施的新思路
数据库·人工智能·agent
米小虾3 小时前
从 Prompt 到 Loop:2026 年 AI 工程师必须掌握的 Loop Engineering 实战指南
人工智能·agent
Bigger3 小时前
我写了一个AI图像视频生成工具,免费API+本地部署,分享给大家
人工智能·图像识别·音视频开发
神奇小汤圆3 小时前
LLM 记忆系统:从 Markdown 知识库到 Self-Governing Repo
人工智能
程序员cxuan4 小时前
GPT-5.6 还不发布?不过大家可以先看看 Codex 的白皮书。
人工智能·后端·程序员