从“缸中大脑“到“能动手的 Agent“:Tool Use 如何让 LLM 突破物理限制

从"缸中大脑"到"能动手的 Agent":Tool Use 如何让 LLM 突破物理限制

那个在显卡里疯狂跑的 LLM,本质上还是个词语接龙的游戏。它是被困在服务器里的缸中大脑------看不见屏幕,摸不到键盘,只能预测下一个词的概率。但今天,我们见证了一场技术革命:Tool Use 让这个"缸中大脑"学会了调用 API、读取数据库、操作物理世界的工具。这不是魔法,是协议。


一、核心论点:LLM + Tools = Agent

用户以为豆包能自动搜索网页、Claude 能分析 Excel 表格,是 LLM 自己完成的。其实不是。

LLM 只会做一件事:Next Token Prediction(预测下一个词)。它无法:

  • 访问实时数据(股价、天气)
  • 执行计算
  • 读取文件
  • 调用 API

但加上 Tools,它就变成了 Agent。

复制代码
LLM(缸中大脑) + Tools(外部能力) = Agent(能动手的智能体)

今天的核心问题是:一个只能预测下一个词的概率模型,怎么突破物理限制,去调用 API、读数据库、操作工具?

答案藏在三个关键词里:认知植入 → 意图识别 → Runtime 介入


二、认知植入:工具降维为语言

2.1 LLM 的根本限制

LLM 不懂什么是天气 API,也不懂数据查询,但它听得懂语言

所以,Tool Use 的第一步,是把复杂的软件接口函数,翻译成 LLM 能理解的"说明书"------这就是认知植入

2.2 JSON Schema:函数的"说明书"

看这段代码(index.mjs 第 13-30 行):

javascript 复制代码
const tools = [
  {
    "type": "function",
    "function": {
      "name": "get_closing_price",          // 函数名
      "description": "获取指定股票的收盘价",  // 功能描述
      "parameters": {                        // 参数 Schema
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "description": "股票名称"
          }
        },
        "required": ["name"]
      }
    }
  }
]

这就是认知植入的核心 :把一个复杂的软件工具(get_closing_price 函数),降维成一个存储的文本描述(JSON Schema)。

字段 作用 为什么需要
name 函数标识符 LLM 据此生成调用
description 功能描述 LLM 据此决策是否调用
parameters JSON Schema LLM 据此生成合法参数
required 必填字段 约束 LLM 必须提供

2.3 Schema 的本质

Schema 是对数据结构的正式定义和约束说明,描述数据应如何组织、验证其合法性,并确保不同系统间数据交换的一致性。

类比数据库设计:

sql 复制代码
CREATE TABLE users (
  name VARCHAR(100) NOT NULL UNIQUE
);

JSON Schema 做的是同样的事------用结构化语言约束数据格式

2.4 为什么描述必须具体清晰?

LLM 是概率模型,描述写得越具体,LLM 决策越准确

javascript 复制代码
// ❌ 差的描述
"description": "获取价格"

// ✅ 好的描述
"description": "获取指定股票的收盘价,输入股票名称,返回最新收盘价"

前者会让 LLM 困惑:"什么价格?商品价格?股价?房价?"后者则精确指向股票场景。

一句话总结:认知植入的本质,是把"软件能力"翻译成"语言说明书",让 LLM 能理解工具的存在和用法。


三、意图识别:LLM 的决策机制

3.1 用户提问时的推理过程

用户问:"青岛啤酒的收盘价是多少?"

LLM 的推理引擎开始工作:

复制代码
1. 检查训练语料:我能直接回答吗?
   → 不能,股价是实时数据,不在训练数据里

2. 检查认知植入:有工具吗?
   → 有,get_closing_price 工具

3. 匹配意图:这个问题需要调用工具吗?
   → 需要,用户问的是股价

4. 生成调用:根据 Schema 生成参数
   → tool_calls: [{ name: "get_closing_price", arguments: '{"name":"青岛啤酒"}' }]

3.2 LLM 返回的结构

