第 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 怎么知道这个工具是干嘛的?靠三样东西:
- 函数名
get_weather-- Agent 知道这和天气有关 - docstring
获取指定城市的天气信息-- Agent 知道具体用途 - 参数签名
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 会:
- 调用
translate_to_spanish工具(其实是运行spanish_agent) - 调用
translate_to_french工具(其实是运行french_agent) - 把两个结果整合后回复用户
每个翻译 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 去干活。
小结
这一章你学到了:
- 为什么需要工具:LLM 的知识是静态的,工具让它能获取实时数据、执行操作
- @function_tool:一行装饰器把 Python 函数变成 Agent 的工具
- 参数描述的两种方式:
Annotated[str, "描述"]或 docstring 的 Args 段落 - 返回 Pydantic 模型:工具返回值不一定是字符串,BaseModel 更结构化、更安全
- WebSearchTool:SDK 内置的网页搜索工具,开箱即用
- agent.as_tool():把一个 Agent 当工具用,"老板-下属"模式,主 Agent 保持控制权
下一步预告
现在 Agent 能聊天、能用工具干活了。但一个 Agent 终究精力有限,遇到复杂任务需要多个 Agent 分工协作——比如一个负责分诊,另一个负责具体处理。
下一章,我们来学习多 Agent 协作与转接(Handoff)。