别被AI骗了!深度拆解 LLM Tool Use 背后的“缸中大脑”与代码真相

导语

最近在做 AI Agent 相关的业务时,我发现很多初中级开发同学对 Tool Use(工具调用)的理解还停留在"调个 API"的表层。大家往往觉得 AI 好像有了自我意识,能自己查股票、看天气。

但作为开发者,我们必须清醒:AI 并没有自我意识,这一切都是一个精心设计的"错觉" 。那个在显卡里疯狂跑的 LLM,本质上还是个"成语接龙"游戏,它是被困在服务器里的"缸中大脑"。它看不见屏幕,摸不到键盘,更别提去操作物理世界了。

那么,一个只能预测下一个词的概率模型,是怎么突破物理限制去调用 API、读数据库的?今天,我们就通过一段真实的 Node.js 代码,把 Tool Use 的底层逻辑扒个底朝天!


一、 核心概念:LLM + Tools = Agent 的"认知植入"

在讲代码前,我们先建立一个核心认知:工具调用,本质上是把"函数"降维成了"语言"

大模型不懂什么是天气 API,也不懂 SQL 查询,但它听得懂自然语言。我们在配置 tools 时,其实是在做一件非常精妙的事------认知植入。我们通过 JSON Schema 这种约束格式,把复杂的软件接口函数翻译成 LLM 能理解的"说明书"。

当用户问"青岛啤酒的收盘价是多少"时,LLM 发现训练语料里没有这个实时数据,但它通过"认知植入"知道有个叫 get_closing_price 的工具。于是,它严格按照说明书,生成了一段"自然语言代码"(JSON 格式的参数),然后中断执行,把这段代码交还给我们的 Runtime(比如 Node.js)去真正执行。

记住这个公式:LLM 负责决策与翻译,Runtime 负责执行与反馈。


二、 痛点与场景:为什么我们需要 Tool Use?

在传统软件开发中,我们习惯了"硬编码"逻辑。但在 AI 时代,我们面临两个核心痛点:

  1. LLM 的知识存在滞后性与幻觉:你问它今天的茅台股价,它要么瞎编,要么告诉你它不知道。
  2. AI 无法与物理世界交互:它不能直接连你的 MySQL,也不能帮你发钉钉消息。

Tool Use 完美解决了这两个问题。 它让 LLM 退回到它最擅长的事情上------意图识别与逻辑推理 ,而把"脏活累活"(查库、调接口)交回给传统软件世界。这就形成了新旧范式的结合:AI 做大脑,传统代码做手脚。


三、 重难点剖析:代码背后的"三步走"设计

结合提供的代码,我们来剖析 Tool Use 中最核心的 3 个重难点。

重难点 1:JSON Schema 的"认知植入"艺术

设计者为什么这么写?

代码中定义了 tools 数组,里面包含了 namedescriptionparameters。这不是随便写的,这是给 LLM 的"操作手册"。如果描述不清晰,LLM 就会猜错参数。

如何攻克/理解?

description 写得像教小白一样具体。parameters 里的 required 字段是强约束,告诉 LLM 哪些参数是必须的,防止它生成残缺的调用指令。

一、一句话总结

JSON Schema 就是给 JSON 数据定规矩:规定数据长什么样、字段有没有、类型是什么,用来约束、校验大模型输出内容,防止格式乱套。


二、放到你 Tool Calling 代码里

你写的这一整块就是 JSON Schema:

js

运行

css 复制代码
parameters: {
    type: 'object',
    properties: {
        name: {
            type: 'string',
            description: '股票名称'
        }
    },
    required: ['name']
}

它干两件事:

  1. **给大模型阅读(最重要)**告诉 AI:
  • 参数必须是一个对象
  • 必须包含 name 字段
  • name 必须是字符串
  • 不能随便多加其他字段

AI 会严格遵守这套规则来生成 arguments 里的 JSON 字符串,不会乱写字段、不会类型错乱。

  1. 服务端校验模型输出的 JSON 会被 API 自动校验,不符合 Schema 格式就直接拦截,保证拿到的数据一定合法。

三、如果没有 JSON Schema 会发生什么?

没有约束,AI 自由发挥,可能输出:

json

json 复制代码
{"stock_name":"青岛啤酒"}

