LangChain 自定义工具全解析:从 @tool 到 bind_tools 的完整调用链路

目录

[LangChain 自定义工具全解析:从 @tool 到 bind_tools 的完整调用链路](#LangChain 自定义工具全解析:从 @tool 到 bind_tools 的完整调用链路)

[一、为什么工具需要 "名称 + 描述 + 参数"?](#一、为什么工具需要 "名称 + 描述 + 参数"?)

[二、方式一:用 @tool 装饰器定义工具(最常用)](#二、方式一:用 @tool 装饰器定义工具(最常用))

[三、方式二:用 StructuredTool.from_function 手动包装](#三、方式二:用 StructuredTool.from_function 手动包装)

[3.1 先定义参数 Schema](#3.1 先定义参数 Schema)

[3.2 写普通函数(不加 @tool)](#3.2 写普通函数(不加 @tool))

[3.3 包装为 StructuredTool](#3.3 包装为 StructuredTool)

[3.4 手动模拟大模型调用来测试](#3.4 手动模拟大模型调用来测试)

四、把工具绑定给模型:bind_tools

五、工具调用的完整底层流程

[5.1 模型返回了什么?](#5.1 模型返回了什么?)

[5.2 "调不调用" 是 LLM 自己决定的](#5.2 "调不调用" 是 LLM 自己决定的)

[5.3 完整的两轮调用流程](#5.3 完整的两轮调用流程)

六、总结


前言:

本文记录 LangChain 中如何定义工具、包装工具、绑定给模型,以及模型实际调用工具时的底层流程。

一、为什么工具需要 "名称 + 描述 + 参数"?

在写代码之前,先想清楚一个问题:一个工具为什么需要 name、description、args 这三个属性?

答案很简单 ------这三个属性本质就是提示词(Prompt)。

大模型本身并不知道你写了什么函数,它只看到你喂给它的工具描述。模型通过这三行信息来做决策:

表格

属性 作用 类比
name 工具叫什么 函数名
description 工具干什么用的 函数的 docstring
args 需要传什么参数 函数签名

模型靠这三条信息决定两件事:① 要不要调用工具;② 调用时传什么参数。 所以自定义工具时,函数名、文档字符串(docstring)、类型提示一个都不能少 ------LangChain 的 schema 会做校验,缺了就报错。


二、方式一:用 @tool 装饰器定义工具(最常用)

@tool 装饰器会自动把普通 Python 函数包装成 LangChain 的 Tool 对象,函数签名和 docstring 就是工具的 schema。

python 复制代码
from langchain_core.tools import tool

@tool
def add(a: int, b: int) -> int:
    """两数相加

    Args:
        a: 第一个整数
        b: 第二个整数
    """
    return a + b

包装完成后,可以直接调用和查看工具的元信息:

python 复制代码
print(add.invoke({"a": 2, "b": 3}))   # 5
print(add.name)                        # add
print(add.description)                # 两数相加\n\n    Args:\n        a: 第一个整数\n ...
print(add.args)                        # {'a': {'title': 'A', 'type': 'integer'}, 'b': {'title': 'B', 'type': 'integer'}}

注意 :函数参数里的 : int 不是 Python 语法的运行时行为,而是类型提示。但在自定义工具场景中,它必须写明------LangChain 靠它生成 JSON Schema 做参数校验。docstring 同理,写得越清楚,模型选得越准。


三、方式二:用 StructuredTool.from_function 手动包装

如果不想用装饰器(比如函数已经存在、不想动它),可以用 StructuredTool.from_function 手动指定工具的各项属性。

3.1 先定义参数 Schema

用 Pydantic 的 BaseModel + Field 来声明参数结构:

python 复制代码
from langchain_core.tools import StructuredTool
from pydantic import BaseModel, Field

class AddInput(BaseModel):
    a: int = Field(description="第一个整数")
    b: int = Field(description="第二个整数")

3.2 写普通函数(不加 @tool)

python 复制代码
def Add(a: int, b: int):
    return a + b

3.3 包装为 StructuredTool

python 复制代码
add_tool = StructuredTool.from_function(
    func=Add,               # 要执行的函数本体
    name="ADD",             # 工具名称
    description="两数相加",  # 工具描述,LLM 靠它判断何时调用
    args_schema=AddInput,   # 工具参数结构
)

四种核心参数一览:

表格

参数 作用
func 要执行的函数本体
name 工具名,如 "ADD"
description 工具描述,LLM 靠它判断何时调用
args_schema 参数结构(Pydantic Model),告诉模型需要传什么参数
response_format="content-and-artifact" 可选,把结果中的文本内容和结构化数据分开返回

3.4 手动模拟大模型调用来测试

包装好之后,我们可以手动构造一个 tool_call 字典来模拟大模型决定调用工具时的行为:

python 复制代码
add_tool.invoke({
    "name": "ADD",                    # 工具名
    "args": {"a": 3, "b": 4},         # 传入参数
    "type": "tool_call",              # 必填,类型标记
    "id": "123",                      # 必填,关联调用工具与调用方(处理并发用)
})

这一步的意义:脱离大模型,直接测试工具本身能不能跑通。type 和 id 是必填字段,其中 id 用来在并发场景下关联 "谁调用了哪个工具"。


四、把工具绑定给模型:bind_tools

定义好工具之后,需要把它 "告诉" 大模型,这一步用 bind_tools() 完成。

python 复制代码
from langchain_openai import ChatOpenAI
from langchain_core.tools import tool
from typing import Annotated

model = ChatOpenAI(model="gpt-4o-mini")

@tool
def Add(
    a: Annotated[int, ..., "第一个整数"],
    b: Annotated[int, ..., "第二个整数"],
) -> int:
    """两数相加"""
    return a + b

@tool
def Muti(
    a: Annotated[int, ..., "第一个整数"],
    b: Annotated[int, ..., "第二个整数"],
) -> int:
    """两数相乘"""
    return a * b

# 绑定工具
tools = [Add, Muti]
model_calculate = model.bind_tools(tools=tools)

直接用自然语言提问试试:

python 复制代码
print(model_calculate.invoke("2乘3等于多少?").content)

注意 :这一步模型只做了 "工具选择"------ 判断该调哪个工具、传什么参数 ------还没有真正执行工具。


五、工具调用的完整底层流程

5.1 模型返回了什么?

当模型决定调用工具时,返回结果的 content 字段是空的 ,真正的信息在 tool_calls 字段里:

  • 函数名:要调用哪个工具
  • 参数值:传入什么参数

5.2 "调不调用" 是 LLM 自己决定的

即使你绑定了工具,模型也会根据问题与工具的相关性,自行判断要不要调用。

  • 问题和工具相关 → 模型返回 tool_calls
  • 问题和工具无关 → 模型直接在 content 里回答,不调工具

如果想强制模型必须调用工具 ,可以在 bind_tools() 时传:

python 复制代码
model_calculate = model.bind_tools(tools=tools, tool_choice="any")

5.3 完整的两轮调用流程

一次完整的工具调用,需要两次和模型交互:

python 复制代码
from langchain_core.messages import HumanMessage

# ① 定义消息列表
messages = [HumanMessage("2乘3等于多少?6加6等于多少?")]

# ② 第一次调用:模型返回 tool_calls(告诉你要调什么工具、传什么参)
ai_msg = model_with_tools.invoke(messages)

# ③ 把 AI 的回复(含 tool_calls)追加到消息列表
messages.append(ai_msg)

# ④ 你手动执行工具,拿到结果后包装成 ToolMessage,也追加进去
# tool_msg = ToolMessage(...)
# messages.append(tool_msg)

# ⑤ 第二次调用:模型拿到工具执行结果后,生成最终自然语言回答
print(model.invoke(messages).content)

六、总结

整个链路一句话概括:

定义工具 → 绑定给模型 → 模型根据问题选择工具并输出 tool_calls → 执行工具 → 把结果回传给模型 → 模型生成最终答案。

两个关键点记住就行:

  1. 工具的 name /description/args 本质就是提示词,写得越清楚,模型选得越准。
  2. 第一次 invoke 只做 "选择",第二次 invoke 才出 "答案"------ 中间隔了一步手动执行工具。
相关推荐
Pointer Pursuit1 小时前
Linux环境变量
linux·服务器·c语言·链表·1024程序员节
weixin_539541771 小时前
Vue 中组件生命周期及uniapp页面生命周期的详细介绍
1024程序员节
AI技术新视界1 小时前
RSI的如何改进智能体的策略
1024程序员节
王大师王文峰1 小时前
Java+AI 面试题(二):业务场景设计 10 例【客服机器人、合同审查、Text2SQL、告警降噪、成本优化】场景题答法与追问链路
java·开发语言·人工智能
紧跟先前的步伐1 小时前
Go AI Agent 实战 13|大模型流式输出 Streaming 的实现
人工智能·ai·golang·1024程序员节
初阳7851 小时前
【基于Qt5仿音乐播放器(三)】——音乐管理
1024程序员节
CRMEB系统商城1 小时前
Redis + 消息队列 + 连接池:CRMEB PRO 性能优化的三驾马车
大数据·数据库·人工智能·小程序·php·1024程序员节
怪我冷i1 小时前
Rust 语言测试的组织结构
1024程序员节
星恒随风1 小时前
Linux网络编程基础(二):彻底理解局域网通信、MAC与IP地址、数据封装、端口号、网络字节序及Socket接口
linux·网络·笔记·学习·tcp/ip·1024程序员节