OpenAI Agents SDK #21:让 LLM 当裁判,但别让它审无限轮

精读 `llm_as_a_judge.py` 89 行源码,拆解双 Agent 循环架构(`story_outline_generator` + `evaluator`)、`EvaluationFeedback` dataclass 三值评分设计、`to_input_list()` 全量上下文链 + `auto_mode` 双层安全阀,并与上期 Deterministic Flows 对比(迭代改进 vs 门控硬停),最后给出三条生产级落地建议:结构化输出是循环终止前提、while 必须有代码出口 + 语义出口双保险、Judge Prompt 校准流程。

研究速览

你有没有遇到过这种情况:写完一段功能,对着生成结果看了半天,不知道它算好还是算差。
人工校验太慢,跑不上规模。写规则校验,覆盖不了语义质量。算法打分,又不理解业务意图。
LLM-as-a-Judge 解决的就是这个问题——用一个 LLM 去评估另一个 LLM 的输出质量
OpenAI Agents SDK 官方在 examples/agent_patterns/ 里给了 89 行的完整实现。读完之后你会发现,真正的设计精髓不是「用 LLM 当裁判」这个 idea 本身,而是它背后的循环控制策略

一、核心思想:评价比创作更容易

先说一个反直觉的事实。
官方 agent_patterns/README.md 这样描述 LLM-as-a-Judge 的价值1
"LLMs can often improve the quality of their output if given feedback. A common pattern is to generate a response using a model, and then use a second model to provide feedback. You can even use a small model for the initial generation and a larger model for the feedback, to optimize cost."
小模型生成,大模型反馈。生成阶段跑 mini 级模型降成本,评估阶段用旗舰模型保质量——这是一个有意思的不对称设计。
为什么可行?因为「评价」这件事比「创作」更容易。知乎作者「龟壳」在其 LLM-as-a-Judge 指南里点出了这一点2:「评价比创作简单。即使是大模型自己生成的内容,它在特定的『裁判提示词』引导下,往往能识别出生成时的错误。」
模型自我校验的逻辑成立的前提,就是这个不对称。
官方文档将这种模式归入「Orchestrating via code(代码编排)」大类3。与 LLM 自主规划流程相反,这里的控制权在 Python 代码手里——while 循环、退出条件、反馈注入,全部由代码显式管理,「更确定、更可预测,在速度、成本和性能方面更优」。

二、双 Agent 架构:89 行里的两个角色

llm_as_a_judge.py 源码4 只定义了两个 Agent:
story_outline_generator = Agent(
    name="story_outline_generator",
    instructions=(
        "Generate a story outline based on the user's input."
        # 无 output_type,输出纯文本大纲
    ),
)

evaluator = Agent(
    name="evaluator",
    instructions=(
        "Evaluate the story outline and provide feedback. "
        "Never give it a pass on the first try. "
        "After 5 attempts, you can give it a pass if the story outline "
        "is good enough - do not go for perfection."
    ),
    output_type=EvaluationFeedback,  # 强制结构化输出
)
角色分工很清晰:
  • story_outline_generator:内容生成者,给什么反馈就改什么,没有自我评判的职责
  • evaluator:质量裁判,通过 output_type=EvaluationFeedback 约束只能输出结构化评分,不能说废话
主循环的骨架是这样的4
with trace("LLM as a judge"):
    input_items: list[TResponseInputItem] = [{"content": user_msg, "role": "user"}]

while True:
        # Step 1: 生成
        story_outline_result = await Runner.run(
            story_outline_generator,
            input_items,
        )

# Step 2: 评分
        evaluator_result = await Runner.run(
            evaluator,
            story_outline_result.to_input_list(),
        )
        result: EvaluationFeedback = evaluator_result.final_output

# Step 3: 判断退出或继续
        if result.score == "pass":
            break

# Step 4: 把反馈追加进 input_items,下一轮 generator 看到
        input_items = story_outline_result.to_input_list() + [
            {"content": f"Feedback: {result.feedback}", "role": "user"}
        ]
这个 while 循环是整个模式的核心。Generator 拿到的不只是「用户原始请求」,而是「原始请求 + 上一轮生成的大纲 + 评审反馈」。它每轮看到的是越来越长的上下文,而不是在同一张白纸上重来。

三、EvaluationFeedback:裁判只能说三种话

