第 2 章:给 Agent 装上工具

系列教程:OpenAI Agents SDK 从入门到实战

本章目标:让 Agent 从"只会说"变成"能动手干活"。


为什么需要工具?

上一章的 Agent 能聊天了,但它本质上还是个"嘴炮选手"——你问它北京天气怎么样,它只能根据训练数据编一个答案。

问题在于:LLM 的知识是静态的,它不知道"今天"北京多少度,也没法帮你查数据库、调接口。

工具就是解决这个问题的。你给 Agent 挂上一个 get_weather 函数,Agent 发现用户在问天气,就自己决定去调这个函数,拿到真实数据,再用自然语言组织回复。

整个流程:

用户提问 --> Agent 思考 --> "我需要查天气" --> 调用 get_weather("北京") --> 拿到数据 --> 组织语言回复

关键点:Agent 自己决定要不要调工具、调哪个工具、传什么参数。你不需要写 if-else。


@function_tool:最简单的工具

Agents SDK 提供了 @function_tool 装饰器,一行代码就能把普通 Python 函数变成 Agent 可以调用的工具。

from agents import function_tool

@function_tool
def get_weather(city: str) -> str:
    """获取指定城市的天气信息。"""
    # 实际项目里你会调真实 API,这里用模拟数据
    return f"{city}:晴天,15-22°C"

就这样。然后把它塞到 Agent 的 tools 列表里:

agent = Agent(
    name="天气助手",
    instructions="你是一个天气查询助手。",
    tools=[get_weather],
)

Agent 就会在需要的时候自动调用它。

先来一个完整可运行的例子,把上面的片段串起来感受一下效果:

"""最简单的工具示例:天气查询"""
import asyncio
from openai import AsyncOpenAI
from agents import Agent, OpenAIChatCompletionsModel, Runner, function_tool, set_tracing_disabled

# ---- 基础配置(和第1章一样)----
set_tracing_disabled(True)
client = AsyncOpenAI(
    base_url="http://localhost:8317/v1",  # 改成你的 API 地址
    api_key="sk-12345678",                      # 改成你的 API Key
)
model = OpenAIChatCompletionsModel(model="gpt-5.2", openai_client=client)

# ---- 定义工具 ----
@function_tool
def get_weather(city: str) -> str:
    """获取指定城市的天气信息。"""
    return f"{city}:晴天,15-22°C"

# ---- 创建 Agent,挂上工具 ----
agent = Agent(
    name="天气助手",
    instructions="你是一个天气查询助手。用户问天气时,使用 get_weather 工具查询。",
    model=model,
    tools=[get_weather],
)

# ---- 运行 ----
async def main():
    result = await Runner.run(agent, input="北京天气怎么样?")
    print(result.final_output)

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

运行一下:

python 02_simple_tool.py

你会看到 Agent 自动调了 get_weather("北京"),拿到 "北京:晴天,15-22°C" 后,用自然语言组织了回复,比如:"北京今天天气晴朗,气温在 15 到 22 度之间。"

核心就这么几步:定义函数 → 加 @function_tool → 塞进 tools 列表。接下来我们看看背后的细节。

那 Agent 怎么知道这个工具是干嘛的?靠三样东西:

  1. 函数名 get_weather -- Agent 知道这和天气有关
  2. docstring 获取指定城市的天气信息 -- Agent 知道具体用途
  3. 参数签名 city: str -- Agent 知道需要传什么

所以写工具时,命名和文档要走心。函数名叫 foo、docstring 为空,Agent 就懵了。


工具参数的讲究

用 Annotated 给参数加说明

city: str 这个类型注解告诉 Agent "这是个字符串",但没说是什么字符串。你可以用 Annotated 加上更精确的描述:

from typing import Annotated

@function_tool
def get_weather(city: Annotated[str, "要查询天气的城市名,比如'北京'、'上海'"]) -> str:
    """获取指定城市的天气信息。"""
    return f"{city}:晴天,15-22°C"

Annotated[str, "描述文字"] 会把描述信息写进 JSON Schema,Agent 能看到更详细的参数说明,调用时更准确。

对于多个参数的场景,这种写法特别有价值:

@function_tool
def search_flights(
    departure: Annotated[str, "出发城市"],
    destination: Annotated[str, "目的城市"],
    date: Annotated[str, "出发日期,格式 YYYY-MM-DD"],
) -> str:
    """搜索航班信息。"""
    return f"{date} {departure} -> {destination}:找到 3 个航班"

docstring 也能描述参数