你代码里取 .name 就会拿到 undefined,程序直接出错。

有了 Schema,AI 只能写 name,不能自己乱造字段。


四、对应整条链路

  1. 你编写 JSON Schema(参数规则)
  2. 大模型读取规则,严格按照结构生成 JSON 文本
  3. 文本放进 toolCall.function.arguments
  4. 你用 JSON.parse() 转成对象
  5. 安心取值,不用担心字段不存在、类型错误

五、通俗比喻

JSON Schema = 表格模板:规定必须有 "股票名称" 这一栏,而且只能填文字,不能填数字。AI 填表时只能照着模板填,不能自己新增栏目、乱填内容。


补充小知识点

  • Schema 不止用于 Function Calling,也用于结构化输出(让 AI 强制返回固定格式 JSON);
  • properties 定义字段结构,required 定义必填项,二者共同构成 Schema 主体。

重难点 2: 意图识别 对话中断与"自言自语"的上下文管理

设计者为什么这么写?

AI 会停止和你的对话,转而开始自言自语(思考),它严格按照刚刚定义的那套说明书,去生成一段自然语言代码

注意代码中的 messages.push 逻辑。当 LLM 返回 tool_calls 时,它并没有直接回答用户,而是返回了一个包含 idfunction.namearguments 的对象。此时,对话被中断了

如何攻克/理解?

这是新手最容易晕的地方。LLM 的 tool_calls 不是最终答案,而是执行请求。我们需要:

  1. 把 LLM 的 tool_calls 原封不动地追加到 messages 中(role 为 assistant)。
  2. 本地执行函数后,把结果追加到 messages 中(role 为 tool,且必须带上 tool_call_id 进行关联)。
  3. 再次调用 LLM,让它根据工具返回的结果,生成最终的人类可读回复。

重难点 3:Runtime 的"桥梁"作用

设计者为什么这么写?

代码里的 get_closing_price 是一个纯传统的 JS 函数。LLM 绝对不可能直接执行它。

如何攻克/理解?

开发者必须充当"中间人"。LLM 吐出 JSON 字符串(toolCall.function.arguments),我们需要用 JSON.parse 解析,然后手动路由 到对应的本地函数。执行完后,再把结果喂回给 LLM。 JSON.parse(字符串):把JSON 格式的文本字符串,转换成 JavaScript 对象。

放到你这段代码里

js

运行

matlab 复制代码
// toolCall.function.arguments 的值是字符串:'{"name":"青岛啤酒"}'
const args = JSON.parse(toolCall.function.arguments)
  • 转换前:只是一段文字,不能直接点 .name 取值
  • 转换后:变成 JS 对象 { name: "青岛啤酒" },就可以写 args.name

四、 核心交互时序图

为了让大家更直观地理解这个"缸中大脑"是怎么工作的,我画了下面这张时序图:


五、 避坑指南:新手最容易踩的 3 个坑

基于代码和实战经验,给大家总结了几个标准解决方案:

  1. 坑一:忘记把 tool_calls 放入历史消息

    • 错误示范 :LLM 返回 tool_calls 后,直接执行工具,然后把结果发给 LLM,但 messages 里缺少了 LLM 发起调用的那条记录。
    • 正确做法 :必须严格按照 User -> Assistant(tool_calls) -> Tool(result) 的顺序追加消息。LLM 需要看到自己"曾经下达过指令",才能理解后续的结果。
  2. 坑二:tool_call_id 对不上

    • 错误示范 :返回工具结果时,不传 tool_call_id,或者传错了。
    • 正确做法 :一次对话可能触发多个工具调用,必须使用 LLM 返回的 toolCall.id 作为 tool_call_id 进行精确关联,否则 LLM 会陷入逻辑混乱。
  3. 坑三:工具描述(Description)太简略

    • 错误示范description: "获取天气"
    • 正确做法description: "获取指定城市的实时天气情况,包括温度、湿度和天气状况。当用户询问天气、气温时使用此工具。" 描述越像"Prompt",LLM 的决策越精准。

六、 面试高频考点

如果你在面试中遇到 AI Agent 或 LLM 应用开发的问题,这几个底层原理一定要背熟:

Q1:LLM 本身是如何执行工具的?

