OpenAI Agents SDK #22:同一个 Agent 跑三遍,然后选最好的那个

精读 `parallelization.py`(65行)的完整实现:`asyncio.gather × 3` 并发模型、`ItemHelpers.text_message_outputs()` 提取纯文本、`translation_picker` 择优(fan-in)、`trace` 上下文包裹。拆解 Parallelization 的两大使用场景(延迟优化 fan-out 分工 vs 同任务多跑 best-pick),对比 `asyncio.gather` 与 `Agent.as_tool() + parallel_tool_calls` 两条实现路径,并附 Parallelization / Deterministic / LLM-as-Judge 三模式横向对比矩阵。结尾三条实践建议覆盖路径选择、picker 结构化输出、温度设置对 best-pick 质量的影响。

Research Brief

翻译任务,你跑一遍,结果凑合但不够好。
跑两遍?第二遍结果未必比第一遍强。跑三遍然后让另一个 Agent 挑最好的那个——这是 parallelization.py 的核心设计。
整个文件 65 行,51 行有效代码1。读下来会发现,它展示的不只是「同时跑多个 Agent」,而是 Parallelization 模式下两种截然不同的使用思路。

一、两种并行,一个模式

Parallelization 在 SDK 里的官方定位,是「Orchestrating via code(代码编排)」的四种手段之一2
它有两个完全不同的使用场景3
"Running multiple agents in parallel is a common pattern. This can be useful for both latency (e.g. if you have multiple steps that don't depend on each other) and also for other reasons e.g. generating multiple responses and picking the best one."
翻译一下:
场景一:延迟优化(fan-out 分工)——多个互不依赖的子任务并发执行。原本串行要跑 T₁ + T₂ + T₃,并行后只需 max(T₁, T₂, T₃)。比如同时跑「摘要」「情感分析」「关键词提取」三个 Agent,结果彼此无依赖,并行后延迟大幅下降。
场景二:择优(best-pick)——同一个 Agent、同一段输入,利用 LLM 输出的随机性跑多次,再用另一个 Agent 从候选结果中挑最好的。parallelization.py 演示的就是这种用法。
两种场景的并发机制完全一样,但意图不同:前者追求速度,后者追求质量。

二、源码精读:65 行里的四个关键点

先看整体结构:
with trace("Parallel translation"):
    # 三路并发:同 Agent + 同输入 + asyncio.gather
    res_1, res_2, res_3 = await asyncio.gather(
        Runner.run(spanish_agent, msg),
        Runner.run(spanish_agent, msg),
        Runner.run(spanish_agent, msg),
    )

# 从每个 RunResult 提取纯文本
    translations = [
        ItemHelpers.text_message_outputs(res_1.new_items),
        ItemHelpers.text_message_outputs(res_2.new_items),
        ItemHelpers.text_message_outputs(res_3.new_items),
    ]

# 串行调用择优 Agent(fan-in 阶段)
    best = await Runner.run(
        translation_picker,
        f"Input: {msg}\n\nTranslations:\n{chr(10).join(translations)}",
    )
四个关键点逐一拆解。

asyncio.gather × 3:并发模型

asyncio.gather 是 Python 标准库的并发原语,不是 SDK 的特有 API。它把三个 Runner.run 协程提交给同一个事件循环并发调度——三路同时发起 LLM API 请求,等所有响应都回来后才继续1
三个调用参数完全一致:同一个 spanish_agent 实例,同一段 msg。这是刻意为之——利用 LLM 每次采样的随机性,三次运行会产生三个不同的翻译结果。
每个 Runner.run 都有独立的运行上下文,彼此不共享状态。这一点很重要,下文陷阱部分会再提到。

ItemHelpers.text_message_outputs():提取纯文本