看代码(index.mjs 第 77-78 行):

javascript 复制代码
const response = await sendMessage(messages);
const message = response.choices[0].message;

LLM 返回的 message 对象有两种可能:

情况 A:直接回答(不需要工具)

javascript 复制代码
{
  role: "assistant",
  content: "你好,有什么可以帮你?",
  tool_calls: null
}

情况 B:调用工具(需要外部数据)

javascript 复制代码
{
  role: "assistant",
  content: null,
  tool_calls: [{
    id: "call_abc123",
    type: "function",
    function: {
      name: "get_closing_price",
      arguments: '{"name":"青岛啤酒"}'
    }
  }]
}

关键洞察 :LLM 不执行工具,只是决策"该调用什么工具、传什么参数"。它赌这段代码发出后,会有人响应。

3.3 为什么是 tool_calls 数组?

OpenAI 协议支持并行工具调用------LLM 可以一次性请求调用多个工具:

javascript 复制代码
// 用户问:"青岛啤酒的股价和北京天气"
tool_calls: [
  { id: "call_1", function: { name: "get_closing_price", arguments: '{"name":"青岛啤酒"}' } },
  { id: "call_2", function: { name: "get_weather", arguments: '{"city":"北京"}' } }
]

代码中取 [0](第 86 行)是简化处理,假设每次只调用一个工具。

一句话总结:意图识别是 LLM 的核心能力------它依赖强大的模式识别和逻辑推理,判断是否需要工具、调用哪个工具、传什么参数。


四、Runtime 介入:传统软件的执行

4.1 LLM 不能执行,但开发者可以

LLM 只生成调用请求(tool_calls),不执行函数。真正的执行由 Runtime(Node.js/Python/Java)完成。

看代码(index.mjs 第 88-91 行):

javascript 复制代码
const args = JSON.parse(toolCall.function.arguments);  // 解析参数
const price = get_closing_price(args.name);             // 执行本地函数

执行流程

复制代码
LLM 返回 tool_calls
    ↓
Runtime 解析 arguments(JSON 字符串 → 对象)
    ↓
Runtime 调用本地函数 get_closing_price("青岛啤酒")
    ↓
返回结果 "67.92"

4.2 为什么用 JSON.parse

LLM 生成的 arguments字符串

javascript 复制代码
arguments: '{"name":"青岛啤酒"}'  // 字符串

需要解析成对象才能传给函数:

javascript 复制代码
const args = JSON.parse(arguments);
args.name  // "青岛啤酒"

4.3 工具结果不直接返回给用户

关键设计:工具结果不直接返回给用户,而是返回给 LLM。

看代码(index.mjs 第 93-99 行):

javascript 复制代码
messages.push({
  role: 'tool',
  content: price,           // "67.92"
  tool_call_id: toolCall.id // "call_abc123"
})

为什么?

  • 用户问的是自然语言问题,期望自然语言回答
  • 工具返回的是原始数据("67.92"),不是用户友好的回答
  • LLM 需要把原始数据"翻译"成自然语言:"青岛啤酒的收盘价是 67.92 元"

4.4 tool_call_id 的作用

tool_call_id关联工具请求和结果的纽带

javascript 复制代码
// LLM 返回的请求
tool_calls: [{ id: "call_abc123", ... }]

// Runtime 返回的结果
{ role: 'tool', tool_call_id: "call_abc123", content: "67.92" }

LLM 通过 id 知道:"这个 67.92 是刚才那个工具调用的结果"。

支持多工具并发 :如果有多个工具调用,每个都有独立的 id,结果可以精确关联。


五、二次调用:LLM 基于真实数据生成回答

5.1 完整对话历史

看代码(index.mjs 第 104-107 行):

javascript 复制代码
const finalRes = await sendMessage(messages);
console.log(finalRes.choices[0].message.content);

此时 messages 数组包含:

javascript 复制代码
[
  { role: 'user', content: '青岛啤酒的收盘价是多少?' },
  { role: 'assistant', content: null, tool_calls: [{ id: "call_abc123", ... }] },
  { role: 'tool', content: '67.92', tool_call_id: "call_abc123" }
]

