LLM 应用里,输入可以经过精心设计的 Prompt 来约束,但输出是不可穷举的——同一个问题,模型今天答得合规,明天换了版本或换了语境就可能“越界”。输出侧的风险比输入侧更隐蔽:不是攻击者注入,而是模型自己生成的内容触碰了底线。这些底线包括格式错误(JSON 解析失败)、敏感信息泄漏(回答里带出别人手机号)、不当内容(暴力、歧视、越权建议)以及合规红线(医疗、金融、未成年人保护)。输出护栏(Output Guardrails)的职责就是在模型生成之后、呈现给用户之前,加一道可编程的安全闸门。本指南系统覆盖输出护栏的分层架构、格式强制、PII 脱敏、内容过滤、分类器与合规落地,且明确区分于输入侧注入防御——这里只聚焦输出侧。
一、输出侧护栏的定位
1.1 输入侧 vs 输出侧的边界
很多人把安全措施都堆在输入侧,但输出侧承担着不同的职责:
| 维度 | 输入侧防御 | 输出侧护栏 |
|---|---|---|
| 防护对象 | 恶意/异常的输入 | 模型自己生成的内容 |
| 威胁 | Prompt 注入、越狱 | 幻觉、泄漏、不当内容 |
| 手段 | 指令隔离、过滤、净化 | 格式校验、脱敏、分类过滤 |
| 时点 | 请求进入时 | 响应返回前 |
| 关系 | 前置防线 | 兜底防线(最后一道闸门) |
ℹ️ 核心洞察:输入侧做得再好,输出侧也不能省——因为模型是概率系统,任何输入都可能产生越界输出。输出护栏是安全链路上“最后一道闸门”,必须在呈现给用户之前拦截。
1.2 输出护栏的四层架构
输出护栏自底向上分四层,各司其职、层层递进:
| 层 | 职责 | 典型手段 |
|---|---|---|
| 结构层 | 输出能正常被消费 | Schema 校验、重试、修复 |
| 内容层 | 内容合规 | 分类器、关键词、政策模型 |
| 数据层 | 隐私安全 | PII 识别、脱敏、阻断 |
| 合规层 | 满足法规 | 规则引擎、审计日志 |
护栏统一放在生成之后、用户之前——它是安全链路上最后一道闸门,任何输出都要经过“结构 → 内容 → 数据 → 合规”四层校验后才放行给用户。
二、格式强制:让输出一定可被消费
2.1 结构化输出与 Schema 校验
模型输出 JSON 失败是调用方最常见的崩溃源。输出侧要做双重保障:提示词要求 + 输出后校验:
# schema_gate.py — JSON Schema 校验护栏
import json, jsonschema
JSON_SCHEMA = {
"type": "object",
"required": ["summary", "sentiment", "topics"],
"properties": {
"summary": {"type": "string"},
"sentiment": {"enum": ["positive", "neutral", "negative"]},
"topics": {"type": "array", "items": {"type": "string"}},
},
}
def validate_schema(text: str) -> dict | None:
"""解析并校验模型输出,非法返回 None。"""
try:
obj = json.loads(strip_code_fence(text))
jsonschema.validate(obj, JSON_SCHEMA)
return obj
except (json.JSONDecodeError, jsonschema.ValidationError):
return None
2.2 校验失败的恢复策略
校验失败不能直接抛给用户。按失败类型采取不同动作:
| 失败类型 | 表现 | 恢复动作 |
|---|---|---|
| 围栏残留 | 输出被代码围栏包裹 | 剥离围栏再解析 |
| 少量冗余 | 前后有解释文字 | 提取首个 JSON 块 |
| 结构性错误 | 缺字段、类型错 | 用修复器(repair LLM)修正 |
| 完全不可用 | 输出文不对题 | 重试一次或降级 |
def recover_structured_output(text: str, repair_llm=None) -> dict:
"""多级恢复:剥离 → 提取 → 修复 → 重试。"""
for candidate in [text, strip_code_fence(text), extract_json_block(text)]:
parsed = validate_schema(candidate)
if parsed:
return parsed
if repair_llm:
fixed = repair_llm(f"修复以下 JSON:\n{text}")
parsed = validate_schema(fixed)
if parsed:
return parsed
raise OutputSchemaError("无法恢复为合法 Schema")
2.3 格式强制的前置手段
在输出侧校验之外,生成阶段就用结构化输出模式(如 response_format)可以大幅降低失败率:
def structured_generate(query: str, schema: dict) -> dict:
"""生成阶段强制结构化输出 + 输出侧校验兜底。"""
resp = client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": query}],
response_format={"type": "json_schema", "schema": schema},
)
text = resp.choices[0].message.content
return validate_schema(text) # 兜底校验
三、PII 脱敏:守住隐私数据
3.1 输出侧的 PII 风险
输入侧脱敏处理的是“请求里带的敏感信息”,输出侧面对的则是模型可能凭空生成或从上下文泄漏的敏感信息——比如从检索到的文档里原样复述手机号、身份证、地址。
# pii_gate.py — 输出侧 PII 识别与脱敏
import re
PII_PATTERNS = [
(r"1[3-9]\d{9}", "MOBILE"), # 中国大陆手机号
(r"\d{17}[\dXx]", "ID_CARD"), # 身份证
(r"(?:\d{4}[-\s]?){4}", "BANK_CARD"),
(r"[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}", "EMAIL"),
(r"\d{6}-?\d{4}-?\d{2}", "BIRTH"),
]
def find_pii(text: str) -> list[dict]:
"""基于正则 + NER 的 PII 识别。"""
hits = []
for pattern, kind in PII_PATTERNS:
for m in re.finditer(pattern, text):
hits.append({"kind": kind, "value": m.group(),
"start": m.start(), "end": m.end()})
return hits
3.2 脱敏策略:替换、屏蔽还是阻断
不同场景对 PII 的处理策略不同:
| 策略 | 做法 | 适用 |
|---|---|---|
| 替换 | 用 [手机号] 占位 | 需保留语义 |
| 部分打码 | 138****1234 | 保留可用信息 |
| 整体阻断 | 拒绝返回含 PII 内容 | 高敏感场景 |
| 上下文保留 | 确系用户本人的信息放行 | 需要白名单 |
def deidentify(text: str, mode="mask") -> str:
"""按策略处理输出中的 PII。"""
if mode == "mask":
for pattern, _ in PII_PATTERNS:
text = re.sub(pattern, lambda m: m.group()[0] + "*" * (len(m.group()) - 1), text)
return text
if mode == "block":
if find_pii(text):
raise PIISafetyError("输出含敏感信息,已阻断")
return text
if mode == "placeholder":
for pattern, kind in PII_PATTERNS:
text = re.sub(pattern, f"[{kind}]", text)
return text
return text
ℹ️ 核心洞察:PII 护栏的难点不是“识别”,而是“判断归属”。一刀切屏蔽会误伤正常场景(用户查自己的订单,输出自己的手机号是合理的),但宁可对非本人信息一律脱敏,也不要为了省事放行所有 PII——实现上需结合“确属当前用户”的白名单判断。
四、内容过滤:不当内容分类与拦截
4.1 内容风险的类型
输出侧内容过滤要覆盖的不只是攻击性内容,还包括细分的风险类别:
| 类别 | 示例 | 合规依据 |
|---|---|---|
| 仇恨言论 | 种族/性别歧视 | 平台政策 |
| 暴力 | 伤人指导 | 安全红线 |
| 色情 | 露骨内容 | 平台政策 |
| 自残 | 自杀诱导 | 安全红线 |
| 违禁品 | 毒品/武器交易 | 法律法规 |
| 危险技能 | 制造武器步骤 | 安全红线 |
| 医疗/金融建议 | 误导性建议 | 行业法规 |
4.2 多层分类器组合
单一模型做内容分类不够稳。规则关键词 + 轻量分类器 + 强分类器三层组合:
# moderation.py — 三层内容过滤
KEYWORD_BLOCKLIST = ["制作炸弹步骤", "自杀方法", "购买毒品渠道"]
def keyword_screen(text: str) -> str | None:
"""第一层:规则关键词,快且零成本。"""
lowered = text.lower()
for kw in KEYWORD_BLOCKLIST:
if kw in lowered:
return f"命中禁用词:{kw}"
return None
def light_classifier(text: str) -> dict:
"""第二层:小模型分类器,高吞吐。"""
return small_model.moderate(text) # {"category": ..., "score": ...}
def heavy_classifier(text: str) -> dict:
"""第三层:强模型复审,低误杀。"""
return large_model.moderate(text) # 用于边界与高价值流量
三层的裁决逻辑组合为:关键词命中直接拦截(快、零成本);轻量分类器判为安全则放行;边界情况(轻量分类器不判安全)交给强分类器按硬阈值复审,降低误杀。
| 分类器 | 吞吐 | 误杀 | 误放 | 适用 |
|---|---|---|---|---|
| 关键词规则 | 极高 | 高 | 低 | 兜底、前置 |
| 轻量模型 | 高 | 中 | 中 | 全量过滤 |
| 强模型复审 | 低 | 低 | 低 | 边界与高风险 |
五、敏感话题与越界建议的护栏
5.1 敏感话题的「软性拦截」
有些话题不能直接封杀(比如用户问医疗常识),但模型不能给出“确定性的专业结论”。护栏要做责任声明与风险提示注入:
def safe_medical_output(text: str) -> str:
"""医疗话题:在专业结论前注入免责声明。"""
if detect_topic(text, "medical"):
return text + "\n\n> 以上内容仅为信息参考,不构成医疗建议。请咨询执业医师。"
return text
5.2 危险建议拦截
对“如何实施 X 的危险行为”类输出,直接阻断并给出替代引导:
def intercept_dangerous(output: str, query: str) -> dict:
"""识别危险意图:阻断 + 转介帮助资源。"""
intent = intent_classifier(query, output) # 识别查询与输出的危险意图
if intent["level"] == "high":
return {
"allow": False,
"user_message": "无法提供相关内容。若你或他人正面临困难,请联系专业帮助。",
"internal_log": intent,
}
return {"allow": True, "output": output}
输出侧还有一个特殊风险:看似正常但事实错误。对高风险场景(数字、法规、引用),护栏要加入事实校验——把输出中的关键数字与引用逐一对照源文档,未在源文档中找到的即判定为幻觉并拦截。
六、合规落地与审计
6.1 政策规则引擎
企业合规要求(未成年人保护、金融适当性、广告法)难以全部写进模型。用规则引擎承载政策:
# policy_engine.py — 可配置的政策规则引擎
POLICY_RULES = [
{"id": "R001", "trigger": "contains_financial_advice",
"action": "require_disclaimer", "channel": ["chat", "api"]},
{"id": "R002", "trigger": "pii_detected",
"action": "mask", "severity": "high"},
{"id": "R003", "trigger": "adult_content",
"action": "block", "severity": "critical"},
]
def evaluate_policy(output: str, context: dict) -> list[dict]:
"""对输出跑全部政策规则,返回命中的规则与动作。"""
triggered = []
for rule in POLICY_RULES:
if triggers[rule["trigger"]](output, context):
triggered.append(rule)
return triggered
6.2 审计留痕:每一条护栏决策都可追溯
合规要求的不仅是拦截,还有记录。每条输出要留审计日志:
def audit_log(entry: dict):
"""护栏决策审计:谁、何时、何种输出、何种裁决。"""
record = {
"timestamp": datetime.utcnow().isoformat(),
"trace_id": entry["trace_id"],
"query_hash": sha256(entry["query"]).hexdigest(),
"output_snippet": truncate(entry["output"], 200),
"layers": entry["layers"], # 各层护栏结果
"verdict": entry["verdict"],
"operator": entry.get("operator"), # 人工复核人
}
append_to_audit_store(record)
| 审计项 | 用途 |
|---|---|
| 阻断记录 | 合规举证、误杀复盘 |
| 脱敏记录 | 隐私事件溯源 |
| 人工复核记录 | 争议处置留痕 |
| 规则变更记录 | 政策演进可追溯 |
分类器会误判。高风险裁决(如强模型复审拦截)应进人工复核队列,复核结果再回流微调分类器阈值与关键词表——形成“模型裁决 → 人工复核 → 阈值调优”的反馈闭环。
七、护栏自身的质量与性能
7.1 护栏不能拖慢主链路
内容过滤是串在生成后的必经环节,延迟必须可控。分层设计同时服务延迟与成本:
| 层 | 平均耗时 | 通过率 | 说明 |
|---|---|---|---|
| 关键词 | <1ms | 90%+ | 绝大多数内容秒过 |
| 轻量分类器 | 50-150ms | 约 10% 进入 | 边界才需要 |
| 强模型复审 | 300-800ms | <3% | 高风险才触发 |
def fast_path_filter(text: str) -> dict | None:
"""快速通道:关键词 + 轻量分类器,低延迟通过。"""
kw = keyword_screen(text)
if kw:
return {"allow": False, "reason": kw}
light = light_classifier(text)
if light["category"] == "safe":
return {"allow": True} # 快路径放行
return None # 进入慢路径复审
7.2 护栏的误杀与误放指标
护栏本身要有质量指标:误杀率(正常内容被拦)与误放率(危险内容漏过):
def guardrail_quality(golden_outputs, guardrail, labels) -> dict:
"""在标注集上评估护栏:误杀、误放、精确率、召回率。"""
fp = 0 # 误杀:正常被拦
fn = 0 # 误放:危险被放
for output, label in zip(golden_outputs, labels):
verdict = guardrail(output)
if label == "safe" and not verdict["allow"]:
fp += 1
if label == "danger" and verdict["allow"]:
fn += 1
n = len(golden_outputs)
return {
"false_block_rate": fp / n,
"false_release_rate": fn / n,
}
护栏规则变更后必须做回归测试:新规则与旧规则在回归集上对比,重点检查“旧规则放行、新规则新拦截”的样例是否误伤了白名单内容——规则演进不能以牺牲正常输出为代价。
八、实战:一个内容平台的输出护栏体系
8.1 场景与需求
场景:UGC 平台的 AI 写作助手,输出面向公众
需求:格式可解析为 Markdown + JSON 元数据
隐私:不输出任何第三方 PII
内容:不产出仇恨、色情、暴力、违禁品内容
合规:医疗与金融话题带免责声明;未成年人话题特殊处理
8.2 护栏编排实现
# content_platform_guard.py — 完整输出护栏编排
class ContentGuardrailPipeline:
def __init__(self):
self.schema = load_schema("writing_output")
self.moderation = ModerationStack()
self.pii = PIIGate()
self.policy = PolicyEngine()
def check(self, output: str, context: dict) -> dict:
# L1 结构
parsed = validate_schema(output)
if parsed is None:
parsed = recover_structured_output(output, self.repair_llm)
if parsed is None:
return self.fail("output_schema", context)
text = parsed.get("content", "")
# L2 内容
mod = self.moderation.check(text)
if not mod["allow"]:
return self.fail("moderation", context, mod)
# L3 数据
safe_text = self.pii.deidentify(text, mode="mask")
# L4 合规
rules = self.policy.evaluate(safe_text, context)
for rule in rules:
if rule["action"] == "block":
return self.fail("policy", context, rule)
if rule["action"] == "require_disclaimer":
safe_text = append_disclaimer(safe_text, rule)
return {"verdict": "PASS", "output": safe_text}
8.3 上线验证与监控
def validate_guardrail_deploy(golden, pipeline) -> dict:
"""上线前验证:质量、误杀率、误放率、延迟。"""
quality = guardrail_quality(golden["texts"], pipeline, golden["labels"])
latency = measure_avg_latency(pipeline)
return {
"false_block_rate": quality["false_block_rate"],
"false_release_rate": quality["false_release_rate"],
"p95_latency_ms": latency["p95"],
"pass": quality["false_release_rate"] < 0.001, # 误放必须极低
}
总结:输出护栏的四道闸门
| 闸门 | 职责 | 关键手段 | 失败后果 |
|---|---|---|---|
| 结构闸门 | 输出可被消费 | Schema 校验 + 恢复 | 解析崩溃 |
| 内容闸门 | 输出合规 | 三层分类器 | 不当内容外泄 |
| 数据闸门 | 隐私安全 | PII 识别 + 脱敏 | 隐私泄漏 |
| 合规闸门 | 满足法规 | 规则引擎 + 审计 | 合规风险 |
输出护栏的本质,是把“模型生成内容无法穷举”的不确定性,转化为“每一段输出都要通过可编程闸门”的确定性工程。它与输入侧防御互补:输入侧防“坏输入进来”,输出侧防“坏内容出去”。结构校验保证能消费,PII 脱敏保证不泄漏,内容过滤保证不合规,审计留痕保证可追溯——四道闸门齐备,模型才能安全地“开口说话”。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。