← 返回
安全技术

AI 急停开关怎么设计:靠中间件拦截不如让能力本身不存在

✍️ zhirenhun 📅 2026/11/4 👁 5 阅读 ⏱ 22 分钟
AI 急停开关怎么设计:靠中间件拦截不如让能力本身不存在

“我该用哪个护栏框架?”——对紧急关停开关来说,这个问题本身就问错了。世界上最好的拦截器,拦截的仍然是已经存在的东西。

代码之前先做个简短披露:我在 KarmaBox 工作,这个产品正是按本文推荐的方式强制执行 Agent 权限的,所以我是在为自己的产品说话。有三点可以让各位自行核查:下面的模式是约 40 行无依赖的 Python,你可以自己运行、自己攻击;每条新闻事实都注明了来源和日期;最后一节列出了这个模式解决不了的问题——包括我雇主同样没解决的那部分。

为什么偏偏这周值得你花一个晚上读这篇文章:11 月 3 日,Google 宣布,学校如果关闭其 AI 功能,就会连 Google 搜索一起失去——据其教育业务副总裁所说,“搜索和 AI 已经深度整合”(TechCrunch,2026 年 11 月 3 日)。先不谈政治。作为工程师,不妨把它读成一份架构层面的自白:他们把关闭开关做进了一个一旦拨动就会让整栋楼断电的层。本文讲的就是不要造出这种东西。

先定义,再写代码

真正的紧急关停开关是移除一项能力。它不隐藏、不拦截,也不指示模型拒绝——它让这项能力对所有调用方、通过所有路径都无法触达。如果“关闭”实现在某个位置,而这项能力仍能被调用,那你拥有的只是一道闸门:嘴上说着不行,背后的线路却依然通着电。

这个区别很重要,因为闸门和“不存在”的失效方向正好相反。闸门一旦崩溃、配置错误或被绕过去,就会失效开放——能力照常运行。而“不存在”不可能失效开放,因为开关另一侧根本没有东西可运行。凌晨两点,差别就在这里。

“关闭”的两种架构

大多数 Agent 技术栈把“关闭”实现为中间件:工具保持注册状态,由策略层拦截调用。

# Architecture A: middleware gate
agent → tool_call("send_email") → policy middleware → [allow? deny?] → send_email()

# Architecture B: registry absence
agent → tools_for(role)  →  send_email was never in the list

在架构A中,send_email存在于Agent的工具列表里。模型能看到它、围绕它做规划、尝试调用它,由中间件在调用时决定是否放行。而在架构B中,工具面按角色从注册表组装,被关闭的能力压根不会被组装进来。模型调用不了它看不见的东西,藏在文档里的提示词注入也同样无能为力。


两种失败模式对比,并排呈现:

其中一行值得特别强调,因为本周的事件让它变得具体:审计问题。谷歌的公告还声称学校可以逐项开关每项服务——但两个月后,学区想核实究竟关掉了什么,唯一依据就是控制台显示的内容。一个无法独立审计的开关,不过是带界面的承诺。注册表让审计变得廉价:组装出来的列表就是事实依据。

约40行代码实现的模式

零依赖,三级权限:direct(直接运行)、approval(人工批准后运行)、absent(完全不组装)。

# registry.py — capability registry with three tiers
from dataclasses import dataclass
from typing import Callable

TIERS = ("direct", "approval", "absent")

@dataclass(frozen=True)
class Capability:
    name: str
    fn: Callable
    tier: str  # one of TIERS

class Registry:
    def __init__(self):
        self._caps: dict[str, Capability] = {}

    def register(self, name, fn, tier):
        if tier not in TIERS:
            raise ValueError(f"unknown tier: {tier}")
        self._caps[name] = Capability(name, fn, tier)

    def tools_for(self, role_policy: dict[str, str]) -> dict[str, Capability]:
        """Assemble the tool surface for one role.
        A capability whose tier is 'absent' is NOT in the result.
        There is no gate to bypass — it simply isn't mounted."""
        surface = {}
        for name, tier in role_policy.items():
            cap = self._caps.get(name)
            if cap is None or tier == "absent":
                continue  # never mounted
            surface[name] = Capability(name, cap.fn, tier)
        return surface

    def audit(self, role_policy: dict[str, str]) -> list[tuple[str, str]]:
        """Export the effective capability list — this is the audit artifact."""
        mounted = self.tools_for(role_policy)
        return sorted(
            (name, mounted[name].tier if name in mounted else "absent")
            for name in role_policy
        )

def dispatch(surface, name, approver=None, **kwargs):
    cap = surface.get(name)
    if cap is None:
        raise LookupError(f"{name}: not a capability of this role")  # fail closed
    if cap.tier == "approval":
        if approver is None or not approver(name, kwargs):
            raise PermissionError(f"{name}: approval required, none given")
    return cap.fn(**kwargs)

关于几条关键代码的说明。tools_for 是“关闭”生效的地方:absent 的能力在装配阶段就被跳过,因此模型的工具列表——也就是你序列化进 API 调用的那份——永远不会包含它。dispatch 对超出范围的一切都失败关闭:未知或不存在的工具会直接抛错,而不会落到默认处理器上。至于 audit,它之所以存在,是因为 Google 那个故事给出的无声教训是:控制台和现实会脱节;你的审计器拿到的是装配后的列表,而不是设置界面的截图。

三项测试,以及每项失败意味着什么

def test_absent_is_unreachable(registry):
    surface = registry.tools_for({"send_email": "absent"})
    assert "send_email" not in surface
    try:
        dispatch(surface, "send_email", to="x@y.z")
        assert False, "dispatch must raise"
    except LookupError:
        pass