Runner.run 返回的是 RunResult,里面的 new_itemsList[RunItem]——涵盖助手消息(MessageOutputItem)、推理内容(ReasoningItem)、工具调用记录等富元数据4
ItemHelpers.text_message_outputs(res.new_items) 做的事情很纯粹:从这个列表里只取文本消息,拼成一个字符串1
在 parallelization.py 里,这个函数把三个 RunResult 各自提取成一行翻译文本,拼成 translations 传给 translation_picker

translation_picker:fan-in 阶段

这是整个 best-pick 模式的关键——结果聚合是串行的,不是并行的。
translation_picker = Agent(
    name="translation_picker",
    instructions="You pick the best Spanish translation from the given options.",
)
它的职责就一件事:给定原文 + 三个候选翻译,选出最好的那个1
translation_pickerasyncio.gather 结束之后才被调用,是一次独立的串行 LLM 请求。整体时序是:fan-out(三路并发)→ fan-in(串行择优),总耗时约等于 max(T₁, T₂, T₃) + T_picker

trace("Parallel translation"):追踪包裹

with trace("Parallel translation"):
    # ... 整个工作流
trace 是 SDK 提供的上下文管理器,把包裹的代码块在 OpenAI 追踪系统中标记为单个工作流1。用 with trace(...) 一包,asyncio.gather 里的三路并发 + 最后的 translation_picker 就在追踪界面里是同一条 trace 记录。
有个注意事项留到陷阱部分说。

三、两种并行实现路径

SDK 和 OpenAI Cookbook 展示了两条实现并行化的路径5
维度asyncio.gather 路径Agent.as_tool() 路径
实现方式Python 原生异步并发,代码显式控制meta_agent = Agent(tools=[a.as_tool(),...], model_settings=ModelSettings(parallel_tool_calls=True))
延迟更低——无额外规划调用开销更高——Meta Agent 先做一次规划请求,决定调用哪些工具
执行确定性高——代码决定哪些 Agent 运行、几路低——LLM 动态决定调用哪些工具
自定义空间高——可自由加条件分支、错误处理、多层 fan-out低——依赖 LLM 规划能力
适用场景延迟敏感、执行路径固定更便利、希望 LLM 自主规划调用顺序
Cookbook 的总结是这样的5
"Ultimately, the approach you use will depend on the balance you want between: 1. Convenience vs. customization ... 2. Planning vs. determinism ... 3. Latency sensitivity"
选哪条路,取决于你更看重「便利性 vs 自定义」「LLM 规划 vs 确定性执行」「延迟敏感度」这三组权衡。
另一个相关控制维度:SDK v0.16.0(2026-05-07)新增了 ToolExecutionConfig(max_function_tool_concurrency=...) 6,用于控制 SDK 侧本地函数工具的最大并发执行数。这与 ModelSettings.parallel_tool_calls(控制模型是否允许在一次响应中并行发出多个工具调用)是两个完全独立的维度,不要混淆。

四、API 速查:Parallelization 涉及的完整调用栈

这个模式的 SDK 侧原语非常薄17
API层次作用
asyncio.gatherPython 标准库并发调度多个协程
Agent(name, instructions)SDK定义 Agent 角色
Runner.run(agent, input)SDK异步执行单个 Agent,返回 RunResult
RunResult.new_itemsSDK运行过程中产生的所有富元数据项
ItemHelpers.text_message_outputs(items)SDK从 RunItem 列表提取纯文本
trace("workflow_name")SDK将整段代码标记为单个追踪工作流
Agent.as_tool(...)SDK将 Agent 包装为工具(第二种并行路径使用)
官方 Results 文档里有一句话值得记住4
"Most applications only need a few result properties or helpers: final_output, to_input_list(), new_items, last_agent..."
parallelization.py 用的是 new_items + ItemHelpers.text_message_outputs(),没有用 final_output——因为 best-pick 场景下需要「所有文本」,而不是「最终输出」。这是一个容易犯错的细节。
关于默认模型:SDK v0.17.0(2026-05-08)将默认模型从 gpt-4.1 改为 gpt-5.4-mini6parallelization.py 里的 spanish_agenttranslation_picker 都没有显式设置 model,跑示例时用的是当前 SDK 的默认模型。三路并发意味着这个模型会被同时发起三个 API 请求,成本是单次的三倍。

