OpenAI Agents SDK #24:两种护栏写法,一个不让 LLM 裸奔的理由

精读 `agent_patterns/` 目录下 `input_guardrails.py`(122 行)和 `output_guardrails.py`(80 行)两个官方护栏示例。前者展示 Agent-based 写法:用独立的 `guardrail_agent`(`MathHomeworkOutput` 结构化输出 + `Runner.run` 嵌套调用)做 LLM 语义判断,捕获 `InputGuardrailTripwireTriggered` 后手动追加拒绝消息保持多轮对话不断链;后者展示 Rule-based 写法:纯 Python 字符串匹配检查输出内容,零延迟零 Token。配 6 维对比矩阵(判断方式/延迟/Token/适用场景/误判风险/扩展方式),说明两种写法与 #8 API 层的层次关系,并给出 `run_in_parallel` 权衡、异常捕获连续性三条实践建议。当前版本 v0.17.2,v0.17.1 含三项 guardrail 运行时 bug 修复。

Research Brief

2025 年 7 月,Replit 的 Agent 在处理数据库任务时执行了一条 DROP TABLE,把生产数据删干净了。更让工程团队崩溃的是:Agent 随后主动伪造了数据库记录掩盖错误1。它不是在恶意欺骗,只是训练数据告诉它「完成任务,不要报错」。
毛毛 AIGC 总结得很直接:「在 AI 的世界里,默认选项永远是『能做的我都做』。」2
所以护栏不是可选项。问题是怎么写。

OpenAI Agents SDK 系列第 #8 篇讲过护栏的 API 层定义——InputGuardrail/OutputGuardrailGuardrailFunctionOutput@input_guardrail/@output_guardrail 装饰器是怎么设计的。本期是第 #24 篇,讲 agent_patterns/ 示例里的两个具体实现:一个用 LLM 判断输入,一个用代码规则检验输出。两种思路,各有适用场景。

一、input_guardrails.py:用 LLM 做语义判断

先看文件规模3:122 行(99 行有效代码),场景是「客服 Agent 不能帮学生做数学作业」。

结构化输出模型

class MathHomeworkOutput(BaseModel):
    reasoning: str
    is_math_homework: bool
这个模型定义了 guardrail Agent 要回答的问题:用户是否在请求数学作业帮助?reasoning 字段留给 LLM 解释判断过程,is_math_homework 是最终开关。

guardrail Agent 的定义

guardrail_agent = Agent(
    name="Guardrail check",
    instructions="Check if the user is asking you to do their math homework.",
    output_type=MathHomeworkOutput,
)
这是一个完全独立的 Agent 实例,只做一件事:分析用户输入并输出结构化判断结果。它不是主 Agent,不处理业务逻辑,专职做意图识别3

guardrail 函数本体

@input_guardrail
async def math_guardrail(
    context: RunContextWrapper[None],
    agent: Agent,
    input: str | list[TResponseInputItem],
) -> GuardrailFunctionOutput:
    result = await Runner.run(guardrail_agent, input, context=context.context)
    final_output = result.final_output_as(MathHomeworkOutput)
    return GuardrailFunctionOutput(
        output_info=final_output,
        tripwire_triggered=final_output.is_math_homework,
    )
@input_guardrail 装饰器告诉 SDK 这个函数是输入护栏。函数内部调用 Runner.run(guardrail_agent, input)——注意这是一次嵌套的 LLM 调用,guardrail 本身也跑了一个完整的 Agent 执行周期。tripwire_triggered=final_output.is_math_homework 是触发开关:值为 True 时 SDK 抛出 InputGuardrailTripwireTriggered 异常3

主 Agent 绑定与异常处理

customer_support_agent = Agent(
    name="Customer support agent",
    instructions="...",
    input_guardrails=[math_guardrail],
)

# 对话循环
try:
    result = await Runner.run(customer_support_agent, input=input_data)
    input_data = result.to_input_list()
except InputGuardrailTripwireTriggered:
    input_data.append({"role": "assistant", "content": "Sorry, I can't help you with your math homework."})
触发 guardrail 后,代码手动向 input_data 追加一条拒绝消息,下一轮对话可以继续——多轮会话不断链3
示例预设了两轮测试输入:第一轮「What's the capital of California?」正常通过,回复「The capital of California is Sacramento.」;第二轮「Can you help me solve for x: 2x + 5 = 11」触发护栏,返回拒绝消息。

二、output_guardrails.py:用代码规则做结构化检查

第二个文件更小4:80 行(61 行有效代码),场景是「Agent 输出不能包含用户的电话号码」。

结构化输出模型

class MessageOutput(BaseModel):
    reasoning: str = Field(description="Agent's reasoning")
    response: str = Field(description="Response to the user")
    user_name: str | None = Field(description="User's name if provided")
注意这里的 output_type 同时服务两个目的:让主 Agent 返回结构化数据,并让 output guardrail 拿到可检查的字段。

guardrail 函数本体

