Chapter 03

Tools 工具集成

函数工具是 Agent 连接世界的桥梁。掌握 @function_tool 装饰器、Pydantic 参数类型、错误处理,以及内置工具的使用。

函数工具:Python 函数即工具

Agents SDK 最优雅的设计之一:将普通 Python 函数转换为 Agent 可调用的工具,无需手写 JSON Schema。函数的文档字符串自动变成工具描述,类型注解自动生成参数 schema。

from agents import function_tool, Agent, Runner
from datetime import datetime
import asyncio

# ── 最简单的工具:同步函数 ─────────────────────────────────
@function_tool
def get_current_time(timezone: str = "Asia/Shanghai") -> str:
    """获取当前时间。

    Args:
        timezone: 时区名称,例如 'Asia/Shanghai'、'America/New_York'
    Returns:
        格式化的当前时间字符串
    """
    from zoneinfo import ZoneInfo
    tz = ZoneInfo(timezone)
    now = datetime.now(tz)
    return now.strftime("%Y-%m-%d %H:%M:%S %Z")

# ── 异步工具:调用外部 API ─────────────────────────────────
@function_tool
async def fetch_weather(city: str) -> str:
    """查询城市当前天气。

    Args:
        city: 城市名称(中文或英文均可)
    Returns:
        天气信息字符串,包含温度、湿度、天气状况
    """
    import httpx
    async with httpx.AsyncClient() as client:
        # 使用免费天气 API(替换为实际 API)
        resp = await client.get(
            f"https://wttr.in/{city}?format=3",
            timeout=10
        )
        return resp.text.strip()

# ── 挂载到 Agent ───────────────────────────────────────────
time_weather_agent = Agent(
    name="时间天气助手",
    instructions="你可以查询时间和天气,准确回答用户问题。",
    model="gpt-4o-mini",
    tools=[get_current_time, fetch_weather]
)

result = Runner.run_sync(time_weather_agent, "北京现在几点,天气怎么样?")
print(result.final_output)

@function_tool 的工作原理

装饰器在底层做了三件事,值得深入理解:

Python 函数(带类型注解和文档字符串) │ ▼ @function_tool 装饰 ┌─────────────────────────────────────────┐ │ 1. 解析类型注解 → 生成 JSON Schema │ │ str → {"type": "string"} │ │ int → {"type": "integer"} │ │ Optional[str] → 非必填参数 │ │ │ │ 2. 解析文档字符串 → 工具描述 │ │ 函数级 docstring → description │ │ Args 节 → 参数描述 │ │ │ │ 3. 包装执行器 → 捕获异常,格式化结果 │ └─────────────────────────────────────────┘ │ ▼ FunctionTool 对象(Agent 可直接使用)

Pydantic 模型作为工具参数

对于复杂的结构化输入,使用 Pydantic 模型作为参数类型,自动生成嵌套的 JSON Schema:

from pydantic import BaseModel, Field
from agents import function_tool
from typing import Literal

# ── 定义参数模型 ───────────────────────────────────────────
class EmailParams(BaseModel):
    to: list[str] = Field(description="收件人邮箱列表")
    subject: str = Field(description="邮件主题")
    body: str = Field(description="邮件正文(支持 Markdown)")
    priority: Literal["high", "normal", "low"] = "normal"
    cc: list[str] = Field(default=[], description="抄送列表(可选)")

class DatabaseQuery(BaseModel):
    table: str = Field(description="查询的数据库表名")
    filters: dict[str, str] = Field(default={}, description="过滤条件 key-value 对")
    limit: int = Field(default=10, ge=1, le=100, description="返回记录数上限")
    order_by: str = Field(default="created_at", description="排序字段")

# ── 接受 Pydantic 模型的工具 ───────────────────────────────
@function_tool
async def send_email(params: EmailParams) -> str:
    """发送邮件。支持多收件人、优先级设置和抄送功能。"""
    # SDK 自动将 LLM 的 JSON 反序列化为 EmailParams 对象
    print(f"发送邮件到 {params.to},主题:{params.subject}")
    # 实际调用 SMTP/SendGrid 等邮件服务...
    return f"邮件已发送给 {', '.join(params.to)}"

@function_tool
async def query_database(query: DatabaseQuery) -> str:
    """查询数据库。返回符合条件的记录。"""
    # 参数已经是类型安全的 Pydantic 对象
    sql = f"SELECT * FROM {query.table}"
    if query.filters:
        conditions = " AND ".join([f"{k}='{v}'" for k, v in query.filters.items()])
        sql += f" WHERE {conditions}"
    sql += f" ORDER BY {query.order_by} LIMIT {query.limit}"
    # 执行实际查询...
    return f"执行查询:{sql},返回 {query.limit} 条结果"

工具错误处理

工具执行失败时,默认行为是将异常信息传递给 LLM,LLM 可能陷入"无限重试"死循环。正确的错误处理策略是返回结构化错误信息:

