跳转至

第3章 工具调用和执行

学习目标

  • 掌握用 Python 函数定义 Tool 的方法
  • 理解工具 schema 自动生成原理
  • 学会异步执行工具与结果回传
  • 掌握工具错误处理与重试策略
  • 能够实现并行工具调用以提升效率

问题导入

某团队给 Agent 接了 5 个工具:查订单、查库存、查物流、发短信、建工单。上线后发现:①模型经常传错参数(把订单号当成了手机号);②查订单和查库存本可同时进行,Agent 却串行调用,响应慢;③工具偶发超时,Agent 直接崩掉。

这三个问题的根因是工具层的工程化不足。工具是 Agent 的"手脚"——定义不清则模型乱用,执行不当则性能与稳定性都差。本章用 OpenAI Agents SDK 讲解如何规范地定义工具、自动生成 schema、异步执行、并行调用,以及如何优雅地处理工具错误。

核心概念

1. 用 Python 函数定义 Tool

OpenAI Agents SDK 提供 @function_tool 装饰器,把普通 Python 函数一键转为 Agent 可用的工具。函数的类型注解和 docstring 直接成为工具的 schema,无需手写 JSON Schema。

from agents import function_tool

@function_tool
def query_order(order_id: str) -> str:
    """查询订单状态。order_id:订单号,如 A1001"""
    return f"订单 {order_id} 状态:已发货"

要点:必须有完整类型注解;docstring 描述工具用途与参数;返回值应为字符串或可序列化对象。

2. 工具 Schema 自动生成

@function_tool 会读取函数的:

  • 函数名 → 工具名
  • docstring → 工具描述(模型据此判断何时调用)
  • 参数类型注解 → 参数 schema(type、required)
  • 参数默认值 → optional 参数

这避免了手写 JSON Schema 的繁琐与错误,"函数即文档即 schema"是现代 Agent 框架的共识。

3. 异步执行与结果回传

工具执行可能耗时(HTTP 请求、数据库查询),应使用异步函数避免阻塞:

@function_tool
async def query_order(order_id: str) -> str:
    async with httpx.AsyncClient() as c:
        r = await c.get(f"{API}/orders/{order_id}")
    return r.text

SDK 的 Runner 是异步的,会自动 await 异步工具。工具返回值作为 observation 回传给 LLM,进入下一轮决策。

4. 工具错误处理

工具失败的三类与对策:

  • 参数错误:模型传错参数。把错误信息回传,让模型修正后重试。
  • 执行错误:工具内部异常(网络、权限)。捕获异常,返回结构化错误信息。
  • 业务错误:如订单不存在。直接回传,让模型据此回答用户。

关键原则:永远不要让异常直接抛到 Agent 外层,而是转为可读的字符串回传,让模型自行决策。

5. 并行工具调用

现代 LLM(GPT-4o 等)支持单轮内并行发起多个工具调用。例如用户问"查我的订单 A1 和 A2 的状态",模型可一次性返回两个 tool_call,SDK 会并发执行,再把两个结果一起回传。这显著降低了串行等待延迟。

触发并行的条件:①模型判断多个调用无依赖;②Prompt 鼓励并行;③框架支持并发执行(Agents SDK 默认支持)。

原理图

flowchart LR
    User[用户输入] --> LLM[LLM 决策]
    LLM --> |tool_call 1| T1[工具 A]
    LLM --> |tool_call 2| T2[工具 B]
    LLM --> |tool_call 3| T3[工具 C]

    T1 --> R1[结果1]
    T2 --> R2[结果2]
    T3 --> R3[结果3]

    R1 --> Merge[结果合并回传]
    R2 --> Merge
    R3 --> Merge
    Merge --> LLM2[LLM 下一轮决策]
    LLM2 --> Final[最终回答]

    subgraph 错误处理
        E1[参数错误] --> RT1[回传错误让模型修正]
        E2[执行异常] --> RT2[捕获并返回错误信息]
        E3[业务错误] --> RT3[回传让模型回答]
    end

    style T1 fill:#bbdefb
    style T2 fill:#bbdefb
    style T3 fill:#bbdefb
    style Merge fill:#c8e6c9