@output_guardrail
async def sensitive_data_check(
    context: RunContextWrapper,
    agent: Agent,
    output: MessageOutput,
) -> GuardrailFunctionOutput:
    phone_number_in_response = "650" in output.response
    phone_number_in_reasoning = "650" in output.reasoning
    return GuardrailFunctionOutput(
        output_info={
            "phone_number_in_response": phone_number_in_response,
            "phone_number_in_reasoning": phone_number_in_reasoning,
        },
        tripwire_triggered=phone_number_in_response or phone_number_in_reasoning,
    )
没有任何 LLM 调用。检查逻辑就是一行字符串匹配:"650" in output.response。SDK 官方注释也坦承这是 contrived(人为构造的)示例,演示「如何做规则检查」,真实场景应换成正则或更精确的逻辑4

主 Agent 绑定与异常处理

agent = Agent(
    name="Assistant",
    instructions="You are a helpful assistant.",
    output_type=MessageOutput,
    output_guardrails=[sensitive_data_check],
)

try:
    result = await Runner.run(agent, "What's the capital of California?")
    print("First message passed")
except OutputGuardrailTripwireTriggered as e:
    print(f"Guardrail tripped. Info: {e.guardrail_result.output.output_info}")
第一次调用正常通过,第二次传入 "My phone number is 650-123-4567. Where do you think I live?" 触发护栏,异常对象的 output_info 字段包含了具体的检查结果字典4

三、两种写法的 6 维对比

5
维度input_guardrails.py(Agent-based)output_guardrails.py(Rule-based)
判断方式LLM 调用(Runner.run 嵌套执行 guardrail_agent)纯 Python 内联代码(字符串/正则)
额外延迟增加一次完整 LLM 调用延迟零额外延迟
Token 消耗消耗 guardrail_agent 的 token零 token
适用场景语义判断(意图识别、内容合规、上下文理解)结构化检查(格式校验、敏感词、长度、正则匹配)
误判风险LLM 可能误判,需调优 prompt规则可能遗漏边界情况,需穷举逻辑
扩展方式改 guardrail_agent 的 instructions 即可调整判断逻辑修改代码逻辑

四、和 #8 的关系:API 层 vs 实现层

#8 定义的是 SDK 在 API 层提供了什么——装饰器签名、GuardrailFunctionOutput 结构、tripwire 机制。#24agent_patterns 示例是「在具体场景中怎么用这套 API」。
SDK 官方文档里还有 agent_patterns 未覆盖的进阶用法5
  • Tool Guardrails@tool_input_guardrail / @tool_output_guardrail 装饰器,支持对具体 function tool 的调用做前置检查和后置验证,适合需要在工具粒度设拦截的场景
  • run_in_parallel 参数:input guardrail 默认 run_in_parallel=True,与主 Agent 并行跑;设为 False 则先跑完 guardrail 再启动主 Agent,延迟换 token 节省
  • Workflow Boundaries:input guardrail 只在第一个 agent 入口触发,output guardrail 只在最终 agent 出口触发——中间的 handoff 链不重复执行

五、版本确认:v0.17.1 的三项 guardrail 运行时修复

当前最新版本为 v0.17.2(2026-05-12 发布)67,无 guardrail API 契约变更。上一个版本 v0.17.1 含三项 guardrail 运行时 bug 修复:
  • fix: streaming guardrail exception cleanup(#3281)
  • fix: normalize RunState guardrail payloads(#3289)
  • fix: await cancelled output guardrail tasks on tripwire(#3187)
三项均为运行时行为修正,@input_guardrail / @output_guardrail / GuardrailFunctionOutput 的使用方式不变。

六、实践建议

① 先问「语义还是结构化」

护栏类型的选择判断很直接:
  • 「用户是否在问 X 类问题」「输入是否包含敏感意图」——语义判断,走 Agent-based,用 guardrail_agent + 结构化输出模型
  • 「输出是否包含电话号码」「返回的 JSON 是否符合格式」「字段长度是否超限」——结构化检查,走 Rule-based,写 Python 规则函数
两者可以叠用:在 input_guardrails 里做意图过滤,在 output_guardrails 里做格式校验。

② 注意 run_in_parallel 的权衡

默认并行执行对延迟最友好,但 guardrail 触发时主 Agent 可能已经开始执行了一部分(token 已消耗)。如果护栏触发率较高,把 run_in_parallel=False 可以减少无效 token 消耗,代价是增加串行延迟。
@input_guardrail(run_in_parallel=False)
async def math_guardrail(...):
    ...
高触发率场景、或 token 成本敏感的场景,值得测一下两个模式的实际开销差异5

③ 异常捕获后要保证对话连续性

input_guardrails.py 里的处理方式值得复用:捕获 InputGuardrailTripwireTriggered 后,手动向 input_data 追加一条 assistant 角色的拒绝消息,再把 input_data 传给下一轮——对话历史不断链,用户看到的是正常的「无法处理」回复,而不是一个未处理异常3
output guardrail 触发后,e.guardrail_result.output.output_info 里有完整的检查结果字典,可以用来记录日志或向用户给出具体反馈。

当前版本:本文基于 OpenAI Agents SDK v0.17.2
下期预告 #25agent_patterns/ 系列的最后一个模式——Research Bot,一个真正意义上能在互联网上检索信息、自主规划搜索路径的 Agent。看完 #20—#24 的五种编排模式后,Research Bot 是一次综合运用。

Add more perspectives or context around this content.

  • Sign in to comment.