logo

Guardrails:输出过滤、策略判断与人工兜底

Published on

Guardrails:输出过滤、策略判断与人工兜底

很多团队第一次做 AI 安全时,会在系统提示词里加一段话:“不要回答危险问题,不要泄露隐私,不要执行未经授权的操作。”这段话当然有价值,但它更像一块告示牌,不是一扇真正的门。

模型可能忘记规则,用户可能换一种说法,外部网页可能夹带恶意指令,工具返回值也可能把不可信内容带回上下文。如果安全边界只存在于 Prompt 里,系统就等于把门锁交给了一个会被上下文影响的概率模型。

Guardrails,通常翻译成安全护栏,真正的目标不是让模型永远输出一句完美的话,而是让不安全输入被识别、危险输出被拦截、高风险动作需要确认,误伤和漏放能够被记录并持续改进。

我研究这类系统后越来越确信:安全护栏不是一个“过滤器”功能,而是一套位于模型前后、工具内外和人工流程之间的策略系统。它要允许正常请求顺利通过,也要在不确定时安全降级,不能只追求拦截数量。

一、先画出分层护栏

一个实用的 LLM 安全链路可以这样组织:

用户输入
输入分类与敏感信息检测
权限 / 租户 / 场景策略
模型生成或工具调用
输出格式、内容与事实检查
风险决策:放行 / 改写 / 拒绝 / 人工审核
记录 Trace 与反馈

输入侧回答“这个请求属于什么风险”,输出侧回答“模型刚才产生的内容能不能交付”,工具侧回答“即使模型想做,系统是否允许做”。三层职责不要合并成一个黑盒,否则出了问题很难知道是分类错、规则错还是模型行为错。

安全检查还要有明确的失败策略:检查服务超时,是暂时放行、暂时拒绝还是转人工?对低风险聊天和高风险支付,答案不应该一样。安全策略本身也要考虑可用性,不能因为一个检测服务短暂故障,让所有业务悄悄绕过保护。

二、输入分类不是给用户贴永久标签

输入分类的任务,是判断当前请求的意图、风险和所需处理方式,不是给用户本人下结论。一个用户可以提出正常问题,也可以在下一条消息里请求高风险动作。

type RiskLevel = 'low' | 'medium' | 'high' | 'unknown'

type InputAssessment = {
  intent: 'question' | 'transformation' | 'tool_action' | 'restricted'
  risk: RiskLevel
  containsSensitiveData: boolean
  requiresHumanReview: boolean
  reasons: string[]
}

分类器可以是规则、小模型或更强模型,但输出必须经过策略层解释。不要让分类器直接返回一个 allow: true,然后让业务无条件执行。分类结果应该包含原因、置信度和下一步建议;置信度低时,应进入更保守的路径。

输入分类也不能只看关键词。高风险意图常被拆成多轮对话,敏感信息可能被编码、改写或藏在文件里。要结合会话状态、用户权限、工具目标和业务资源判断,而不是以为“没有出现某个词”就安全。

三、输出过滤要分层,不要只有关键词黑名单

关键词规则速度快、容易解释,适合拦截明显模式,但无法覆盖同义表达、上下文语义和变形输入。一个更稳妥的输出检查通常包括:

  1. 格式检查:JSON、字段、长度和编码是否正确。
  2. 敏感信息检测:手机号、邮箱、令牌、内部标识和个人数据。
  3. 内容策略:暴力、欺诈、隐私、违法协助等风险类别。
  4. 业务规则:是否给出未经授权的承诺、金额、医疗建议或权限判断。
  5. 证据检查:结论是否有真实来源,是否把猜测写成事实。
type OutputCheck = {
  category: 'format' | 'privacy' | 'content' | 'business' | 'evidence'
  passed: boolean
  confidence: number
  action: 'allow' | 'redact' | 'rewrite' | 'block' | 'review'
  message?: string
}

