失败趋关闭与失败趋开放

A36
概念 · 智能体 AI 详解

失败趋关闭与失败趋开放。

一个跑得起来的智能体技术栈里,几乎每一道控制都是失败趋开放的,而且它开放的方式会被你的仪表盘读成「干净通过」——那个超时的注入分类器什么也没返回,而「什么也没有」看起来恰好就是「没发现注入」。这才是这个概念有用的形态:真正要紧的问题不是一道控制坏掉时该拦还是该放,而是系统究竟能不能知道它坏了。把这个次序搞对,方向几乎会自己选出来;搞错,你就有了一套停机时无人可见的安全架构。

STEP 1

两个方向,借自一个「控制就是一根线」的世界。

这两个词来自物理安全工程,那里的失效模式是机构本身的属性。失败趋关闭的门在断电时锁死;失败趋开放的门则松开,好让人从着火的楼里出来。请注意两者共同的假定:失效是可被察觉的,因为那东西要么有电,要么没电。

  • 失败趋关闭(fail-secure)。检查给不出答案时,一律拒绝。你用可用性换遏制力:一个坏掉的授权器会让工作停下,而不是挥手放行。
  • 失败趋开放(fail-safe)。检查给不出答案时,一律允许。你用遏制力换可用性,理由是被保护的东西不如被挡住的东西重要。

软件继承了这套词汇,却悄悄丢掉了那个假定。一个返回空策略集的网络策略引擎并不显得坏掉了,它看起来像一个「没有适用规则」的系统。一个抛出异常、而异常在往上三层被吞掉的智能体工具权限检查,同样不显得坏掉了——调用只是继续走了下去。方向是一行代码的决定。可察觉性才是工程。

让这件事保持具体的那个区分是:一道控制的失效方向,只在它本身就是那个「做决定的东西」的位置上才有意义。如果模型无论如何都能继续往下走——因为那道「控制」只是提示词里的一句话——那它就没有失效方向。它只有一个服从率。为什么这条线是大多数团队还没画下的那条,见护栏。

STEP 2

智能体技术栈在构造上就失败趋开放,而且是三重。

这不是纪律问题。智能体被搭建出来的方式有三条结构性性质,把每一个默认值都推向「允许」,而每一条都值得一眼认出来。

  • 空结果意味着「什么也没发现」,而「什么也没发现」意味着允许。这是最大的那条,而且几乎从来不是有意写成这样的。一个没匹配到任何片段的脱敏器、一个没返回任何标签的分类器、一个没产出任何拒绝项的检索过滤器、一份在页面上没匹配到任何元素的选择器清单——它们在「成功但无事可做」和「失败」两种情形下返回的是同一个值。任何接口形如「返回问题清单」的控制,在它报错的那一刻就是失败趋开放的,除非调用方能把「空清单」和「没有清单」分开。
  • 指令没有错误路径。一条住在系统提示词里的规则不会失败,因为它根本不会运行。它只会被压过去——被更长的上下文、被更近的一条指令、被智能体刚读到的一个听起来很合理的页面。从外面看,这与一道在每个请求上都失败趋开放的强制控制毫无区别,这也正是指令层级是一种偏好而不是一次权限检查的原因。
  • 智能体会重试,于是失败趋关闭会经由人退化成失败趋开放。一个撞上被拒检查的循环不会停下;它会改写措辞再试一次,烧掉预算,最后某个有生产权限的人为了放行这次发布,把那道检查关了。一道没有快速、可读的恢复路径的失败趋关闭控制,其半衰期是以事故为单位计的。这正是急停开关围着设计的那种失效模式,也是重试策略不断与之相撞的那一种。
# fails open on every error, and the metric says 0 blocks — as designed
findings = injection_scan(doc)      # raises, caught somewhere above
if findings:                        # [] on a clean doc AND [] on a dead scanner
    block()

# the same control with a third state, which is the whole fix
verdict = injection_scan(doc)       # CLEAN | FINDINGS | UNAVAILABLE
if verdict.unavailable:
    degrade(reason="scanner down")  # a decision, recorded, with a metric

第二种写法并没有说该走哪个方向。它让方向变得可表达,而这是有意地走其中任一个方向的前提。

STEP 3

方向由这道控制「是什么」决定,而不是由风险听起来有多可怕决定。