Judge 模式的稳定运行,依赖这个 dataclass 的设计:
@dataclass
class EvaluationFeedback:
    feedback: str
    score: Literal["pass", "needs_improvement", "fail"]
两个字段,职责截然不同4
  • feedback:文本说明,给 generator 看的,解释「哪里不够好、怎么改」
  • score:机器可读的三值评分,给代码逻辑看的,决定「继续还是停止」
scoreLiteral["pass", "needs_improvement", "fail"] 而不是简单的 bool,保留了「不够好但也不算差」的中间状态。这与 evaluator instructions 里的「do not go for perfection」形成呼应——不要只有「完美」和「失败」两种结果。
通过 output_type=EvaluationFeedback 参数,评审 Agent 被强制只能输出符合这个 schema 的 JSON。代码层面直接解包:
result: EvaluationFeedback = evaluator_result.final_output
官方 results/ 文档说明5final_output 的静态类型是 Any(因为 handoffs 可能改变最终运行的 agent),但配合 output_type 参数使用时,实际类型是可预期的。生产代码里可以加一行 assert isinstance(result, EvaluationFeedback) 做运行时保护。

四、三个关键 API 机制

to_input_list():全量上下文链

每轮 generator 运行后,代码通过 story_outline_result.to_input_list() 拿到完整的多轮对话历史4。这个默认的 preserve_all 模式会保留所有轮次的消息记录,不裁剪历史。
这与上一期讲的 deterministic.py 形成对比6:deterministic 模式直接把 final_output(一个字符串)传给下一步,干净但无历史。judge 模式需要 evaluator 看到「用户原始需求 + 所有历史大纲 + 所有历史反馈」,才能做出有上下文的评分判断,所以要保留完整链路。
反馈注入的方式也值得注意:
input_items = story_outline_result.to_input_list() + [
    {"content": f"Feedback: {result.feedback}", "role": "user"}
]
反馈以 role="user" 的新消息追加,而不是修改 agent 的 instructions。这样每轮的历史不会被覆盖,generator 的系统提示词也保持稳定。

auto_mode 安全阀

while True 循环在无人值守场景下是定时炸弹。源码用了三层保护7
# 检测环境变量
is_auto_mode = os.getenv("EXAMPLES_INTERACTIVE_MODE") == "auto"
max_rounds = 3 if is_auto_mode else None
rounds = 0

while True:
    # ...
    if result.score == "pass":
        break
    if max_rounds is not None:
        rounds += 1
        if rounds >= max_rounds:
            break
EXAMPLES_INTERACTIVE_MODE=auto 时,最多跑 3 轮就强制退出。这个设计让同一份代码可以在「交互模式」(人工干预、不限轮次)和「CI/自动化」(有上限、可预期成本)下无缝切换。
生产环境里,max_rounds 就是你的成本预算。评估一轮多少钱,乘以 max_rounds,上限就在那里。

Evaluator Instructions 的两条策略

这两行 prompt 藏着整个质量-成本平衡的精华4
"Never give it a pass on the first try."
"After 5 attempts, you can give it a pass if the story outline is good enough - do not go for perfection."
第一条强制至少迭代一次——防止 evaluator 因为「第一次看着还行」就直接放行,跳过改进机会。第二条设置收敛止损——防止 evaluator 无限追求完美导致循环失控。
这是 prompt engineering 层面对循环行为的约束,比代码 max_rounds 更软,但更符合语义意图。两者组合,才是完整的退出条件设计。

五、Judge vs Deterministic:何时迭代,何时直接停

这两个模式在形式上都用了结构化输出 + 代码控制流,但解决的是完全不同的问题46
维度Judge 模式Deterministic 模式
流程形态while True 迭代循环线性流水线,单次通过
不通过时反馈注入,generator 重试exit(0) 硬停止
质量判断者LLM(evaluator agent)LLM(输出 bool)
上下文策略to_input_list() 全量历史final_output 字符串传递
典型场景内容质量需要迭代改进的任务门控检查——不符合条件就整流程终止
选哪个,取决于你面对的是「质量改进问题」还是「资格检验问题」。
写作类、总结类、翻译类——这些任务的质量可以通过多轮反馈逐步提升,适合 judge 模式。代码风格检查、字段合法性校验、科幻故事判定——这些任务不存在「给反馈再改」的空间,通过就通过,不通过就整个流程停下来,适合 deterministic 的 gate 机制。
官方文档的归类也在说同一件事3:「While orchestrating via LLM is powerful, orchestrating via code makes tasks more deterministic and predictable, in terms of speed, cost and performance.」两种模式都属于代码编排——控制权始终在代码里,只是循环形态不同。

