在之前的学习中,我们眼中的大模型(LLM)更像是一个博学但封闭的“大脑”。它能陪你聊天、写诗、解释复杂的概念,但它有一个致命的弱点:它无法触及外部世界。
它不知道此时此刻北京的天气(因为它只有历史数据),它算不好复杂的加减乘除(因为它是基于概率预测文字,而不是计算器),它更无法帮你查询数据库。
今天这一课,我们将通过 LangChain 的Tools(工具)模块,打破这层次元壁。我们将学习如何将自定义函数变成大模型可以调用的“工具”,让 AI 从“只会聊天”进化为“能干实事”。
为什么需要 Tools?
大模型的核心能力是理解意图和生成文本。当我们把“工具”引入系统时,我们实际上是在通过 Prompt 告诉大模型:
“嘿,我这里有一个查天气的函数和一个做加法的函数。如果用户问的问题涉及到这两个领域,不要自己瞎编,请告诉我你需要用哪个函数,以及参数是什么。”
这就是Function Calling(函数调用)的核心逻辑。
在本文中,我们将构建一个基于 DeepSeek 模型的应用,通过两个具体的工具——天气查询器和加法计算器,来演示这一过程。
第一步:定义工具(The “Hands”)
在 LangChain 中,定义一个工具不仅仅是写一个函数,我们还需要告诉大模型这个工具是干什么用的(Description),以及它需要什么参数(Schema)。
我们需要引入zod库来进行参数的类型验证,这是连接自然语言与程序代码的桥梁。
1. 模拟一个天气数据库
首先,因为我们没有真实的 API key,我们用一个对象来模拟数据库:
// 模拟的天气数据库 const fakeWeatherDB = { 北京: { temp: "30摄氏度", condition: "晴", wind: "微风"}, 上海: { temp: "28摄氏度", condition: "多云", wind: "强"}, 广州: { temp: "26摄氏度", condition: "阴", wind: "中"}, }2. 创建天气工具
我们使用@langchain/core/tools中的tool方法来封装逻辑。
请注意schema部分。我们使用z.object定义了输入必须包含city字段,并且必须是字符串。这非常关键,因为大模型会根据这个定义,从用户的自然语言(如“北京今天咋样”)中提取出{ city: "北京" }。
import { tool } from '@langchain/core/tools'; import { z } from 'zod'; const weatherTool = tool( async ({ city }) => { const weather = fakeWeatherDB[city]; if(!weather) { return `城市${city}的天气信息不存在`; } return `城市${city}的天气是${weather.temp}, ${weather.condition}, 风力${weather.wind}`; }, { name: "get_weather", description: "查询指定城市的今日天气情况", // 给大模型看的说明书 schema: z.object({ city: z.string().describe("要查询天气的城市") // 参数描述 }) } )3. 创建加法工具
同理,大模型通常不擅长精确数学计算。我们可以给它一个计算器:
const addTool = tool( async ({a, b}) => String(a + b), // 将结果转为字符串返回给模型 { name: 'add', description: '计算两个数字的和', schema: z.object({ a: z.number(), b: z.number() }) } )第二步:绑定模型(The “Brain”)
有了工具,我们还需要让“大脑”知道它们的存在。我们将使用ChatDeepSeek模型,并通过.bindTools()方法将刚才定义的工具箱挂载到模型上。
import { ChatDeepSeek } from '@langchain/deepseek'; import 'dotenv/config'; const model = new ChatDeepSeek({ model: 'deepseek-chat', temperature: 0 // 设置为0,让模型更理性,专注于任务执行 }).bindTools([addTool, weatherTool]);.bindTools()是整个流程的点睛之笔。它并不会改变模型的内部权重,而是将工具的描述和 Schema 转换成特定格式的 System Prompt 发送给大模型,让模型进入“待命状态”。
第三步:调用与执行(The Action)
现在,让我们看看当我们向模型提问时,究竟发生了什么。
场景一:用户询问天气
const res = await model.invoke("北京今天的天气怎么样?");如果是普通的聊天模型,它可能会回答:“作为一个 AI 我无法联网…”。
但因为绑定了工具,模型会分析语意,发现这匹配了 get_weather 的描述。
此时,模型并没有直接执行代码(切记,大模型只是文本生成器,它运行不了 JS 代码)。它返回的res对象中包含了一个特殊的属性:tool_calls。
让我们打印看看res.tool_calls[0]长什么样:
{ name: 'get_weather', args: { city: '北京' }, type: 'tool_call', id: 'call_00_xyz...' }模型准确地提取了函数名和参数!
JavaScript 小贴士:优雅的可选链(Optional Chaining)
在处理模型返回结果时,我们需要非常小心。因为模型不一定总是调用工具。如果用户问“你好”,模型可能只返回文本,此时tool_calls是 undefined。
在传统代码中,我们可能需要这样写防御性代码:
// 繁琐的写法 if (res.tool_calls && res.tool_calls.length > 0) { // 执行逻辑 }但在 ES6+ 中,我们可以使用可选链操作符 (?.)来让代码更优雅:
// 优雅的写法 if (res.tool_calls?.length) { // 只有当 tool_calls 存在且长度不为 0 时,才执行 }它的作用是安全地访问嵌套对象的属性,一旦中间某个值为null或undefined,表达式会立即短路返回undefined,而不会报错导致程序崩溃。
第四步:闭环(执行工具逻辑)
最后一步,我们需要在代码中捕获模型的“意图”,并在本地执行真正的函数,然后获取结果。
if(res.tool_calls?.length) { const toolCall = res.tool_calls[0]; // 获取第一个工具调用请求 console.log("模型请求调用工具:", toolCall.name); if (toolCall.name === 'add') { // 调用加法工具 const result = await addTool.invoke(toolCall.args); console.log("最终结果:", result); } else if (toolCall.name === 'get_weather') { // 调用天气工具 const result = await weatherTool.invoke(toolCall.args); console.log("最终结果:", result); } }当我们运行这段代码时,控制台将输出:
最终结果: 城市北京的天气是30摄氏度, 晴, 风力微风
如果用户问数学题呢?
如果我们把输入改为:model.invoke(“3 + 5等于多少?”)。
模型会返回 name: ‘add’ 和 args: { a: 3, b: 5 }。
我们的代码会命中 add 分支,最终输出 8。
总结与思考
通过今天的课程,我们揭开了 AI Agent(智能体)最基础的雏形:
- 定义:利用
zod和tool定义清晰的函数边界。 - 绑定:利用
bindTools赋予大模型选择权。 - 解析:利用
tool_calls获取模型的决策。 - 执行:在本地运行代码并获取结果。
为什么这很重要?
这就好比给大脑(LLM)接上了手(Tools)。虽然在这个简单的例子中,我们需要手动写 if/else 来判断执行哪个工具,但在更高级的 LangChain Agent 模块中,这一过程是自动化的——模型会自己“观察”结果,甚至根据结果进行下一轮的思考(ReAct 模式)。