从零开始掌握LangChain工具调用:让AI拥有"动手能力"
前言
在传统的AI对话中,大模型只能依靠自己的知识回答问题。但当我们想让AI查询实时天气、计算复杂数学、访问数据库时,单纯的语言模型就显得力不从心了。
LangChain的Tools(工具)模块完美解决了这个问题------它让大模型具备了调用外部工具的能力,就像给AI装上了"双手",可以主动获取和处理信息。
本文将手把手教你实现一个完整的工具调用系统,包括天气查询和数学计算两个实战案例。
环境准备
1. 创建项目并安装依赖
首先创建项目文件夹并选择项目文件夹进行初始化:
bash
npm init -y
安装必要的依赖包:
bash
# 安装LangChain核心包和DeepSeek集成
npm install @langchain/core @langchain/deepseek
# 安装环境变量管理
npm install dotenv
# 安装参数验证工具
npm install zod
2. 配置环境变量
创建 .env 文件:
env
DEEPSEEK_API_KEY=你的DeepSeek_API密钥
如果你还没有DeepSeek API密钥,可以访问 DeepSeek官网 注册获取。
基础概念理解
在开始编码前,我们需要理解几个核心概念:
什么是Tool(工具)?
在LangChain中,Tool是一个可以被AI模型调用的函数,它包含三个核心要素:
- 执行逻辑:函数具体做什么
- 名称:工具的唯一标识
- 描述:告诉AI这个工具什么时候该用
- 参数定义:规定工具需要什么参数
工具调用的工作流程
markdown
用户提问 → AI分析 → 需要工具? → 是 → 返回工具调用信息
↓ 否
直接回答
实战一:实现加法计算工具
让我们从最简单的数学计算开始,实现一个加法工具。
创建加法工具
新建 index.js 文件,开始编写代码:
javascript
import { ChatDeepSeek } from '@langchain/deepseek'
import 'dotenv/config'
import { tool } from "@langchain/core/tools"
import { z } from 'zod'
// 创建加法工具
const addTool = tool(
// 第一个参数:工具的执行函数
async ({ a, b }) => {
// 将结果转为字符串返回
return String(a + b)
},
// 第二个参数:工具的配置
{
name: "add", // 工具名称
description: "计算两个数字的和", // 工具描述
// 参数结构定义
schema: z.object({
a: z.number().describe("第一个加数"),
b: z.number().describe("第二个加数")
})
}
)
console.log("加法工具定义完成:", addTool)
创建模型并绑定工具
javascript
// 创建DeepSeek模型实例
const model = new ChatDeepSeek({
model: 'deepseek-chat', // 使用deepseek聊天模型
temperature: 0 // 设置为0使输出更确定
}).bindTools([addTool]) // 绑定加法工具
// 测试调用
const res = await model.invoke("3+5等于多少?")
console.log( res.content)
### 处理工具调用结果
```javascript
// 检查是否需要调用工具
if (res.tool_calls?.length) {
// 获取第一个工具调用
const toolCall = res.tool_calls[0]
console.log("调用的工具:", toolCall.name)
console.log("工具参数:", toolCall.args)
// 根据工具名称执行对应的工具
if (toolCall.name === "add") {
const result = await addTool.invoke(toolCall.args)
console.log("计算结果:", result)
}
}
- 效果图

实战二:实现天气查询工具
接下来实现一个更实用的天气查询工具,模拟从数据库获取天气信息。
创建模拟天气数据库
javascript
// 模拟天气数据库
const fakeWeatherDB = {
北京: {
temp: "30°C",
condition: "晴",
wind: "微风"
},
上海: {
temp: "28°C",
condition: "多云",
wind: "东风 3 级"
},
广州: {
temp: "32°C",
condition: "阵雨",
wind: "南风 2 级"
}
}
创建天气查询工具
javascript
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("要查询天气的城市")
})
}
)
### 绑定多个工具
```javascript
// 同时绑定加法和天气工具
const model = new ChatDeepSeek({
model: 'deepseek-chat',
temperature: 0
}).bindTools([addTool, weatherTool])
// 测试天气查询
const res = await model.invoke("上海的天气怎么样?")
console.log("模型内容:", res.content)
console.log("工具调用:", res.tool_calls)
// 处理工具调用
if (res.tool_calls?.length) {
const toolCall = res.tool_calls[0]
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)
}
}
- 执行查询天气工具效果图

