
目录LangChain 自定义工具全解析从 tool 到 bind_tools 的完整调用链路一、为什么工具需要 名称 描述 参数二、方式一用 tool 装饰器定义工具最常用三、方式二用 StructuredTool.from_function 手动包装3.1 先定义参数 Schema3.2 写普通函数不加 tool3.3 包装为 StructuredTool3.4 手动模拟大模型调用来测试四、把工具绑定给模型bind_tools五、工具调用的完整底层流程5.1 模型返回了什么5.2 调不调用 是 LLM 自己决定的5.3 完整的两轮调用流程六、总结前言本文记录 LangChain 中如何定义工具、包装工具、绑定给模型以及模型实际调用工具时的底层流程。一、为什么工具需要 名称 描述 参数在写代码之前先想清楚一个问题一个工具为什么需要 name、description、args 这三个属性答案很简单 ——这三个属性本质就是提示词Prompt。大模型本身并不知道你写了什么函数它只看到你喂给它的工具描述。模型通过这三行信息来做决策表格属性作用类比name工具叫什么函数名description工具干什么用的函数的 docstringargs需要传什么参数函数签名模型靠这三条信息决定两件事① 要不要调用工具② 调用时传什么参数。所以自定义工具时函数名、文档字符串docstring、类型提示一个都不能少 ——LangChain 的 schema 会做校验缺了就报错。二、方式一用tool装饰器定义工具最常用tool装饰器会自动把普通 Python 函数包装成 LangChain 的 Tool 对象函数签名和 docstring 就是工具的 schema。from langchain_core.tools import tool tool def add(a: int, b: int) - int: 两数相加 Args: a: 第一个整数 b: 第二个整数 return a b包装完成后可以直接调用和查看工具的元信息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 的BaseModelField来声明参数结构from langchain_core.tools import StructuredTool from pydantic import BaseModel, Field class AddInput(BaseModel): a: int Field(description第一个整数) b: int Field(description第二个整数)3.2 写普通函数不加 tooldef Add(a: int, b: int): return a b3.3 包装为 StructuredTooladd_tool StructuredTool.from_function( funcAdd, # 要执行的函数本体 nameADD, # 工具名称 description两数相加, # 工具描述LLM 靠它判断何时调用 args_schemaAddInput, # 工具参数结构 )四种核心参数一览表格参数作用func要执行的函数本体name工具名如ADDdescription工具描述LLM 靠它判断何时调用args_schema参数结构Pydantic Model告诉模型需要传什么参数response_formatcontent-and-artifact可选把结果中的文本内容和结构化数据分开返回3.4 手动模拟大模型调用来测试包装好之后我们可以手动构造一个 tool_call 字典来模拟大模型决定调用工具时的行为add_tool.invoke({ name: ADD, # 工具名 args: {a: 3, b: 4}, # 传入参数 type: tool_call, # 必填类型标记 id: 123, # 必填关联调用工具与调用方处理并发用 })这一步的意义脱离大模型直接测试工具本身能不能跑通。type和id是必填字段其中id用来在并发场景下关联 谁调用了哪个工具。四、把工具绑定给模型bind_tools定义好工具之后需要把它 告诉 大模型这一步用bind_tools()完成。from langchain_openai import ChatOpenAI from langchain_core.tools import tool from typing import Annotated model ChatOpenAI(modelgpt-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(toolstools)直接用自然语言提问试试print(model_calculate.invoke(2乘3等于多少).content)注意这一步模型只做了 工具选择—— 判断该调哪个工具、传什么参数 ——还没有真正执行工具。五、工具调用的完整底层流程5.1 模型返回了什么当模型决定调用工具时返回结果的content字段是空的真正的信息在tool_calls字段里函数名要调用哪个工具参数值传入什么参数5.2 调不调用 是 LLM 自己决定的即使你绑定了工具模型也会根据问题与工具的相关性自行判断要不要调用。问题和工具相关 → 模型返回tool_calls问题和工具无关 → 模型直接在content里回答不调工具如果想强制模型必须调用工具可以在bind_tools()时传model_calculate model.bind_tools(toolstools, tool_choiceany)5.3 完整的两轮调用流程一次完整的工具调用需要两次和模型交互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→ 执行工具 → 把结果回传给模型 → 模型生成最终答案。两个关键点记住就行工具的 name /description/args 本质就是提示词写得越清楚模型选得越准。第一次 invoke 只做 选择第二次 invoke 才出 答案—— 中间隔了一步手动执行工具。