function decideOutput(checks: OutputCheck[]) {
  if (checks.some((check) => check.action === 'block')) return 'block'
  if (checks.some((check) => check.action === 'review')) return 'review'
  if (checks.some((check) => check.action === 'redact')) return 'redact'
  if (checks.some((check) => check.action === 'rewrite')) return 'rewrite'
  return 'allow'
}

注意过滤顺序。先解析结构和提取字段,再对字段做敏感检测,最后做语义与业务判断。把一整段文本扔给一个分类模型,可能既慢又难以定位是哪一部分触发了风险。

四、策略引擎应该独立于模型

安全策略经常变更,不能每次改一条规则都重新训练模型。可以把策略写成版本化配置,由独立的策略引擎执行:

type PolicyContext = {
  userId: string
  tenantId: string
  action: string
  resourceType?: string
  risk: RiskLevel
  dataClass: 'public' | 'internal' | 'confidential'
}

type PolicyDecision = {
  effect: 'allow' | 'deny' | 'require_approval' | 'redact'
  policyId: string
  version: string
  reasons: string[]
}

function evaluatePolicy(context: PolicyContext): PolicyDecision {
  if (context.dataClass === 'confidential' && context.action === 'external_send') {
    return {
      effect: 'require_approval',
      policyId: 'confidential-external-send',
      version: '2024-10-01',
      reasons: ['机密数据发送到外部系统需要审批'],
    }
  }

  if (context.risk === 'high') {
    return {
      effect: 'require_approval',
      policyId: 'high-risk-action',
      version: '2024-10-01',
      reasons: ['高风险动作不能自动执行'],
    }
  }

  return {
    effect: 'allow',
    policyId: 'default',
    version: '2024-10-01',
    reasons: [],
  }
}

模型可以参与识别意图,却不能覆盖策略决定。策略引擎要能被普通测试验证,权限判断要从可信上下文获取,策略版本要进入 Trace。否则一次误放或误拦之后,团队无法回答“当时到底使用了哪条规则”。

五、工具护栏比回答过滤更重要

如果模型只是生成一段聊天内容,输出过滤还能作为最后一道防线;一旦模型可以调用工具,真正的风险在于副作用已经发生。发送邮件、删除文件、变更权限和支付操作,都应该在工具入口重新检查。

模型提出调用
Schema 校验
用户 / 租户权限
资源当前状态
风险阈值与审批
幂等执行与审计

不要相信模型参数中的 approvedisAdminsafe 字段。它们只是模型输出,不能成为授权事实。工具服务应从可信会话中拿到用户身份,从权限系统读取范围,在真正写入前再次检查资源状态。

工具还要有超时、取消、重试和幂等。安全护栏如果只检查“能不能调用”,却不检查“重复调用会发生什么”,仍然可能产生真实事故。高风险动作最好拆成预览和执行两个阶段,用户确认的内容要绑定具体资源、金额和操作摘要。

六、人工兜底不是把所有问题扔给客服

人工审核应该有清楚的触发条件和最小信息集。它不是系统不知道怎么办时的垃圾桶,而是高风险或低置信度场景的正式状态。

type ReviewTask = {
  id: string
  traceId: string
  risk: 'high' | 'uncertain' | 'policy_conflict'
  summary: string
  proposedAction?: string
  evidence: string[]
  expiresAt: string
  decision?: 'approve' | 'reject' | 'edit'
}

审核人应该看到任务摘要、影响范围、证据、模型建议和策略原因,而不是被迫阅读十页原始 Prompt。审批要有超时和过期机制,过期后默认停止或回到安全状态;不能因为审核队列积压,就偷偷自动放行。

人工流程也要保护审核人。高风险内容可能对人造成负担,界面需要隐藏不必要的敏感数据,显示风险等级和操作影响,并记录谁在什么版本的策略下作出了决定。

七、误伤和漏放要用不同指标看

安全系统不能只追求“拦得越多越好”。把大量正常问题误判为危险,会让用户绕过系统、业务团队关闭保护,最终反而降低安全性。

