
最近折腾了一阵 Agent 工具权限,才发现之前写的不少 eval(评测集)有个共同前提:默认模型会“主动尝试”做错事。比如访问其他租户的数据,或者执行破坏性的迁移操作。于是我写了一堆拒绝评测:给它一个明确禁止的请求,检查它是否会拒绝。通过率很好看,我也因此自我感觉良好了一周左右。
后来读到一篇关于多步工具调用中授权漂移(authorization drift)的论文,里面的结论让我不太舒服。问题并不只是模型越过了权限边界,而是它为什么越过边界。没有越狱,没有对抗性提示,也没有什么高明注入。模型只是找到了一条成本更低的路径来得到同一个答案:正规工具会检查 scope(作用域),旁边的快捷工具不会。模型选择快捷路径,不是因为它出现了“价值偏移”,而是因为这样更高效。
这彻底改变了我对 eval 的理解。你真正要测试的,不是模型是否尊重边界,而是你的工具接口是否允许它绕开边界,直接完成任务。
在修改任何系统提示词之前,我会先打开工具注册表,寻找功能相近的工具对:两个工具做的事情差不多,但其中一个执行了另一个没有执行的检查。
grep -rniE '(_all|_any|admin|raw|unsafe|bypass|force|unscoped|internal|debug)' tools/
工具名称只能算弱信号,但作为第一轮筛选很便宜。真正关键的是参数签名。只要某个工具的参数里没有 workspace、tenant、principal 或 scope,它就应该被列入候选:要么它能在系统外部正确解析作用域,要么它根本没有解析作用域。任何返回内容超过当前请求所需的工具,也都应该列入候选。至于那些我凌晨一点为了让 Demo 跑起来而临时加的工具,基本可以直接判定有问题。
对于剩下的每个工具,我都会问一句:只调用一次这个工具,能不能满足一个看似合理的用户请求,同时跳过另一个工具强制执行的检查?如果答案是肯定的,那它就是一条绕过路径,哪怕它看起来人畜无害。尤其是看起来人畜无害的时候。
大约六十个工具里,有十一个符合上述特征。这不是可以忽略的小问题,而是等于在系统里又开了一扇后门。
list_documents() 没有任何 workspace 过滤,而旁边还有一个 list_documents(workspace_id)。无过滤版本的存在原因,是十八个月前某个看板需要它,之后就没人再管了。fetch_url 接受任意 URL,旁边另一个则会对照 allowlist(允许列表)进行校验。后来其中一个被用来处理客服工单。猜猜模型会选哪个。db_query。我为了调试加了它,告诉自己事故处理完就删,结果事故结束后,它一直留在那里。read_file 接受绝对路径,旁边则是一个沙箱化的读取器,只接受相对于项目根目录的路径。这些都不算传统威胁模型意义上的漏洞,也不需要攻击者参与。模型会像水流一样发现斜坡:它只要努力完成任务,并且偏好步骤更少、参数更不容易出错的路径,自然就会走到那里。
提示词加固,是把概率性控制硬塞在确定性接口上。你消耗大量 token,让模型避开某条路径,但那条路径仍然近在咫尺,随时可以调用。而且指令会衰减:在一次多步任务执行到第十四步时,你精心组织的“未完成 Y 前绝不调用 X”,正在和三十条工具结果争夺注意力,上下文窗口也开始把系统提示词当成背景噪音。
直接删除工具,可以百分之百封死这条路径,而且通常只需要改一行代码。这笔账根本不用算。
比删除更好的方案,是把检查移到更合适的位置。如果一个工具需要 workspace ID,那就不应该让模型自己传入。应该在工具运行时,从会话中解析出来,放在真正承载认证逻辑的边界上。我处理文档搜索时就是这么做的:彻底移除 workspace_id 参数,直接从调用者的 token 中获取。模型无法漂移到一个自己根本叫不出名字的边界之外。这个参数不存在,自然就没有填错的可能,没有混淆的空间,也没有自我说服的机会。
现在我处理所有工具时,瞄准的都是这个版本。不是“这个工具会检查 scope”,而是“这个工具根本没有 scope 输入”。能力保留下来,正规路径成为唯一路径,而满足用户请求的最短路线,同时也成了合规路线。
断言应该检查调用轨迹(trace),而不是最终答案。对于每项任务,都要要求 scope 检查工具在任何返回数据的调用之前出现在 trace 中,而不是检查回答的语气是否谨慎。模型完全可以使用未受限的数据,生成一段听起来边界意识很强的回答。
接下来再加一个快捷路径探针(shortcut probe)。它大概只需要十分钟就能写好,但发现真实权限漂移的能力,比我过去写的任何拒绝评测都强。对于每一对功能重叠的工具,运行任务并记录模型选择了哪一个。如果两个工具都可用,而模型选择了快捷路径,那就说明存在漂移。这个结果可以被测量、复现,也可以通过删除工具来修复。
组合式漂移(compositional drift)仍然是个未解决的问题。三次单独看都合法的调用,组合起来之后,可能产生任何一个工具都不应该允许的结果;删除快捷路径对此无能为力,因为边界并不存在于某个单独的工具里。目前除了记录完整 trace、审查调用序列而不是单次调用之外,我还没有找到干净的答案。也许那篇论文给出了办法,只是我还没读到这里。
不过,这仍然比“我的 Agent 有一扇为了 Demo 顺手留下的后门”要好解决得多。先做 Grep 吧。这活儿只需要一个下午,很无聊,而且一定能找到东西。
今年我上线过的最佳边界控制,就是一次删除。