上篇你把 Prompt 模板、LCEL 和结构化输出跑通了——大脑会说话、会按字段吐结果。但旅游助手还要查天气、算行程、搜资料。这篇系统讲 Tools:@tool 怎么定义,一轮调用与多步循环有何差别,多工具如何分包工程化。
小明的工具全堆在一个文件里
小明跟着快速入门篇写了 get_weather 和 add,同事又要「搜一下什么是 LCEL」。他继续往同一个 tools.py 里塞:
@tool
def get_weather(...): ...
@tool
def add(...): ...
@tool
def mock_search(...): ...
@tool
def book_ticket(...): ...
文件过三百行,system_prompt 写成「天气用 A,算术用 B,搜索用 C……」一长串。改搜索逻辑时还总误伤天气。一轮调用遇到「先查天气再算数」又不够用。
小明去找老张:”手脚我会装,但怎么长成一只手、再长成工具箱?”
老张说:”三层看:
-
-
多步循环 —— 手写 loop 或
create_agent
-
分包 ——
tools/ 目录 + TOOLS 列表”
什么是 Tools(Agent 的手脚)
老张在白板上写导图原话:
完整 Agent 至少两件套:
模型 = 大脑 → 推理、规划
工具 = 手脚 → 执行、与外界交互
@tool 交给模型的三件事:
名称 = 函数名
输入 = 函数入参
作用 = 函数注释(docstring)
“调用方式近似普通函数:get_weather.invoke({\"city\": \"杭州\"})。
LangChain 还有不少预定义外部工具,官网集成列表可按需接;本课先把本地 @tool 与分包吃透。”
| 阶段 |
你得到什么 |
| 定义 + 一轮 |
看清 bind_tools → tool_calls → ToolMessage |
| 多步 agent-loop |
多工具直到无 tool_calls;或 create_agent |
| multi-tools 分包 |
tools/*.py + 统一 TOOLS |
代码拆解:从 @tool 到工具箱
1. 定义工具:名称、入参、注释
tools.py:
from langchain.tools import tool
@tool
def get_weather(city: str) -> str:
"""查询城市天气(模拟数据)。"""
data = {
"北京": "晴,15°C,东风3级",
"上海": "多云,18°C,南风2级",
"杭州": "小雨,17°C,东北风2级",
}
return data.get(city, f"{city}:晴,20°C(模拟)")
TOOLS = [get_weather]
TOOL_MAP = {t.name: t for t in TOOLS}
main.py 预览 schema:
print(get_weather.name, get_weather.description)
print(get_weather.args)
“模型看见的不是你的 Python 源码细节,而是这套 schema。注释写糊,它就选糊。”
2. 一轮工具调用(看清协议)
agent.py:
def run_one_tool_round(question: str) -> str:
model = get_chat_model(temperature=0)
model_with_tools = model.bind_tools(TOOLS)
messages = [HumanMessage(content=question)]
ai = model_with_tools.invoke(messages)
messages.append(ai)
if not ai.tool_calls:
return ai.content or ""
for tc in ai.tool_calls:
name, args = tc["name"], tc["args"]
result = TOOL_MAP[name].invoke(args)
messages.append(ToolMessage(content=str(result), tool_call_id=tc["id"]))
final = model.invoke(messages)
return final.content or ""
“节奏固定:bind_tools → 可能产生 tool_calls → TOOL_MAP[...].invoke → ToolMessage 回填 → 再 invoke 出自然语言。
一轮版适合学协议;多步任务会不够”
3. agent-loop:多步直到结束
两个工具:
@tool
def get_weather(city: str) -> str:
"""查询城市天气(模拟)。"""
...
@tool
def add(a: float, b: float) -> str:
"""计算两数之和。"""
return str(a + b)
TOOLS = [get_weather, add]
手写循环核心:
for step in range(1, max_steps + 1):
ai = model_with_tools.invoke(messages)
messages.append(ai)
if not ai.tool_calls:
return ai.content or ""
for tc in ai.tool_calls:
result = TOOL_MAP[tc["name"]].invoke(tc["args"])
messages.append(ToolMessage(content=str(result), tool_call_id=tc["id"]))
框架版(快速入门篇已见):
agent = create_agent(
get_chat_model(temperature=0),
tools=TOOLS,
system_prompt="你是助手。需要查天气或做加法时必须调用工具,不要编造。",
)
“停止条件:无 tool_calls → 结束;手写版还有 max_steps 强制收尾。验收题:「北京天气怎么样?再算 10+20。」——至少两个工具。”
4. multi-tools:分包工程化
目录:
multi-tools/
tools/
weather.py
calculator.py
search.py
__init__.py ← 汇总 TOOLS
agent.py
main.py
tools/weather.py 注释写清何时用:
@tool
def get_weather(city: str) -> str:
"""查询中国城市当前天气(模拟数据)。用户问天气、气温、下雨时使用。"""
...
tools/__init__.py:
from tools.calculator import calculator
from tools.search import mock_search
from tools.weather import get_weather
TOOLS = [get_weather, calculator, mock_search]
agent.py:
def build_agent():
return create_agent(
get_chat_model(temperature=0),
tools=TOOLS,
system_prompt=(
"你是助理。天气用 get_weather,算术用 calculator,"
"其它查资料用 mock_search。不要编造工具结果。"
),
)
“扩展口诀:新增文件 → 写入 TOOLS。system_prompt 只做路由提示,细节仍靠每个工具的 docstring。”
ask 里遍历消息打印调用轨迹,排障很香:
for m in result["messages"]:
if name == "AIMessage" and getattr(m, "tool_calls", None):
for tc in m.tool_calls:
print(f" [调用] {tc['name']}({tc['args']})")
if name == "ToolMessage":
print(f" [结果] {m.content}")
小明复述:”@tool 交 schema;一轮看协议;循环干多步;多了就按文件分包,清单进 TOOLS。”
总结
老张说:”第六篇只办一件事——把手脚从「能用」升级到「好管」。”
“三个核心理解:
-
@tool = schema 工厂 —— 名称、入参、注释交给模型
-
一轮学协议,多步才叫 Agent —— 无
tool_calls 结束;框架可用 create_agent
-
多工具必分包 ——
tools/*.py + TOOLS 清单,system_prompt 做路由”
LangChain 支线进度:
… → Prompts / LCEL / 结构化输出
↓
Tools 与 Tool Agent(本篇)
↓
Memory → AI私厨实战 → Runtime → …
小明说:”下一篇该给 Agent 装记忆了——不然每轮都像失忆。”
“对。下一篇:Memory:短期 / 持久化 / 管理策略。”
模型是大脑,工具是手脚;@tool 让模型看得见手脚说明书,分包让你还敢继续加手。