图:工具调用流程——LLM 发起多个并行 tool_call,框架并发执行后合并结果回传,错误统一转字符串回传

示例代码

1. 工具定义与 schema 自动生成

"""
第3章示例:定义工具函数并让 Agent 调用
依赖:pip install openai-agents
"""
import asyncio
from agents import Agent, Runner, function_tool


@function_tool
def query_order(order_id: str) -> str:
    """查询订单状态。

    Args:
        order_id: 订单号,格式如 A1001
    """
    db = {"A1001": "已发货", "A1002": "处理中", "A1003": "已完成"}
    return f"订单 {order_id} 状态:{db.get(order_id, '不存在')}"


@function_tool
def query_inventory(sku: str) -> str:
    """查询商品库存。

    Args:
        sku: 商品 SKU 编码
    """
    db = {"SKU001": 100, "SKU002": 0, "SKU003": 30}
    stock = db.get(sku, 0)
    return f"商品 {sku} 库存:{stock} 件" + ("(缺货)" if stock == 0 else "")


# 查看自动生成的 schema
print("=== query_order schema ===")
print(query_order.to_json_schema())


agent = Agent(
    name="电商助手",
    instructions="你是电商客服。可查询订单和库存。回答要简洁。",
    tools=[query_order, query_inventory],
    model="gpt-4o-mini",
)


async def main():
    result = await Runner.run(agent, "帮我查下订单 A1001 和 A1003 的状态")
    print("\n回复:", result.final_output)


if __name__ == "__main__":
    asyncio.run(main())

运行结果示例:

=== query_order schema ===
{'name': 'query_order', 'description': '查询订单状态...', 'parameters': {...}

回复: 订单 A1001 状态:已发货;订单 A1003 状态:已完成。

2. 异步工具与并行调用

"""
异步工具 + 并行调用:模拟多个无依赖查询并发执行
"""
import asyncio
import time
from agents import Agent, Runner, function_tool


@function_tool
async def query_order(order_id: str) -> str:
    """查询订单状态(模拟网络耗时)"""
    await asyncio.sleep(1)  # 模拟 IO
    return f"订单 {order_id}:已发货"


@function_tool
async def query_inventory(sku: str) -> str:
    """查询库存(模拟网络耗时)"""
    await asyncio.sleep(1)
    return f"商品 {sku}:库存 50 件"


agent = Agent(
    name="并行助手",
    instructions="遇到多个独立查询时,一次性发起所有工具调用,不要串行。",
    tools=[query_order, query_inventory],
    model="gpt-4o-mini",
)


async def main():
    start = time.time()
    result = await Runner.run(
        agent, "查订单 A1001 状态,再查商品 SKU001 库存"
    )
    elapsed = time.time() - start
    print("回复:", result.final_output)
    print(f"总耗时:{elapsed:.2f}s(并行应约 1s,串行约 2s)")


if __name__ == "__main__":
    asyncio.run(main())

3. 工具错误处理

"""
工具错误处理:捕获异常,转字符串回传,让模型自行决策
"""
import asyncio
from agents import Agent, Runner, function_tool


@function_tool
def risky_api(endpoint: str) -> str:
    """调用不稳定的 API。

    Args:
        endpoint: 接口路径
    """
    import random
    if random.random() < 0.6:
        raise RuntimeError(f"{endpoint} 调用超时")
    return f"{endpoint} 返回:OK"


# 包装工具,统一捕获异常
def safe_tool(fn):
    @function_tool(name=fn.__name__, description=fn.__doc__)
    def wrapper(*args, **kwargs):
        try:
            return fn(*args, **kwargs)
        except Exception as e:
            return f"[工具错误]{fn.__name__} 执行失败:{e},请重试或换方案"
    return wrapper


safe_risky = safe_tool(risky_api)

agent = Agent(
    name="容错助手",
    instructions=(
        "你调用工具时可能失败。若工具返回 [工具错误],"
        "分析原因后重试(最多 2 次),仍失败则向用户说明并给兜底方案。"
    ),
    tools=[safe_risky],
    model="gpt-4o-mini",
)


async def main():
    result = await Runner.run(agent, "请调用 risky_api 查询 /users 数据", max_turns=8)
    print("回复:", result.final_output)


if __name__ == "__main__":
    asyncio.run(main())

企业案例

案例 1:多工具电商客服 Agent

某电商客服 Agent 接入 12 个工具:订单查询、物流查询、库存查询、退款、换货、优惠券查询、发票开具、地址修改、投诉建单、FAQ 检索、人工转接、满意度回访。团队用 @function_tool 统一定义,靠 docstring 区分用途。关键经验:①工具命名规范(动词_名词),降低模型选错概率;②每个工具的 docstring 写明"何时该用"和"参数格式";③副作用工具(退款、建单)加二次确认;④并行调用让多查询场景响应从 6s 降到 1.5s。

案例 2:工具错误降级

某金融 Agent 调用行情接口偶发超时。团队设计三级降级:①首次失败指数退避重试(1s/2s);②二次失败切换备用数据源;③备用源也失败则返回缓存数据并标注"数据可能延迟"。错误信息结构化回传给模型,由模型决定是否告知用户。这套机制让工具层可用性从 95% 提升到 99.9%。

常见问题

Q1:工具函数返回复杂对象(dict/list)怎么办?

A:SDK 会自动 JSON 序列化。但建议返回精简字符串,避免长 JSON 撑爆上下文。复杂对象先做摘要再回传。

Q2:如何让模型更准确地选对工具?

A:①docstring 写清楚"何时用"而非只是"做什么";②工具命名语义化;③工具数量控制在 10 个以内,过多用分组或子 Agent 路由;④在 system prompt 中给出工具选择示例。

Q3:并行工具调用一定更快吗?

A:不一定。无依赖时并行更快;有依赖(后一个需要前一个的结果)必须串行。模型有时会误判依赖,可在 prompt 中明确"以下查询相互独立可并行"。

Q4:副作用工具(如发邮件、退款)如何安全调用?

A:①在工具内加幂等性校验(同一请求只执行一次);②高风险操作要求二次确认(模型先问用户"确认退款?"再调);③记录审计日志;④生产环境加白名单和限流。

Q5:工具 schema 自动生成有什么坑?

A:①无类型注解的参数会被忽略;②Optional 参数需显式默认值;③docstring 格式不规范会导致描述缺失;④复杂嵌套类型(如自定义类)不被支持,应拆为基本类型。

面试题

  1. 简答题:@function_tool 是如何从 Python 函数生成工具 schema 的?

参考要点:读取函数名、docstring、参数类型注解、默认值,自动生成 name/description/parameters。

  1. 设计题:一个 Agent 有 20 个工具,如何组织以降低模型选错率?

参考要点:按业务域分组、用子 Agent 路由、工具数量控制在单 Agent 10 个内、命名规范化。

  1. 实现题:如何让两个无依赖的工具并行执行?请写出关键代码。

参考要点:用 async 异步工具 + 模型并行 tool_call + asyncio.gather;或依赖 SDK 自动并发。

  1. 原理题:为什么工具异常不应直接抛出,而要转字符串回传?

参考要点:抛出会终止整个 Agent;回传让模型决策(重试/换方案/告知用户),保持循环连续性。

  1. 对比题:手写 JSON Schema 与自动 schema 生成各有什么优劣?

练习题

  1. 工具定义:为"日程管理 Agent"定义 3 个工具(创建日程、查询日程、删除日程),观察自动生成的 schema。

  2. 并行优化:写一个 Agent 同时查询 3 个城市的天气,验证并行调用是否将总耗时从 3s 降到约 1s。

  3. 错误降级:为示例 3 增加备用数据源,主工具失败时自动切换,统计主备成功率。

小结

工具是 Agent 干预真实世界的接口。本章核心:用 @function_tool 把 Python 函数声明为工具,schema 自动生成;异步工具避免阻塞,并行调用降低延迟;错误统一转字符串回传,让模型自主决策重试或降级;副作用工具需幂等与确认。掌握工具后,Agent 有了"手脚",下一章解决 Agent 的"记忆"——如何在多轮交互中保持状态。