六、生产场景:Judge 能做什么,做不到什么

先搭 Judge,再写功能

SGLang 核心开发者 Chenyang Zhao(SGLang 是部署在 40 万+ GPU 上的开源推理引擎)在 LinkedIn 上说了一件让很多人印象深刻的事8
「我做的第一件事是搭一个 LLM-as-a-Judge 的 evaluation 框架。先别急着加功能,先回答一个最朴素的问题:你的改动,真的让 agent 答得更准了吗?」
结果:大多数看起来 promising 的优化,实测效果为零。
这不是个别案例。LLM-as-a-Judge 框架的价值在于它提供了可复现的基准——没有这个基准,你的所有「优化」都是感性判断。

Prompt 设计:让 Judge 只做原子判断

在 Judge Agent 的 instructions 里,有一条通用建议2:把评估标准拆分到最小粒度,每个标准只问一个问题,最终的「总分」或「是否通过」由代码层逻辑完成,而不是让 LLM 综合判断。
比如不要问「这段回答整体质量如何(1-5 分)」,而是分开问:
  • 是否正确回答了用户的问题?(是/否)
  • 是否引入了幻觉信息?(是/否)
  • 是否控制在 200 字以内?(是/否)
低精度评分(是/否 > 1-100)比高精度评分更稳定,因为 LLM 对「7 分」和「8 分」的区别判断往往缺乏一致性。
温度建议设到 0-0.2,评估本身不需要创造力。

成本控制:Judge 不是免费午餐

Tian Pan(前 Uber/Brex 工程师)在其影子流量框架里给出了一个分层策略9
  • 第 1 层(100% 流量):格式校验、安全过滤器,几乎免费
  • 第 2 层(10-20% 流量):嵌入相似度,成本低
  • 第 3 层(2-5% 流量):LLM-as-Judge 逐对评估,每次 5-30 秒,真实 API 成本
  • 第 4 层(<1% 流量):人工审查
LLM-as-Judge 只处理 2-5% 的流量就能获得统计意义上的置信度。对 100% 请求跑 Judge,是在把预算烧在边际收益递减的地方。而且 GPT-4 与人类评估者在逐对比较上的一致率超过 80%——这个数字支撑了「LLM-as-Judge 可以作为大规模人类判断代理」的论断。

七、实践建议

① output_type 是结构化的前提,不只是格式偏好
EvaluationFeedback 里的 score: Literal["pass", "needs_improvement", "fail"] 是整个循环可以用代码控制的基础。如果让 evaluator 输出自然语言「这个大纲写得还不错,有一些细节可以改进」,你就没有办法写出 if result.score == "pass": break。结构化输出不是锦上添花,是循环能终止的前提条件。
② while 循环必须有出口,而且要有两个
一个语义出口(evaluator 给 pass)+ 一个保底出口(max_rounds 计数器)。只靠 LLM 决定什么时候停,LLM 就有可能永远不停——或者在某次调用异常时死循环。auto_mode 里的 max_rounds=3 是一个好起点,生产环境根据成本预算调整。
③ Judge 的 Prompt 要先校准,再上线
野生运营在其评估实践指南里总结了一个核实流程10:随机抽 10% 的数据集,由领域专家人工打分,然后比对 LLM Judge 的评分结果,调整 instructions 直到两者一致性达标。未经校准的 Judge 可能在某类输出上系统性偏松或偏严,评估结果失去参考价值。搭好框架只是开始,校准才是让它可信的过程。

当前版本:本文基于 OpenAI Agents SDK v0.17.0(2026-05-08 发布)。
下期预告 #22agent_patterns 的下一个模式——Parallelization:用 asyncio.gather 同时启动多个 Agent,最后由 picker agent 从并行结果中择优。比对 judge 的深度优先,parallelization 是广度优先——两种截然不同的质量策略。
封面图由 AI 生成

围绕这条内容继续补充观点或上下文。

  • 登录后可发表评论。