跳到主要内容

护栏系统的自研与外购:内容审查 API 已成为安全关键路径上的核心依赖

· 阅读需 11 分钟
Tian Pan
Software Engineer

你为了加快上线速度而购买的托管审核 API,现在已经成了你安全关键路径上的一个同步外部依赖。这句话并非观点——而是被如实重绘后的架构图。在供应商服务降级的日子里,你面临两个选择,且两者都很糟糕:故障开启(fail open),此时护栏在最需要的时候恰恰失效了;或者故障关闭(fail closed),护栏的故障直接导致了功能的停摆。大多数团队是在事故发生时才发现自己选了哪一个,而不是在此之前。

团队选择供应商的原因并非因为懒惰。在内部构建内容分类器、提示词注入检测器和 PII 脱敏工具,看起来像是背离实际产品开发的六个月漫长弯路,而供应商通常提供免费额度和五分钟即可完成的集成。这种集成确实很快。但随之而来的架构后果是,第三方现在介入了每一次面向用户的生成请求路径,其可用性、延迟和行为特征是你无法控制且未曾建模的。

这篇文章的主旨是将这一决定视为架构决策,而非采购决策。

故障开启 / 故障关闭的抉择是关键决策,而非默认设置

当托管护栏超时、返回 5xx 错误或开始表现异常时,你的代码必须做出响应。两个默认选项是故障开启(未经检查直接放行请求)和故障关闭(拦截请求)。团队几乎总是通过一行配置更改来选择其中之一,且几乎总是在没有记录权衡代价的情况下做出的选择。

故障开启意味着在供应商服务降级的瞬间,你的安全控制就成了摆设。护栏专门设计的防范请求会在你最需要检查的窗口期长驱直入,因为护栏的失效模式往往与流量异常相关,而流量异常通常又与滥用行为相关。故障关闭则意味着你将产品的可用性与供应商绑定。护栏 99.9% 的 SLA 设定了你功能 SLA 的上限,“护栏供应商宕机”会演变成你的值班人员必须响应的事故,尽管你的模型或数据本身毫无问题。

正确的答案很少是“二选一”。这是一个与特定检查的尾部风险相关的、针对每个策略的决策。一个剥离明显 PII 的输出脱敏层或许可以故障开启,辅以日志记录和异步重试,因为在 10 分钟窗口内漏掉脱敏的代价虽然烦人,但是可控的。而一个针对能退款或发邮件的工作流的越狱(jailbreak)分类器,则应该故障关闭,因为漏掉一次越狱攻击可能会导致六位数的事故损失。这种决定应该体现在代码中,并附带解释权衡的注释,而不是隐藏在 SDK 的隐式默认行为里。

决策框架:按尾部风险类别划分,而非按功能划分

当你不再问“我们是否应该购买护栏”,而是开始问“对于哪些失效模式,供应商的回归测试失败是可以接受的?”时,自建还是购买的决策会变得更加清晰。

购买广泛的低风险覆盖。 托管审核 API 在高流量类别(如色情、仇恨言论、暴力、自残)上经过了良好的调优,且供应商的评估套件可以作为你这些类别流量的合理代理。供应商服务回归的代价是几天内稍差的用户体验,这是你可以承受的。在内部构建这些意味着要耗费数个工程季度来重造轮子,而且效果可能不如供应商,因为他们见过的流量更多。

针对那些尾部风险极高的失效模式构建专用护栏,因为这些模式下的供应商回归会导致事故级的后果。典型的例子包括:特定领域的 PII(你客户的保单号、内部账户格式、架构中的私有标识符)、工具调用授权检查,以及与你特定的合规性要求挂钩的拒绝策略。供应商不了解你的流量,不具备与你一致的动力去捕捉你的极端情况,也不会在他们的分类器在你关心的类别上悄然退化时主动告知。拥有这些护栏意味着你可以针对自己预留的事故案例编写评估方案,这是唯一能真正捕捉到你曾遇过的失效模式的闭环。

混合模式才是现实的选择。针对通用伤害使用购买的“宽网”;针对那些在事故复盘会议上反复出现的失效模式,构建自己的专用护栏。

购买护栏真正需要的集成纪律

如果你要把供应商放在安全关键路径上,那么集成工作绝不仅仅是“添加一个 SDK 调用”。这是一个包含三个部分的分布式系统小问题,而团队通常会跳过这些:

一个能度过供应商宕机期的缓存层。 由相同的护栏、相同的策略版本评估的相同提示词,不应产生不同的结果,因此请以 (input_hash, policy_version, vendor_version) 为键,设置短 TTL 的缓存。这不仅仅是为了节省成本——它是一个关键机制,让你在供应商服务降级时仍能处理大部分流量,并为以后的影子评估(shadow evaluation)提供基础。

针对你自己的预留测试集进行影子评估。 每一个护栏供应商都会悄悄更改他们的分类器。类别可能保持不变,但校准水平会发生偏移。如果没有你自己标注的内部评估集,供应商的偏移在客户投诉之前是不可见的。定期针对同一组固定案例运行线上供应商接口,跟踪每个类别随时间变化的分数差异,并对回归进行报警。这也是你发现新的“全能审核”模型在你的产品所依赖的那个特定类别上其实变差了的方式。

