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/OutputGuardrail、GuardrailFunctionOutput、@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 机制。#24 的 agent_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 运行时修复
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。
下期预告 #25:agent_patterns/系列的最后一个模式——Research Bot,一个真正意义上能在互联网上检索信息、自主规划搜索路径的 Agent。看完 #20—#24 的五种编排模式后,Research Bot 是一次综合运用。
Add more perspectives or context around this content.