Agent 智能体开发实战指南(二):工具调用系统深度解析

系列导读:这是《Agent 智能体开发实战指南》系列的第二篇,将深入讲解 Agent 的工具调用系统,包括@tool 装饰器原理、工具设计原则、多工具协作等核心内容。


一、工具:Agent 的"手脚"

1.1 为什么需要工具?

如果把 LLM 比作"大脑",那么工具就是"手脚"——没有工具,再聪明的大脑也无法影响现实世界。

工具的本质:将外部能力封装成 LLM 可以理解和调用的接口。

┌─────────────┐     ┌─────────────┐     ┌─────────────┐
│   LLM 大脑   │ ──→ │  工具接口   │ ──→ │   外部系统   │
│  (决策者)    │     │  (翻译层)   │     │   (执行者)   │
└─────────────┘     └─────────────┘     └─────────────┘

1.2 工具的三大要素

要素作用示例
名称LLM 识别工具的标识get_weather
描述LLM 理解工具用途“查询指定城市的天气”
参数LLM 传递必要信息city: str

二、@tool 装饰器详解

2.1 基础用法

from langchain_core.tools import tool

@tool(description="获取股价,传入股票名称,返回字符串信息")
def get_price(name: str) -> str:
    return f"股票{name}的价格是 40 元"

装饰器做了什么?

  1. 提取函数名 get_price 作为工具名
  2. 解析参数签名 name: str 确定输入要求
  3. 解析返回类型 -> str 确定输出格式
  4. 绑定 description 供 LLM 理解用途
  5. 将普通函数转换为 LangChain 的 Tool 对象

2.2 完整 Tool 对象结构

# @tool 装饰后,实际生成的对象结构:
Tool(
    name="get_price",
    description="获取股价,传入股票名称,返回字符串信息",
    args_schema={
        "name": {"type": "string", "description": "股票名称"}
    },
    func=<function get_price at 0x...>
)

2.3 多参数工具示例

@tool(description="查询两个城市之间的天气对比")
def compare_weather(city1: str, city2: str) -> str:
    """获取两个城市的天气并进行对比"""
    # LLM 会自动传入两个参数
    weather1 = get_weather(city1)
    weather2 = get_weather(city2)
    return f"{city1}: {weather1} | {city2}: {weather2}"

三、工具描述编写指南

3.1 描述的重要性

description 是 LLM 选择工具的唯一依据。糟糕的描述会导致:

  • LLM 不知道何时调用该工具
  • LLM 传入错误的参数
  • LLM 完全忽略该工具

3.2 优秀描述的三要素

# 描述太简略
@tool(description="获取数据")

# 描述不够清晰
@tool(description="获取股票价格")

# 优秀的描述
@tool(description="获取股票实时价格,传入股票名称(如'华胜天成'、'贵州茅台'),返回包含股价、涨跌幅的字符串信息")

优秀描述包含

  1. 功能说明:这个工具做什么
  2. 参数说明:需要传入什么,格式是什么
  3. 返回说明:会返回什么,格式是什么
  4. 示例值:给出典型参数示例

3.3 描述模板

@tool(description="[功能],传入 [参数名 + 格式 + 示例],返回 [返回内容 + 格式]")

实战示例

# 天气查询
@tool(description="查询指定城市的天气情况,传入城市名称(如'深圳'、'北京'),返回包含气温、湿度、风向的详细天气信息")

# 用户信息查询
@tool(description="获取当前用户的 ID,无需参数,返回纯字符串格式的用户 ID")

# 数据检索
@tool(description="从知识库中检索相关内容,传入搜索关键词,返回最匹配的 3-5 条参考资料")

四、多工具协作场景

4.1 场景:股票查询 + 介绍

from langchain.agents import create_agent
from langchain_community.chat_models.tongyi import ChatTongyi
from langchain_core.tools import tool


@tool(description="获取股票价格,传入股票名称,返回当前股价字符串")
def get_price(name: str) -> str:
    return f"股票{name}的价格是 40 元"


@tool(description="获取股票公司信息,传入股票名称,返回公司业务介绍字符串")
def get_info(name: str) -> str:
    return f"股票{name},是一家 A 股上市公司,专注于计算机领域。"


agent = create_agent(
    model=ChatTongyi(model="qwen3-max"),
    tools=[get_price, get_info],
    system_prompt="你是一个智能助手,可以回答股票相关问题,请告知我思考过程,让我知道你为什么调用某个工具"
)

for chunk in agent.stream(
    {"messages": [{"role": "user", "content": "华胜天成股价多少,并介绍一下"}]},
    stream_mode="values"
):
    latest_message = chunk['messages'][-1]
    if latest_message.content:
        print(latest_message.content)
    try:
        if latest_message.tool_calls:
            print(f"工具调用:{[tc['name'] for tc in latest_message.tool_calls]}")
    except AttributeError:
        pass

4.2 执行流程分析

用户:华胜天成股价多少,并介绍一下
  ↓
Agent 思考:用户问了两个问题——股价 + 公司介绍
  ↓
行动 1:调用 get_price("华胜天成")
  ↓
观察 1:获得股价信息
  ↓
行动 2:调用 get_info("华胜天成")
  ↓
观察 2:获得公司介绍
  ↓
生成答案:整合两条信息,回复用户

4.3 工具调用顺序

LLM 会自动决定工具调用顺序,通常遵循:

  1. 依赖关系:先获取必要数据,再进行处理
  2. 逻辑顺序:按人类思考的自然顺序
  3. 效率优先:能并行则并行(LangChain 支持并行调用)

五、工具设计最佳实践

5.1 单一职责原则

❌ 错误示例:一个工具做太多事

@tool(description="处理用户所有请求")
def handle_everything(query: str) -> str:
    # 又查天气,又查股价,又写报告...
    pass

✅ 正确示例:每个工具专注一件事

@tool(description="查询天气")
def get_weather(city: str) -> str:
    pass

@tool(description="查询股价")
def get_price(stock: str) -> str:
    pass

@tool(description="生成报告")
def generate_report(user_id: str) -> str:
    pass

5.2 错误处理

工具内部应该处理异常,返回友好提示

@tool(description="查询天气,传入城市名称")
def get_weather(city: str) -> str:
    try:
        # 调用天气 API
        response = weather_api.get(city)
        return format_weather(response)
    except CityNotFoundError:
        return f"未找到城市'{city}'的天气信息,请检查城市名称是否正确"
    except APIError as e:
        return f"天气服务暂时不可用,请稍后重试(错误码:{e.code})"

为什么?

  • 避免 Agent 因工具异常而崩溃
  • 让 LLM 知道发生了什么,可以调整策略
  • 提供更好的用户体验

5.3 返回值格式

原则:返回 LLM 容易理解的格式

# ❌ 返回复杂对象
def get_weather(city: str) -> dict:
    return {"temp": 26, "humidity": 50, "wind": "南风 1 级"}

# ✅ 返回格式化字符串
def get_weather(city: str) -> str:
    return f"城市{city}天气为晴天,气温 26 摄氏度,空气湿度 50%,南风 1 级"

原因:LLM 处理自然语言最擅长,结构化数据反而需要额外解析。

5.4 参数设计

原则:参数越少越好,类型越简单越好

# 参数太多
@tool(description="查询天气")
def get_weather(city: str, date: str, unit: str, include_hourly: bool) -> str:
    pass

# 简化参数
@tool(description="查询指定城市的天气")
def get_weather(city: str) -> str:
    # 内部使用默认值:今天、摄氏度、不含小时预报
    pass

六、实战:完整的工具集设计

6.1 智扫通客服 Agent 工具集

from langchain_core.tools import tool
from rag.rag_service import RagSummarizeService

rag = RagSummarizeService()

# 1. 知识库检索工具
@tool(description="从向量存储中检索参考资料,传入搜索关键词,返回最相关的知识内容")
def rag_summarize(query: str) -> str:
    return rag.rag_summarize(query)

# 2. 天气查询工具
@tool(description="获取指定城市的天气,传入城市名称,返回包含气温、湿度、风向的字符串信息")
def get_weather(city: str) -> str:
    return f"城市{city}天气为晴天,气温 26 摄氏度,空气湿度 50%,南风 1 级,AQI21"

# 3. 用户位置工具
@tool(description="获取用户所在城市的名称,无需参数,返回纯字符串格式的城市名")
def get_user_location() -> str:
    return "深圳"  # 实际应从用户 profile 获取

# 4. 用户 ID 工具
@tool(description="获取当前用户的 ID,无需参数,返回纯字符串格式的用户 ID")
def get_user_id() -> str:
    return "1001"  # 实际应从 session 获取

# 5. 时间工具
@tool(description="获取当前月份,无需参数,返回'YYYY-MM'格式的字符串")
def get_current_month() -> str:
    return "2025-03"

# 6. 外部数据工具
@tool(description="从外部系统获取指定用户在指定月份的使用记录,传入 user_id 和 month,返回使用数据字符串")
def fetch_external_data(user_id: str, month: str) -> str:
    # 从数据库或 API 获取
    return external_data.get(user_id, {}).get(month, "")

# 7. 上下文标记工具
@tool(description="无入参,无返回值,调用后标记当前为报告生成场景,触发提示词切换")
def fill_context_for_report():
    return "fill_context_for_report 已调用"

6.2 工具分类管理

agent/tools/
├── agent_tools.py      # 核心业务工具
├── common_tools.py     # 通用工具(时间、位置等)
└── system_tools.py     # 系统工具(日志、监控等)

七、调试技巧

7.1 打印工具调用日志

for chunk in agent.stream(input_dict, stream_mode="values"):
    latest_message = chunk['messages'][-1]
    
    if latest_message.content:
        print(f"🤖 Agent: {latest_message.content}")
    
    try:
        if latest_message.tool_calls:
            tool_names = [tc['name'] for tc in latest_message.tool_calls]
            print(f"🔧 工具调用:{tool_names}")
    except AttributeError:
        pass

7.2 常见问题排查

问题可能原因解决方案
Agent 不调用工具description 不清晰重写工具描述
调用错误工具工具名相似区分工具命名
参数传错参数类型不明确在 description 中说明
工具返回被忽略返回格式混乱统一返回格式

八、本章小结

核心要点

  1. @tool 装饰器:将 Python 函数转换为 LLM 可调用的 Tool 对象
  2. description 至关重要:决定 LLM 是否正确选择工具
  3. 单一职责:每个工具只做一件事,做好一件事
  4. 错误处理:工具内部处理异常,返回友好提示
  5. 返回值格式:优先返回自然语言字符串

下章预告

下一篇我们将深入 ReAct 框架,学习:

  • ReAct 的思考 - 行动 - 观察循环
  • 多步推理策略
  • 如何引导 Agent 按正确顺序调用工具
  • ReAct 实战案例解析

  1. Agent 智能体开发实战指南(一):从 LLM 到 Agent 的认知升级
  2. Agent 智能体开发实战指南(二):工具调用系统深度解析(本文)
  3. Agent 智能体开发实战指南(三):ReAct 框架深度解析
  4. Agent 智能体开发实战指南(四):流式输出与状态管理
  5. Agent 智能体开发实战指南(五):中间件系统与动态提示词
  6. Agent 智能体开发实战指南(六):RAG 与向量存储实战
  7. Agent 智能体开发实战指南(七):项目架构设计与工程化实践
  8. Agent 智能体开发实战指南(八):UI 集成与生产部署

本文是《Agent 智能体开发实战指南》系列的第二篇,下一篇将深入讲解 ReAct 框架。

Logo

这里是“一人公司”的成长家园。我们提供从产品曝光、技术变现到法律财税的全栈内容,并连接云服务、办公空间等稀缺资源,助你专注创造,无忧运营。

更多推荐