如果这一步失败:说明你的“关闭”是一道闸门,而非真正的不存在——某处仍有一条回退路径能解析到该工具。正是这个测试区分了两种架构;其余都只是装饰。


def test_approval_blocks_without_human(registry):
    surface = registry.tools_for({"post_public": "approval"})
    try:
        dispatch(surface, "post_public", body="hi")  # no approver passed
        assert False, "must require approval"
    except PermissionError:
        pass

如果这一步失败:说明你的审批层级已经悄悄降级成了直接层级——这正是表格中中间件一列所预测的那种偏移。你很可能会发现,有人在某次事故处理期间加了一个“临时”默认审批人。

def test_audit_matches_surface(registry, role_policy):
    audit = dict(registry.audit(role_policy))
    surface = registry.tools_for(role_policy)
    for name, tier in audit.items():
        assert (name in surface) == (tier != "absent")

如果这一步就失败了:你的审计产物在说谎。这比没有审计更糟,因为人们会信任它。先修好这个问题,再去修它报告的任何东西。

审计端点

把 registry.audit(role_policy) 暴露为内部端点或 CLI 命令,并在每次部署时生成快照。再进一步,把快照做成 CI 产物:用一个 job 把组装出的能力面与上一个发布版本做 diff,只要出现未经评审的扩权就让构建失败——某个能力从 absent 变成任何其他状态,或者某个角色里冒出新工具。收窄可以自动通过;扩权必须有人署名。这样一来,“3 月 12 日这个 Agent 能做什么”就变成了一个 git log 问题,而不是考古项目。当厂商告诉你某项能力“可以关掉”时,这就是你该向他们索要的产物——实际生效的能力清单,而不是设置界面的截图。(对此刻正在向 Google 追问这个问题的学校来说:根据 11 月 3 日的报道,Search-AI 的答案是,只要 Search 开着,就永远不会有这样一份清单把它暴露出来。接线方式本身就是策略。)

还有一个值得了解的方向,同一天发布:Anthropic 开源了 FlashProve,研究用密码学证明某段输出由哪个模型生成——按模型发布指纹,不泄露权重,可在本地验证(TechCrunch,2026 年 11 月 3 日)。注册表审计告诉你挂载了哪些能力;可证明推理指向下一层——证明这些能力背后是哪个模型。目前处于研究阶段,尚无部署;但这就是审计正在缓慢爬向的终局。

迁移现有技术栈:绞杀者路径

没人能在这个 sprint 里重写自己的 Agent 平台,所以下面是我们真正推荐的渐进路径——经典的绞杀者模式,用在工具面上。四个阶段,每个阶段都能独立上线。

第一阶段:盘点各道关卡。用 grep 找出今天所有拦截工具调用的地方——中间件、装饰器、dispatcher 里那片 if 语句坟场。每个拦截点都是一项自认为有开关的能力。把它们连同出错时的当前默认行为列出来:你至少会发现一个 fail open 的,找到它就是这次迁移的第一个收获。

第二阶段:包装,而非替换。在现有网关前面引入注册表。注册表负责组装工具面;旧网关原地不动,如今守的是上游已经过滤过的能力。什么都不会坏,因为什么都没删——你只是加了一道更严格的过滤器。

第三阶段:双跑,并加不匹配告警。跑几周,记录所有这样的情况:旧网关拦下了注册表已挂载的东西,或者——更有意思的方向——注册表排除了网关本来会放行的东西。每一处不匹配,要么是策略 bug,要么是没写下来的依赖。第二种我们会当作本周 sysadmin 帖子里那个 accounting-filters 发现来对待:没人记录过、却在承重的耦合证据。

第四阶段:按角色逐个切换,然后删除。一次把一个角色迁到只由注册表执行,从风险最低的角色开始。最后删网关,不是最先删——通过注册表够不到的死代码只是烦人;注册表还没覆盖就删掉的网关,就是事故。

总有人提的两个反对意见,老实回答。延迟:基本没有——组装在会话开始时做一次,不是每次调用都做;tools_for 就是个字典过滤。运维成本:确实有,现在改一个能力要发部署,而不是改配置开关。这不是要尽量缩小的缺点,这恰恰是重点。一次能力变更留下 diff、评审人和回滚路径,这就是审计线索——跳过这三样的配置开关,正是故障表里中间件那一列的成因。

这个模式解决不了什么

诚实环节,包括对我雇主的诚实。注册表模式解决不了判断:哪个能力该归哪一层是人来定的,判断错了,执行再完美也还是错。解决不了已经发出去的数据:你今天卸掉的能力,上个季度该干的都干完了。解决不了间接影响:direct-tier 工具在自己的范围内照样能被滥用(搜索工具可以靠查询外泄数据;注册表看不见)。也给不了你密码学验证——我的审计端点是你必须信任的日志;FlashProve 那个方向才是最终的解法,目前还没人交付,包括我参与的产品。

值得带回你自己技术栈的问题:你的 Agent 现在能看到的每个工具——如果策略服务凌晨两点挂了,它会往哪边失败?

来源

  1. TechCrunch(Aisha Malik),《谷歌将允许学校禁用其AI工具,但代价是牺牲搜索功能》,2026年11月3日

  2. TechCrunch(Russell Brandom),《Anthropic正在开发一套验证AI模型身份的系统》,2026年11月3日。FlashProve属于研究项目,代码已公开,尚未部署。

  3. 本文代码仅作示例,不依赖任何外部库,专为本文编写——你可以运行、修改,发现问题欢迎告诉我。

——

广告 广告
🧑‍💻

zhirenhun

一个热爱技术的程序员,喜欢分享前沿AI知识和开发经验。

ai architecture security python