第二次调用 LLM 时,它看到了完整上下文

  • 用户问了什么
  • 自己决策调用什么工具
  • 工具返回了什么结果

5.2 LLM 的最终回答

有了真实数据,LLM 才能生成用户友好的回答:

复制代码
"青岛啤酒的收盘价是 67.92 元。"

不是直接返回 "67.92",而是用自然语言包装------这才是用户期望的交互体验。


六、完整时序图

复制代码
用户          Agent          LLM           本地函数
 │             │              │              │
 │──"青岛啤酒股价?"──→│              │              │
 │             │──messages───→│              │
 │             │              │(检查训练语料:不能回答)
 │             │              │(检查认知植入:有工具)
 │             │              │(意图识别:需要调用)
 │             │←─tool_calls─│              │
 │             │              │              │
 │             │──JSON.parse──→              │
 │             │──执行函数──────────────────→│
 │             │←──"67.92"───────────────────│
 │             │              │              │
 │             │──messages+结果──→│          │
 │             │              │(基于数据生成回答)
 │             │←─"青岛啤酒收盘价是67.92元"─│
 │←──回答──────│              │              │

七、代码架构总结

javascript 复制代码
// Layer 1: LLM 客户端(缸中大脑)
const client = new OpenAI({ apiKey, baseURL });

// Layer 2: 工具协议层(认知植入)
const tools = [{ type: "function", function: { name, description, parameters } }];

// Layer 3: 函数实现层(传统软件)
function get_closing_price(name) { ... }

// Layer 4: Agent 编排层(意图识别 + Runtime 介入)
async function main() {
  // 第一次调用:LLM 决策
  const response = await sendMessage(messages);
  
  // Runtime 执行
  const args = JSON.parse(toolCall.function.arguments);
  const result = get_closing_price(args.name);
  
  // 第二次调用:LLM 总结
  const finalRes = await sendMessage(messages + tool_result);
}

八、知识结构总览

复制代码
Tool Use 核心机制
│
├── 认知植入(工具降维为语言)
│   ├── JSON Schema:函数的"说明书"
│   ├── description:LLM 决策依据
│   ├── parameters:参数约束
│   └── required:必填字段
│
├── 意图识别(LLM 决策机制)
│   ├── 检查训练语料:能否直接回答
│   ├── 检查认知植入:有无工具
│   ├── 匹配意图:是否需要工具
│   └── 生成调用:tool_calls 数组
│
├── Runtime 介入(传统软件执行)
│   ├── JSON.parse:解析参数
│   ├── 执行函数:调用本地代码
│   ├── tool_call_id:关联请求和结果
│   └── 返回结果:不直接给用户
│
└── 二次调用(LLM 总结)
    ├── messages:完整对话历史
    ├── 基于真实数据生成回答
    └── 自然语言包装:用户友好

结语

Tool Use 不是魔法,是协议

它解决了一个根本矛盾:LLM 只会预测下一个词,但用户需要它调用 API、读数据库、操作工具。

解决方案藏在三个步骤里:

  1. 认知植入:把软件能力翻译成语言说明书(JSON Schema)
  2. 意图识别:LLM 决策该调用什么工具、传什么参数
  3. Runtime 介入:传统软件执行函数,把结果喂回 LLM

LLM 负责"思考",Runtime 负责"动手"。

这才是 Agent 的本质------不是 LLM 变聪明了,而是我们给缸中大脑装上了手脚。用户看到的"智能",是协议编织的幻觉。但这个幻觉,恰好能解决问题。

Tool Use 的真正价值,不是让 LLM 突破物理限制,而是让传统软件的能力,通过语言协议,被 LLM 调用。这是新旧范式的融合------概率模型与确定性代码的协作。

下一次,当你看到豆包自动搜索网页、Claude 分析 Excel 表格时,请记住:LLM 只是在预测下一个词,真正干活的是你写的代码。