大模型本质上是一个被“封印”在训练数据里的推理大脑,它没有联网能力,也无法直接操作你的本地数据库或 API。 引入 Tools 的目的,就是给大模型装上“手和眼”。当用户提出需求时,大模型可以通过调用预先定义好的工具代码,去获取外部事实或执行真实动作。

在架构层面,大模型并不具备代码执行环境,其核心职责为“规划与决策”。完整的工具调用是一个标准的多轮对话流转闭环(Message Flow):
HumanMessage):用户向系统发起自然语言查询(例:“北京今天天气如何?”)。AIMessage 携带 tool_calls):模型经推理判断需要借助外部工具获取信息,生成结构化的工具调用指令(如:调用 get_weather 函数,入参 city="北京")。此时模型进入挂起等待状态。ToolMessage):业务应用框架(如 LangChain)解析并拦截该指令,在服务端运行环境(Server/Runtime)中执行对应的 Python 实体函数。函数执行完毕后,将其返回值封装为 ToolMessage 追加至对话上下文中。
ToolMessage 必须严格携带模型下发指令时的 tool_call_id,以确保在并发调用场景下,执行结果与原始请求能够精确映射。AIMessage):大模型接收到包含执行结果的对话上下文后,恢复推理过程,结合获取到的事实数据,最终生成自然语言回复返回给终端用户。 复制代码from langchain.messages import HumanMessage
from langchain_core.tools import tool# 定义工具并与模型绑定
@tool
def get_weather(city: str):
"""获取天气的工具"""
return f"{city}天气晴朗~"model_with_tools = model.bind_tools([get_weather])# 1. 用户请求
messages = [HumanMessage("今天北京天气如何")]# 2. 模型决策并下发调用指令 (此时返回的 response 是 AIMessage,包含 tool_calls)
response = model_with_tools.invoke(messages)
messages.append(response) # 核心步骤:必须把“模型要调工具”这一想法也记录进上下文tool_calls = response.tool_calls
if tool_calls:
for tool_call in tool_calls:
if tool_call["name"] == "get_weather":
# 3. 应用层拦截并在本地执行代码
# (注:直接传入 tool_call 字典,底层会自动执行函数并将返回值、tool_call_id 封装为 ToolMessage)
tool_response = get_weather.invoke(tool_call)
messages.append(tool_response) # 将包含真实结果的 ToolMessage 追加进上下文
# 4. 模型基于完整的上下文(问题 -> 决策 -> 事实结果)进行最后推理
final_response = model_with_tools.invoke(messages)
print(final_response.content) # 输出最终回答:北京天气晴朗~
在实际开发中,当我们的 Agent 拥有数十个工具时,如果依然使用 if tool_call["name"] == "xxx": 来逐一判断,会导致代码极度臃肿且难以扩展。
为了解决这个问题,我们可以采用 工具字典映射 (Tools Map) 的设计模式来实现自动化的动态分发(工具路由)。针对不同的工具定义方式,有两种优雅的写法:
@tool 包装的工具 (自动封装)如果你的工具使用了 @tool 装饰器,LangChain 的 invoke 方法会自动处理参数解包,并直接返回标准的 ToolMessage:
复制代码# 假设我们有多个工具
tools = [get_weather, search_web, get_time]# 1. 启动时构建映射字典 (key为工具名,value为工具对象)
tools_map = {tool.name: tool for tool in tools}# ... 获取 response 后遍历 tool_calls ...
for tool_call in response.tool_calls:
tool_name = tool_call["name"]
if tool_name in tools_map:
# 2. 动态取出对应的工具并直接 invoke,彻底消灭 if-else 嵌套
selected_tool = tools_map[tool_name]
tool_response = selected_tool.invoke(tool_call)
messages.append(tool_response)
@tool 的原生 Python 函数 (手动封装)如果你绑定的是纯原生 Python 函数,由于它们没有被 LangChain 包装,它们无法直接接收 tool_call 字典对象,返回值也不会自动变成 ToolMessage。我们需要手动解包并组装消息:
复制代码# 假设这些都是纯原生 Python 函数,没有加 @tool
raw_tools = [get_weather, search_web, get_time]# 1. 构建映射字典 (注意这里取的是原生的 func.__name__)
tools_map = {func.__name__: func for func in raw_tools}for tool_call in response.tool_calls:
tool_name = tool_call["name"]
if tool_name in tools_map:
selected_func = tools_map[tool_name]
# 2. 手动解包 args 并传入原生函数执行
raw_result = selected_func(**tool_call["args"])
# 3. 极其重要:必须手动组装 ToolMessage,并将 call_id 严丝合缝地绑定进去!
tool_response = ToolMessage(
content=str(raw_result), # 函数返回的真实结果
name=tool_call["name"], # 被调用的工具名
tool_call_id=tool_call["id"] # 大模型下发的唯一 call_id
)
messages.append(tool_response)
ToolNode (终极开箱即用方案)随着生态的发展,官方在 LangGraph 库中提供了一个终极杀器 —— ToolNode。如果你不想手写字典映射和 for 循环,你可以直接把工具列表丢给它,它会自动在底层完成所有的解析、调用、和 ToolMessage 封装!
复制代码from langgraph.prebuilt import ToolNode# 假设我们有多个工具
tools = [get_weather, search_web, get_time]# 1. 极其简单:直接用 ToolNode 包装整个工具列表
tool_node = ToolNode(tools)# 2. 当模型返回了包含 tool_calls 的 response 时,直接将其装在消息列表里扔给 tool_node
# ToolNode 会自动提取内部的 tool_calls,去对应的工具里执行,并返回封装好的一批 ToolMessage!
tool_node_response = tool_node.invoke({"messages": [response]})# 3. 将自动执行产生的所有 ToolMessage 追加到对话历史中
messages.extend(tool_node_response["messages"])
虽然大模型可以调用外部工具,但大模型的本质是基于概率的“文字接龙”,充满不确定性。在真实的业务落地中(如向数据库写入数据),大模型在调用工具时极有可能产生幻觉、传错参数类型、甚至凭空捏造不存在的参数。 因此,为了保证执行的确定性和系统的安全性,我们必须对工具的“入参”进行严格管控。在 LangChain 中,入参校验可以分为三种方式:通过 Docstring (弱校验)、通过 Pydantic (强校验) 以及通过手写 JSON Schema (弱校验,极不推荐)。
什么是弱校验? 大模型仅靠阅读代码里的类型提示和注释来“自觉”遵守传参规则。如果在执行时它依然传错了参数(比如该传数字传了字符串),程序不会在进入底层函数前进行友好的强制拦截,通常会导致业务代码抛出意外的运行时崩溃。
底层本质:由于大模型底层只认 JSON Schema 格式,因此“通过 Docstring 提取”的本质,其实是 LangChain 框架在底层自动帮你把 Python 注释和类型提示转换成了 JSON Schema。这与后文提到的“手写 JSON Schema”在发给大模型的最终效果上是完全一致的,只不过这里是由框架全自动代劳了。
在弱校验下,定义工具有两种方式:
@tool 装饰器StructuredTool,自动提取注释作为说明书。.invoke({"city": "北京"}) 以字典形式传参。 复制代码from langchain_core.tools import tool@tool
def get_weather(city: str) -> str:
"""获取指定城市的天气"""
return f"{city}今天晴天"
@tool 装饰器get_weather("北京"),不会破坏旧代码调用。 复制代码def get_weather(city: str) -> str:
"""获取指定城市的天气"""
return f"{city}今天晴天"
无论你选择哪种方式,发给大模型的 Schema 是基本一样的,但在你本地用 Python 直接调用(或写单元测试)时,两者的传参方式有天壤之别,这里是新手的重灾区!
@tool 时 (包含后文的 Pydantic 强校验):你的函数已经被 LangChain 挟持并包装成了 StructuredTool 对象。
get_weather("北京") (按原函数习惯传参会直接抛出 TypeError 崩溃!)get_weather.invoke({"city": "北京"}),即必须把参数包成字典,且必须用 .invoke() 方法触发。@tool 时:它依然是一个纯粹的原生 Python 函数。
get_weather("北京") (一切照旧,完全兼容你原本的业务系统直接调用)。前面提到,无论哪种方式,底层都会将其翻译成 JSON Schema。如果你想在本地亲眼看看这个“说明书”长什么样,可以使用以下方法:
复制代码# 方法 1: 直接查看 @tool 包装后的参数 Schema
print(get_weather.args_schema.schema())# 方法 2: 使用 LangChain 的底层工具,查看最终发给 OpenAI 的完整格式
from langchain_core.utils.function_calling import convert_to_openai_tool
import jsonopenai_tool_schema = convert_to_openai_tool(get_weather)
print(json.dumps(openai_tool_schema, indent=2, ensure_ascii=False))
@tool,何时不使用?@tool。这能统一 LangChain 的组件规范,并且最关键的是,只有加了 @tool 装饰器,你才能接入后面要讲的 Pydantic args_schema 进行强校验。@tool。直接把它喂给 bind_tools 即可。因为如果你给一个旧函数强行戴上 @tool 的帽子,它在本地就被转成了 StructuredTool,别的业务代码必须改用字典 .invoke({}) 传参,这会导致你原本正常的业务系统大面积报错瘫痪。企业级应用开发的核心法则是引入强校验:Docstring 专职负责工具的宏观功能描述,Pydantic 专职负责微观参数的强校验。(99% 的正规商业级场景都必须这么写)
为什么必须用 Pydantic 强校验?
model.invoke(),然后自己写代码去执行 tool.invoke()),当遇到 Pydantic 拦截报错时,程序会直接抛出异常并终止,不会自动重试。AgentExecutor)**接管时,重试机制才会生效。Agent 底层自带了异常捕获的循环逻辑,它会捕获 Pydantic 抛出的精准异常信息,将其作为“执行失败”的反馈重新发送给大模型,大模型据此进行“自我反思”并修正参数,实现全自动纠错。 复制代码from pydantic import BaseModel, Field
from langchain_core.tools import tool# 1. Pydantic 专职负责入参的强定义与强校验
class WeatherInput(BaseModel):
city: str = Field(description="具体的城市名称,如:北京")
unit: str = Field(description="温度单位", default="celsius")# 2. Docstring 只管宏观介绍,args_schema 接入 Pydantic 安检门
@tool(args_schema=WeatherInput)
def get_weather(city: str, unit: str = "celsius"):
"""
根据城市名称获取当地的天气情况。
"""
return f"{city}的天气是晴天,温度单位:{unit}"
为了让上述 Pydantic 的强校验真正发挥“自动重试”的威力,必须搭配 AgentExecutor 使用,而非纯手工调用 model.invoke():
复制代码from langchain.agents import create_tool_calling_agent, AgentExecutor
from langchain_core.prompts import ChatPromptTemplate# 1. 准备工具列表
tools = [get_weather]# 2. 定义提示词模板 (注意必须包含 agent_scratchpad 用来暂存工具调用的报错信息)
prompt = ChatPromptTemplate.from_messages([
("system", "你是一个有用的助手,请利用工具回答用户问题。"),
("human", "{input}"),
("placeholder", "{agent_scratchpad}"),
])# 3. 创建智能体
agent = create_tool_calling_agent(model, tools, prompt)# 4. 创建智能体执行器 (也就是那个帮我们写好了 while 循环和 try...except 的“管家”)
# max_iterations = 3 表示最多允许大模型犯错重试 3 次
agent_executor = AgentExecutor(agent=agent, tools=tools, verbose=True, max_iterations=3)# 5. 极简调用。遇到 Pydantic 报错时,管家会自动拦截并把错误信息塞回给大模型让其修正重做
response = agent_executor.invoke({"input": "今天北京天气如何"})
print(response["output"])
除了使用 Pydantic,你也可以选择直接手写标准的 JSON Schema 字典,并把它传给 @tool(args_schema=...) 或者在底层的 API 调用中直接指定。
机制:大模型底层 API(如 OpenAI)原生接收的就是 JSON Schema 格式的参数描述说明书。Pydantic 的底层机制,其实也是在运行时把类结构自动转换成了 JSON Schema 发给大模型。因此,手写 JSON Schema 相当于“越过 Pydantic 这个中间商,直接用底层协议对话”。
为什么在实战中极不推荐?
(结论:在工程实战中,永远优先使用 3.2 的 Pydantic。把 JSON Schema 仅当作了解底层通信原理的知识储备即可,千万不要手写它。)
tool_choice 的使用当我们将工具绑定给大模型后,可以通过 tool_choice 参数来强制管控大模型的工具使用行为:
tool_choice="auto" (默认状态:自由发挥)
tool_choice="none" (强制禁用)
tool_choice="required" / "any" (强制必用)
复制代码# 强制大模型必须调用工具
model_with_tools = model.bind_tools([get_weather], tool_choice="required")