一个明确的、成文的降级策略。 当缓存未命中且供应商宕机时,会发生什么?对于某些类别,“故障开启并在日志中标记”是一个合理的答案。“路由到阈值更宽松的内部小型分类器”是另一个选择。“拒绝请求并显示维护信息”是第三个。错误的答案是大多数团队目前采用的:一种随意的回退行为,即 SDK 的 HTTP 客户端抛出异常时的默认反应——如果给开发者十分钟时间思考,几乎没人会写出这种逻辑。

这三个部分大约需要两个工程师周的工作量。跳过它们就是“使用供应商”与“以一种直到出事才显现的方式深度绑定供应商”之间的区别。

团队很少诚实预测的拥有成本计算

购买决策通常始于一个对比:供应商每千次请求 $X 美元,对比自研分类器需要花费 Y 个工程师月。这种对比在两个地方会失效。

供应商的定价随流量线性增长。而自研护栏的构建成本会随着业务量摊销,每请求成本主要由你无论如何都要支付的 GPU 或 CPU 成本决定。公开估算显示,托管护栏和安全过滤的开销在底层模型成本之上还要增加 10–30% 的 Token 支出,而自定义的 NeMo Guardrails 或 Llama-Guard 式部署的成本在每 1K 请求 0.20 美元到 0.80 美元之间。在某个流量阈值以下,购买更便宜。超过该阈值,购买比自研更贵,而且一旦你把购买的护栏自身的观测性、重试预算和值班负担计算在内,这个交叉点比采购 PPT 上显示的要近得多。

诚实的预测必须包含流量增长。一个在 18 个月内达到交叉点并忽略它的团队,最终会面临一份失去谈判筹码的供应商合同,因为供应商知道 Prompt 和策略不会按季度频率迁移。如果“自研 vs 购买”的框架将流量视为持平,那么就是在为未来一次团队既没资金也不乐意做的迁移埋下伏笔。

对比失效的另一个地方是假设供应商的定价模式是稳定的。多年来,托管审核一直被定价为其他服务的近乎零成本的附加组件;这是一个市场事实,而非物理事实。一旦供应商决定将安全性视为利润中心而非引流产品,你的单位经济效益会因为一封邮件而改变。

自研端的失败模式:“这只是一个 Prompt”

自研的论点通常在纸面上取胜。但在实践中,当自研护栏因为“这只是一个 Prompt”而被 Prompt 团队接管时,这个论点就输了。这是自研路径最常见的失败模式,值得点名提醒。

由 Prompt 团队拥有的护栏会继承该团队的评估盲点。Prompt 团队针对中位数情况进行优化,因为他们构建的评估套件就是中位数情况的评估套件。而护栏在长尾情况下会失效。由 Prompt 团队拥有的护栏看起来很棒,直到值班工程师在六个月后抽取了一份真实拒绝案例的样本,发现了一类评估集从未见过的越狱攻击。更糟糕的是:护栏 Prompt 已经被反复修改以修复个别客户的报告,现在已经没人能重构出它实际执行的策略了。

修复方法是结构性的,而非文化性的。自研护栏需要自己的评估集、自己的负责人和自己的变更管理规范——与调用模型的 Prompt 分开。评估集应包括红队用例、真实的生产拒绝案例,以及一个 Prompt 团队不能针对其进行调优的留存测试集。一旦决定自研,将护栏视为与 Prompt 不同的工件,是团队能做的最有用的一件事。

护栏是特定于产品的约束,而非通用基础设施

有一个框架能让整个决策变得更容易:护栏不是通用基础设施。它们看起来像通用基础设施,是因为它们被包装成了那样——审核 API 的形态与 TLS 终止服务或 CDN 相同。但这个类比是失败的。CDN 对你的产品没有意见;而审核 API 有,你的产品也必须有回应,因为其执行的策略是你产品团队才能做出的决策的下游产物。

“哪些类别重要、阈值是多少、失败时如何回退”这些具体问题属于产品策略,而非基础设施采购。购买决策并没有外包这些问题——它只是将答案外包给了一个其答案必然是通用的供应商。对于通用答案就足够的类别,这没问题;但对于不适用的类别,这就是个严重的问题。

因此,精确定义的“自研 vs 购买”问题不是“哪个更便宜”或“哪个发布更快”。而是“我愿意委托哪些约束?”答案可以根据类别、策略、表面层级而有所不同——一旦你这样界定,混合架构就自然而然地形成了:一个薄网关,通过购买的广撒网式审核、自研的窄口径分类器和显式的回退策略来路由输入和输出,并利用影子评估和缓存结果使供应商部分变得可控。

那些被坑的团队,是那些为了速度选择供应商,却从未写下回退策略、从未运行过影子评估,并在第一次事故中发现第三方掌控了其产品安全故事中承重部分的团队。而没被坑的团队则将购买决策视为任何其他架构承诺:一个需要缓存、观测性、评估套件和回退方案的东西。供应商不是问题——无人负责的耦合才是。

References:Let's stay in touch and Follow me for more thoughts and updates