你用过 ChatGPT 的联网搜索,用过 Claude 分析 Excel 表格,用过豆包查询实时天气。你可能会觉得:这些 AI 真厉害,什么都会。
但真相是------ 那个在显卡里疯狂跑的 LLM,本质上是个词语接龙游戏。
它是被困在服务器里的"缸中大脑"------看不见屏幕,摸不到键盘,不知道自己跑在哪台机器上。它唯一会的技能是:给定上文,预测下一个 token。
那么问题来了:一个只会预测下一个词的概率模型,是怎么突破物理限制的?它怎么调 API?怎么查数据库?怎么操作现实世界的工具?
这篇文章,我把整个机制拆给你看。
一、一个精心设计的错觉
先讲一个你可能没意识到的事。
你用豆包问"今天几号",它秒回。你以为豆包自己知道日期。
它不知道。
LLM 的训练数据有截止日期。就像一个在 2024 年被关进缸里的人,他对 2025 年、2026 年的世界一无所知。
那它怎么回答的?流程是这样的:
javascript
你:"今天几号?"
↓
LLM(缸中大脑):"我训练数据里没有...但我'记得'有个工具叫 get_current_date..."
↓
LLM 输出:"帮我调 get_current_date"(这是给机器看的,不是给你看的)
↓
Runtime(外部程序)执行:new Date() → "2026-06-25"
↓
Runtime 把结果塞回给 LLM:"现在时间是 2026-06-25"
↓
LLM 转述给你:"今天是 2026 年 6 月 25 日。"
你看到的是「AI 回答了问题」。实际发生的是「AI 表达了意图 → 外部代码执行了操作 → AI 把这个结果包装成自然语言」。
AI 的自我意识?作为开发者,这是一个精心设计的错觉。
整个 Tool Use 机制,就是在精心维护这个错觉。
二、三段式流程:一套完整的"认知---决策---执行"闭环
我把它抽象成三个阶段:
arduino
① 认知植入 → ② 意图识别 → ③ Runtime 介入
告诉 LLM LLM 决策 外部代码
"有哪些工具" "该用哪个" "真正执行"
下面逐段拆开。
阶段一:认知植入 --- 把函数翻译成语言
LLM 不懂什么是 API,不懂什么是函数签名,不懂什么是数据库查询。
但它懂语言。
所以你要做一件事:把你代码里的函数,"翻译"成 LLM 能看懂的说明书。这个翻译工具,叫 JSON Schema。
javascript
// 这是你的代码 --- 强类型、确定性执行的函数
function get_closing_price(name) {
// 查数据库、调第三方 API...
return "67.92";
}
// 这是给 LLM 看的"说明书" --- 纯文本描述
{
"type": "function",
"function": {
"name": "get_closing_price",
"description": "获取股票的收盘价",
"parameters": {
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "股票名称"
}
},
"required": ["name"]
}
}
}
左边是代码世界 ------ 编译器理解、确定性执行。右边是语言世界 ------ LLM 理解、概率推断。
JSON Schema 就是这两个世界的翻译层。 一个复杂的软件工具,被降维成了一个 LLM 能"读懂"的文本说明书。
这一步发生在什么时候?每次 API 请求时 ,这些工具定义随 tools 参数一起发给 LLM:
javascript
const response = await client.chat.completions.create({
model: "deepseek-v4-flash",
messages: messages,
tools: tools, // ← 认知植入
tool_choice: "auto"
});
LLM 收到后「学会」了:哦,有个叫 get_closing_price 的工具,能查股票收盘价,需要一个 name 参数。
这就是认知植入,Tool Use 的地基。
值得注意的是:description 不能随便写。LLM 是个概率模型,不是编译器,它靠描述来判断"该不该用这个工具"。如果描述模棱两可,LLM 就会用错或不用的。工具描述必须具体、清晰、无歧义。
阶段二:意图识别 --- LLM 的"自言自语"
认知植入到位了。现在用户问一句:"青岛啤酒的收盘价是多少?"
LLM 的推理过程是这样的:
vbnet
Step 1: 我的训练数据中有今天青岛啤酒的股价吗?
→ 没有,股价是实时数据
Step 2: 那我能用认知植入里的工具吗?
→ 有一个 get_closing_price
→ 描述是"获取股票的收盘价"
→ 匹配!
Step 3: 这个工具需要什么参数?
→ name(必填)
→ 用户提到了"青岛啤酒"
→ 参数:name = "青岛啤酒"
Step 4: 生成 tool_call
→ 停止对用户说话
→ 输出结构化的调用指令
然后 LLM 实际输出的东西是这样的:
json
{
"role": "assistant",
"content": null,
"tool_calls": [
{
"id": "call_abc123",
"type": "function",
"function": {
"name": "get_closing_price",
"arguments": "{\"name\": \"青岛啤酒\"}"
}
}
]
}
注意 content: null ------ 这是意图识别阶段最关键的信号。LLM 不再对你说话了,它转而在"自言自语",生成一段给机器解析的结构化指令。
它不是随意输出的。function.name、arguments 的格式,全由阶段一注入的 JSON Schema 约束。LLM 靠的是强大的模式识别和逻辑推理能力,在概率世界中生成准确的结构化输出。
更有意思的是:LLM 其实在"赌"。 它不能执行代码,不知道自己生成的这段指令能不能真的被人响应。它只是根据训练数据中的模式推断:这种结构的输出,通常会有一个外部程序来处理。
就像一个被关在房间里的人,对着对讲机报了一串指令,赌走廊那头有人听了会去执行。
阶段三:Runtime 介入 --- 代码真正执行的那一刻
LLM 只负责"表达意图",真正动手的是 Runtime ------ 你写的 Node.js / Python / Java 代码。
javascript
// Runtime 解析 LLM 的指令并执行
const tool_call = assistantMsg.tool_calls[0];
const args = JSON.parse(tool_call.function.arguments); // { name: "青岛啤酒" }
const price = get_closing_price(args.name); // "67.92"
Runtime 可以做任何事:查数据库、调第三方 API、读写文件、操作硬件------这些才是真正"突破 LLM 物理限制"的动作。LLM 只是在生成意图和包装结果。
然后是整个流程中最容易被忽视的关键点:
Runtime 的执行结果,不是直接返回给用户的,而是喂回给 LLM。
javascript
// 结果注入上下文,再发给 LLM
messages.push({
role: 'tool',
content: price, // "67.92"
tool_call_id: tool_call.id // "call_abc123"
});
const finalResponse = await send_message(messages);
// LLM 生成:"青岛啤酒今日收盘价为 67.92 元。"
为什么必须这样做?因为 LLM 是唯一的用户交互接口 。如果把 67.92 直接丢给用户,用户看到的是一个冰冷数字,没有上下文、没有解读。Runtime 只管执行,LLM 管表达。
三、三个关键机制
以上是主线流程。但真正让这个机制在生产环境跑起来的,是三个容易被忽视的细节。
1. tool_call_id:因果链的锚点
LLM 可以一次生成多个 tool_calls:
javascript
// 用户:"茅台股价和上海天气?"
tool_calls: [
{ id: "call_aaa", name: "get_closing_price", arguments: '{"name":"茅台"}' },
{ id: "call_bbb", name: "get_weather", arguments: '{"city":"上海"}' },
]
问题来了:LLM API 本身是无状态 HTTP。每次请求都是一次独立的对话,服务器不记你是谁、刚才聊了什么。你必须手动把全部对话历史带过去。
在这种情况下,如果结果不能和调用一一对应,LLM 就无法正确处理。尤其是同一个函数被调了两次(比如查两个城市的天气),没有 ID 的话,LLM 根本分不清哪个结果对应哪个调用。
arduino
没有 ID:"结果里有 25°C 和 18°C,但哪个是上海哪个是北京?"
有 ID: "call_bbb 的结果是 25°C → 这是上海的天气"
tool_call_id 就是在无状态 HTTP 世界中,维持"这个结果是因为那个调用产生的"这个因果关系的唯一锚点。
2. tool_choice:LLM 行为的总开关
有时候你需要控制 LLM 的行为边界。tool_choice 就是那个开关:
| 设置 | 效果 |
|---|---|
"auto" |
默认,LLM 自己决定要不要用工具 |
{type: "any"} |
必须至少调用一个工具 |
{type: "tool", name: "xxx"} |
强制只调用某个工具 |
{type: "none"} |
禁止调用任何工具 |
日常开发基本用 "auto",但在特定场景下(比如你必须让 LLM 调用某个工具来走特定流程),其他选项能省下大量 prompt 对抗的时间。
3. stop_reason:LLM 的状态信号灯
每次 LLM 停下来,都会告诉你"为什么停的":
| 信号 | 含义 | 你该做什么 |
|---|---|---|
end_turn |
讲完了 | 展示给用户 |
tool_use |
帮我执行工具 | 执行并把结果喂回去 |
max_tokens |
说不完就被截了 | 增大上限或调整策略 |
refusal |
被安全机制拦了 | 降级处理 |
你的代码中 stop_reason 决定了程序走向。它不是可有可无的元数据,而是驱动 Agent 循环的状态机信号。
四、一个容易被忽略的认知:不是两轮,是循环
我见过很多开发者以为 Tool Use 是固定的"两轮 HTTP 请求":
请求1 → LLM返回tool_calls → 执行 → 请求2 → LLM返回最终结果
这个理解在简单场景下成立,但它掩盖了一个关键事实:
Tool Use 的本质是一个 while 循环,不是固定两轮。
ini
while (true) {
response = LLM(messages, tools);
if (不需要工具了) break; // end_turn
results = 执行所有tool_calls;
messages.push(results); // 注入结果
// 下一轮循环,LLM 判断是否还要再调工具
}
为什么需要多轮?举个真实例子:
css
用户:"帮我分析茅台和青岛啤酒哪个更值得投资,顺便看看两地的天气"
轮次 1: LLM 调 get_closing_price("茅台")
轮次 2: 拿到茅台价格,调 get_closing_price("青岛啤酒")
轮次 3: 拿到两个价格,调 get_weather("贵州")、get_weather("青岛")
轮次 4: 综合全部数据,生成最终分析 → end_turn
四轮 HTTP 请求才完成一个用户问题。如果 LLM 发现还需要更多数据,甚至可以继续调。
理解这个 while 循环,是理解 Agent 的基础。
五、把一切串起来:消息数组的完整演化
如果你看一遍整个过程中消息数组的变化,一切都会变得清晰:
css
第 1 轮请求:
[{ role: "user", content: "茅台股价和上海天气?" }]
第 1 轮响应(LLM 返回 tool_calls):
{ role: "assistant", content: null,
tool_calls: [{id:"c1", name:"get_closing_price", arguments:'{"name":"茅台"}'},
{id:"c2", name:"get_weather", arguments:'{"city":"上海"}'}] }
第 2 轮请求(Runtime 注入结果):
[
{ role: "user", content: "茅台股价和上海天气?" },
{ role: "assistant", content: null, tool_calls: [...] },
{ role: "tool", content: "1488.21", tool_call_id: "c1" },
{ role: "tool", content: "晴 25°C", tool_call_id: "c2" }
]
第 2 轮响应(LLM 生成最终回复):
{ role: "assistant", content: "贵州茅台收盘价 1488.21 元,上海晴 25°C。" }
消息数组的 role 轮换规律是严格的:user → assistant(tool_calls) → tool → assistant(最终回复)。理解这个规律,你就理解了整个对话的骨架。
六、一张图总结
javascript
┌───────────────────────────────────────────────────────────┐
│ Tool Use 完整循环 │
├───────────────────────────────────────────────────────────┤
│ │
│ ① 用户输入 + tools 定义(认知植入) │
│ ↓ │
│ ② LLM 推理 → 意图识别 │
│ 生成 tool_calls(id + name + arguments) │
│ content = null(停止对用户说话) │
│ ↓ │
│ ③ Runtime 执行(突破物理限制的唯一环节) │
│ 查数据库 / 调 API / 读文件 / 操作硬件 │
│ ↓ │
│ ④ 结果注入消息数组,再次发给 LLM │
│ LLM 判断:还需要工具吗? │
│ ├── 要 → 回到 ②(while 循环) │
│ └── 不要 → 生成最终回复,返回用户 │
│ │
└───────────────────────────────────────────────────────────┘
七、写在最后
Tool Use 是 LLM Agent 的核心机制。把整个过程拆到底,你会看到一套精妙的接力:
- LLM 负责理解意图、生成调用指令、包装结果输出。但它什么都执行不了。
- JSON Schema 是翻译层,把代码世界和语言世界对接起来。
- Runtime 负责真正执行------调用 API、查数据库、操作文件。但它的结果不直接给用户。
- tool_call_id 在无状态 HTTP 中锚定因果链。
- while 循环(而非固定两轮)才是完整的运行模式。
用一句大白话总结:
LLM 是大脑,只负责想和说。Runtime 是手,负责做。JSON Schema 是它们之间的暗号。而你作为开发者,就是搭建这套"大脑指挥手"系统的人。
从这个角度看,"AI 有没有自我意识"不是个哲学问题,而是个工程问题------这个错觉能被维护到什么程度,取决于你的 Tool Use 系统设计得有多好。