LLM 护栏(派生 guardrail agent)
高灵活度
代码护栏(Python 逻辑)
低延迟
从提示注入攻击到 PII 数据泄露,系统拆解 OpenAI Agents SDK Guardrails 机制的完整技术体系。覆盖三种 Guardrail 类型(InputGuardrail / OutputGuardrail / Tool Guardrail)的设计边界、@input_guardrail 与 @output_guardrail 装饰器的完整带注释代码示例、GuardrailFunctionOutput 数据结构与 TripwireTriggered 异常处理、InputGuardrail 并发执行机制的成本逻辑,以及「语义判断用 LLM、结构校验用代码」的选择框架。附三条可直接执行的落地建议,结尾预告 #9 Streaming。
리서치 브리프
| 类型 | 运行时机 | 运行范围 |
|---|---|---|
| InputGuardrail | 用户输入进入 Agent 前 | 仅在代理链第一个 Agent 处运行 |
| OutputGuardrail | Agent 完成最终输出后 | 仅在最终输出 Agent 处运行 |
| Tool Guardrail | 每次工具函数调用时 | 每个 @function_tool 调用前后各一次 |
from pydantic import BaseModel
from agents import (
Agent, GuardrailFunctionOutput, InputGuardrailTripwireTriggered,
RunContextWrapper, Runner, TResponseInputItem, input_guardrail,
)
# ① 定义 guardrail 内部 Agent 的输出结构
class HomeworkOutput(BaseModel):
is_math_homework: bool
reasoning: str
# ② 专门用于判断输入的「内卫 Agent」——小模型、便宜
guardrail_agent = Agent(
name="Homework Guardrail",
instructions="判断用户是否在要求帮做数学作业。",
output_type=HomeworkOutput,
)
# ③ 用 @input_guardrail 装饰器定义检查函数
@input_guardrail
async def math_guardrail(
ctx: RunContextWrapper[None],
agent: Agent,
input: str | list[TResponseInputItem],
) -> GuardrailFunctionOutput:
# 运行内卫 Agent,得到结构化判断
result = await Runner.run(guardrail_agent, input, context=ctx.context)
return GuardrailFunctionOutput(
output_info=result.final_output, # 可选:将判断结果存入 output_info,供后续审计
tripwire_triggered=result.final_output.is_math_homework, # 关键布尔值
)
# ④ 主 Agent 绑定 guardrail
main_agent = Agent(
name="Customer Agent",
instructions="你是一个客服助手,只回答产品相关问题。",
input_guardrails=[math_guardrail],
)
# ⑤ 运行时捕获异常
async def main():
try:
result = await Runner.run(main_agent, "帮我解一下这道方程:2x+5=13")
print(result.final_output)
except InputGuardrailTripwireTriggered as e:
# tripwire 被触发时,Runner 立即抛出此异常,不会继续执行主 Agent
print(f"输入被拦截:{e.guardrail_result.output.output_info.reasoning}")@input_guardrail 同步和异步函数都支持,装饰器自动包装成 InputGuardrail 实例5tripwire_triggered=True 是唯一的拉闸信号——Runner 一旦看到这个值为 True,立刻抛出 InputGuardrailTripwireTriggered 异常,主 Agent 连一个 token 都不会生成output_info 是 Any 类型,放什么都行:判断原因、置信度分数、完整的 Pydantic 对象,都可以,异常触发时会随 guardrail_result 一起传递出来from agents import (
Agent, GuardrailFunctionOutput, OutputGuardrailTripwireTriggered,
RunContextWrapper, Runner, output_guardrail,
)
import re
# 代码护栏示例:检测输出中是否含有 PII(手机号)
@output_guardrail
async def pii_guardrail(
ctx: RunContextWrapper[None],
agent: Agent,
output: str, # 这里是主 Agent 的原始输出文本
) -> GuardrailFunctionOutput:
# 简单正则:检测 11 位手机号
phone_pattern = re.compile(r"1[3-9]\d{9}")
contains_pii = bool(phone_pattern.search(output))
return GuardrailFunctionOutput(
output_info={"detected_pii": contains_pii},
tripwire_triggered=contains_pii,
)
main_agent = Agent(
name="Customer Agent",
instructions="你是一个客服助手。",
output_guardrails=[pii_guardrail],
)
async def main():
try:
result = await Runner.run(main_agent, "帮我查一下张三的联系方式")
print(result.final_output)
except OutputGuardrailTripwireTriggered as e:
print("输出包含敏感信息,已被拦截。")@output_guardrail 装饰器不支持 run_in_parallel 参数5。OutputGuardrail 的执行顺序固定在主 Agent 完成之后,逻辑上也只能这样:没有输出,检查什么?@dataclass
class GuardrailFunctionOutput:
tripwire_triggered: bool # 是否拉闸。True = 立即中断执行
output_info: Any = None # 任意附加信息(审计、原因、置信度……)tripwire_triggered 是执行控制位——Runner 每次运行 guardrail 后都会检查这个值5。output_info 是审计数据位——你可以在里面存任何东西:判断原因({"reason": "clean"})、结构化日志,甚至完整的 Pydantic 模型实例。这些数据会随异常对象传递出来,供上层代码记录和展示。InputGuardrailTripwireTriggered:其 .guardrail_result 是 InputGuardrailResult,包含 guardrail(InputGuardrail 对象)和 output(GuardrailFunctionOutput)OutputGuardrailTripwireTriggered:其 .guardrail_result 是 OutputGuardrailResult,额外携带 agent_output(代理的原始输出)和 agent(Agent 对象)@dataclass
class InputGuardrail(Generic[TContext]):
guardrail_function: ...
name: str | None = None
run_in_parallel: bool = True # 默认 True:与主 Agent 并发运行!run_in_parallel=True(默认值)意味着:guardrail 和主 Agent 同时启动。guardrail 触发 tripwire 的那一刻,Runner 中断主 Agent 的执行,就像赛跑中途拉断了终点线绳子,不等选手跑完。run_in_parallel=False:@input_guardrail(run_in_parallel=False)
async def blocking_guardrail(ctx, agent, input):
# 此 guardrail 会在主 Agent 启动前完成,确保检查结果可靠
...from agents import function_tool, tool_input_guardrail, ToolGuardrailFunctionOutput
# 工具输入护栏:检查参数中是否含有 API Key
@tool_input_guardrail
async def check_no_api_key(ctx, tool_call):
if "sk-" in str(tool_call.arguments):
# reject_content() 拒绝工具调用,返回错误消息给主 Agent
return ToolGuardrailFunctionOutput.reject_content(
"参数中检测到 API Key,已拒绝执行此工具调用。"
)
return ToolGuardrailFunctionOutput.allow()
@function_tool(tool_input_guardrails=[check_no_api_key])
def call_external_api(endpoint: str, payload: str) -> str:
"""调用外部 API"""
...ToolGuardrailFunctionOutput.allow() 和 .reject_content(message) 是互斥的返回——不是 tripwire/bool 那套,而是更直接的「放行/拒绝」语义@function_tool 创建的工具,不覆盖 WebSearchTool、FileSearchTool、ComputerTool 等内置工具,也不覆盖 Handoffs4sk- 前缀(API Key 检测)——字符串匹配gpt-4o-mini)做 guardrail agent,避免守门员比被守的门更贵。from agents import RunConfig, Runner
# 全局输入护栏:对所有 Agent(包括 Handoff 链的第一个)生效
config = RunConfig(
input_guardrails=[pii_input_guardrail, injection_guardrail],
output_guardrails=[pii_output_guardrail],
)
result = await Runner.run(
starting_agent=my_agent,
input="用户消息",
run_config=config,
)output_info 接审计,不要扔掉GuardrailFunctionOutput.output_info 里存的判断原因和置信度,是事后审计和模型迭代的宝贵数据。不要在 guardrail 函数里只返回 tripwire_triggered=True 就完事——把触发原因、输入片段、用的是哪个检测模型一并记录下来,写入你的日志系统或 Tracing(参考 #7 篇)9。tripwire_triggered 拉闸保证快速失败。把这几个机制搞清楚,再结合「语义判断用 LLM、结构校验用代码」的分工原则,这套安全体系基本就稳了。Runner.run_streamed() 是你需要掌握的接口。事件流的结构、如何在流中处理 Tool Call、流式 Guardrail 的行为差异——下篇见。
이 콘텐츠를 둘러싼 관점이나 맥락을 계속 보강해 보세요.