五、三种模式的横向对比

agent_patterns/ 目录下,parallelization 和 deterministic、llm_as_a_judge 是并列的三种编排模式,覆盖了 Agent 编排的三个核心维度189
维度ParallelizationDeterministicLLM-as-Judge
执行模型asyncio.gather 并发,fan-out + fan-in串行链式 await,单次通过while True 循环迭代
总耗时max(T₁,T₂,T₃) + T_pickerT₁ + T₂ + T₃N × (T_gen + T_eval)
质量策略广度优先:同时跑多个,挑最好流水线:每步依赖前一步,含 gate 门控深度优先:同一结果反复打磨
不达标时三个候选里 picker 选最好的exit(0) 硬停止追加反馈,generator 重试
代码复杂度最低(51 行有效代码)中(含结构化输出、质量门控)最高(含状态管理、循环控制)
典型场景多路独立子任务 / 同任务多跑取优固定步骤流水线 / 资格校验需要迭代改进的内容生成
一个记法:任务步骤互相独立→ 并行;步骤有依赖→ 串行;结果需要反复打磨→ 循环。三种模式刚好对应三个象限,很少有模棱两可的情况。

六、边界情况:三路并行下的 trace 陷阱

GitHub Issue #481 记录了一个边界情况10:在 asyncio.gather 里并行运行多个 Runner.run 时,trace 系统会产生多个 trace,但只有一个被正确填充数据,其余是空的。该问题被标记为 bug/needs-more-info 并已关闭,目前没有找到官方修复记录。
这背后是一个 asyncio 的并发隔离性质——每个 Runner.run 运行在独立的上下文里,trace 上下文管理器在并发场景下的行为和串行场景有差异。
如果你的生产代码依赖 trace 做调试或监控,在 asyncio.gather 场景下需要额外留意追踪数据的完整性,不要把「trace 里没看到」等同于「没有执行」。

七、实践建议

① 选路径的关键问题:你的执行计划是固定的吗?
如果在写代码时就知道要并行哪些 Agent、跑几路,用 asyncio.gather——延迟最低,执行可预期,错误处理也由代码自己控制。
如果你希望 LLM 在运行时自主决定「要不要并行、并行哪些工具」,用 Agent.as_tool() + ModelSettings(parallel_tool_calls=True)——更灵活,代价是一次额外的规划 API 调用和更长的上下文。
两者不是优劣关系,是不同的控制权归属。
② best-pick 模式里,picker 输出加结构化约束
translation_picker 的默认 instructions 是纯文本选择,返回也是纯文本。在生产场景里,可以给 picker 加一个 output_type,比如 @dataclass class PickResult: chosen_index: int; reason: str。这样 picker 的输出是机器可读的,downstream 逻辑可以直接拿索引,不需要再解析一次纯文本。
③ 三路并发≠三倍成本预算必须放大
三路 asyncio.gather 确实是并发发出三个 API 请求,但如果用的是 gpt-5.4-mini 这样的轻量模型,单次翻译任务的成本本身就很低。关键问题不是「三倍贵」,而是「三倍返回的候选质量差异够不够大」——如果 temperature 很低导致三次结果几乎一样,picker 无从择优,这个模式就失去了意义。确保 spanish_agent 的采样温度足够高,才能让三路结果真正不同。

当前版本:本文基于 OpenAI Agents SDK v0.17.0(2026-05-08 发布)。
下期预告 #23agent_patterns/ 的下一个模式——Input Guardrails + Output Guardrails。 SDK 里的 guardrail 不只是一个过滤层,它本身也是并行运行的——asyncio.gather 在这里以另一种方式出现。

Add more perspectives or context around this content.

  • Sign in to comment.