哪些权限不该给 Agent:按副作用划分,不按能力划分
一个人带一个 Agent 干活的时候,权限这件事根本不成为问题。它能调什么工具、能改哪些文件,你心里有数,出了岔子回退一步就行。
问题是从第二个 Agent 开始的。同一个仓库里同时跑着几个代理,各写各的文件,看上去互不相干。然后某一次构建突然失败了,报错指向一个文件,而那个文件的内容读起来像半句话——它正被另一个代理写到一半。再然后,一次提交完成了,退出码正常,回头一看,某个代理产出的文件根本没进去。
这时候你会本能地去想「权限」。我一开始划权限的方式是按能力划的:这个代理是干工程活的,那就给它构建和提交的权限;那个只写文案,那就只给写文件的权限。这条线划下来,看着挺合理,然后照样崩。
因为「谁有能力做」和「谁做了会出事」是两件事。
判据只有一个问题:这个动作碰不碰共享状态
我现在划权限,不看角色,只对着每一个具体动作问三句话。这三句话不需要任何理论,读到这里就能拿去对自己手上的流程问一遍:
第一句:这个动作写的东西,同一时刻还有别人在写吗?
代理各自写各自的 markdown 文件,答案是没有。文件名不同,互不覆盖,一个写砸了不影响另一个。而版本控制的提交作用在一份共享的索引上,答案是有。
第二句:这个动作读的东西,此刻正在被别人改吗?
这一句最容易漏,因为「读」听起来天然安全。构建是个典型:它本身不改内容文件,只是把整个内容目录读一遍。但它读的那个目录,正是所有代理同时往里写东西的地方。读一个正在被改的东西,和写一个正在被改的东西,后果是一样的——你会读到半成品。
第三句:这个动作出了问题,能不能只回滚它自己?
一个代理写坏了自己那份文件,删掉重写就行,影响范围是它自己。构建产物目录被两个会话同时写,出问题的时候你分不清哪一份是谁的,也没法只撤销其中一份。
三句话问完,动作自然分成两栏:只影响自己产物的,和会触碰共享状态的。判据不是「谁有能力做」,而是「哪些动作对共享状态有副作用」。 写各自的文件没有副作用;构建和提交有。
注意这条线不等于「读 / 写」那条线。构建从内容目录的角度看是读,从输出目录的角度看是写;提交从工作区的角度看是读,从索引的角度看是写。按读写划会划错,按副作用划才划得对。
判出来之后有两种处置,不是只有一种
分完栏之后,我一开始只想到一个办法:把有副作用的动作收走。这是其中一种,还有另一种。
处置一,收归单点串行。 我这条流水线上,这一条最后落成了一句写进流程文档的约束:
子代理只写 md,构建与版本控制由主导者独占。
这句话的第二半是被实测逼出来的。我另外记过一条约束,理由写得很直白:代理运行期禁止构建,先查后构建存在竞态,实测崩在一个正在写入的半成品文件上。「先查后构建」的意思是,先看一眼有没有代理还在跑,看着没有了再构建——这个逻辑本身就是漏的,因为你查完到你构建之间,那个空档里代理可以随时开始写。所以最后不是「查一下再构建」,是「代理运行期一律不构建」。
处置二,把共享资源本身消掉。 不是所有有副作用的动作都必须收归一处。我遇到过另一种情况:多个会话并发在同一个仓库里干活,各自都要构建,构建输出目录是同一个,于是互相覆盖。这个动作确实有副作用,但它的副作用来源是那个共享的输出目录——给每个会话指定各自独立的输出目录,副作用就消失了,也就不必排队。
所以完整的处置是:先按副作用判,判到有副作用的,再问一句「这个共享的东西能不能拆开」。能拆就拆,拆不开才收归单点串行。 提交索引拆不开,所以只能串行;输出目录拆得开,所以隔离就够了。
这两种处置的成本差别很大。收归单点意味着并发度被主导者卡住,代理再多也得排队等那一个环节;隔离资源不损失并发。能用第二种就别用第一种。
边界:什么情况下这条判据不适用
按副作用划权限是有明确适用范围的,范围不交代清楚,这条判据很容易被拿去用在不该用的地方。
只有一个 Agent、或者严格串行的时候,这套划分是纯开销。 副作用之所以致命,前提是「同一时刻有别人」。串行执行的流程里不存在这个前提,你把构建交给谁都一样,多划一层权限只是给自己添麻烦。
共享状态本身不存在的时候也不适用。 如果每个代理跑在各自独立的工作副本里,写、构建、提交全在自己那份里完成,最后由人来合并,那就没有共享状态可言,三句话问下来全是「没有」。这时候真正的成本转移到了合并环节,那是另一个问题,不是权限问题。
这条判据只回答「能不能并发做」,不回答「做得对不对」。 这是最容易误用的地方。把构建收归主导者独占之后,构建不会再崩在半成品文件上了——但构建通过不等于事情办成了。命令正常退出、退出码是 0、脚本打印了成功,这些和产物真的对不对是两回事。我这条产线上出过的事故里,有相当一部分是这一类:没报错,但事情没办成。权限划分解决的是「互相踩踏」,解决不了「静默失败」。后者要靠回查目标状态,跟谁有权限做没关系。
还有一种情况判据会给出反直觉的答案:有些看起来很危险的动作其实没有副作用,有些看起来很轻的动作反而有。 代理写一个新文件,动作听着挺重,但它对别人零影响;主导者顺手跑一次构建,动作听着挺轻,如果此刻有代理在写,它就是那个会崩的人。判据不看动作的分量,只看它落在谁的地盘上。
最后提醒一句我自己吃过亏的地方:这条线划完要写下来,写成任务文档里的一句约束,而不是留在脑子里。因为每一次派活都要重新交代一遍权限边界,写不下来就一定会漏掉某一次;而且约束背后的理由——「实测崩在半成品文件上」——必须跟约束写在一起,否则下一个人(或者下一次的你)会觉得这条限制多余,顺手就把它去掉了。
这篇的判据来自我自己带 Agent 干活的实践,样本有限。它更像一份可以拿去验证的假设,而不是一份可以照抄的规范。
延伸阅读
- 上一篇(接手脚):Agent 的能力清单该怎么列
- 下一篇(接手脚):Agent 的关键外部能力要预留降级路径
- 专题导读与七个阶段的地图:把 Agent 当成一个要上岗的员工来带:这个专题讲什么