LLM 输出护栏与内容安全

LLM 应用里,输入可以经过精心设计的 Prompt 来约束,但输出是不可穷举的——同一个问题,模型今天答得合规,明天换了版本或换了语境就可能“越界”。输出侧的风险比输入侧更隐蔽:不是攻击者注入,而是模型自己生成的内容触碰了底线。这些底线包括格式错误(JSON 解析失败)、敏感信息泄漏(回答里带出别人手机号)、不当内容 …

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 护栏不能拖慢主链路

内容过滤是串在生成后的必经环节,延迟必须可控。分层设计同时服务延迟与成本:

层平均耗时通过率说明
关键词<1ms90%+绝大多数内容秒过
轻量分类器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 脱敏保证不泄漏,内容过滤保证不合规,审计留痕保证可追溯——四道闸门齐备,模型才能安全地“开口说话”。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「LLM」更多文章

  1. 推理增强技术工程化:CoT/ToT/ReAct
  2. 模型路由与选型:大小模型分层调度
  3. 混合检索 RAG:BM25 与向量融合