大模型只会动嘴不会动手?Tool工具调用——给AI装上真干活的机械臂
大模型只会动嘴不会动手?Tool工具调用——给AI装上真干活的机械臂
上一章我们完成了 RAG 检索增强生成,大模型已经能根据知识库资料回答问题了。但现实业务中,用户问的不仅是"文档里写了什么",还有"我的订单发货了吗"“这个商品打完折多少钱”“仓库还有没有货”——这些信息不在任何文档里,而在业务系统的数据库中。这一章解决的就是:怎么让大模型调用外部接口,从"只会说"变成"会干活"。
免责声明
- 内容性质:本文仅为 LangChain 工具调用教学演示,不代表 DeepSeek 官方文档或教程,与任何模型厂商无合作关系。文中以 DeepSeek 为例,但代码同样适用于 OpenAI、通义千问、文心一言等任意兼容 OpenAI 接口格式的大模型,替换
model和base_url即可。- 生产免责:本文所有数据均为模拟测试数据(
fake_orders、fake_inventory等),代码仅用于教学演示,不可直接用于线上生产环境。正式业务需增加接口鉴权、身份校验、参数过滤防注入、异常熔断、日志审计、限流、数据加密等生产能力。- 开源声明:本文代码采用 MIT 开源协议,可自由复制、修改、使用,但请保留原始作者署名。LangChain 框架本身遵循 MIT 协议。
密钥安全提醒:
.env文件中存储的是真实 API 密钥,严禁将.env文件、明文密钥上传至博客、公开代码仓库(GitHub / Gitee 等)或任何公开平台。建议在.gitignore中添加.env排除规则。
阅读前置条件:本文假设你已掌握 Python 基础语法(函数、装饰器、类型注解)、了解大模型基本概念(Prompt、消息角色),并已阅读本系列前几章(模型调用、Prompt 模板、结构化输出、RAG)。如尚未了解,建议先阅读本系列第 1-8 章。
本章整体脉络
第一部分:概念原理(为什么需要Tool → Tool vs普通函数 → Tool四要素)
第二部分:实操上手(定义Tool → 手动调用 → 描述规范 → 多参数Tool → Pydantic校验)
第三部分:Agent入门(什么是Agent → create_agent → Agent如何选工具 → 三者关系)
第四部分:电商客服Agent实战(多工具定义 → 创建Agent → 完整对话流程 → 项目结构)
第五部分:调优答疑(常见问题 → 调试方法 → 本章重点 → 落地场景)
第一部分:概念原理篇
本部分目标:理解 Tool 的底层逻辑,搞清楚"大模型为什么不能直接查数据库"以及"Tool 到底在帮模型干什么"。纯理论,不写代码。
1.1 为什么需要 Tool
大模型擅长语言理解和生成,但它有一个致命短板:不知道实时业务数据。
用户问:
我的订单 A1001 发货了吗?
模型不能凭空知道订单状态。它需要调用业务系统接口,拿到真实数据,再组织语言回复。
流程可以理解为:
用户问题
-> 模型判断需要查订单
-> 调用订单查询工具
-> 得到订单数据
-> 模型组织自然语言回复
通俗比喻:想象你雇了一个客服(大模型),他口才极好、什么都知道一点,但他没见过你们公司的系统。用户打电话问"我的货发了吗",他不能凭嘴编。你得给他一个电脑(Tool),告诉他"点击这个按钮就能查到"。他只需要决定"该不该点这个按钮",点完拿到结果后用自己的话告诉用户。
专业定义:Tool 是把外部能力(API、数据库、计算器等)包装成大模型可以调用的函数。模型通过函数签名(名称、参数、描述)理解工具的功能,在合适的时机发起调用,并基于返回结果生成最终回复。
1.2 Tool 和普通函数的区别
普通 Python 函数:
def get_order_status(order_id: str) -> str:
return "订单已发货"
LangChain Tool:
from langchain_core.tools import tool
@tool
def get_order_status(order_id: str) -> str:
"""根据订单号查询订单状态。"""
return "订单已发货"
Tool 仍然是 Python 函数,但多了几个关键信息:
| 信息 | 作用 |
|---|---|
| 工具名称 | 模型识别可以调用哪个工具 |
| 参数类型 | 模型知道应该传什么参数 |
| 工具描述 | 模型判断什么时候使用这个工具 |
| 返回值 | 工具调用后返回给模型的数据 |
关键认知:普通函数是写给程序员看的,程序员知道函数名就去调了;Tool 是写给大模型看的,模型通过描述来判断"该不该用这个工具",通过参数类型来判断"传什么进去"。描述写得烂,模型就不会用。
第一部分小结
- 大模型擅长语言,但不掌握实时业务数据
- Tool = 把外部能力包装成模型可调用的函数
- Tool 比普通函数多了四个关键信息:名称、参数类型、描述、返回值
- 工具描述是模型决定"用不用"的核心依据
第二部分:实操上手篇
本部分目标:动手定义 Tool,手动调用 Tool,理解工具描述的规范,掌握多参数工具和 Pydantic 参数校验。所有代码均为教学 Demo,线上业务需改造为真实 API 调用。
2.1 定义第一个 Tool
创建 01_tool_basic.py:
from langchain_core.tools import tool
@tool
def get_order_status(order_id: str) -> str:
"""根据订单号查询订单状态,包括是否付款、是否发货、快递单号和签收状态。"""
fake_orders = {
"A1001": "已付款,等待发货",
"A1002": "已发货,快递单号 SF123456",
"A1003": "已签收",
}
return fake_orders.get(order_id, "没有查询到该订单")
# 查看工具的元信息(这些信息会发给模型)
print(get_order_status.name) # 工具名称:get_order_status
print(get_order_status.description) # 工具描述:根据订单号查询订单状态...
print(get_order_status.args) # 参数结构:{'order_id': {'type': 'string'}}
运行:
python 01_tool_basic.py
输出:
get_order_status
根据订单号查询订单状态,包括是否付款、是否发货、快递单号和签收状态。
{'order_id': {'type': 'string'}}
这三个信息(名称、描述、参数结构)会作为上下文发给模型,帮助模型决定如何调用工具。
如果不指定
name,默认name就是函数名。推荐显式使用英文小写加下划线命名,兼容性更好。
2.2 手动调用 Tool
Tool 可以像普通组件一样用 .invoke() 手动调用。这一步的目的是:先确认工具本身没有问题,再交给 Agent 使用。
from langchain_core.tools import tool
@tool
def get_order_status(order_id: str) -> str:
"""根据订单号查询订单状态,包括是否付款、是否发货、快递单号和签收状态。"""
fake_orders = {
"A1001": "已付款,等待发货",
"A1002": "已发货,快递单号 SF123456",
"A1003": "已签收",
}
return fake_orders.get(order_id, "没有查询到该订单")
# 手动调用工具,传入参数字典
result = get_order_status.invoke({"order_id": "A1002"})
print(result) # 已发货,快递单号 SF123456
print(type(result).__name__) # str
运行:
python 01_tool_basic.py
输出:
已发货,快递单号 SF123456
str
手动调用的意义:就像给新员工培训时,先让他单独操作一遍系统,确认操作流程没问题,再让他独立接待客户。
2.3 工具描述要写清楚
工具描述是模型判断"该不该调用这个工具"的唯一依据。描述写得含糊,模型要么不用,要么乱用。
不推荐:
@tool
def query(order_id: str) -> str:
"""查询。"""
...
模型看到"查询"两个字,完全不知道这工具能查什么、什么时候该用。
推荐:
@tool
def get_order_status(order_id: str) -> str:
"""根据订单号查询订单状态,包括是否付款、是否发货、快递单号和签收状态。"""
...
好的工具描述应该说明六件事:
| 要素 | 说明 | 举例 |
|---|---|---|
| 工具能做什么 | 这个工具的功能是什么 | 查询订单状态 |
| 什么时候用 | 什么场景下应该调用 | 用户询问订单进度、发货状态时 |
| 参数是什么 | 每个参数的含义和格式 | order_id 是订单编号,例如 A1001 |
| 参数是否必填 | 哪些参数必须传,哪些可选 | order_id(必填),warehouse(可选,默认上海仓) |
| 返回什么 | 调用后模型会拿到什么数据 | 返回订单状态文本,包含付款/发货/签收信息 |
| 调用示例 | 给模型一个参数填充样例 | 调用示例:get_order_status(order_id=“A1001”) |
其中前四点是基础规范,后两点是工业界提升工具调用成功率的实战经验。
必填/选填参数标注:当工具参数较多时,务必在描述中标注哪些必填、哪些选填,否则模型可能漏传关键参数:
@tool
def search_orders(user_id: str, status: str = "all", limit: int = 10) -> str:
"""查询用户的订单列表。user_id(必填)是用户编号,例如 U001;status(可选,默认 all)筛选订单状态,可选值 all/paid/shipped/signed;limit(可选,默认 10)返回数量上限。调用示例:search_orders(user_id="U001", status="shipped")"""
...
工具名称也建议使用英文小写加下划线:
get_order_status
calculate_discount_price
get_inventory
这种命名兼容性更好,模型识别更准确。
2.4 多参数 Tool
业务工具通常会有多个参数。比如根据原价和折扣率计算折后价格:
创建 02_discount_tool.py:
from langchain_core.tools import tool
@tool
def calculate_discount_price(original_price: float, discount_rate: float) -> str:
"""根据商品原价和折扣率计算折后价格。original_price是原价(元),discount_rate是折扣率(0.8表示8折)。调用示例:calculate_discount_price(original_price=299.0, discount_rate=0.8)"""
discounted_price = round(original_price * discount_rate, 2)
return f"原价 {original_price} 元,折扣率 {discount_rate},折后价格为 {discounted_price} 元"
# 手动调用
result = calculate_discount_price.invoke({
"original_price": 299.0,
"discount_rate": 0.8,
})
print(result)
运行:
python 02_discount_tool.py
输出:
原价 299.0 元,折扣率 0.8,折后价格为 239.2 元
参数类型一定要写清楚:
original_price: float # 原价
discount_rate: float # 折扣率(0.8 表示 8 折)
模型会根据类型注解和描述,生成正确的工具调用参数。
2.5 使用 Pydantic 描述工具参数
当参数较多,或者需要更严格的字段校验时,可以使用 Pydantic 定义参数结构。
创建 03_tool_schema.py:
from pydantic import BaseModel, Field
from langchain_core.tools import tool
class InventoryInput(BaseModel):
"""库存查询参数"""
product_id: str = Field(description="商品编号,例如 P1001", pattern="^P[0-9]{4}$")
warehouse: str = Field(description="仓库名称,例如 上海仓、北京仓")
@tool(args_schema=InventoryInput)
def get_inventory(product_id: str, warehouse: str) -> str:
"""查询指定商品在指定仓库中的库存数量。"""
fake_inventory = {
("P1001", "上海仓"): 35,
("P1001", "北京仓"): 12,
("P2001", "上海仓"): 0,
}
count = fake_inventory.get((product_id, warehouse))
if count is None:
return "没有查询到该商品的库存信息"
return f"{product_id} 在 {warehouse} 当前库存为 {count} 件"
# 查看参数结构(包含了 Pydantic 的校验规则)
print(get_inventory.args)
# 正常调用
result = get_inventory.invoke({
"product_id": "P1001",
"warehouse": "上海仓",
})
print(result)
运行:
python 03_tool_schema.py
输出:
{'product_id': {'type': 'string', 'description': '商品编号,例如 P1001', 'pattern': '^P[0-9]{4}$'}, 'warehouse': {'type': 'string', 'description': '仓库名称,例如 上海仓、北京仓'}}
P1001 在 上海仓 当前库存为 35 件
为什么用 Pydantic?不是直接写参数类型就行吗?
直接写参数类型确实能跑通,但 Pydantic 提供了参数校验能力。比如上面的 pattern="^P[0-9]{4}$" 限制了商品编号必须是 P 开头加四位数字。
除了正则校验,Pydantic 还支持数值范围、非空、枚举等常用生产级校验:
from typing import Literal
from pydantic import BaseModel, Field
class OrderQueryInput(BaseModel):
"""订单查询参数(生产级校验示例)"""
order_id: str = Field(
description="订单编号,例如 A1001",
pattern="^A[0-9]{4}$", # 正则校验:A开头+4位数字
min_length=5, # 最小长度
max_length=10, # 最大长度
)
status_filter: Literal["all", "paid", "shipped", "signed"] = Field(
default="all", # 枚举校验:只允许这几个值
description="订单状态筛选",
)
limit: int = Field(
default=10,
ge=1, # 最小值:大于等于 1
le=100, # 最大值:小于等于 100
description="返回数量上限",
)
如果你传一个不符合格式的编号:
# 错误的编号格式
result = get_inventory.invoke({
"product_id": "AAA1001", # 不符合 ^P[0-9]{4}$ 格式
"warehouse": "上海仓",
})
直接报错:
pydantic_core._pydantic_core.ValidationError: 1 validation error for InventoryInput
product_id
String should match pattern '^P[0-9]{4}$'
[type=string_pattern_mismatch, input_value='AAA1001', input_type=str]
Pydantic 校验的价值:
| 场景 | 不用 Pydantic | 用 Pydantic |
|---|---|---|
| 参数少、格式简单 | 直接写类型注解即可 | 没必要,杀鸡用牛刀 |
| 参数多、需要校验 | 手动 if 判断,代码臃肿 | Field 加 pattern/范围限制,自动校验 |
| 防止模型传错参数 | 错误参数进入函数,可能出错 | 在入口就被拦住,直接报 ValidationError |
| 企业项目多人协作 | 每人写法不同,难以统一 | Schema 统一管理,一目了然 |
第二部分小结
@tool装饰器把普通函数变成 LangChain Tool.invoke()手动调用,先验证工具本身没问题- 工具描述是模型选工具的唯一依据,必须写清楚功能+场景+参数+返回值+必填/选填标注+调用示例
- 多参数工具直接在函数签名中声明类型
- Pydantic 适用于参数多、需要格式校验(正则、范围、枚举)的企业场景
第三部分:Agent 入门篇
本部分目标:理解 Agent 是什么,掌握
create_agent的基本用法,搞清楚 Agent、Tool、Model 三者的关系。
3.1 什么是 Agent
前面几章,模型的工作模式是:
用户问题 → 模型 → 直接回答
模型拿到问题就回答,不需要做任何决策。但真实业务场景远比这复杂。
用户可能连续问多个问题,每个问题需要不同的工具:
用户:我的订单 A1002 发货了吗? → 需要调用查订单工具
用户:那打折完多少钱? → 需要调用算价格工具
用户:上海仓还有几件? → 需要调用查库存工具
谁来决定"这个问题该用哪个工具"?答案是 Agent。
通俗比喻:把大模型想象成一个刚入职的客服,工具是公司给他配的各种系统(订单系统、库存系统、价格计算器)。Agent 就是这个客服的工作模式——他听到用户问题后,先判断需要查哪个系统,然后去操作,拿到结果后回复用户。不需要你提前告诉他会问到什么问题,他自己判断。
专业定义:Agent 是以大模型为大脑、以工具为手脚的自主决策系统。它接收用户问题后,由模型决定是否调用工具、调用哪个工具、传什么参数,拿到工具返回结果后再决定是直接回复还是继续调用其他工具,直到问题解决。
Agent 的完整决策循环:
用户问题
→ 模型思考:需要调用工具吗?
→ 是 → 选择工具 + 生成参数 → 调用工具 → 拿到结果
→ 模型再思考:结果够了吗?需要再调工具吗?
→ 否 → 组织自然语言回复
→ 返回给用户
3.2 Tool 与 Agent 的关系
| 对比项 | Tool | Agent |
|---|---|---|
| 角色 | 手脚(执行具体操作) | 大脑(决定做什么操作) |
| 决策能力 | 没有,被动等待调用 | 有,自主判断该不该用工具 |
| 类比 | 公司里的各个业务系统 | 会使用这些系统的员工 |
| 谁来调用 | Agent 或程序员手动 .invoke() | 模型(在 create_agent 中自动决策) |
一句话总结:Tool 是"能干什么",Agent 是"什么时候干什么"。
3.3 用 create_agent 创建第一个 Agent
LangChain 版本说明:LangChain 1.0 对 Agent API 做了重大调整。0.x 时代 Agent 系统碎片化严重,需要根据场景选择 create_react_agent、create_tool_calling_agent、create_structured_chat_agent 等不同函数,再配合 AgentExecutor 包装,代码复杂且学习曲线陡峭。1.0 版本统一为 create_agent,一个函数搞定所有场景,API 更简洁。如果你搜到的教程还在用 create_react_agent + AgentExecutor,说明是 0.x 旧版教程,建议参考 LangChain 1.0 官方文档。本文所有代码基于 1.0+ 版本。
创建 04_agent_basic.py:
import os
from dotenv import load_dotenv
from langchain.chat_models import init_chat_model
from langchain_core.tools import tool
from langchain.agents import create_agent
# 加载环境变量
load_dotenv()
# 第一步:定义工具
@tool
def get_order_status(order_id: str) -> str:
"""根据订单号查询订单状态,包括是否付款、是否发货、快递单号和签收状态。"""
fake_orders = {
"A1001": "已付款,等待发货",
"A1002": "已发货,快递单号 SF123456",
"A1003": "已签收",
}
return fake_orders.get(order_id, "没有查询到该订单")
# 第二步:初始化模型
# 以 DeepSeek 为例,也可替换为 OpenAI、通义千问、文心一言等任意兼容 OpenAI 格式的模型
model = init_chat_model(
model="deepseek-v4-flash",
model_provider="openai",
api_key=os.getenv("DEEPSEEK_API_KEY"),
base_url=os.getenv("DEEPSEEK_BASE_URL", "https://api.deepseek.com"),
temperature=0,
)
# 第三步:创建 Agent
# 把工具列表和模型传给 create_agent,Agent 就拥有了调用工具的能力
agent = create_agent(
model=model,
tools=[get_order_status],
)
# 第四步:提问
# Agent 会自动判断:这个问题需要查订单吗?需要的话调哪个工具?
response = agent.invoke(
{"messages": [{"role": "user", "content": "帮我查一下订单 A1002 的状态"}]}
)
# 打印最终回复
print(response["messages"][-1].content)
运行:
python 04_agent_basic.py
输出:
订单 A1002 的状态是:已发货,快递单号 SF123456。
发生了什么?Agent 内部的决策过程:
1. 用户问:"帮我查一下订单 A1002 的状态"
2. 模型思考:这需要查询订单 → 选择 get_order_status 工具
3. 模型生成调用参数:{"order_id": "A1002"}
4. 框架执行工具:get_order_status.invoke({"order_id": "A1002"})
5. 工具返回:"已发货,快递单号 SF123456"
6. 模型拿到结果,组织自然语言回复给用户
3.4 Agent 如何选择工具
Agent 靠什么判断该用哪个工具?靠工具描述。
当你把多个工具传给 Agent 时,模型会读取每个工具的描述,与用户问题进行语义匹配:
工具列表:
- get_order_status "根据订单号查询订单状态"
- calculate_discount_price "根据原价和折扣率计算折后价格"
- get_inventory "查询指定商品在指定仓库中的库存数量"
用户问题:"A1002 发货了吗?"
→ 模型匹配:查询订单状态 → get_order_status
用户问题:"299 元打 8 折多少钱?"
→ 模型匹配:计算折后价格 → calculate_discount_price
用户问题:"上海仓 P1001 还有几件?"
→ 模型匹配:查询库存 → get_inventory
这就是为什么前面反复强调工具描述要写清楚——描述就是模型选工具的唯一依据。
3.5 Agent、Tool 与 Agent 执行循环
在 LangChain 1.0+ 的架构中,有三个核心概念:
| 概念 | 含义 | 类比 |
|---|---|---|
| Tool | 具体能力的封装(查订单、算价格等) | 员工能使用的各种业务系统 |
| Model | 大模型,负责理解问题和决策 | 员工的大脑 |
| Agent 执行循环 | 框架内置的调度引擎,负责管理模型和工具的交互循环 | 公司的工作流程制度 |
三者协作流程:
Agent 执行循环(调度引擎)
┌──────────────────────┐
│ │
用户问题 ────→ │ Model(大脑/决策) │
│ ↓ 判断要调工具 │
│ Tool(手脚/执行) │
│ ↓ 返回结果 │
│ Model(再决策) │
│ ↓ 不需要再调 │
最终回复 ←──── │ 组织自然语言回复 │
│ │
└──────────────────────┘
- Model:读问题、选工具、生成参数、组织回复——全是它干
- Tool:被动执行,被调了就跑,跑完返回结果
- Agent 执行循环:在幕后管理"模型→工具→模型→工具"的循环,直到模型说"不需要再调工具了"
create_agent 帮你把这三者打包到一起,你只需要提供 Model 和 Tools 列表。
第三部分小结
- Agent = 以模型为大脑、以工具为手脚的自主决策系统
- 模型靠工具描述选择该用哪个工具
- LangChain 1.0+ 使用
create_agent替代旧版create_react_agent+AgentExecutor,API 更简洁 - Tool 是"能干什么",Agent 是"什么时候干什么",Agent 执行循环是"怎么循环干"
第四部分:电商客服 Agent 实战篇
本部分目标:把前面定义的所有工具组合起来,创建一个完整的电商客服 Agent,实现多工具自主选择和连续对话。以下代码为教学 Demo,模拟数据不可直接用于生产环境。
4.1 场景设计
模拟一个电商客服场景,Agent 需要能处理以下问题:
1. 查订单状态 → get_order_status(order_id)
2. 算折扣价格 → calculate_discount_price(original_price, discount_rate)
3. 查商品库存 → get_inventory(product_id, warehouse)
用户可能连续问多个问题,Agent 需要自动判断每次该用哪个工具。
4.2 项目结构
langchain_blog_ch09/
├── .env # 环境变量(禁止提交到 Git!)
├── .gitignore # Git 忽略规则(必须包含 .env)
├── utils/
│ └── model_factory.py # 模型工厂
├── tools/
│ ├── order_tools.py # 订单工具
│ ├── price_tools.py # 价格工具
│ └── inventory_tools.py # 库存工具
├── agent_service.py # Agent 服务(核心)
├── 01_tool_basic.py # 基础:定义+手动调用
├── 02_discount_tool.py # 多参数工具
├── 03_tool_schema.py # Pydantic 校验
├── 04_agent_basic.py # 单工具 Agent
└── 05_ecommerce_agent.py # 电商客服 Agent(完整实战)
4.3 环境变量配置
项目根目录创建 .env 文件:
DEEPSEEK_API_KEY=你的DeepSeek密钥
DEEPSEEK_BASE_URL=https://api.deepseek.com
安全警告:
.env文件包含真实 API 密钥,严禁提交到 Git 仓库或任何公开平台。请在项目根目录创建.gitignore文件并添加以下内容:.env __pycache__/ *.pyc如果密钥已泄露,请立即前往对应厂商后台立即重置密钥。
4.4 模块一:模型工厂(utils/model_factory.py)
"""模型工厂:统一初始化大模型实例
以 DeepSeek 为例,也可替换为 OpenAI、通义千问、文心一言等
任意兼容 OpenAI 接口格式的模型,只需修改 model 和 base_url。
"""
import os
from dotenv import load_dotenv
from langchain.chat_models import init_chat_model
# 加载环境变量
load_dotenv()
def get_deepseek_model(temperature: float = 0):
"""获取 DeepSeek 聊天模型实例
Args:
temperature: 温度值,0 表示确定性输出(客服场景推荐 0)
Returns:
ChatModel 实例
"""
return init_chat_model(
model="deepseek-v4-flash",
model_provider="openai",
api_key=os.getenv("DEEPSEEK_API_KEY"),
base_url=os.getenv("DEEPSEEK_BASE_URL", "https://api.deepseek.com"),
temperature=temperature,
)
4.5 模块二:工具集(tools/)
订单工具(tools/order_tools.py):
"""订单相关工具
教学 Demo 使用模拟数据,线上业务需替换为真实 API 调用,
并增加接口鉴权、参数过滤、异常处理等生产能力。
"""
from langchain_core.tools import tool
@tool
def get_order_status(order_id: str) -> str:
"""根据订单号查询订单状态,包括是否付款、是否发货、快递单号和签收状态。参数 order_id 是订单编号,例如 A1001。调用示例:get_order_status(order_id="A1001")"""
fake_orders = {
"A1001": "已付款,等待发货",
"A1002": "已发货,快递单号 SF123456",
"A1003": "已签收",
}
return fake_orders.get(order_id, "没有查询到该订单")
价格工具(tools/price_tools.py):
"""价格相关工具
教学 Demo 使用模拟计算,线上业务需对接真实促销/价格系统。
"""
from langchain_core.tools import tool
@tool
def calculate_discount_price(original_price: float, discount_rate: float) -> str:
"""根据商品原价和折扣率计算折后价格。original_price 是原价(元),discount_rate 是折扣率(0.8 表示 8 折)。调用示例:calculate_discount_price(original_price=299.0, discount_rate=0.8)"""
discounted_price = round(original_price * discount_rate, 2)
return f"原价 {original_price} 元,折扣率 {discount_rate},折后价格为 {discounted_price} 元"
库存工具(tools/inventory_tools.py):
"""库存相关工具
教学 Demo 使用模拟数据,线上业务需对接真实库存系统,
并增加用户身份校验(防止越权查询其他仓库数据)。
"""
from pydantic import BaseModel, Field
from langchain_core.tools import tool
class InventoryInput(BaseModel):
"""库存查询参数"""
product_id: str = Field(description="商品编号,例如 P1001", pattern="^P[0-9]{4}$")
warehouse: str = Field(description="仓库名称,例如 上海仓、北京仓")
@tool(args_schema=InventoryInput)
def get_inventory(product_id: str, warehouse: str) -> str:
"""查询指定商品在指定仓库中的库存数量。调用示例:get_inventory(product_id="P1001", warehouse="上海仓")"""
fake_inventory = {
("P1001", "上海仓"): 35,
("P1001", "北京仓"): 12,
("P2001", "上海仓"): 0,
}
count = fake_inventory.get((product_id, warehouse))
if count is None:
return "没有查询到该商品的库存信息"
return f"{product_id} 在 {warehouse} 当前库存为 {count} 件"
4.6 模块三:Agent 服务(agent_service.py)
"""电商客服 Agent 服务:工具组合 + Agent 创建 + 对话
注意:本模块为教学 Demo,chat 函数使用简单的消息条数截断。
生产环境中长对话会触发 Token 上限溢出,需要增加更完善的
消息截断或摘要压缩机制(参考下方"上下文管理"说明)。
"""
from langchain.agents import create_agent
from utils.model_factory import get_deepseek_model
from tools.order_tools import get_order_status
from tools.price_tools import calculate_discount_price
from tools.inventory_tools import get_inventory
# 所有工具列表
ALL_TOOLS = [
get_order_status,
calculate_discount_price,
get_inventory,
]
# 对话历史最大保留轮数(超过后截断旧消息,防止 Token 溢出)
MAX_HISTORY_ROUNDS = 20
def create_ecommerce_agent():
"""创建电商客服 Agent
Returns:
Agent 实例,可通过 .invoke({"messages": [...]}) 调用
"""
model = get_deepseek_model(temperature=0)
agent = create_agent(
model=model,
tools=ALL_TOOLS,
)
return agent
def chat(agent, user_message: str, messages: list = None) -> str:
"""与 Agent 对话
Args:
agent: Agent 实例
user_message: 用户消息
messages: 历史消息列表(支持多轮对话)
Returns:
Agent 的回复文本
"""
if messages is None:
messages = []
# 追加当前用户消息
messages.append({"role": "user", "content": user_message})
# 上下文长度保护:超过最大轮数时截断旧消息,保留最近 MAX_HISTORY_ROUNDS 条
# 生产环境建议改用摘要压缩(保留关键上下文),而非简单截断
if len(messages) > MAX_HISTORY_ROUNDS:
messages = messages[-MAX_HISTORY_ROUNDS:]
# 调用 Agent
response = agent.invoke({"messages": messages})
# 获取最终回复
reply = response["messages"][-1].content
# 追加到历史消息,供下一轮使用
messages.append({"role": "assistant", "content": reply})
return reply, messages
上下文管理说明:上面的
MAX_HISTORY_ROUNDS是简单截断方案。生产环境中,长对话可能积累大量 Token 导致模型上下文溢出(报context_length_exceeded错误)。两种处理方案:
- 简单截断(本 Demo 采用):保留最近 N 轮消息,丢弃更早的。缺点是丢失早期上下文,用户追问时可能"失忆"。
- 摘要压缩(生产推荐):当消息超过阈值时,用模型对旧消息生成摘要,用摘要替换原始消息。保留关键信息,控制 Token 数量。
此外,多轮对话也可以使用 LangChain 的 Memory 组件(
ConversationBufferWindowMemory等)自动管理,后续章节会介绍。
4.7 完整实战:电商客服 Agent(05_ecommerce_agent.py)
"""电商客服 Agent 完整实战:多工具 + 连续对话
教学 Demo,模拟数据不可直接用于生产环境。
线上业务需增加:接口鉴权、用户身份校验、参数过滤、异常处理、日志审计。
"""
from agent_service import create_ecommerce_agent, chat
def main():
# 创建 Agent
agent = create_ecommerce_agent()
# 历史消息列表(用于多轮对话)
messages = []
# 连续对话测试
questions = [
"帮我查一下订单 A1002 发货了吗?",
"那这个商品 299 元打 8 折是多少钱?",
"上海仓 P1001 还有几件?",
"北京仓呢?",
]
for question in questions:
print(f"\n{'='*60}")
print(f"用户:{question}")
reply, messages = chat(agent, question, messages)
print(f"客服:{reply}")
# 交互模式(可选)
# while True:
# question = input("\n用户:")
# if question.lower() in ("quit", "exit", "q"):
# break
# reply, messages = chat(agent, question, messages)
# print(f"客服:{reply}")
if __name__ == "__main__":
main()
运行:
python 05_ecommerce_agent.py
输出示例:
============================================================
用户:帮我查一下订单 A1002 发货了吗?
客服:订单 A1002 已发货,快递单号 SF123456,请注意查收。
============================================================
用户:那这个商品 299 元打 8 折是多少钱?
客服:原价 299.0 元,折扣率 0.8,折后价格为 239.2 元。
============================================================
用户:上海仓 P1001 还有几件?
客服:P1001 在上海仓当前库存为 35 件。
============================================================
用户:北京仓呢?
客服:P1001 在北京仓当前库存为 12 件。
4.8 Agent 内部发生了什么
以第一个问题为例,Agent 的完整执行过程:
第 1 轮:
用户:"帮我查一下订单 A1002 发货了吗?"
模型思考 → 选择工具:get_order_status
模型生成参数 → {"order_id": "A1002"}
框架执行 → get_order_status.invoke({"order_id": "A1002"})
工具返回 → "已发货,快递单号 SF123456"
模型拿到结果 → 判断:信息已足够,不需要再调工具
模型组织回复 → "订单 A1002 已发货,快递单号 SF123456,请注意查收。"
第四个问题"北京仓呢?"更能体现 Agent 的价值:
第 4 轮:
用户:"北京仓呢?"
模型结合上下文 → 理解:用户在问 P1001 在北京仓的库存
模型思考 → 选择工具:get_inventory
模型生成参数 → {"product_id": "P1001", "warehouse": "北京仓"}
框架执行 → get_inventory.invoke(...)
工具返回 → "P1001 在北京仓当前库存为 12 件"
模型组织回复 → "P1001 在北京仓当前库存为 12 件。"
模型自动从上下文中推断出 product_id 是 P1001(上一轮对话提到的),用户不需要重复说。
第四部分小结
- 多工具 Agent 的关键是:工具描述写清楚,模型就能自动选对
create_agent传入 tools 列表即可,不需要手动编写选择逻辑- 连续对话时传入 messages 历史列表,Agent 能理解上下文
- 长对话需做上下文长度管理(截断或摘要压缩),防止 Token 溢出
- 项目结构按功能拆分模块,便于维护和扩展
第五部分:调优答疑篇
本部分目标:解决 Tool 和 Agent 使用中的常见问题,提供调试方法,总结本章重点和落地场景。
5.1 常见问题
Q1:模型不调工具,直接编了一个答案怎么办?
检查三个地方:
| 排查项 | 说明 | 修复方法 |
|---|---|---|
| 工具描述太模糊 | 模型不知道这个工具能干什么,所以不用 | 描述写清楚功能+场景+参数+返回值 |
| temperature 太高 | 高温度下模型倾向于"自由发挥"而非调用工具 | Agent 场景设为 0 |
| 工具列表没传进去 | create_agent 的 tools 参数为空 | 检查 tools=[tool1, tool2, ...] 是否正确传入 |
Q2:模型选错了工具怎么办?
多个工具描述重叠时容易混淆。比如同时有"查询订单"和"查询物流"两个工具,描述都写"查询"。
解决方法:在描述中明确区分使用场景:
# 差:两个工具描述都含糊
@tool
def get_order_status(order_id: str) -> str:
"""查询订单信息。"""
...
@tool
def get_logistics_info(order_id: str) -> str:
"""查询订单信息。"""
...
# 好:描述明确区分
@tool
def get_order_status(order_id: str) -> str:
"""查询订单的付款和发货状态(是否付款、是否发货、是否签收)。"""
...
@tool
def get_logistics_info(order_id: str) -> str:
"""查询订单的物流轨迹信息(快递公司、运输路线、当前位置)。"""
...
Q3:模型传的参数格式不对怎么办?
使用 Pydantic 的 Field(pattern=...) 加正则校验,在工具入口直接拦住错误参数。参考第二部分的 Pydantic 校验案例。
Q4:工具执行报错了怎么办?
在工具函数内部加 try-except,返回友好的错误信息而不是抛异常。模型能理解错误信息并给用户合理回复。完整的异常处理应覆盖三类常见故障:
import httpx
from langchain_core.tools import tool
@tool
def get_order_status(order_id: str) -> str:
"""根据订单号查询订单状态。"""
try:
# 模拟调用业务系统接口(教学 Demo 使用假数据)
# 线上业务替换为真实 HTTP 请求:
# response = httpx.get(f"{API_BASE_URL}/orders/{order_id}", timeout=5)
# response.raise_for_status()
# return response.json()["status"]
fake_orders = {"A1001": "已付款", "A1002": "已发货"}
return fake_orders.get(order_id, "没有查询到该订单")
except httpx.TimeoutException:
# 超时:业务系统响应过慢
return "订单系统响应超时,请稍后再试"
except httpx.ConnectError:
# 接口崩溃:业务系统不可用
return "订单系统暂时不可用,请稍后再试"
except Exception as e:
# 兜底:其他未预期异常(如 tool_call 格式解析失败)
# 生产环境应记录日志(如 logging.error),不要把原始异常暴露给用户
return f"查询失败,请稍后重试或联系客服"
生产环境安全提示:上面的异常处理仅返回友好提示,不暴露内部错误细节。生产环境中还应该:①用
logging记录完整异常堆栈;②对接监控告警(如钉钉/飞书 Webhook);③对高频超时做熔断降级。
Q5:Agent 调用了工具但不回复,直接停了怎么办?
检查模型返回的消息类型。Agent 内部的消息流:
HumanMessage → AIMessage(tool_call) → ToolMessage → AIMessage(final)
如果缺少最后的 AIMessage(final),可能是模型上下文超限或工具返回结果过长。尝试精简工具返回值或减少工具数量。
Q6:多轮对话报 context_length_exceeded 怎么办?
对话历史太长导致 Token 超过模型上下文上限。三种解决方案:
| 方案 | 适用场景 | 优缺点 |
|---|---|---|
| 简单截断 | Demo / 测试 | 保留最近 N 轮,丢弃旧消息;简单但会"失忆" |
| 摘要压缩 | 生产环境 | 用模型对旧消息生成摘要,保留关键信息;Token 更省但多一次 API 调用 |
| Memory 组件 | 快速集成 | LangChain 内置 ConversationBufferWindowMemory 等,自动管理;灵活但需配置 |
5.2 调试五步法
Agent 不按预期工作时,按以下顺序排查:
Step 1:先手动调用工具 → 确认工具本身能跑通
↓ 没问题
Step 2:检查工具描述 → 模型能看懂这个描述吗?
↓ 没问题
Step 3:检查工具列表 → create_agent 的 tools 参数传对了吗?
↓ 没问题
Step 4:检查 temperature → 设为 0 了吗?
↓ 没问题
Step 5:打印 Agent 内部消息 → 看模型到底选了什么、传了什么
打印 Agent 内部消息的调试代码:
response = agent.invoke(
{"messages": [{"role": "user", "content": "帮我查一下订单 A1002"}]}
)
# 打印所有中间消息,看模型的决策过程
for msg in response["messages"]:
print(f"类型: {msg.__class__.__name__}")
print(f"内容: {msg.content}")
# 如果是工具调用消息,打印工具名和参数
if hasattr(msg, "tool_calls") and msg.tool_calls:
for tc in msg.tool_calls:
print(f" 调用工具: {tc['name']}")
print(f" 参数: {tc['args']}")
print("---")
输出示例:
类型: HumanMessage
内容: 帮我查一下订单 A1002
---
类型: AIMessage
内容:
调用工具: get_order_status
参数: {'order_id': 'A1002'}
---
类型: ToolMessage
内容: 已发货,快递单号 SF123456
---
类型: AIMessage
内容: 订单 A1002 已发货,快递单号 SF123456,请注意查收。
---
5.3 本章重点
核心概念
- Tool = 把外部能力包装成模型可调用的函数
- Agent = 以模型为大脑、工具为手脚的自主决策系统
- 模型靠工具描述选择该用哪个工具
- Agent 执行循环 = 框架内置的调度引擎,管理"模型→工具→模型"的循环
核心 API 速查表
| API | 用途 | 示例 |
|---|---|---|
@tool | 把普通函数装饰为 LangChain Tool | @tool def my_func(x: str) -> str: ... |
tool.invoke({...}) | 手动调用工具,验证功能是否正常 | get_order_status.invoke({"order_id": "A1001"}) |
tool.name | 查看工具名称 | print(get_order_status.name) |
tool.description | 查看工具描述 | print(get_order_status.description) |
tool.args | 查看参数结构 | print(get_order_status.args) |
Field(description=..., pattern=...) | Pydantic 字段描述和正则校验 | Field(pattern="^A[0-9]{4}$") |
Field(ge=..., le=...) | 数值范围校验 | Field(ge=1, le=100) |
create_agent(model, tools) | 创建 Agent | agent = create_agent(model=model, tools=[tool1]) |
agent.invoke({"messages": [...]}) | 调用 Agent | agent.invoke({"messages": [{"role": "user", "content": "..."}]}) |
五条关键规则
- 工具描述是模型选工具的唯一依据:写清楚功能+场景+参数+返回值+必填/选填标注+调用示例
- 先手动调用确认工具没问题,再交给 Agent:
tool.invoke()先验证 - Agent 场景 temperature 设为 0:减少模型"自由发挥",提高工具调用准确率
- 多工具描述要明确区分:避免功能重叠导致模型选错
- 长对话必须做上下文管理:截断或摘要压缩,防止 Token 溢出
完整流程
定义工具(@tool + 描述 + 类型注解)
→ 手动调用验证(tool.invoke)
→ 初始化模型(init_chat_model)
→ 创建 Agent(create_agent(model, tools))
→ 对话(agent.invoke({"messages": [...]}))
→ Agent 自主决策:选工具 → 调用 → 拿结果 → 回复
5.4 Tool + Agent 落地场景
学完本章后,这套技术能用在哪些实际业务场景?
| 场景 | 工具示例 | Agent 价值 |
|---|---|---|
| 智能客服 | 查订单、查物流、算价格、查退换货政策 | 用户问什么,Agent 自动选工具回答,无需人工路由 |
| 企业知识库助手 | RAG 检索、文档摘要、邮件发送 | 先检索知识库,再调邮件工具发送结果,一条链路完成 |
| 数据分析机器人 | SQL 查询、数据可视化、报表导出 | 用户说"看下上个月销售数据",Agent 自动查库+生成图表 |
| 自动化运维 | 服务器状态查询、日志检索、告警发送 | 运维说"看看 A 服务器 CPU",Agent 自动查监控+拉日志 |
| 智能办公助理 | 日程管理、邮件处理、会议预定 | 用户说"帮我约张三下周开会",Agent 自动查日程+发邀请 |
以上场景都需要在本文 Demo 基础上增加用户身份校验、参数过滤防注入、接口鉴权、异常处理、日志审计、限流等生产能力。本文代码是入门骨架,生产落地需补齐工程化能力。
本文代码采用 MIT 开源协议,可自由复制、修改、使用。如果觉得有帮助,欢迎点赞收藏。下一章将介绍如何让 Agent 拥有记忆——支持多轮对话上下文,用户追问时不需要从头说起。
更多推荐
所有评论(0)