前言

在大语言模型(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

流程拆解(关键!)

  1. 模型决策:LLM 分析用户问题,判断需要调用multiplyadd工具,生成包含参数的tool_call指令;
  2. 工具执行:通过selected_tool.invoke(tool_call)执行工具,selected_tool是「工具本体」(有执行能力),tool_call是「任务指令」(含参数);
  3. 结果整合:工具执行结果封装为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是被@toolStructuredTool封装后的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_callid字段:工具执行后,ToolMessage会携带tool_call_id,与请求的id一一对应,模型可通过该 ID 关联工具指令和结果。

六、总结与进阶建议

LangChain 工具调用的核心是「标准化」和「自主性」:

  • 标准化:通过Tool类、Runnable接口、消息类型,让不同工具和模型无缝协同;
  • 自主性:LLM 自主判断工具调用逻辑,无需人工干预,是 AI Agent 的基础。

进阶方向

  1. 自定义工具:封装 API 调用(如天气查询、数据库操作),扩展 LLM 能力边界;
  2. 工具链:将多个工具按流程串联(如「搜索→分析→生成报告」);
  3. 错误处理:添加工具调用失败重试、参数校验失败反馈等逻辑;
  4. 向量数据库集成:结合 RAG 技术,让工具调用支持知识库检索(如文档问答)。
Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