除了 Annotated,SDK 也支持从 docstring 中提取参数说明(Google 风格、NumPy 风格都行):

@function_tool
def get_weather(city: str) -> str:
    """获取指定城市的天气信息。

    Args:
        city: 要查询天气的城市名称
    """
    return f"{city}:晴天,15-22°C"

两种方式选一种就行,效果一样。Annotated 更紧凑,docstring 更适合参数多的场景。

返回 Pydantic 模型

工具不一定只返回字符串。你可以返回一个 Pydantic BaseModel,SDK 会自动把它序列化成 JSON 传给 Agent:

from pydantic import BaseModel, Field

class Weather(BaseModel):
    city: str = Field(description="城市名")
    temperature_range: str = Field(description="温度范围")
    conditions: str = Field(description="天气状况")

@function_tool
def get_weather(city: Annotated[str, "要查询天气的城市"]) -> Weather:
    """获取指定城市的天气信息。"""
    return Weather(
        city=city,
        temperature_range="14-20°C",
        conditions="晴,有风",
    )

用 Pydantic 模型的好处: - 结构清晰:字段和描述一目了然 - 类型安全:有验证,传错了会直接报错 - 可复用:同一个模型可以在多个工具之间共享


内置工具:WebSearchTool

自定义工具之外,SDK 还提供了一些开箱即用的内置工具。WebSearchTool 让 Agent 能直接搜索互联网:

from agents import Agent, WebSearchTool

agent = Agent(
    name="搜索助手",
    instructions="你是一个搜索助手,帮用户搜索最新的互联网信息。",
    tools=[WebSearchTool()],
)

WebSearchTool 是 OpenAI Responses API 提供的托管工具,搜索操作在服务端完成,你不需要自己对接搜索引擎。

它支持一些可选配置:

from agents import WebSearchTool

# 可以指定搜索上下文量和用户位置
web_search = WebSearchTool(
    search_context_size="high",  # "low" / "medium" / "high"
    user_location={              # 让搜索结果更贴合地理位置
        "type": "approximate",
        "city": "Beijing",
        "country": "CN",
    },
)

注意WebSearchTool 目前只支持 OpenAI 的 Responses API,如果你用的是第三方兼容接口(比如 Ollama),这个工具可能不可用。自定义的 @function_tool 不受限制。


Agent 作为工具(as_tool)

这是一个很强大的模式:把一个 Agent 当作另一个 Agent 的工具来用

先搞清楚它和 Handoff(后面章节会讲)的区别:

特性 as_tool(工具模式) Handoff(交接模式)
对话历史 子 Agent 不继承对话历史 子 Agent 继承完整对话历史
控制权 主 Agent 保持控制,子 Agent 只是"干活的下属" 控制权完全移交给子 Agent
返回方式 子 Agent 的输出作为工具返回值,主 Agent 继续处理 子 Agent 直接面对用户

简单说:as_tool 是"老板派下属干活",Handoff 是"老板把活交给别人负责"。

看个翻译的例子:

from agents import Agent

# 定义专门的翻译 Agent
spanish_agent = Agent(
    name="spanish_agent",
    instructions="你负责把用户的消息翻译成西班牙语。只输出翻译结果,不要加解释。",
)

french_agent = Agent(
    name="french_agent",
    instructions="你负责把用户的消息翻译成法语。只输出翻译结果,不要加解释。",
)

# 编排 Agent:把翻译 Agent 作为工具挂载
orchestrator = Agent(
    name="翻译调度员",
    instructions=(
        "你是一个翻译调度员。根据用户的需求,使用对应的翻译工具。"
        "如果用户要翻译成多种语言,依次调用对应的工具。"
        "你自己不要翻译,一定要用工具。"
    ),
    tools=[
        spanish_agent.as_tool(
            tool_name="translate_to_spanish",
            tool_description="把文本翻译成西班牙语",
        ),
        french_agent.as_tool(
            tool_name="translate_to_french",
            tool_description="把文本翻译成法语",
        ),
    ],
)