from agents import function_tool
from agents.exceptions import ToolCallError

# ── 方式 1:返回错误字符串(LLM 会看到并据此决策)───────────
@function_tool
async def search_database(query: str, table: str) -> str:
    """在数据库中搜索记录。"""
    try:
        # 白名单校验:防止 SQL 注入和误操作
        allowed_tables = {"users", "products", "orders"}
        if table not in allowed_tables:
            return f"错误:表 '{table}' 不在允许列表中。可用表:{allowed_tables}"

        results = await db_search(table, query)  # 实际数据库查询

        if not results:
            return "查询成功,但没有找到匹配记录。请尝试更宽泛的搜索词。"

        return f"找到 {len(results)} 条记录:\n" + "\n".join(str(r) for r in results[:5])

    except ConnectionError:
        return "数据库连接失败。请稍后重试,或告知用户系统暂时不可用。"
    except Exception as e:
        return f"查询失败:{type(e).__name__}: {str(e)[:200]}"

# ── 方式 2:failure_error_function(工具级错误处理器)───────
def handle_tool_error(error: Exception, tool_name: str) -> str:
    """统一的工具错误处理函数,返回 LLM 友好的错误描述"""
    error_map = {
        ConnectionError: "服务暂时不可用,请稍后重试",
        TimeoutError: "请求超时,可能网络较慢",
        PermissionError: "权限不足,无法执行此操作",
        ValueError: f"参数错误:{str(error)}",
    }
    for exc_type, msg in error_map.items():
        if isinstance(error, exc_type):
            return f"工具 {tool_name} 执行失败:{msg}"
    return f"工具 {tool_name} 执行失败(未知错误)"

# 在 Agent 级别配置全局错误处理
resilient_agent = Agent(
    name="健壮助手",
    instructions="工具失败时根据错误信息决定是重试还是告知用户。",
    model="gpt-4o-mini",
    tools=[search_database],
)

内置工具:WebSearch

SDK 内置了 WebSearch 工具,无需任何额外配置(需要 OpenAI 平台支持):

from agents import Agent, Runner
from agents.tools import WebSearchTool

# WebSearchTool 使用 OpenAI 平台的搜索能力
# 无需 API Key,计费在 OpenAI 账单中
search_agent = Agent(
    name="搜索助手",
    instructions="""你是网络搜索助手。用户提问时,先搜索最新信息,
    再综合搜索结果给出准确回答。注明信息来源。""",
    model="gpt-4o",
    tools=[WebSearchTool()]
)

import asyncio
result = asyncio.run(
    Runner.run(search_agent, "OpenAI Agents SDK 最新版本是什么?有哪些新功能?")
)
print(result.final_output)

内置工具:FileSearch(向量存储检索)

FileSearch 工具允许 Agent 搜索你上传到 OpenAI 平台的文件,实现 RAG 检索功能:

from agents import Agent
from agents.tools import FileSearchTool
from openai import AsyncOpenAI

async def setup_rag_agent():
    client = AsyncOpenAI()

    # 步骤 1:创建 Vector Store
    vector_store = await client.beta.vector_stores.create(
        name="产品文档库"
    )

    # 步骤 2:上传文档
    with open("product_docs.pdf", "rb") as f:
        await client.beta.vector_stores.files.upload_and_poll(
            vector_store_id=vector_store.id,
            file=f
        )

    # 步骤 3:创建带 FileSearch 的 Agent
    rag_agent = Agent(
        name="产品知识助手",
        instructions="""你是产品知识助手,回答问题时先搜索产品文档库。
        如果文档中没有相关信息,明确说明"文档中未找到该信息"。""",
        model="gpt-4o",
        tools=[
            FileSearchTool(
                vector_store_ids=[vector_store.id],
                max_num_results=5   # 最多检索 5 个片段
            )
        ]
    )
    return rag_agent

工具链设计原则

好的工具设计

  • 单一职责:每个工具只做一件事
  • 返回值语义清晰:成功/失败/空结果有区别
  • 错误信息对 LLM 友好:告诉 LLM 该怎么办
  • 参数验证在工具内完成,不依赖 LLM 判断
  • 限制返回长度,避免占满 Context
  • 文档字符串精确描述使用时机

常见错误

  • 工具功能太多,LLM 不知该如何使用
  • 工具名称含糊(如 "process"、"handle")
  • 未处理异常,导致 Agent 因异常卡死
  • 返回原始 JSON/HTML,LLM 解析困难
  • 返回超长内容,耗尽 Context 窗口
  • 工具描述没有"何时不应使用"的说明
安全提示 永远不要让 Agent 直接执行用户提供的代码(eval/exec),不要让工具暴露数据库的完整访问权限。遵循最小权限原则:工具只能访问它需要的资源,不能访问其他资源。使用白名单而非黑名单来限制工具的操作范围。