精炼回答 :LLM 本身不具备执行任何工具的能力。它本质上是一个 Next Token Prediction(下一个词预测)模型。Tool Use 是通过在 System Prompt 中注入 JSON Schema 进行"认知植入",让 LLM 输出符合特定格式的文本(tool_calls)。真正的执行是由外部的 Runtime(如 Node.js/Python)解析这段文本后,通过传统代码去调用的。

Q2:为什么工具调用通常需要两次甚至多次请求 LLM?

精炼回答 :因为 LLM 无法在生成 tool_calls 的同时生成最终答案。第一次请求,LLM 识别意图并输出工具调用参数,此时对话中断;外部 Runtime 执行工具并将结果(role: tool)追加到上下文中;第二次请求,LLM 阅读工具返回的结果,再进行自然语言的总结与回复。这是一个"思考-行动-观察"的循环。

Q3:JSON Schema 在 Tool Use 中扮演什么角色?

精炼回答:它是连接"自然语言"与"强类型代码"的桥梁。它将复杂的软件接口函数降维成 LLM 能理解的"语言说明书",通过严格的类型约束(如 string, object, required)来限制 LLM 的概率随机性,确保生成的函数调用参数是合法且可被代码解析的。


结语

从"成语接龙"到"全能 Agent",Tool Use 让我们看到了大模型突破物理边界的无限可能。但作为开发者,我们要时刻保持清醒:AI 负责想象与决策,我们负责兜底与执行。 只有理解了这套"精心设计的错觉",我们才能写出真正健壮、可靠的 AI 应用。

javascript

运行

javascript 复制代码
import OpenAI from 'openai';
import dotenv from 'dotenv';
dotenv.config();

const openai = new OpenAI({
  apiKey: process.env.DEEPSEEK_API_KEY,
  baseURL: process.env.DEEPSEEK_API_URL,
});

// 工具列表,附带JSON Schema参数约束
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 '66';
  } else if (name === '贵州茅台') {
    return '3000';
  } else {
    return '未找到股票';
  }
}

// 封装请求大模型
async function send_message(messages) {
  const response = await openai.chat.completions.create({
    model: 'deepseek-v4-flash',
    messages,
    tools,
    tool_choice: 'auto',
  });
  return response;
}

async function main() {
  let messages = [
    { role: 'user', content: '青岛啤酒的收盘价是多少?' }
  ];

  // 第一轮对话,模型决定调用工具
  const response = await send_message(messages);
  const message = response.choices[0].message;
  console.log('模型返回message 对象:', JSON.stringify(message));

  // 修复:不要写成字符串 'message.role'
  messages.push({
    role: message.role,
    content: message.content,
    tool_calls: message.tool_calls
  });

  // 判断是否触发工具调用
  if (message.tool_calls) {
    const toolCall = message.tool_calls[0];

    if (toolCall.function.name === 'get_closing_price') {
      // 解析模型输出的JSON参数字符串
      const args = JSON.parse(toolCall.function.arguments);
      const price = get_closing_price(args.name);
      console.log('股票价格:', price);

      // 把工具执行结果塞入上下文
      messages.push({
        role: 'tool',
        tool_call_id: toolCall.id,
        content: price
      });

      console.log('更新后完整对话上下文:', messages);

      // 第二轮对话:让大模型整理结果,输出自然语言回答
      const finalRes = await send_message(messages);
      console.log('最终结果:', finalRes.choices[0].message.content);
    } else if (toolCall.function.name === 'get_weather') {
      // 天气工具逻辑在这里扩展
    }
  }
}

main();
相关推荐
掘金者阿豪3 小时前
高可用读写分离实战(二):我把数据库主库停了,结果整个集群的反应和我想象的不一样
后端
掘金者阿豪3 小时前
《高可用读写分离集群实战》系列(一)
后端
Dilee3 小时前
Spring AI 2.0.0 Prompt 最小 Demo:system、user、template 到底怎么分工
后端
未秃头的程序猿3 小时前
Java 26正式发布!这3个新特性,让代码量直接减半
java·后端·面试
小旭Coding4 小时前
卧靠!Go 传给前端的 int64 竟然变成了这个?
后端
用户298698530144 小时前
Word 文档文本查找与替换的 Java 实现方案
java·后端
kunge20134 小时前
深度剖析Claude Code 的CLAUDE.md加载逻辑
后端·vibecoding