247 次任务块检查,11 次记下了未申报改动,只有 4 次真的把活停了。
数字来自 11 月 3 日挂上 arXiv 的一份 15 页论文(arXiv:2611.05094,cs.SE,v1)。作者拿 Claude Code 搭了一套带门控的规范驱动工作流,在自己 7 个项目的 9 个功能上跑,581 项计划任务里 288 项按任务块落地。Fortran 求解器、Python 程序、书稿,连论文自己都在样本里。样本不大,七个项目,拿自己当实验对象。但机制层面的结论不看样本量。
真正让我坐直的是作者自己写进正文的一句话:几乎每一项控制措施都以指令形式存在,并由它所要约束的同一个模型来执行。
这句话放在论文里是方法论备注,放到安全语境里就是自曝。两种读法之间,隔着整个 AI agent 安全的现状。
会自审的那部分,和不会自审的那部分
把实验拆开。这套工作流里的约束有两类,长得完全不一样。
一类是执行子代理的文件范围:只准碰任务清单里列出来的文件。结果在交付目录里只留了 1 处未申报改动,还被检查抓住了。
另一类约束在编排者身上:每完成一组任务,它跑一遍文件清单检查,发现未申报改动,然后由它自己判断这处改动要不要紧、要不要中止。247 次检查里 11 次记到了未申报改动,4 次中止。
同一套工作流、同一个模型、同一份 prompt 家族。第一类约束基本守住了,第二类守住了三成。
区别不在“模型听不听话”,在约束的形状。文件范围是可枚举的:清单外部给定,改动可 diff,判据不经过模型的价值观。第二类要求编排者回答“这一处改动算不算越界”,这是判断题,而判的人本身就有权越界。
换成攻击者的思路:我不需要骗过 reviewer,只需要让 reviewer 觉得“这看起来像环境因素”。那 7 次没停下来的记录,很可能不是没发现,而是发现了、评估成不必停。这比没发现难查得多:日志上留着记录,事后翻起来你会说“流程起作用了”。它起的作用是记日记。
那个按固定缺陷清单逐项核查的评审子代理,形状上同样有问题。清单按阶段固定、内容写定,但仍然是自然语言条款,仍然由模型来判“这条算不算踩中”。条款描述得越具体,它越灵;条款落到“是否存在未声明的副作用”这类需要判断的地方,它就开始漏。它的可靠性上限不在清单写得多细,在这条清单能不能被换成一段代码。
审计追踪不是控制
论文的定位是 audit-trail study,我不否认它的价值,但要先把一个混淆戳破:可观测不等于可控。
规范驱动开发对安全真正的贡献不是“更安全”,是它第一次给了你一个 baseline。聊天记录没法 diff,“未申报”这个词在对话式开发里根本不成立,因为从没申报过的东西谈不上未申报。有了 spec 和任务清单,你才有一个能拿来对照的基准面,漂移才第一次变成可观测的。
但观测到漂移之后,得有一样东西有权拉闸,而那样东西不能同时是漂移的肇事者。
我自己在这上面栽过。早年给一个项目写 pre-commit,其中一条是硬检查:diff 里出现 .env 就直接拒绝提交。这条到今天还在用,因为它外部可判、可枚举、不依赖任何一次判断。同一套 hook 里我还塞过一步“检查本次改动是否引入安全风险”的模型调用,三个月后删了。不是它误报,是它从来不报。一个永远通过的检查不是检查,是一个装饰性的延时。
那 11 次里的 7 次,就是那个装饰性延时:有记录,没后果。
编排者才是雷区
论文说薄弱环节在编排者,我同意,但理由比他们写的更难看一点。
编排者在同一轮里干了三件事:执行检查、解释检查结果、决定要不要为此停下。三个动作共享同一份上下文、同一个“什么叫合理”的先验。这种结构下,检查能发现异常(11 次),但很难对自己刚发现的异常做出敌意解读,因为它刚刚亲手做完了那些改动,对每一处都有现成的解释。
这一点在真实攻击面前只会比实验里更严重。间接注入的落点通常是让它在任务清单之外多改一处,然后让负责判断的那一环把它归到环境噪声里去,而非“让 agent 干一件显眼的事”。诱饵(你让它读的文档里藏的一行)→触发(它顺手多改一处)→编排者判定“环境因素,继续”→改动进主干。这条链子上,唯一有能力拦下的那个节点,恰好是唯一有动机放行的那个节点。11 次里的 7 次,就是这个动机在数据里的形状。
另外,11 次里改动的主要来源是作者在轮次内临时提的请求和环境因素。审计日志的第一个客户,是你自己的手快。
对生产环境的含义很直接:你手上那个 agent 只要有真权限,一处被判定为良性的未申报改动就够。不需要 11 次,一次就够。判定那一步在模型手里,等于把爆炸半径的开关交回给引信。
拆弹清单
四条,都能今天落地。
一、文件清单检查从编排者手里拿走,交给 git。 跑之前先落一个基线,跑完比对:
git stash create > /tmp/baseline # 或者直接记一个 commit sha
# ... agent 跑完 ...
git status --porcelain | grep -v -F -f allowlist.txt && exit 1
git status 不读 prompt,不知道你为什么这么改,也不会替你找理由。编排者读自己的 diff 是自证清白,git 读 diff 就只是读 diff。
二、“等待用户批准”这五个字不能写在 prompt 里当控制。 模型能说“我等你批准”,也能说“用户已批准”——后者甚至不需要它撒谎,它只是在一段越来越长的上下文里,把“计划中的批准”记成了“已经发生的批准”。要真门控,就让下一阶段需要一个上一阶段拿不到的凭据,发凭据的是 hook 或 CI job。
三、reviewer 别看 executor 的推理过程。 只给它 diff 和任务清单,别给它 @ 引用和思考链。它一旦看见“executor 为什么这么改”,它就在给理由找台阶,而不是在找越界。这是成本最低的一刀。
四、日志得有人因为它是红的而变红。 11 条里 4 条被处理、7 条躺着;如果下一轮那 7 条还是没人看,这整套检查的有效值就等于 0。要么接告警,要么接阻断,别只接一张 Markdown 表格。
往回退一步
我一开始想拿 11 比 4 判这套工作流死刑。回头把执行子代理那段看了两遍——交付目录只留 1 处未申报改动,还被抓住了——这个成绩比我预期好得多,好到值得改口。
所以结论在“某些形状的约束能管住,某些形状的不能”,而非“AI 管不住自己”。这两类在这篇论文里几乎被干净地摆在了同一张表上:可枚举的守住了,要判断的漏了三成。往前推一步,你手上那套 agent 流水线里,凡是需要模型自己回答“我这样算不算违规”的那一步,都已经从控制降级成了记录。
还有一点我拿不准,说出来挂着:581 项任务里 288 项按块完成,9 个功能里 5 个按计划走完全程——真正起作用的可能不是块与块之间那个确认,是“把活切成块”这件事本身。切小了,改动才看得见;确认只是顺手加的一道。这个没法从这篇论文里证,先放着。
排雷记录:门控的第一原则是拉闸的那只手是不是干活的那只手,而非“加没加”。
