大模型只会动嘴不会动手?Tool工具调用——给AI装上真干活的机械臂

上一章我们完成了 RAG 检索增强生成,大模型已经能根据知识库资料回答问题了。但现实业务中,用户问的不仅是"文档里写了什么",还有"我的订单发货了吗"“这个商品打完折多少钱”“仓库还有没有货”——这些信息不在任何文档里,而在业务系统的数据库中。这一章解决的就是:怎么让大模型调用外部接口,从"只会说"变成"会干活"。

免责声明

  • 内容性质:本文仅为 LangChain 工具调用教学演示,不代表 DeepSeek 官方文档或教程,与任何模型厂商无合作关系。文中以 DeepSeek 为例,但代码同样适用于 OpenAI、通义千问、文心一言等任意兼容 OpenAI 接口格式的大模型,替换 modelbase_url 即可。
  • 生产免责:本文所有数据均为模拟测试数据(fake_ordersfake_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 的关系

对比项ToolAgent
角色手脚(执行具体操作)大脑(决定做什么操作)
决策能力没有,被动等待调用有,自主判断该不该用工具
类比公司里的各个业务系统会使用这些系统的员工
谁来调用Agent 或程序员手动 .invoke()模型(在 create_agent 中自动决策)

一句话总结:Tool 是"能干什么",Agent 是"什么时候干什么"。

3.3 用 create_agent 创建第一个 Agent

LangChain 版本说明:LangChain 1.0 对 Agent API 做了重大调整。0.x 时代 Agent 系统碎片化严重,需要根据场景选择 create_react_agentcreate_tool_calling_agentcreate_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 错误)。两种处理方案:

  1. 简单截断(本 Demo 采用):保留最近 N 轮消息,丢弃更早的。缺点是丢失早期上下文,用户追问时可能"失忆"。
  2. 摘要压缩(生产推荐):当消息超过阈值时,用模型对旧消息生成摘要,用摘要替换原始消息。保留关键信息,控制 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)创建 Agentagent = create_agent(model=model, tools=[tool1])
agent.invoke({"messages": [...]})调用 Agentagent.invoke({"messages": [{"role": "user", "content": "..."}]})

五条关键规则

  1. 工具描述是模型选工具的唯一依据:写清楚功能+场景+参数+返回值+必填/选填标注+调用示例
  2. 先手动调用确认工具没问题,再交给 Agenttool.invoke() 先验证
  3. Agent 场景 temperature 设为 0:减少模型"自由发挥",提高工具调用准确率
  4. 多工具描述要明确区分:避免功能重叠导致模型选错
  5. 长对话必须做上下文管理:截断或摘要压缩,防止 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 拥有记忆——支持多轮对话上下文,用户追问时不需要从头说起。

更多推荐