OpenAI Agents SDK #26:Research Bot 拆解——一个真正会上网查资料的 Agent 是怎么造的
本期深拆 OpenAI Agents SDK 官方示例 Research Bot 的完整架构。三角色分工(PlannerAgent 规划 5-20 个搜索词、SearchAgent × WebSearchTool 并发上网搜索、WriterAgent 流式合成报告),通过 ResearchManager 集中编排而非 handoff。文章逐一解析每个 Agent 的代码、WebSearchTool 四个完整参数、asyncio.as_completed 并发调度机制,并横向对比 WebSearch / FileSearch / HostedMCP 三类 HostedTool 的选型场景,最后给出 3 条生产级建议。中文社区对 Research Bot / WebSearchTool 三个主题均处于真空状态,本期为中文首发深度解读。
Research Brief
大部分 Agent 示例的「工具调用」,其实只是在本地查个数据库、调个函数。
但 Research Bot 不一样:用户给一个问题,它自己拆解成十几个搜索词,并行上网搜,最后写出一份有结构的调研报告。这才是「自主搜索」该有的样子。
今天把它从里到外拆一遍。
架构总览:三个 Agent,一个管家,没有 handoff
Research Bot 是 OpenAI Agents SDK 的官方示例,位于
examples/research_bot/(顶层 examples 目录,不在 agent_patterns/ 子目录里)1,由 7 个文件组成。整体架构是三角色分工:
- PlannerAgent:拿到用户问题,输出一份搜索计划(5-20 个搜索词+理由)
- SearchAgent:拿到单个搜索词,上网搜,写 2-3 段摘要
- WriterAgent:拿到全部摘要,写成 5-10 页的 Markdown 报告
协调方式不是 handoff,而是通过
ResearchManager 类直接调用 Runner.run() / Runner.run_streamed(),三个 Agent 各司其职,中间不传控制权 2。用户问题
↓
PlannerAgent(一次)
↓ 输出 WebSearchPlan(N 个搜索项)
SearchAgent × N(asyncio 并发)
↓ 每个输出 300 词摘要
WriterAgent(一次,流式)
↓
结构化报告(short_summary + markdown + follow_up_questions)这个架构背后有一个有趣的设计选择:没有用 #24 讲的 Routing 模式,也没有用 handoff 把控制权丢来丢去,而是
ResearchManager 这个「外部编排者」全程掌控流程。它知道什么时候该调谁、以什么顺序调、失败了怎么处理。PlannerAgent:不搜索的「规划师」
先看任务最重的第一棒——PlannerAgent 3。
from openai.types.shared.reasoning import Reasoning
from pydantic import BaseModel
from agents import Agent, ModelSettings
class WebSearchItem(BaseModel):
reason: str # 为什么要搜这个词
query: str # 搜索词本身
class WebSearchPlan(BaseModel):
searches: list[WebSearchItem]
planner_agent = Agent(
name="PlannerAgent",
instructions="You are a helpful research assistant. Given a query, "
"come up with a set of web searches to perform to best "
"answer the query. Output between 5 and 20 terms to query for.",
model="gpt-5.5",
model_settings=ModelSettings(reasoning=Reasoning(effort="medium")),
output_type=WebSearchPlan,
)几个设计点值得注意:
没有任何 tools。PlannerAgent 不上网,不查数据库,纯靠模型推理把问题拆成搜索计划。这是刻意的——「规划」和「执行」拆开,各自用最合适的模型。
结构化输出。
output_type=WebSearchPlan 强制输出 Pydantic 模型,每个搜索项包含 reason(为什么搜这个)和 query(搜什么)两个字段。这个 reason 字段很有意思——不是给人看的,是传给 SearchAgent 的输入。SearchAgent 拿到「搜索词 + 原因」,上下文更足,摘要质量会更好。开了 medium 推理强度。
Reasoning(effort="medium") 让模型在输出前多想一步。对于「把复杂问题拆成好搜索词」这类任务,这个开销是值得的。调用方式很干净:
result = await Runner.run(planner_agent, f"Query: {query}")
plan = result.final_output_as(WebSearchPlan)SearchAgent + WebSearchTool:真正上网的那一步
这里是整个架构的核心 4:
from agents import Agent, WebSearchTool
search_agent = Agent(
name="Search agent",
model="gpt-5.5",
instructions=(
"You are a research assistant. Given a search term, you search the web "
"for that term and produce a concise summary of the results. The summary "
"must be 2-3 paragraphs and less than 300 words. Capture the main points. "
"Write succinctly, no need to have complete sentences or good grammar. "
"This will be consumed by someone synthesizing a report, so its vital "
"you capture the essence and ignore any fluff."
),
tools=[WebSearchTool()],
)WebSearchTool() 就这么一行,不需要 API Key,不需要配置第三方服务。它是 SDK 内置的 HostedTool,直接挂在 OpenAI Responses API 上 5。WebSearchTool 的全部参数
WebSearchTool 是 dataclass,目前支持 4 个参数:| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
user_location | UserLocation | None | None | 地理位置偏好,影响搜索结果的地域相关性 |
filters | Filters | None | None | 基于属性过滤搜索结果 |
search_context_size | "low" | "medium" | "high" | "medium" | 搜索上下文量,影响结果的丰富程度和 Token 消耗 |
external_web_access | bool | None | None | 是否获取实时互联网内容;设为 False 可只用缓存/索引内容 |
Research Bot 示例里用的是全默认——
WebSearchTool() 什么都不传。实际生产中,search_context_size 是最常需要调的参数:"high" 结果更丰富但 Token 翻倍,"low" 省钱但可能漏掉关键内容。asyncio 并发:联动 #22 Parallelization
PlannerAgent 可能一次输出 5-20 个搜索项,如果串行执行,等待时间线性增长。
ResearchManager._perform_searches() 用的是 asyncio.create_task + asyncio.as_completed 并发跑所有 SearchAgent 实例 2:tasks = [asyncio.create_task(self._search(item)) for item in plan.searches]
results = []
for task in asyncio.as_completed(tasks):
result = await task
if result is not None:
results.append(result)这和 #22 讲的 Parallelization 模式一脉相承——用
asyncio.as_completed 而不是 asyncio.gather,好处是哪个搜索先完成就先处理,不用等最慢的那个。失败了也不阻塞:except Exception: return None,失败的搜索直接丢弃,继续跑剩余的。WriterAgent:把碎片摘要变成完整报告
前面 N 个 SearchAgent 各自产出 300 词摘要,WriterAgent 拿到全部内容,一次性合成 6:
from pydantic import BaseModel
from agents import Agent, ModelSettings
class ReportData(BaseModel):
short_summary: str # 2-3 句话的执行摘要
markdown_report: str # 5-10 页完整 Markdown 报告
follow_up_questions: list[str] # 后续研究方向
writer_agent = Agent(
name="WriterAgent",
model="gpt-5-mini",
model_settings=ModelSettings(reasoning=Reasoning(effort="medium")),
output_type=ReportData,
instructions="You are a senior researcher. You will be provided with the "
"original query and some initial research. You should first come up "
"with an outline for the report that describes the structure and "
"flow of the report. Then, generate the report and return that as "
"your final output.",
)模型用的是
gpt-5-mini 而不是 gpt-5.5——报告合成是「按已有材料整理」,对推理深度要求低于规划,省成本合理。执行方式用的是流式:
result = Runner.run_streamed(writer_agent, input=f"Original query: {query}\nSummarized search results: {search_results}")
async for event in result.stream_events():
# 每 5 秒更新一次 UI 状态消息
...Runner.run_streamed() + stream_events() 让用户能看到报告在实时生成,而不是盯着空白等 30 秒。报告长、等待时间长的场景下这个体验差异很大。输出的
ReportData 分三段:short_summary(给快速阅读者)、markdown_report(完整内容)、follow_up_questions(给下一步研究用)。最后一个字段实际上是在为下一轮 Research Bot 调用做准备——如果你想把它做成对话式深度研究工具,这里就是入口。HostedTool 三兄弟:Web / File / MCP 怎么选
| WebSearchTool | FileSearchTool | HostedMCPTool | |
|---|---|---|---|
| 数据来源 | 实时互联网 | OpenAI Vector Store(你的私有文档) | 远程 MCP 服务器 |
| 必填参数 | 无(全默认可用) | vector_store_ids(至少一个) | tool_config(含服务器 URL) |
| 适合场景 | 需要最新信息、公开内容 | 内部知识库、产品文档检索 | 需要调用外部结构化 API、数据库 |
| 平台约束 | 需要 OpenAIResponsesModel | 需要 OpenAIResponsesModel | 需要 OpenAIResponsesModel |
三者共同的约束:都是 OpenAI Responses API 托管工具,只能搭配
gpt-5.5 / gpt-5-mini 等支持 Responses API 的模型,不兼容 Chat Completions 后端。换模型时这一点要留意。FileSearchTool 的参数比 WebSearchTool 更精细,支持 ranking_options 排序配置和 filters 文件属性过滤,适合需要精准召回的内部知识库场景。Research Bot 官方 README 里也提到,可以给 WriterAgent 加上 FileSearchTool,让它在搜完互联网后还能查一遍内部文档 8。3 条生产级建议
1. 控制搜索规模,
search_context_size 要提前测PlannerAgent 可能给出 20 个搜索词,每个 SearchAgent 都用
search_context_size="high",一次运行的 Token 消耗会很可观。建议先用 "low" 跑几轮测报告质量,再决定是否升 "medium" 或 "high"。同时可以在 PlannerAgent 的 instructions 里把最大搜索数从 20 缩到 10,根据实际报告质量再往上调。2. 补上失败日志和最低成功数阈值
示例里的
except Exception: return None 是正确的「不阻塞」设计,但生产环境里静默失败很危险。建议加两件事:① 失败时记录 item.query 到日志;② 在 _perform_searches() 里检查 len(results) >= MIN_REQUIRED_SEARCHES,如果成功数不够就提前退出或重试,而不是把 3 条摘要的结果当 15 条用,再让 WriterAgent 生成一份「看起来完整但其实缺大半材料」的报告。3. FileSearchTool 补私有知识,HostedMCPTool 接实时数据
Research Bot 是很好的扩展基础。加
FileSearchTool 给 WriterAgent,让它在合成报告时能搜公司内部文档;给 SearchAgent 加 HostedMCPTool 接入实时数据库(价格、库存、API 数据),就能超出「公开互联网」的信息范围。这三个 HostedTool 的组合,基本能覆盖「互联网 + 私有文档 + 实时数据」三层信息源。下期预告:
#27 继续深入 examples/ 目录——看看 Customer Service Bot 怎么把 handoff、guardrails、human-in-the-loop 三件套组合成一个可落地的客服 Agent 系统。
Add more perspectives or context around this content.