深入理解:为什么需要两轮调用?
!!! 核心知识点:为什么res.content是空的?
很多初学者会遇到一个困惑:为什么执行 console.log(res.content) 打印出来的是不是想要的答案?这其实是大模型工具调用的核心设计原理。
深入理解工具调用的两阶段过程
当大模型决定调用工具时,它会经历以下过程:
javascript
arduino
// 初学者常犯的错误:只做了第一步
const res = await model.invoke("上海的天气怎么样?")
console.log(res.content) // "" (空) - 为什么是空的?!
原因解释:
大模型的响应分为两种情况:
| 场景 | res.content | res.tool_calls | 含义 |
|---|---|---|---|
| 直接回答 | 有答案 | undefined | 模型用自己的知识回答 |
| 需要工具 | 为空 | 有工具信息 | 模型说:"我需要先查数据,请执行工具后再告诉我" |
类比理解
这就像你和助手对话:
- 你问:"上海的天气怎么样?"
- 助手想:"我需要查一下天气数据"
- 助手说:"请稍等,我查一下"(返回 tool_calls,content 为空)
- 你去查:打开天气APP(执行工具)
- 你告诉助手:"上海28°C,多云"(工具结果)
- 助手回答:"上海今天28°C,多云..."(最终回答)
完整的工具调用流程
要获得最终答案,需要进行两轮调用:
javascript
php
// 第一轮:模型决定是否需要工具
const firstResponse = await model.invoke("上海的天气怎么样?")
// 此时 firstResponse.content 为空
// firstResponse.tool_calls 包含工具信息
if (firstResponse.tool_calls?.length) {
// 执行工具
const weatherData = await weatherTool.invoke(firstResponse.tool_calls[0].args)
// 第二轮:把工具结果给模型,生成最终回答
const finalResponse = await model.invoke([
{ role: "user", content: "上海的天气怎么样?" },
{ role: "assistant", content: "", tool_calls: firstResponse.tool_calls },
{ role: "tool", content: weatherData, tool_call_id: firstResponse.tool_calls[0].id }
])
console.log("最终回答:", finalResponse.content) // ✅ 现在有答案了
}
对比测试:理解不同场景
javascript
javascript
import {ChatDeepSeek} from '@langchain/deepseek'
import 'dotenv/config'
import {tool} from "@langchain/core/tools"
import {z} from 'zod'//用于定义工具的输入参数类型
//tool 就是一个函数,定义一个加法工具
const fakeWeatherDB={
北京: { temp: "30°C", condition: "晴", wind: "微风" },
上海: { temp: "28°C", condition: "多云", wind: "东风 3 级" },
广州: { temp: "32°C", condition: "阵雨", wind: "南风 2 级" }
}
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("要查询天气的城市")
})
}
)
console.log(weatherTool)
const addTool = tool(
//两数相加
//等下大模型来调用
//参数 对象,以结构出来a b
async({a,b})=>String(a+b),
{
name:"add",
description:"计算两个数字的和",
//参数schema
schema:z.object({
a:z.number(),
b:z.number()
})
}
)
const model=new ChatDeepSeek({
model:'deepseek-chat',
temperature:0
}).bindTools([addTool,weatherTool])
// const res=await model.invoke('3+5等于多少?')
const res=await model.invoke("上海的天气怎么样?")
console.log(res.content)
//使用?因为大模型可能不会调用工具
//? 可选链运算符 es6新增的,有利于代码的简洁和优雅
if(res.tool_calls?.length){
// console.log(res.tool_calls[0])
if(res.tool_calls[0].name==="add"){
const result=await addTool.invoke(res.tool_calls[0].args)
console.log("计算结果:",result)
}else if(res.tool_calls[0].name==="get_weather"){
const result=await weatherTool.invoke(res.tool_calls[0].args)
console.log("天气结果:",result)
}
}
常见问题解答
Q1: 什么时候需要使用 bindTools?
当你希望模型具备调用外部工具的能力时,就需要使用 bindTools。如果不绑定工具,模型就只能依靠自身知识回答。
Q2: 如何让工具返回更复杂的数据?
工具函数可以返回任何类型的数据,不仅仅是字符串。例如可以返回对象:
javascript
const complexTool = tool(
async ({ id }) => {
const data = await fetchData(id)
return {
success: true,
data: data,
timestamp: new Date().toISOString()
}
},
// ... 配置
)
Q3: 为什么有些问题不触发工具调用?
模型会根据问题和工具描述来决定是否调用工具。比如你问"你好",模型不会调用天气工具,因为描述中说明这是查天气用的。
总结
通过本文的学习,我们掌握了:
- Tool的基本概念:给AI添加外部能力的函数
- 工具的定义方法 :使用
tool()函数创建 - 参数验证:使用 Zod 确保参数正确
- 工具绑定 :通过
bindTools()让模型具备工具调用能力 - 结果处理:如何执行工具并生成最终回答
工具调用是让AI从"聊天机器人"进化为"智能助手"的关键一步。掌握了这个技能,你就可以开发出能查询数据库、调用API、处理复杂任务的智能应用。
希望这篇文章对你有帮助,欢迎在掘金点赞收藏,有问题在评论区留言讨论!