用 @tool 定义 Python 函数为工具,绑定到 Agent——模型会自动决定何时调用,调用结果再反馈给模型,形成 ReAct 推理-行动循环。
LLM 本身只能处理文本——它没有实时联网能力、无法执行代码、也不能调用外部 API。工具(Tool)就是赋予 Agent 这些能力的机制:把 Python 函数包装成工具后,模型可以在推理过程中"调用"它,获取真实数据,再把结果纳入后续推理。
问 LLM "467 的平方根是多少"——它可能给出 21.6 或类似近似值,但不保证精确。绑定 square_root 工具后,模型会调用 Python 的 x ** 0.5 返回精确值 21.61018278497431。这是工具存在的根本价值:让 Agent 能利用外部精确计算或实时数据。
LangChain 提供 @tool 装饰器,把普通 Python 函数变成可被 Agent 调用的工具。这是推荐的标准写法。
from langchain.tools import tool
# 推荐写法:明确指定工具名称和描述
@tool("square_root", description="Calculate the square root of a number")
def square_root(x: float) -> float:
return x ** 0.5
@tool 装饰器做了什么?它把函数包装成一个 LangChain StructuredTool 对象,该对象包含:
| 属性 | 来源 | 说明 |
|---|---|---|
name |
装饰器第一个参数 "square_root" |
模型调用工具时使用的名称标识 |
description |
装饰器 description= 参数 |
告诉模型这个工具的用途,模型靠这个决定何时调用 |
args_schema |
函数参数的类型注解自动推断 | 参数的 JSON Schema(x: float → {"x": {"type": "number"}}) |
func |
被装饰的原始函数 | 实际执行逻辑 |
description 是模型决策的核心依据。模型看不到工具的源代码,只看 name 和 description。如果描述不准确,模型可能在错误的时候调用(或不调用)工具。好的工具描述应该:说清楚工具做什么、什么时候应该用它、输入输出是什么。
工具定义好后,可以不经过 Agent,直接用 tool.invoke() 测试其功能是否正确:
# 直接调用工具测试,不需要 Agent
result = square_root.invoke({"x": 467})
print(result) # → 21.61018278497431
在把工具绑定到 Agent 之前,先用 tool.invoke() 独立测试工具逻辑。这样能快速排查是工具本身有 bug,还是 Agent 调用工具时出了问题——两类问题的修复方式完全不同。
通过 create_agent 的 tools 参数,把工具列表传给 Agent:
from langchain.agents import create_agent
from langchain.messages import HumanMessage
agent = create_agent(
model=deepseek_model(),
tools=[square_root], # 工具列表(可以绑定多个工具)
system_prompt="You are an arithmetic wizard. Use your tools to calculate the square root and square of any number."
)
question = HumanMessage(content="What is the square root of 467?")
response = agent.invoke({"messages": [question]})
print(response['messages'][-1].content)
模型没有自己"算"这个值——它调用了 square_root 工具,得到精确结果 21.61018278497431,然后在回复里使用这个值。
Agent 的工具调用遵循 ReAct(Reasoning + Acting) 模式:
如有需要可多次循环"推理 → 调用工具 → 推理",直到 LLM 认为可以给出最终答案为止
HumanMessage("What is the square root of 467?")tool_calls=[{"name": "square_root", "args": {"x": 467}}]square_root(x=467),得到 21.61018278497431,封装为 ToolMessage工具调用后,response['messages'] 列表包含 4 条消息,比普通调用多了一条 ToolMessage:
from pprint import pprint
pprint(response['messages'])
print("\n--- tool_calls 详情 ---")
print(response["messages"][1].tool_calls)
tool_call = response["messages"][1].tool_calls[0]
# 结构如下:
# {
# "name": "square_root", # 调用的工具名
# "args": {"x": 467}, # 传入的参数
# "id": "call_00_...", # 工具调用 ID(用于匹配 ToolMessage)
# "type": "tool_call" # 固定值
# }
工具调用时,response['messages'][-1] 是最终的文字回答(AIMessage),而不是工具结果。工具结果在 messages[2] 的 ToolMessage 里。通常我们只需要 response['messages'][-1].content 获取最终文字答案。
| 实践 | 推荐做法 | 避免 |
|---|---|---|
| 工具名称 | 动词+名词,清晰表达功能:"search_web"、"get_stock_price" |
模糊的名称:"tool1"、"helper" |
| 描述 | 说明用途、何时用、输入输出。示例:"Search the web for real-time information. Use when the question requires current data." |
过于简短:"search" |
| 类型注解 | 所有参数都加类型注解(x: float),LangChain 自动生成 schema |
省略类型注解(会导致 schema 不准确) |
| 返回值 | 返回字符串或可序列化的 dict,方便模型理解 | 返回复杂对象或二进制数据 |
| 错误处理 | 返回描述性错误字符串,让模型知道失败原因 | 抛出异常(会中断整个 Agent 执行) |
@tool("name", description="...")tool.invoke({"x": 467}) 可独立测试messages[-1].content