当用户说"把 Hello World 翻译成西班牙语和法语",orchestrator 会:

  1. 调用 translate_to_spanish 工具(其实是运行 spanish_agent
  2. 调用 translate_to_french 工具(其实是运行 french_agent
  3. 把两个结果整合后回复用户

每个翻译 Agent 独立运行,不共享对话历史,执行完就把结果返回给主 Agent。


完整可运行代码

把前面讲的知识点串起来,写一个完整的例子:

"""
第2章完整示例:给 Agent 装上工具
包含:@function_tool、Annotated 参数、Pydantic 返回值、Agent 作为工具
"""

import asyncio
from typing import Annotated

from pydantic import BaseModel, Field
from openai import AsyncOpenAI
from agents import (
    Agent,
    OpenAIChatCompletionsModel,
    Runner,
    function_tool,
    set_tracing_disabled,
)

# ---- 基础配置 ----

# 关闭追踪(本地开发用,避免上报失败)
set_tracing_disabled(True)

# 配置兼容 OpenAI 接口的服务(以 Ollama 为例)
client = AsyncOpenAI(
    base_url="http://localhost:8317/v1",  # 改成你的 API 地址
    api_key="sk-12345678",                      # 改成你的 API Key
)
model = OpenAIChatCompletionsModel(
    model="gpt-5.2",  # 改成你的模型名称
    openai_client=client,
)


# ---- Pydantic 模型:结构化的天气数据 ----

class WeatherInfo(BaseModel):
    """天气信息"""
    city: str = Field(description="城市名")
    temperature_range: str = Field(description="温度范围(摄氏度)")
    conditions: str = Field(description="天气状况描述")


# ---- 工具定义 ----

@function_tool
def get_weather(city: Annotated[str, "要查询天气的城市名"]) -> WeatherInfo:
    """获取指定城市的当前天气信息。"""
    # 模拟数据,实际项目中换成真实天气 API
    weather_db = {
        "北京": WeatherInfo(city="北京", temperature_range="14-20°C", conditions="晴,西北风3级"),
        "东京": WeatherInfo(city="东京", temperature_range="16-22°C", conditions="多云转晴"),
        "纽约": WeatherInfo(city="纽约", temperature_range="8-15°C", conditions="阴,有小雨"),
    }
    if city in weather_db:
        return weather_db[city]
    # 找不到就返回默认值
    return WeatherInfo(city=city, temperature_range="未知", conditions="暂无数据")


@function_tool
def calculate(expression: Annotated[str, "要计算的数学表达式,如 '2 + 3 * 4'"]) -> str:
    """计算一个数学表达式并返回结果。"""
    try:
        # 注意:eval 仅供演示,生产环境请用 asteval 等安全方案
        result = eval(expression)
        return f"{expression} = {result}"
    except Exception as e:
        return f"计算出错:{e}"


# ---- Agent 作为工具 ----

# 定义一个专门做英译中的 Agent
translator_agent = Agent(
    name="translator",
    instructions="你是一个英译中翻译专家。把用户给你的英文内容翻译成地道的中文。只输出翻译结果。",
    model=model,
)

# ---- 主 Agent:挂载所有工具 ----

agent = Agent(
    name="多功能助手",
    instructions=(
        "你是一个多功能助手,可以查天气、做计算、翻译英文。\n"
        "根据用户的需求选择合适的工具。\n"
        "如果用户的问题不需要工具,直接回答即可。"
    ),
    model=model,
    tools=[
        get_weather,
        calculate,
        # 把翻译 Agent 当工具用
        translator_agent.as_tool(
            tool_name="translate_english_to_chinese",
            tool_description="将英文文本翻译成中文",
        ),
    ],
)


# ---- 运行示例 ----

async def main():
    # 场景1:查天气(Pydantic 返回值)
    print("=== 查天气 ===")
    result = await Runner.run(agent, input="东京天气怎么样?")
    print(result.final_output)
    print()

    # 场景2:做计算
    print("=== 做计算 ===")
    result = await Runner.run(agent, input="帮我算一下 (100 + 200) * 3")
    print(result.final_output)
    print()

    # 场景3:翻译(Agent 作为工具)
    print("=== 翻译 ===")
    result = await Runner.run(
        agent,
        input="帮我翻译这句话:The quick brown fox jumps over the lazy dog",
    )
    print(result.final_output)
    print()

    # 场景4:不需要工具的普通聊天
    print("=== 闲聊 ===")
    result = await Runner.run(agent, input="你好,你能做什么?")
    print(result.final_output)


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

运行:

python 02_agent_with_tools.py

你会看到 Agent 根据不同的输入自动选择不同的工具。查天气时返回结构化数据,做计算时调用 calculate,翻译时派 translator_agent 去干活。


小结

这一章你学到了:


下一步预告

现在 Agent 能聊天、能用工具干活了。但一个 Agent 终究精力有限,遇到复杂任务需要多个 Agent 分工协作——比如一个负责分诊,另一个负责具体处理。

下一章,我们来学习多 Agent 协作与转接(Handoff)

← 第1章 第一个Agent 第3章 多Agent协作 →