LangChain 工具调用全解析:从基础到实战,吃透 LLM 与外部工具的协同逻辑
在大语言模型(LLM)的应用中,纯文本生成已无法满足复杂场景需求 ——LLM 本身无法实时联网、精准计算或操作外部系统。LangChain 的工具调用能力正是为解决这一痛点而生,它让 LLM 能像人类一样「自主选择工具、传递参数、获取结果」,实现从「文本生成」到「任务执行」的跨越。本文结合实战代码、核心原理和常见问题,带你从零吃透 LangChain 工具调用的全流程。告诉 LangChain「这
前言
在大语言模型(LLM)的应用中,纯文本生成已无法满足复杂场景需求 ——LLM 本身无法实时联网、精准计算或操作外部系统。LangChain 的工具调用能力正是为解决这一痛点而生,它让 LLM 能像人类一样「自主选择工具、传递参数、获取结果」,实现从「文本生成」到「任务执行」的跨越。本文结合实战代码、核心原理和常见问题,带你从零吃透 LangChain 工具调用的全流程。
一、核心概念速览:先搞懂 3 个关键术语
在深入实战前,先明确工具调用的核心组件,避免被概念绕晕:
- 工具(Tool):封装了具体功能的可执行单元(如加法计算、网络搜索),本质是被 LangChain 标准化后的函数,具备
invoke()方法用于执行。 - tool_call:LLM 生成的「工具调用指令」,是包含
name(工具名)、args(参数)、id(唯一标识)、type="tool_call"(固定类型)的结构化字典,相当于「给工具的任务通知单」。 - 消息类型:工具调用依赖 3 类标准消息(LangChain 统一格式):
HumanMessage:用户提问(如「2 乘 3 等于多少?」);AIMessage:LLM 的响应,可能包含工具调用指令(tool_calls属性);ToolMessage:工具执行后的结果,需传递给 LLM 用于生成最终答案。
- Runnable 接口:LangChain 组件的标准接口,工具、模型、解析器均实现此接口,因此都支持
invoke()方法,是组件协同的基础。
二、工具创建的 3 种方式:从极简到灵活配置
LangChain 提供多种工具创建方式,可根据需求选择,核心是「将普通函数标准化为 LangChain 工具」。
方式 1:@tool 装饰器(最简推荐)
@tool是最常用的工具创建方式,本质是StructuredTool.from_function的语法糖,自动封装工具名、描述和参数校验。
from langchain_core.tools import tool
from typing_extensions import Annotated
# 基础版:依赖函数注释
@tool
def add(a: int, b: int) -> int:
"""两数相加:计算两个整数的和"""
return a + b
# 增强版:用Annotated指定参数描述(LLM可读取)
@tool
def multiply(
a: Annotated[int, ..., "第一个整数"],
b: Annotated[int, ..., "第二个整数"]
) -> int:
"""两数相乘:计算两个整数的积"""
return a * b
# 调用工具
print(add.invoke({"a": 2, "b": 3})) # 输出:5
print(multiply.name) # 输出:multiply(默认取函数名)
print(multiply.description) # 输出:两数相乘:计算两个整数的积
方式 2:@tool + Pydantic(复杂参数校验)
当需要更精细的参数约束(如必填项、范围限制)时,可结合 Pydantic 模型定义输入 schema,替代函数注释。
from pydantic import BaseModel, Field
from langchain_core.tools import tool
# 定义Pydantic模型,指定参数约束和描述
class AddInput(BaseModel):
"""两数相加的输入参数"""
a: int = Field(..., description="第一个整数,必须大于0")
b: int = Field(..., description="第二个整数,必须大于0")
# 绑定输入schema
@tool(args_schema=AddInput)
def add(a: int, b: int) -> int:
return a + b
# 调用工具(若传入a=-1,会触发Pydantic校验报错)
print(add.invoke({"a": 3, "b": 4})) # 输出:7
方式 3:StructuredTool.from_function(全量配置)
适用于需要自定义工具名、响应格式(如分离文本内容和原始数据)的场景,灵活性最高。
from typing import Tuple, List
from langchain_core.tools import StructuredTool
from pydantic import BaseModel, Field
# 定义输入schema
class AddInput(BaseModel):
a: int = Field(description="第一个整数")
b: int = Field(description="第二个整数")
# 定义工具函数(返回二元组:content+artifact)
def add(a: int, b: int) -> Tuple[str, List[int]]:
nums = [a, b]
content = f"{nums}相加的结果是{a+b}" # 给LLM的文本内容
return content, nums # artifact:原始数据,供后续分析
# 创建工具,指定响应格式
add_tool = StructuredTool.from_function(
func=add,
name="ADD", # 自定义工具名
description="两数相加:精准计算两个整数的和", # 工具描述(LLM用于判断是否调用)
args_schema=AddInput, #参数描述
response_format="content_and_artifact" # 分离content和artifact
)
# 模拟LLM调用姿势(需传入tool_call必填字段)
result = add_tool.invoke({
"name": "ADD",
"args": {"a": 3, "b": 4},
"type": "tool_call", # 固定值,声明请求类型
"id": "111" # 唯一标识,绑定请求和结果
})
print(result)
# 输出:content='[3, 4]相加的结果是7' name='ADD' tool_call_id='111' artifact=[3, 4]
三、工具调用全流程:LLM 自主决策与执行
工具调用的核心是「LLM 自主判断是否调用工具、调用哪个工具」,完整流程分为 3 步,结合多工具并行调用案例详解:
实战案例:多工具协同(加法 + 乘法)
需求:用户提问「2 乘 3 等于多少?6 加 6 等于多少?」,LLM 自动调用对应工具并返回答案。
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage
from langchain_core.tools import tool
from typing_extensions import Annotated
# 1. 定义工具(复用之前的add和multiply)
@tool
def add(a: Annotated[int, ..., "第一个整数"], b: Annotated[int, ..., "第二个整数"]) -> int:
"""两数相加"""
return a + b
@tool
def multiply(a: Annotated[int, ..., "第一个整数"], b: Annotated[int, ..., "第二个整数"]) -> int:
"""两数相乘"""
return a * b
# 2. 初始化模型并绑定工具
model = ChatOpenAI(model="gpt-4o-mini")
tools = [add, multiply]
model_with_tools = model.bind_tools(tools) # 给模型挂载工具
# 3. 构造用户提问(HumanMessage)
messages = [HumanMessage("2乘3等于多少?6加6等于多少?")]
# 4. 第一步调用模型:生成工具调用指令
ai_msg = model_with_tools.invoke(messages)
messages.append(ai_msg) # 保存指令到上下文
print(ai_msg.tool_calls)
# 输出:[{'name': 'multiply', 'args': {'a':2, 'b':3}, 'id':'xxx', 'type':'tool_call'}, ...]
# 5. 执行工具并构造ToolMessage
for tool_call in ai_msg.tool_calls:
# 根据工具名匹配对应的工具
selected_tool = {"add": add, "multiply": multiply}[tool_call["name"].lower()]
# 执行工具(传入tool_call指令)
tool_msg = selected_tool.invoke(tool_call)
messages.append(tool_msg) # 保存工具结果到上下文
# 6. 第二步调用模型:根据上下文生成最终答案
final_result = model_with_tools.invoke(messages)
print(final_result.content) # 输出:2乘3等于6,6加6等于12
流程拆解(关键!)
- 模型决策:LLM 分析用户问题,判断需要调用
multiply和add工具,生成包含参数的tool_call指令; - 工具执行:通过
selected_tool.invoke(tool_call)执行工具,selected_tool是「工具本体」(有执行能力),tool_call是「任务指令」(含参数); - 结果整合:工具执行结果封装为
ToolMessage,与原始提问、工具指令一起传给模型,模型整理成自然语言答案。
四、底层原理揭秘:为什么这么设计?
很多开发者会困惑:「参数都在 tool_call 里,为什么还要通过 selected_tool.invoke (tool_call) 调用?」,核心逻辑如下:
1. selected_tool 与 invoke 的关系
.invoke()是工具(Tool类实例)的专属方法,不是全局函数,必须通过工具实例调用;selected_tool是「具备执行能力的工具本体」(如加法函数的标准化封装),tool_call是「工具的调用指令、数据」;- 类比:
selected_tool是厨师,tool_call是订单,没有厨师,订单无法被执行;没有订单,厨师不知道做什么。
2. tool_call 为什么是必填项?
- 标识请求类型:
type="tool_call"告诉 LangChain「这是工具调用请求」,区别于普通聊天; - 提供核心数据:
name指定工具、args传递参数、id绑定结果,缺一不可; - 协议约束:LangChain 的工具调用协议要求,确保多工具调用时结果不错乱。
3. Runnable 接口的作用
所有组件(模型、工具、解析器)都实现Runnable接口,因此都支持invoke()方法,这是 LangChain 的「标准化设计」:
- 统一调用方式:无论工具、模型还是链,都用
invoke()触发执行; - 支持链式组合:通过
|运算符将组件串联(如model | parser),简化流程。
五、常见问题 FAQ(解决你的核心困惑)
结合之前的提问,整理工具调用的高频疑问:
Q1:为什么 selected_tool 能调用 invoke?
A:因为selected_tool是被@tool或StructuredTool封装后的Tool类实例,Tool类天生自带invoke()方法,用于解析tool_call并执行工具逻辑。
Q2:tool_call 里已经有工具名,为什么还要手动匹配 selected_tool?
A:这是「白名单机制」,避免 LLM 恶意调用未授权工具。通过{"add": add, "multiply": multiply}限定可调用工具,即使 LLM 返回其他工具名,也无法执行,提升安全性。
Q3:为什么调用工具时必须传入 tool_call?
A:tool_call包含工具执行所需的所有信息:args(参数)、id(唯一标识)、type(请求类型),invoke()方法会自动解析这些字段,无需手动传递参数。
Q4:多工具调用时,如何保证结果对应?
A:依赖tool_call的id字段:工具执行后,ToolMessage会携带tool_call_id,与请求的id一一对应,模型可通过该 ID 关联工具指令和结果。
六、总结与进阶建议
LangChain 工具调用的核心是「标准化」和「自主性」:
- 标准化:通过
Tool类、Runnable接口、消息类型,让不同工具和模型无缝协同; - 自主性:LLM 自主判断工具调用逻辑,无需人工干预,是 AI Agent 的基础。
进阶方向
- 自定义工具:封装 API 调用(如天气查询、数据库操作),扩展 LLM 能力边界;
- 工具链:将多个工具按流程串联(如「搜索→分析→生成报告」);
- 错误处理:添加工具调用失败重试、参数校验失败反馈等逻辑;
- 向量数据库集成:结合 RAG 技术,让工具调用支持知识库检索(如文档问答)。
更多推荐


所有评论(0)