Appearance
工具调用 · 让 Agent 真正"动起手来"
到现在,Agent 会聊、会查资料,但它还能做的闭环里最惊艳的一步:让模型"决定要调用一个函数",然后把函数真的执行起来。
基础篇我们说"工具调用就是那双手"。它的内核,是把"决定"和"执行"分开:
你: "帮我算一下 529 × 87"
LLM: (心里想) 这得算数 → 发出一个"调用工具"指令 → [调用: calculator(529,87)]
代码: 真去算 = 46023 → 把结果回给 LLM
LLM: 得到结果,整理成回答: "结果是 46023"模型不真的计算,它只负责"决定调哪个函数、传什么参数";真正的执行由你的代码完成。 这样一来,模型就摆脱了"只会嘴上说说",能事实求是地调用任何 JS 函数——查天气、查数据库、发请求,什么都行。
这一节会用到 LangChain 的
@tool。如果你之前没写过装饰器,没关系——它就是给函数"打标签",告诉模型"这个函数叫啥、要什么参数、干嘛用的"。
1. 定义一个工具
先写一个最简单的工具,能帮你算数学:
js
// src/tools/math.js
import { tool } from '@langchain/core/tools'
import { z } from 'zod'
export const calculator = tool(
async ({ expression }) => {
// 用 Node 把数学表达式算出来(生产更建议用 mathjs,这里演示)
try {
return eval(expression) // ⚠️ 仅演示;生产不要用 eval!(见文末安全提醒)
} catch {
return '表达式不合法'
}
},
{
name: 'calculator',
description: '计算数学表达式的值,例如 "3.14*2" 或 "(1+2)*3"。适合算数、转换单位等。',
schema: z.object({
expression: z.string().describe('要计算的数学表达式,用 JS 语法'),
}),
}
)拆开看 tool() 的三个关键信息:
| 字段 | 作用 |
|---|---|
name | 给模型看的唯一 ID,没有它模型没法点名要调用哪个工具 |
description | 最重要。它是模型的"说明书"——别小看它,模型靠它判断"该不该用这个工具、什么时候用"。写得越清楚越好 |
schema | 参数的结构。模型会照着这张表填参数(这里是 expression 这个字符串) |
2. 让模型"想调就用":把工具交给模型
用 model.bindTools(...) 把工具列表"绑定"进模型,然后正常对话:
js
// src/tools-demo.js —— 模型决定要调工具时,会返回一个"工具调用请求"
import 'dotenv/config'
import { ChatOpenAI } from '@langchain/openai'
import { calculator } from './tools/math.js'
const model = new ChatOpenAI({
model: process.env.MODEL_NAME ?? 'gpt-4o-mini',
apiKey: process.env.OPENAI_API_KEY,
configuration: { baseURL: process.env.OPENAI_API_BASE },
}).bindTools([calculator]) // ★ 把工具绑给模型
const reply = await model.invoke('帮我算一下 (12 + 28) × 5 等于多少')
// 看模型"怎么想"的:它不会直接给答案,而会想要"调用 calculator"
console.log('模型原始返回:', JSON.stringify(reply, null, 2))
if (reply.tool_calls?.length) {
const call = reply.tool_calls[0]
console.log('\n→ 模型想调用:', call.name) // calculator
console.log('→ 参数是:', JSON.stringify(call.args)) // { expression: '(12+28)*5' }
}关键点:当对话里给了工具,模型不再直接回一句话,而是可能返回一个 tool_calls 数组在里面说"我想调用 calculator,参数是 xxx"。而真正的函数调用,现在还没执行——它只是"请求"。
3. 真正的闭环:执行工具 + 回到模型
"请求"要靠你的代码接住并执行,再把结果带回给模型。用 model.invoke() 配合人工循环:
js
// src/tools-loop.js —— 手动跑通完整的工具调用闭环
import 'dotenv/config'
import { ChatOpenAI } from '@langchain/openai'
import { calculator } from './tools/math.js'
const tools = [calculator]
const model = new ChatOpenAI({
model: process.env.MODEL_NAME ?? 'gpt-4o-mini',
apiKey: process.env.OPENAI_API_KEY,
configuration: { baseURL: process.env.OPENAI_API_BASE },
}).bindTools(tools)
// LangChain 提供了现成的高层 Agent,能自动循环工具。这里为了看懂原理,先手动写一遍。
const messages = [
{ role: 'system', content: '你是计算助手,算对了再回答,别心算。' },
{ role: 'user', content: '(12+28)*5 和 99*99 分别是多少?' },
]
// —— 循环: 模型可能要连续调用多次工具 ——
for (let round = 0; round < 5; round++) {
const reply = await model.invoke(messages)
messages.push(reply)
if (!reply.tool_calls?.length) {
// 没有工具要调了,说明模型已经准备好回答
break
}
// 逐个执行模型要求的工具调用
for (const call of reply.tool_calls) {
console.log(`🔧 执行工具: ${call.name}(${JSON.stringify(call.args)})`)
const toolFn = tools.find(t => t.name === call.name) // 找到对应工具函数
const result = await toolFn.invoke(call.args) // 真正执行!
// 把"工具执行结果"作为一条新消息追加给模型
messages.push({
role: 'tool',
tool_call_id: call.id,
content: JSON.stringify(result),
})
}
}
const final = messages[messages.length - 1]
console.log('\n💬 最终回答:', final.content)跑一下:
bash
node src/tools-loop.js
# 🔧 执行工具: calculator({"expression":"(12+28)*5"})
# 🔧 执行工具: calculator({"expression":"99*99"})
# 💬 最终回答: (12+28)*5 = 200, 99×99 = 9801这条 for 循环就是 Agent 的灵魂,值得盯着看 30 秒:
- 把消息(含历史、含工具执行结果)发给模型;
- 模型说"我要调工具 X(带参数)"或直接说答案;
- 若调工具 → 你执行它 → 结果作为
role:'tool'消息塞回去 → 回到第 1 步(可能还要连环调); - 模型不再调了 → 输出最终回答。
这个"请求→执行→回填→再问"的循环,就是为了解决一个朴素问题:你把模型问懵之前,总能带上工具结果再让它答。
4. 用 LangChain 现成的 Agent(生产更爽)
手动循环是为了让你看懂原理。真实项目直接用 LangChain 的 createReactAgent / ToolNode,它帮你把第 3 步的循环、错误处理、中断恢复都做了:
bash
npm install @langchain/langgraphjs
// src/agent-with-tools.js —— LangGraph 把循环自动化
import 'dotenv/config'
import { ChatOpenAI } from '@langchain/openai'
import { MemorySaver } from '@langchain/langgraph'
import { createReactAgent } from '@langchain/langgraph/prebuilt'
import { calculator } from './tools/math.js'
const model = new ChatOpenAI({
model: process.env.MODEL_NAME ?? 'gpt-4o-mini',
apiKey: process.env.OPENAI_API_KEY,
configuration: { baseURL: process.env.OPENAI_API_BASE },
})
const agent = createReactAgent({
llm: model,
tools: [calculator], // 给 Agent 的工具
checkpointSaver: new MemorySaver(), // 让它能跨对话记住状态(记忆篇细讲)
})
const result = await agent.invoke({
messages: [{ role: 'user', content: '帮我算 1000/7 保留两位小数' }],
})
console.log(result.messages[result.messages.length - 1].content)createReactAgent 内部就把"工具循环"变成了一张图:模型 → (要调工具?) → 工具节点 → 回到模型 → … → 直到输出。你只需给它模型和工具列表,剩下的循环交给框架。
💡 一个现实提醒:允许模型 "任意调用你的函数" 是有风险的。生产工具要么做权限校验、要么让调用需要审批,别把
eval之类裸奔。
5. 一个更真实的工具:查外部服务
工具不一定要算数。接一个真实的 HTTP 接口,Agent 能"上网办事"。比如查实时天气(以 Open-Meteo 为例,免费无需 key):
js
// src/tools/weather.js
import { tool } from '@langchain/core/tools'
import { z } from 'zod'
export const getWeather = tool(
async ({ city, lat, lon }) => {
const url = `https://api.open-meteo.com/v1/forecast?latitude=${lat}&longitude=${lon}¤t_weather=true`
const res = await fetch(url)
const data = await res.json()
const t = data.current_weather?.temperature
return t != null
? `${city} 当前 ${t}°C`
: `查不到 ${city} 的天气`
},
{
name: 'getWeather',
description: '查询某个城市的当前天气。需要经纬度。',
schema: z.object({
city: z.string().describe('城市名'),
lat: z.number().describe('纬度'),
lon: z.number().describe('经度'),
}),
}
)再给它绑上 getWeather,模型就能"为了回答天气问题去调接口"。这就是"联网"的本质——不是模型自己上网,而是模型决定"要不要调一个能上网的函数"。
🧪 本篇自检
- [ ] 能用
tool()定义一个带描述和参数的工具 - [ ] 看懂
tool_calls与role:'tool'的循环 - [ ] 用
createReactAgent跑通带工具自动循环的 Agent - [ ] 能接一个真正的 HTTP 工具(天气/搜索/查库)
这一篇的灵魂,一句话:
工具调用 = 模型负责"决定调哪个函数+填什么参数",你的代码负责"真去执行",再把结果喂回模型,循环到它满意为止。Agent 就此长出了手。
到这里,会对话、有知识、能动手,Agent 骨架基本成型。但还差一样东西,让它的"记性"更像人而不是金鱼—— 记忆与多轮。