「凡跟安全有关的都失败趋关闭」是最直觉的那条规则,而它错得足够频繁,以至于危险——因为它把两类在失效时表现毫无相似之处的组件揉成了一堆。

  • 强制点一律失败趋关闭。授权检查、检索里的租户隔离、出站代理、工具前置条件、开销与速率上限、策略引擎。这些是做决定的东西:不存在任何有意义的意义上「动作可以在没有它们的情况下继续」,而一个不可用却放行的授权器不是降级了——它是缺席了。它们的可用性就是你的可用性,所以给它们缓存、静态兜底策略和预算,而不是一个异常处理块。
  • 概率型检测器多数失败趋开放,但必须把这件事说出来。一个注入分类器、一个毒性过滤器、一个LLM 裁判、一个 PII 打分器。它们从来就没有权威答案可给;它们挪动的是一个概率。把它们设成失败趋关闭,等于把一个召回率问题变成一次停机——而且因为它们坐在每个请求的热路径上,那会是你手上能拿到的最自伤的一种拒绝服务。让它们失败趋开放,并且把这件事往下传:请求继续,但检测器的判定被标记为不可用,而下游可以把这个标记转成更低的自主档、一次人工复核,或者拒绝迈出那个不可逆的步骤。
  • 不可逆性压倒上面两条。唯一一个「检测器缺席就该让世界停下」的位置,是紧贴在一个你收不回来的动作之前——一笔付款、一次删除、一封发给客户的邮件、一次合并。不是因为检测器权威,而是因为「判断错了」的代价不再对称了。这与为失败而设计在体验层处理的是同一种不对称:在后果上关闭,在能力上开放。

这个归类练习花一小时,而它通常会翻出同样的两个 bug:一个被写成应用代码里软过滤器的授权检查;以及一个坐在热路径上、没有超时设定的检测器——它在慢的那些天里其实早就已经是失败趋开放了,因为一个五秒的预算过期了。

STEP 4

当你无法失败趋关闭时,就去缩小那次开放失败够得着的范围。

STEP 3 诚实的结论是:智能体安全面的很大一部分,无法以可接受的代价做成失败趋关闭。检测器不权威、模型的服从不算一次检查,而每一道建议性控制在压力之下都会退化成「允许」。这不是接受风险的理由;这是把「开放状态」做成可以活下来的状态的理由,而那是一个设计动作,不是一个调参动作。

  • 去框住权限,而不是去框住行为。如果注入扫描器挂掉意味着智能体可能照着一个恶意页面行动,那真正的问题是「照着一个恶意页面行动」能办成什么。范围受限、短寿命的凭据,加一份默认拒绝的出站清单,会让这次开放失败变得乏味——这正是爆炸半径的全部论点,也是这份清单里唯一一道在其他一切都倒下时仍然有效的控制。
  • 给每道控制一个与判定分开的健康信号。要对「每千请求评估过的检查次数」告警,而不是对「拦截数」告警。一个拦截率掉到零的检测器,要么是清静的一周,要么是一个死掉的进程,而除非你去数调用次数,这两者长得一模一样。见评测护栏与检测器。
  • 把降级模式做成一个真正的产品状态。「正在无内容检查下运行——不可逆动作需要审批」是一个你可以设计、测试并展示给用户的模式。以完整自主档静默地继续下去,是没人设计它时会发生的事,而它就是默认。这就是优雅降级,只是用在安全组件上,而不是用在模型可用性上。
  • 靠把东西弄坏来测它,而不是靠读代码。杀掉策略引擎、把分类器的端点黑洞掉、让打分器返回格式错误的 JSON,然后看智能体做什么。几乎每一个失败趋开放的意外,都能用这个办法在三十分钟里翻出来,而靠评审一个也翻不出来,因为代码看起来是对的——bug 在「一个空值意味着什么」里。故障注入是这件事的常设版本。

一次坐下来把清单列完:列出从用户请求到对世界产生作用之间的每一道检查,在每一道旁边写两个词——决定者还是信号,然后关闭还是开放。先修决定者,因为任何当前失败趋开放的决定者,是一个活着的授权 bug,而不是一项韧性偏好。接着挑出排名前三的信号,给它们加上第三种返回状态和一个调用计数器,好让「它缺席」变成仪表盘显示得出、下游步骤反应得了的东西。在那之前先别动这些信号的方向:知道一道控制关掉了,比关于「它关掉时该怎么办」的任何一个答案都值钱,而且这是你这周就能交付的那部分。