Agent 工具使用设计模式:多工具组合、结构化输出、工具审批

前面三篇文章分别讲了 Agent 的本质(tool calling 循环)、框架的四层抽象、以及三种 Agentic 设计模式。这些解决了"Agent 是什么"和"怎么设计一个 Agent"的问题。

但 Agent 的能力边界由工具决定。一个只有查询工具的 Agent 只能回答问题,一个有预订工具的 Agent 才能完成交易。

本文以"旅行预订代理"为场景,讲解三种工具使用设计模式。


📦 相关链接


🎯 三种模式解决什么问题

flowchart LR
    A[用户需求] --> B["模式1:<br/>多工具组合"]
    B --> C["模式2:<br/>结构化输出"]
    C --> D["模式3:<br/>工具审批"]
    D --> E["完整的交易型 Agent"]
 
    B -.- B1["问题: 多个工具时<br/>LLM 如何自主编排调用顺序?"]
    C -.- C1["问题: 下游代码如何<br/>可靠消费 Agent 输出?"]
    D -.- D1["问题: 副作用操作<br/>如何加入人工确认?"]

模式 1:多工具组合

问题

前面的课程里,Agent 只有一个工具。现实中的 Agent 通常同时持有多个工具——查询目的地、检查可用性、查航班信息、预订机票。

当工具数量从 1 变成 3+,核心问题变了:LLM 如何决定先调哪个、后调哪个、传什么参数?

解法:把所有工具的 Schema 一次性传给 LLM

不做路由,不做编排。把所有工具的 JSON Schema 放进一个列表,一次性传给 LLM。LLM 自己判断该调谁。

flowchart TD
    A["System Prompt:<br/>你是旅行代理,用工具回答"] --> B["Tools Schema 列表:<br/>get_destinations<br/>check_availability<br/>get_flight_info"]
    B --> C[LLM 收到用户问题]
    C --> D{"LLM 判断<br/>需要调哪个工具?"}
    D -->|"需要目的地列表"| E[调用 get_destinations]
    D -->|"需要可用性"| F[调用 check_availability]
    D -->|"需要航班"| G[调用 get_flight_info]
    E --> H[执行函数, 结果返回 LLM]
    F --> H
    G --> H
    H --> I{"还需要<br/>更多信息?"}
    I -->|是| D
    I -->|否| J[生成最终回复]

这个流程就是 tool calling 循环的完整版。和单工具版本的区别只有一个:TOOLS_SCHEMA 列表里有多个元素

工具定义方式

在原生 SDK 中,每个工具由两部分组成:JSON Schema(描述工具签名)+ Python 函数(实现逻辑)。以 check_availability 为例:

组成部分 内容
JSON Schema name: "check_availability", parameters: {destination: string}
Python 函数 check_availability(destination) → 查字典返回可用性

三个工具分别定义后,统一放入 TOOLS_SCHEMA 列表和 TOOL_MAP 字典。LLM 通过 Schema 知道"有什么工具",通过 tool calling 返回的函数名找到对应实现。

在 qwen-agent 中,用 @register_tool 装饰器注册工具类,Assistant(function_list=[...]) 传入工具名列表。框架内部自动完成 Schema 生成和 tool calling 循环。

核心洞察

多工具组合不需要"编排引擎"。LLM 本身就是编排器——你把工具描述给它,它自己决定调用顺序。你的代码只需要做两件事:

  1. 把工具 Schema 传给 LLM
  2. 根据 LLM 返回的函数名执行对应函数,把结果传回去

这就是 tool calling 循环的全部秘密。


模式 2:结构化输出

问题

自由文本适合人读,不适合代码消费。如果 Agent 输出了"建议预订巴塞罗那,价格 350 美元,航班 BA 2042",下游自动预订系统需要从这句话里提取目的地、价格、航班号——这就是正则噩梦。

解法:Pydantic 模型 + JSON 约束

定义 Pydantic 模型作为输出契约,在 system prompt 中指定 JSON 格式,最后用 model_validate_json() 验证。

输出契约定义:

模型 字段 类型 说明
BookingRecommendation destination str 目的地名称
available bool 是否可预订
flight_details str 航班详情
estimated_cost int 预估费用(美元)
TravelPlan recommendations list[BookingRecommendation] 推荐列表

System prompt 中的格式约束:

最终回复必须是严格的 JSON。格式示例: {"recommendations": [{"destination": "巴塞罗那", "available": true, "flight_details": "BA 2042, 08:30-11:45", "estimated_cost": 350}]} 不要包含 markdown 代码块标记。

完整数据流

flowchart TD
    A["System Prompt:<br/>角色 + 流程 + JSON格式约束"] --> B[LLM 调用工具查询数据]
    B --> C["获取目的地 + 可用性 + 航班"]
    C --> D["LLM 按 JSON Schema 输出"]
    D --> E{"Pydantic<br/>model_validate_json()"}
    E -->|成功| F["返回 TravelPlan 类型对象<br/>下游代码直接 .destination .available"]
    E -->|失败| G["捕获异常<br/>fallback 处理"]

为什么把格式写在 prompt 里而不是用框架参数

qwen-agent 的 Assistant 没有 response_format 参数。MAF 和 LangChain 有,但每个框架的 API 不同。把格式要求写在 prompt 里:

  • 你完全控制输出格式的细粒度
  • 换框架不需要改 prompt
  • Pydantic 做最后一道验证,格式偏差能被立刻发现

和 Lesson 03 结构化输出的区别

Lesson 03 的结构化输出是"纯推理型"——Agent 没有工具,LLM 基于训练数据直接生成 JSON。本课的结构化输出是"工具增强型"——LLM 先调工具获取真实数据,再基于数据生成结构化 JSON。后者的输出不是"编"出来的,是有数据支撑的。


模式 3:工具审批模式

问题

查询类操作可以自动执行,但副作用操作(预订、扣款、发送)不行。你不想让 Agent 不经确认就花掉用户的钱。

解法:敏感工具执行前拦截

sequenceDiagram
    participant User
    participant LLM
    participant ToolLoop
    participant SensitiveTool
 
    User->>LLM: 帮我预订 LHR→BCN 的航班
    LLM->>ToolLoop: 调用 book_flight(origin="LHR", destination="BCN", passenger="Zhang Wei")
    ToolLoop->>ToolLoop: 检查: book_flight ∈ SENSITIVE_TOOLS?
    ToolLoop->>User: ⚠️ Agent 想执行 book_flight,参数: {...}。批准?(y/n)
    User->>ToolLoop: y
    ToolLoop->>SensitiveTool: 执行预订
    SensitiveTool-->>ToolLoop: 已预订,确认号 #TRV-0421
    ToolLoop-->>LLM: 返回结果
    LLM-->>User: 航班已预订,确认号 #TRV-0421

如果用户输入 n,工具不会被真正执行,LLM 收到的是"操作被用户拒绝"的提示,它会据此调整回复。

两种实现方式

实现位置 原生 SDK qwen-agent
拦截点 tool calling 循环中,执行前检查 SENSITIVE_TOOLS 集合 BaseTool.call() 方法内部,input() 询问
敏感标记 全局 SENSITIVE_TOOLS = {"book_flight"} 集合 无独立标记,工具自身决定是否询问
审批交互 input("是否批准执行?(y/n): ") 同左,input() 停住进程等待输入

两种方式本质一样:在函数真正执行前,用 input() 停住进程,等人工确认

框架没有内置审批的原因

MAF 有 approval_mode 参数,qwen-agent 没有。这不是框架的缺陷——审批逻辑本质是业务逻辑,和工具参数、用户权限、风控策略强耦合。框架很难提供一个通用参数覆盖所有场景。自己写 input() 拦截反而最灵活。


💡 三种模式怎么组合

三个模式叠加在同一个旅行预订 Agent 上:

flowchart TD
    subgraph "多工具组合"
        A1["get_destinations<br/>check_availability<br/>get_flight_info<br/>book_flight"]
    end
    subgraph "结构化输出"
        A2["Pydantic TravelPlan<br/>JSON 格式约束"]
    end
    subgraph "工具审批"
        A3["book_flight 标记为敏感<br/>执行前 input() 拦截"]
    end
 
    User["🙋 用户: 帮我查欧洲目的地并预订"] --> A1
    A1 --> A2
    A2 --> A3
    A3 --> Result["✅ 完整的交易闭环:<br/>查询 → 推荐 → 审批 → 预订"]

一个完整的交易型 Agent 需要三个模式同时生效:

  • 多工具组合让 Agent 有能力完成复杂查询
  • 结构化输出让查询结果能被下游系统可靠消费
  • 工具审批让副作用操作不越权

🔑 框架对比速查

概念 原生 SDK qwen-agent
工具定义 JSON Schema + Python 函数,手动维护一致性 @register_tool + BaseTool 子类,参数格式为字典列表
多工具组合 全部放入 TOOLS_SCHEMA 列表 function_list=["tool1", "tool2", ...]
工具参数格式 JSON Schema 标准 properties + required parameters 列表:[{"name": ..., "type": ..., "required": ...}]
结构化输出 system_message 中指定 JSON + Pydantic 验证 同左(框架无 response_format
审批模式 tool calling 循环中检查 SENSITIVE_TOOLS 集合 BaseTool.call() 内部 input() 拦截
tool calling 循环 while True 手动管理 Assistant.run() 内部自动完成

🚀 运行

cd 04-tool-use/code_samples
 
# 原生版(包含多工具 + 结构化输出,审批模式默认跳过)
python 04-python-agent-framework.py
 
# 框架版(同样的三个示例)
python 04-qwen-agent-framework.py
 
# 要体验工具审批模式,编辑代码取消注释 demo_3_approval_mode()

🔮 下一篇

有了工具使用的基础模式,下一篇进入 Agentic RAG——当 Agent 需要检索外部知识时,如何设计检索策略?单一向量检索够不够?怎么让 Agent 自己决定什么时候检索、检索什么?


✍️ 结语

三种工具使用模式的核心思想:

  • 多工具组合:不给 LLM 预设调用路径,让它自己根据工具描述决定调用顺序
  • 结构化输出:不让下游猜格式,用 Pydantic 做数据契约,让 JSON 成为 Agent 和系统之间的接口语言
  • 工具审批:副作用操作不是不能做,而是不能自动做——一行 input() 就是最灵活的安全阀

工具是 Agent 的手。学会设计工具,Agent 才从"能聊天"变成"能办事"。