Skip to content
On this page

工具调用 · 让 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 秒:

  1. 把消息(含历史、含工具执行结果)发给模型;
  2. 模型说"我要调工具 X(带参数)"或直接说答案;
  3. 若调工具 → 你执行它 → 结果作为 role:'tool' 消息塞回去 → 回到第 1 步(可能还要连环调);
  4. 模型不再调了 → 输出最终回答。

这个"请求→执行→回填→再问"的循环,就是为了解决一个朴素问题:你把模型问懵之前,总能带上工具结果再让它答。

4. 用 LangChain 现成的 Agent(生产更爽) ​

手动循环是为了让你看懂原理。真实项目直接用 LangChain 的 createReactAgent / ToolNode,它帮你把第 3 步的循环、错误处理、中断恢复都做了:

bash
npm install @langchain/langgraph
js
// 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}&current_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 骨架基本成型。但还差一样东西,让它的"记性"更像人而不是金鱼—— 记忆与多轮。

要保持清醒 永远不抱有意外的幻想 凭空的期待最要命