皮克斯风超市搞笑动画
2026-06-30 3373427
2026-06-30 0
豆包可以自动搜索网页。比如你问"今天的世界杯有哪些比赛?",它会自动搜索最新赛程。背后需要两个工具:日期获取工具 + 网络搜索工具。搜索流程是 web_search 搜出链接列表 → web_fetch 打开链接读全文 → LLM 整理回答。

Claude 可以分析 Excel 表格。背后需要一个工具:读取文件工具。流程是 read_excel 把 .xlsx 二进制文件解析成纯文本 → LLM 读懂后分析、计算、总结。
AI Agent 可以操作电脑、发邮件、调API……这些能力哪来的?
这些看似"AI 什么都会"的能力,背后全都是同一套模式——LLM 决定要用什么工具,代码真正去执行。
LLM 只负责"想"和"说",Tool 负责"动手"。Agent = LLM + Tools 的组合体。
作为开发者,我们知道这是一个精心设计的错觉——让用户以为是 LLM 完成的,其实不是。
用户看到的是"豆包搜到了新闻",背后的真相是:LLM 说了句"我需要搜索"(tool_calls),代码去调了搜索引擎,LLM 把结果整理成话。用户只看到最后一步。
Agent 的魔力来自 LLM 的"决策力" + 代码的"执行力"——两者缺一不可。
LLM 的本质:一个只能预测下一个词的概率模型(Next Token Prediction)。它被困在服务器里,没有系统权限——看不见屏幕,摸不到键盘,不能联网、不能读文件、不能调 API。
那它是怎么"调用API"、"读取文件"、"搜索网页"的?
LLM 只是输出了一段特定格式的文字(tool_calls JSON)。你的代码读到这段文字,把它翻译成真实的函数调用,执行完再把结果以文字形式塞回去。
整个过程 LLM 始终在"文字世界"里,一步都没离开过。
复制代码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
});
// 现在 client 就是你的"大模型热线"
工具就是函数。LLM 看不懂代码,只看得懂文字。所以必须把复杂的函数降维成它看得懂的"使用说明书"。
复制代码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"]
}
}
}
];
为什么用 JSON Schema?
OpenAI API 的 tools 参数要求使用 JSON Schema 格式来描述函数。原因是:
arguments 时会更准确parameters 是你出的"填空题题目",arguments 是 LLM 做完的"填空题答案"。
复制代码// 工具函数——这才是真正干活的东西
function get_closing_price(name) {
if (name === '青岛啤酒') {
return '67.92';
} else if (name === '贵州茅台') {
return '1488.21';
} else {
return '未找到该股票';
}
}
复制代码async function sendMessage(messages) {
const response = await client.chat.completions.create({
model: "deepseek-chat",
messages,
tools,
tool_choice: "auto", // 让 LLM 自己决定要不要用工具
});
return response;
}
tool_choice 参数详解tool_choice | 效果 | 使用场景 |
|---|---|---|
"auto" | LLM 自己判断要不要用工具 | 通用场景(90% 的情况) |
"required" | LLM 每次必须调用工具 | 需要严格查证的场景 |
"none" | LLM 不准调用工具 | 纯闲聊 |
{ type: "function", function: { name: "xxx" } } | 强制用某个工具 | 专用机器人 |
在请求中传入 tools 参数,本质上就是在做"认知植入"——给 LLM 注入一段它训练时没有的知识。LLM 区分不了"训练时学到的知识"和"请求时塞进去的工具定义"。你说它有 get_closing_price 工具,它就信自己能查股价。
用户问 → LLM 先查自己知识(回答不了)→ 回来看 tools 说明书 → 找到匹配工具 → 决定调用
LLM 有概率随机性,所以工具的 description 需要写得具体且清晰,否则 LLM 可能用错工具或不用工具。
| 层面 | 来源 |
|---|---|
| 格式(JSON 结构长什么样) | 训练时学的。LLM 见过大量 tool_calls 格式的对话样本 |
| 内容(具体调哪个工具、传什么参数) | 当场根据你的 tools 说明书 + 用户问题推导出来的 |
parameters(你的说明书)是"填空题的题目",arguments(LLM 的答案)是"填空题的作答"。
没有说明书,LLM 也会写 tool_calls,但不知道你的工具叫 get_closing_price 还是 get_stock_price。所以 description 写得准不准确,直接决定了 LLM 什么时候用、用什么工具。
复制代码let messages = [
{ role: "user", content: "青岛啤酒的收盘价是多少?" }
];
// 此时对话历史就一条:用户的问题
复制代码const response = await sendMessage(messages);
const message = response.choices[0].message;
console.log("模型返回的message对象 ", JSON.stringify(message, null, 2));
发出去的内容:聊天记录 + 工具说明书。大模型收到后开始推理:
大模型内部推理过程:
tools 说明书 → 有个 get_closing_price 工具,描述是"获取股票收盘价"tool_calls大模型返回的内容(注意 content 是 null):
复制代码{
"role": "assistant",
"content": null,
"tool_calls": [
{
"id": "call_abc123",
"type": "function",
"function": {
"name": "get_closing_price",
"arguments": "{"name":"青岛啤酒"}"
}
}
]
}
大模型停止了跟用户的对话,开始"自言自语"——tool_calls 是说给代码听的暗号,不是说给用户听的。
这里出现了最关键的信号:content: null + tool_calls: [...]。正常对话时 content 有值、tool_calls 不存在;需要工具时 content 是 null、tool_calls 出现。代码靠 if(message.tool_calls) 判断该不该干活。
if 外面!) 复制代码// 不管有没有调工具,都记录 LLM 的回复
messages.push({
role: message.role,
content: message.content,
tool_calls: message.tool_calls,
});
现在对话历史:
复制代码① { role: "user", content: "青岛啤酒收盘价?" }
② { role: "assistant", content: null, tool_calls: [...] }
复制代码if (response.choices[0].message.tool_calls) {
// 有 tool_calls!大模型在求救,进去帮忙
}
if(message.tool_calls) 是整个 Tool Calling 机制的连接点。没有这行判断,LLM 的求救信号就石沉大海。它就是"大脑"和"手脚"之间的神经突触——LLM 想好了要干什么,这行代码决定要不要帮它干。
复制代码const toolCall = response.choices[0].message.tool_calls[0];
// toolCall.function.name = "get_closing_price"
// toolCall.function.arguments = '{"name":"青岛啤酒"}'
复制代码if (toolCall.function.name === 'get_closing_price') {
const args = JSON.parse(toolCall.function.arguments);
// JSON.parse 把字符串 '{"name":"青岛啤酒"}' 转成对象 { name: "青岛啤酒" }
const price = get_closing_price(args.name);
// 执行真函数!传入 "青岛啤酒" → 函数返回 "67.92"
}
LLM 没有执行任何东西! 它只是输出了函数名和参数。是你的代码在这里真正调用了 get_closing_price。
if 里面!) 复制代码messages.push({
role: "tool",
tool_call_id: toolCall.id, // 关联到步骤 5 的求救信
content: price // 工具返回的结果
});
现在对话历史:
复制代码① { role: "user", content: "青岛啤酒收盘价?" }
② { role: "assistant", content: null, tool_calls: [...] }
③ { role: "tool", tool_call_id: "xxx", content: "67.92" }
复制代码const finalRes = await sendMessage(messages);
console.log('最终回答:', finalRes.choices[0].message.content);
// 输出:青岛啤酒的收盘价是 67.92 元。
大模型这次收到的完整上下文:
大模型这次不再输出 tool_calls(因为数据已经有了),而是直接用工具结果组织成自然语言回答。
一个工具到多个工具,只需要在 if/else 链上增加分支:
复制代码if (toolCall.function.name === 'get_closing_price') {
const args = JSON.parse(toolCall.function.arguments);
const result = get_closing_price(args.name);
messages.push({ role: "tool", tool_call_id: toolCall.id, content: result });
} else if (toolCall.function.name === 'get_weather') {
const args = JSON.parse(toolCall.function.arguments);
const result = get_weather(args.city);
messages.push({ role: "tool", tool_call_id: toolCall.id, content: result });
}
架构不需要变,只加一个 else if 就行。
工具调用可能因为各种原因失败:网络超时、参数格式错误、数据源不可用等。
复制代码try {
const result = get_closing_price(args.name);
messages.push({
role: "tool",
tool_call_id: toolCall.id,
content: result
});
} catch (error) {
// 把错误信息也当成"工具结果"塞回去
messages.push({
role: "tool",
tool_call_id: toolCall.id,
content: `查询失败:${error.message},请稍后重试`
});
console.error('工具执行失败:', error);
}// 继续第二次调用,LLM 会读到错误信息并友好地告诉用户
const finalRes = await sendMessage(messages);
开了 stream: true 后,tool_calls 是分块(delta)返回的,不能直接使用。
复制代码// 你期望一次性收到:
{
"tool_calls": [{
"function": { "name": "get_closing_price", "arguments": "{"name":"青岛啤酒"}" }
}]
}// 流式实际收到的是碎片(delta):
第1块: {"tool_calls":[{"function":{"name":"get_c"}}
第2块: "losing_price","arguments":"{"name":"
第3块: ""青岛啤酒"}"}}]}
复制代码let toolCallAccumulator = {};for await (const chunk of stream) {
const delta = chunk.choices[0].delta;
if (delta.tool_calls) {
for (const tc of delta.tool_calls) {
const index = tc.index || 0;
if (!toolCallAccumulator[index]) {
toolCallAccumulator[index] = { function: { name: '', arguments: '' } };
}
if (tc.function?.name) {
toolCallAccumulator[index].function.name += tc.function.name;
}
if (tc.function?.arguments) {
toolCallAccumulator[index].function.arguments += tc.function.arguments;
}
}
}
}// 流结束后,拼接完整
const toolCalls = Object.values(toolCallAccumulator);
复制代码// 第一次调用:不用流式,确保完整拿到 tool_calls
const response = await client.chat.completions.create({
model: "deepseek-chat",
messages,
tools,
tool_choice: "auto",
stream: false, // ← 关键
});// 执行工具...// 第二次调用:可以用流式输出给用户
const stream = await client.chat.completions.create({
model: "deepseek-chat",
messages,
stream: true, // ← 这时候可以开了
});for await (const chunk of stream) {
process.stdout.write(chunk.choices[0]?.delta?.content || '');
}
| 操作 | 放哪里 | 原因 |
|---|---|---|
push(message) — LLM 的回复 | if 外面 | 不管有没有调工具,都要记 |
push({role:"tool", ...}) — 工具结果 | if 里面 | 只有调了工具才有结果 |
| 概念 | 一句话解释 |
|---|---|
| LLM | 只会预测下一个词的"文字接龙大师" |
| Tool | 真正干活的函数(你的代码) |
| Agent | LLM + Tools 的组合体 |
| 认知植入 | 通过 tools 参数告诉 LLM 它有哪些工具 |
| tool_calls | LLM 写给代码的"求救纸条" |
| JSON Schema | 把函数翻译成 LLM 能看懂的说明书 |
| messages | LLM 的"记忆线",顺序决定一切 |
| 两次调用 | 第一次决策,第二次回答 |
Tool Calling 不是什么神奇魔法,它是一套精心设计的"大脑 ↔ 手脚"通信协议。理解了这个机制,你就掌握了构建 AI Agent 的基石。
现在,轮到你用代码去"指挥"LLM 了!