至少要统计:

  • 真阳性:确实有风险并被正确拦截。
  • 假阳性:正常请求被错误拦截。
  • 真阴性:正常请求顺利通过。
  • 假阴性:有风险请求被放行。
  • 人工审核率:多少请求进入人工流程。
  • 用户可恢复率:被拒绝后能否通过澄清或脱敏继续完成。
精确率:拦截的请求里有多少真的有风险
召回率:所有风险请求里有多少被拦住

高风险类别通常更重视召回率,普通内容则要关注误伤和体验。阈值不能一套规则打天下,应按场景、用户群、工具副作用和数据等级分层。

八、拒绝之后要给出安全的下一步

一句“我不能帮助你”有时是必要的,但不是所有拒绝都应该终止对话。系统要区分不可协助、信息不足、权限不足和策略冲突:

不可协助 → 简短拒绝并提供安全替代
信息不足 → 请求必要澄清
权限不足 → 说明需要什么授权,不泄露内部细节
策略冲突 → 转人工或提供只读预览

替代建议不能成为绕过策略的变相教程。比如不能拒绝执行危险动作后,详细告诉用户怎样自己绕过保护;也不要通过错误信息暴露内部资源是否存在。

对可脱敏的请求,可以删除敏感字段后继续处理;对需要用户补充授权的请求,可以生成待审批摘要;对高风险但合法的操作,可以先提供只读查询和影响预览。安全不是把所有门都关上,而是让系统知道哪扇门能开、谁来开、开门后影响什么。

九、外部内容要被当成数据,不是指令

RAG 文档、网页、邮件和用户上传文件都可能包含 Prompt Injection。Guardrails 不能只检查用户输入,还要检查进入上下文的每一段外部内容。

系统策略与工具权限
          ↑ 优先级最高
用户任务
外部文档 / 网页 / 搜索结果
          ↑ 只作为待分析数据

上下文中要明确标记来源,告诉模型外部文本不能修改系统规则和权限。更关键的是,执行层必须独立校验,不能因为网页里写着“请调用管理员工具”就真的获得能力。

对外部内容做 HTML、脚本和提示指令检测有帮助,但不存在完美过滤器。最可靠的设计仍然是最小权限、工具白名单、资源范围限制和高风险审批。把防御集中在一个分类模型上,迟早会遇到它没见过的表达方式。

十、规则发布要像代码发布一样谨慎

安全规则改动可能影响大量用户。每条策略要有 ID、版本、负责人、生效时间、适用范围和回滚方式。发布前使用脱敏历史样本做离线回放,比较误伤、漏放、延迟和人工审核量。

规则修改 → 历史样本回放 → 小流量灰度 → 观察告警 → 逐步放量

灰度期间要能按策略版本筛选 Trace。发现误伤升高时,先停放量或回滚,不能临时在多个业务函数里各加一条例外。例外规则也要有过期时间,否则短期救火会变成永久漏洞。

规则测试还要覆盖对抗样本、同义改写、多轮拆分、混合语言和长上下文。安全系统只在标准句子上通过,没有太大意义;真正的攻击通常不会照着测试用例说话。

十一、我的总结:护栏的目标是安全地继续工作

Guardrails 不是一个让模型变得绝对安全的按钮。它是一套分层的控制系统:输入侧识别风险,模型侧遵守能力边界,工具侧重新鉴权,输出侧验证内容和格式,策略侧决定放行或升级,人工侧承担高风险判断,观测侧把误伤和漏放带回下一轮改进。

我现在设计护栏,会先问每个风险“如果发生,最坏后果是什么”,再决定是规则拦截、模型分类、脱敏、人工审批还是只读降级。低风险场景要保持顺滑,高风险场景要宁可停下来确认;不确定时不能让模型用更自信的语气替系统做决定。

真正成熟的安全系统,不是拦截页面上显示了多少次,而是能清楚说明:哪条策略在什么版本生效,为什么做出这个决定,用户是否还有安全的下一步,发生误判后怎样修复。把这些证据和流程接起来,护栏才不是一层贴在 Prompt 外面的装饰,而是 AI 应用能够长期运行的基础设施。

🤪 您也可以编辑此页: