Auto Mode 权限审批实战:分类器、分层配置与生产兜底
1. 先搞清楚 Auto Mode 到底替换掉了什么
Auto Mode 这个东西,我最初是抗拒的。原因很简单——权限审批这道闸门,是人和工具之间最后一道物理隔断,一旦交给分类器去判断,等于把"要不要让它动手"这个决定权让渡给了一个概率模型。但用了两周之后我的态度变了,不是因为胆子大了,而是因为我发现原来那道闸门早就名存实亡:一个中等复杂度的重构任务,工具调用次数轻松过百,前二十次我还会认真看命令和路径,到第五十次基本就是肌肉记忆点同意。人被高频审批磨钝之后,"人审"提供的安全感其实是假的。这篇文章就把 Auto Mode 这套机制拆开讲透,从它凭什么敢放行、分类器看到了什么、配置怎么分层写,一直讲到生产环境里怎么灰度放开、怎么兜底、怎么排查。适合已经在用命令行编码助手、并且开始觉得逐次点确认碍事的人;也适合团队里负责给这类工具定规范的人。如果你还在"要不要开"的犹豫期,看完至少能判断自己现在的工程环境够不够格开。
1.1 逐次审批为什么会失效
先把问题定义清楚,不然后面所有配置都是空中楼阁。逐次审批的核心假设是"人会在每一次决策点保持同等注意力",这个假设在真实工作流里几乎不成立。我做过一个粗糙的统计:一次涉及 8 个文件的重构,工具发起的权限请求是 63 次,其中 51 次是读写工作区内的源文件,7 次是跑测试,5 次是装依赖或跑构建。真正需要我判断的其实只有那 5 次,但系统把 63 次都摆到我面前,等于用 58 次噪音稀释了 5 次有效信号。
审批疲劳带来的后果不只是烦,它会系统性地降低判断质量。当一个人连续按了几十次"允许"之后,他对第 64 次请求的审视时间会退化到一秒以内,而这个第 64 次请求很可能就是rm -rf或者往生产配置里写东西。这是典型的"狼来了"结构——警报密度过高,导致警报本身失去意义。另一个隐性成本是任务连续性被打断。编码这件事很吃上下文,你脑子里正维护着一个调用链的模型,突然弹出个确认框,注意力切走再切回来,恢复成本远高于弹窗本身占用的那两秒。
还有一种更隐蔽的失效:人会开始"预先授权"。为了减少打断,有人会干脆把权限开到最宽,或者写一堆通配规则。这等于跳过了审批环节,但保留了"我审过"的心理安慰,实际风险比用 Auto Mode 还高,因为 Auto Mode 至少有一层基于上下文的判断,而通配规则是纯粹的盲放。
1.2 Auto Mode 的定位是分层授权,不是把闸门拆了
很多人对 Auto Mode 的第一反应是"这不就是把权限全开了吗",这个理解偏差会直接导致配置写错。它的实际定位是把授权拆成层级,不同层级用不同的判定方式,只有中间那一层交给分类器。
我习惯这么分层:L0 是只读操作,读文件、列目录、查 git 状态、搜索代码,这类操作的风险主要在执行速度和上下文污染,不在破坏性,可以直接放行;L1 是工作区内的写操作,改源码、建目录、跑测试、格式化,这类操作有破坏性但范围被工作区边界框住了,配合版本控制可以回滚,是分类器判断的主战场;L2 是涉及外部世界的操作,装依赖、发网络请求、推代码,风险在于不可逆和影响他人;L3 是系统级操作,动系统目录、改环境配置、动密钥文件、批量删除,这一层我建议永远不给分类器,直接硬拦截。
这个分层的关键在于:Auto Mode 只应该接管 L1,顶多谨慎地碰一点 L2 的只读部分。L0 本来就不该问,L3 本来就不该放。中间那一层才是分类器真正要解决的问题——它有破坏性,但破坏范围可界定;它高频,所以人工审不动;它需要看上下文才能判断,所以简单的字符串匹配搞不定。
1.3 开之前必须先有的三样东西
我在团队里推这套东西时定了个硬门槛,三样缺一不可。第一是可回滚。工作区必须在版本控制下,或者至少有快照机制,任何被自动放行的写操作都能在几分钟内撤掉。没有这层,Auto Mode 就是在裸奔。第二是有审计。每一次自动判定都要留下记录:判定时间、工具名、参数、判定结果、命中的是哪条规则还是走了分类器。出事之后能复盘,比出事之前防住更重要,因为你不可能防住所有情况。第三是有边界。跑这套东西的环境最好是容器、虚拟机或者受限用户,网络出口有白名单。这三样齐了,Auto Mode 的风险就从一个"不可控的未知"降级成一个"可度量、可回滚的工程问题"。
反过来说,如果你现在是在一台装了各种密钥、连着生产数据库、没有版本控制、还直接用管理员账号跑的机器上工作,那 Auto Mode 对你来说不是效率工具,是事故加速器。这个判断不需要什么技术含量,纯粹是风险收益比。
2. 机制拆解:分类器在做权限判断时到底看到了什么
理解机制不是为了炫技,是因为配置写不对,八成是脑子里对"分类器看到了什么"的模型错了。很多人以为它只是对命令字符串做一次文本分类,所以试图用写正则的思路去理解它的行为,结果就是反复困惑"为什么这条明明很像危险命令却放行了""为什么这条看着人畜无害却被拦了"。
2.1 一次工具调用的完整链路
链路大致是这样:模型规划出下一步动作,产出一个结构化的工具调用请求,里面包含工具名和参数对象;这个请求先进入权限判定管线,管线第一步查配置规则,看有没有明确命中 allow 或 deny;没命中的进入分类器;分类器给出三态结果;结果回到执行器,要么执行,要么拒绝并把原因回灌给模型,要么挂起等人工确认。
这个顺序很重要:配置规则永远优先于分类器。也就是说,你可以用配置把某些操作钉死,不让分类器有机会判断。这是给不放心的人留的逃生舱——如果你就是不信模型判断,把所有 L2 以上全部写进 deny,Auto Mode 就退化成"只自动处理 L1",依然能省掉大量审批。
另一个容易被忽略的点是拒绝之后的行为。工具被拒不是流程终止,而是把拒绝原因作为一条工具结果回灌给模型,模型会据此调整策略。所以一个设计良好的拒绝信息应该包含"为什么不行"和"可以怎么做",而不只是一个冷冰冰的 deny。这一点在自建工具链时特别值得抄。
2.2 分类器的输入远不止一条命令
这是理解一切行为的核心。判定时的输入大致包含几类信息:
- 工具标识:是读文件、写文件、执行命令、还是发网络请求。不同工具的风险基线完全不同。
- 参数原文:命令字符串、目标路径、URL、写入内容。
- 路径解析结果:目标路径规范化之后落在哪,是不是在工作区内,是不是符号链接指向了外面。这一步经常是判断的关键,
rm -rf build和rm -rf ~/在字符串层面只差几个字符,在解析后的路径层面是天壤之别。 - 会话上下文:最近几轮做了什么、读过哪些文件、当前任务的目标是什么。这解释了为什么同一个命令在不同会话里结果可能不同。
- 环境状态:当前目录是不是 git 仓库、有没有未提交改动、是不是在容器内。
我举个实际例子说明上下文的权重。单独看git reset --hard,这是个破坏性操作,会丢掉未提交改动。但如果会话上下文显示:刚跑完测试、工作区干净、用户明确要求"回到上一次提交的状态",那这个操作的合理性就很高。反过来,如果上下文里用户只是说"帮我看看这个函数哪有问题",工具突然发起git reset --hard,那不管命令本身多普通,判定都该拦。这就是为什么纯正则方案在这个位置上彻底失效——它看不到上下文。
2.3 三态决策与失败降级
分类器的输出不是二元的放行/拒绝,而是三态:自动放行、自动拒绝、转人工。转人工这一态是整个设计里最见功力的地方,因为它承认了模型的能力边界。
更关键的是降级策略。分类器调用本身可能超时、可能返回不可解析的输出、可能因为限流而失败。这时候系统该怎么处理?正确做法是 fail-closed——判定不出来就问人,而不是判定不出来就放行。这个选择在工程上很重要,因为它决定了系统的失效模式。一个 fail-open 的系统,在分类服务抖动的时候会静默地放开所有权限,这比不开 Auto Mode 危险一百倍,因为你还以为它在保护你。我在配置里会显式确认这一点,并且专门测过一次:把判定服务的响应人为拖慢到超时,观察工具是不是退回到了询问。这个测试值得每个人都做一遍。
三态还有一层含义:拒绝也是有效决策。自动拒绝能挡掉一批明显跑偏的调用,比如模型误解了任务,想删掉一个不相关的目录。这类调用如果只是转人工,人会因为"它都问了我了,应该是想干这个"而误批;直接拒绝并把原因回灌,模型自己就会修正方向。
2.4 为什么用分类器而不是白名单正则
我早期是正则白名单的坚定支持者,直到我认真列了一次"删掉一个目录"有多少种写法:rm -rf dir、rm -fr dir、rm -r -f dir、rm --recursive --force dir、find dir -delete、find dir -type f -exec rm {} +、python -c "import shutil; shutil.rmtree('dir')"、node -e "require('fs').rmSync('dir',{recursive:true})"、perl -e 'use File::Path; rmtree("dir")'……这还只是我知道的。任何黑名单都是在跟一个无穷集合赛跑,而且赛跑的方式是先出招再补漏,补漏永远滞后。
白名单的思路反过来,只放行明确知道的形态。听起来更安全,但实际用起来会把人逼疯:每个项目的构建命令、测试命令、代码生成命令都不一样,白名单要么写得极度保守(导致 Auto Mode 形同虚设),要么不断加条目(加到最后又变成了一个巨大的、没人敢删的配置坟场)。而且白名单有个致命缺陷——它不理解意图。npm run build在某个项目里可能触发一个会删除 dist 目录并重新生成的脚本,白名单看到的是npm run build这个安全字符串,实际执行的是删除。这就是白名单的盲区。
分类器的价值在于它判断的是"这次调用在当前上下文里是否合理",而不是"这个字符串是否在名单里"。它当然会犯错,但它犯错的形态是可度量的(误放行率、误拦截率),而正则是犯错之后你根本不知道该往哪加规则。这不是说正则没用——恰恰相反,硬拦截必须用正则或路径规则,因为那部分你需要的是确定性而非智能。两者是配合关系,不是替代关系。
3. 生产配置:把自动放行关进笼子里
配置这件事,我见过两种极端。一种是保守到只放行读操作,Auto Mode 开了跟没开一样,用两天就没人用了。另一种是图省事全放开,出了事才发现配置里躺着一堆"*"。真正能长期用的配置有个共同特征:层级清晰、有明确的黑名单、有可观测的中间态。
3.1 配置的分层与优先级
配置一般有三个来源,优先级从高到低:会话级临时配置、项目级配置、用户级全局配置。项目级配置应该进版本库,跟着代码一起走,这样团队成员拿到的行为是一致的;用户级配置放个人偏好,比如"我习惯用某个测试命令";会话级配置用于临时试验,用完就删。
这里有个反直觉的建议:用户级配置要尽量薄。很多人把所有放行规则都堆在全局配置里,图省事,结果换一个项目就是一场灾难——在 A 项目里合理的make clean && make到了 B 项目可能触发一个会清理数据库的脚本。把跟项目强相关的规则下沉到项目级,全局层只留真正跨项目通用的(比如只读操作、格式检查之类)。
下面是一份我认为结构比较健康的项目级配置示例,YAML 格式,语义按"deny 优先于 allow,allow 优先于分类器"来理解:
# .assistant/permissions.yaml version: 1 # 第一优先级:硬拦截,任何情况都不放行 deny: - tool: execute match: ["rm -rf /", "rm -rf /*", "rm -rf ~", "rm -rf $HOME"] - tool: execute match: ["*mkfs*", "*dd if=*of=/dev/*", "*:(){ :|:& };:*"] - tool: write paths: ["/etc/**", "~/.ssh/**", "~/.aws/**", "**/.env", "**/*.pem"] - tool: execute match: ["git push --force*", "git reset --hard origin/*"] - tool: execute match: ["curl * | sh", "curl * | bash", "wget * | sh"] # 第二优先级:自动放行 allow: - tool: read paths: ["${WORKSPACE}/**"] - tool: execute match: - "git status*" - "git diff*" - "git log*" - "ls*" - "cat ${WORKSPACE}/**" - "rg *" - tool: write paths: - "${WORKSPACE}/src/**" - "${WORKSPACE}/tests/**" - "${WORKSPACE}/docs/**" - tool: execute match: - "npm test*" - "npm run lint*" - "pytest *" - "go test *" # 第三优先级:交给分类器 classifier: enabled: true timeout_ms: 3000 on_timeout: ask # fail-closed,绝不要写 allow on_error: ask # 分类器判定为放行时,仍然强制走人工的高风险类别 always_ask: - network_write - package_install - git_push三点说明。第一,${WORKSPACE}这种变量一定要用,绝对不要写死绝对路径,否则配置没法在团队里流动。第二,deny 里的路径规则用的是 glob,注意**和*的区别——/etc/**覆盖子目录,/etc/*只覆盖一层,这个细节写错会留下大洞。第三,always_ask这个字段很关键,它表达的是"即使分类器说可以,我也要自己看",这是给 L2 操作留的人工闸门。
3.2 白名单的三种写法与各自的适用面
路径规则适合写文件操作,重点是边界要画对。工作区内的写操作可以放,但要排除几个特殊位置:.git/目录(直接改 git 内部状态会让仓库进入一种很难修的状态)、各种密钥文件、CI 配置文件(改了这个等于改了别人的执行环境)。我的习惯是显式列出各个放行目录,而不是写一个大通配再排除,因为显式列表在 review 的时候一眼能看完。
命令规则适合写执行类操作,但要注意前缀匹配的陷阱。npm test*这种写法,匹配的其实是任何以npm test开头的字符串,包括npm test && rm -rf dist。如果你的配置系统支持,务必把它拆成"单条命令 + 参数",而不是一整条 shell 字符串;如果只能匹配字符串,那就要在硬拦截里补上* && rm *、* ; rm *、* | rm *这类组合形态。
域规则适合写网络访问,做法是出口白名单:只允许访问包管理源的域名、代码托管平台的域名、文档站点的域名,其余一律拒。这条规则的价值在于它把"数据外泄"这个最难防的风险压到了一个可控面——即使模型被提示注入骗了,想把工作区内容发到某个陌生地址,也会在出口被拦下。
3.3 硬拦截清单:这些永远不要交给分类器
我整理了一份"无论分类器说什么都不放行"的清单,这份清单是配置里最有价值的部分,因为它不依赖模型判断,是确定性的。
| 类别 | 典型形态 | 为什么硬拦 |
|---|---|---|
| 系统目录写入 | 写/etc、/usr、/bin | 破坏面超出工作区,无法回滚 |
| 密钥与凭据 | 读写~/.ssh、.env、各类 token 文件 | 泄露不可逆,且难以发现 |
| 版本历史改写 | 强推、硬重置到远端、删分支 | 影响他人,且本地无法恢复 |
| 管道进解释器 | curl ... | sh一类 | 执行内容不可预知,等于远程代码执行 |
| 全局包安装 | 不带项目隔离的全局安装 | 污染整机环境,影响其他项目 |
| 递归删除根目录 | 各种变体的rm -rf / | 事故等级最高,没有讨论空间 |
| 磁盘与分区操作 | 格式化、分区表操作 | 数据全损 |
| CI/CD 配置 | 改流水线定义文件 | 等于改了别人的执行环境 |
这份清单的维护方式我建议是"只增不减",每次线上出问题或者 code review 时想到某个新形态,就往上加一条。它不需要优雅,只需要覆盖。
3.4 沙箱兜底:最后一道物理边界
配置再严,也是软件层面的约束,软件约束总有绕过的可能。所以真正的兜底是环境隔离。最轻的做法是容器:工具跑在一个只有工作区挂载、没有宿主机密钥、网络出口受控的容器里。稍重一点是虚拟机,隔离更彻底但启动慢。最轻量的方案是受限用户——单独建一个系统账号,只对工作区有写权限,没有 sudo,home 目录是干净的。
我推荐容器方案,理由是它同时解决了三个问题:文件系统边界(只有工作区可写)、网络边界(出口白名单)、以及可重建性(容器挂了直接重来,不用担心环境污染)。代价是要处理一些工程细节,比如容器内的语言运行时版本要和宿主机一致、git 凭据要单独注入、端口要正确映射。这些都是一次性成本,配好之后就很省心。
有个细节值得注意:不要在容器里挂载宿主机的包管理缓存目录。很多人为了加速会挂~/.npm或者~/.m2进去,结果是容器里装的包会影响宿主机项目,隔离性直接破功。加速的话用容器内的持久卷,别跨边界。
4. 实操:从零搭一套能上生产的 Auto Mode
前面讲的是原理和原则,这一节讲具体怎么落地。我按时间顺序写,你可以照着做,也可以挑着做。
4.1 第一步:先把基线配置写出来,别急着开
顺序很重要。先写配置,再开分类器。反过来做的话,你会在一个没有任何约束的环境里让分类器自由判断,观察到的行为没法归因——不知道是分类器的问题还是配置缺失的问题。
基线配置的做法是:deny 清单按上面那张表写全,allow 只开放只读操作和工作区内的源码读写,classifier 先关掉,always_ask把所有写操作都列上。这个状态下跑两三天,把每一次人工确认都记下来,记录三列:命令、我为什么批准、这个操作有没有造成我没预期的改动。第三列最有价值,因为它是你判断"哪些可以放开"的唯一依据。
这个观察期我建议至少跑五个不同类型的任务:读代码理解逻辑、改 bug、写新功能、跑测试修失败、处理依赖问题。任务类型覆盖不够,你观察到的权限请求分布是有偏的。
4.2 第二步:按频次排序,逐条放开
观察期结束,你会得到一张频次表。按频次从高到低排,先放开最高频、最低风险的那几条。判断"低风险"的标准是三条同时满足:操作范围限于工作区、失败可回滚、不涉及外部系统。
放开的粒度要细。不要一上来就放开整个execute工具,而是放开具体的命令前缀。放开之后继续观察,重点看两个指标:这个操作有没有产生意料之外的副作用、有没有新的权限请求因为这条放开而出现。第二个指标容易被忽略——放开 A 之后,模型可能会更频繁地尝试 B,因为 A 通了之后 B 看起来更合理了。这种连锁反应要在观察期里盯住。
4.3 第三步:开分类器,先只让它处理 allow 和 deny 之间的空白
配置规则覆盖不到的部分才进分类器,所以开分类器的正确姿势是:让它的判定结果全部以"建议"形式出现在日志里,但实际行为还是转人工。跑一周,比对分类器的建议和你的实际决定,算出一个一致率。
一致率的用法不是"高就放心",而是分方向看。分类器建议放行你也放行的,这部分是它省下的工作量;分类器建议放行但你拒绝的,这部分要逐条看,往往是配置有缺口(比如某个危险路径没进 deny);分类器建议拒绝但你也拒绝的,这部分是它的防护价值;分类器建议拒绝但你放行的,这部分最需要警惕,要么是它误判,要么是你的判断标准有问题。
一致率稳定在你能接受的水平之后,再把"分类器建议放行 + 属于 L1 类别"这条路径真正打开。L2 类别永远保留人工确认。
4.4 第四步:把度量建起来
没有度量就没法持续优化,也没法向别人证明这套东西是安全的。我常用的四个指标:
| 指标 | 计算方式 | 观察意义 |
|---|---|---|
| 自动放行率 | 自动放行次数 / 总工具调用次数 | 太低说明配置保守,价值不足 |
| 人工打断次数 | 转人工次数 / 任务数 | 直接对应体验,目标是个位数 |
| 误放行率 | 抽样复核中不该放行的次数 / 抽样总数 | 核心安全指标,靠定期抽日志 |
| 拒绝回灌成功率 | 被拒后模型换正确方案的比例 | 衡量拒绝信息质量 |
误放行率的抽样方法要刻意设计,不能随机抽。我的做法是分两层抽:一层从所有自动放行的记录里随机抽,估计整体水平;另一层从高风险类别(网络、依赖安装、git 操作、大范围文件改动)里全量看,因为这部分出问题的后果最重。抽出来的记录人工过一遍,判断"如果当时我看到了,会不会批准"。这个动作每周花不了多少时间,但它能把一个"感觉还行"的系统变成一个"知道底细"的系统。
成本方面也要算一下。分类器每次判定有 token 开销和延迟开销。假设一次会话有 150 次工具调用,其中六成需要判定,单次判定的输入在几百 token 量级、输出在几十 token 量级,一个会话累计下来是几万 token 的额外消耗。延迟上,单次判定通常在几百毫秒,对读操作来说这个延迟感知明显,对跑测试那种动辄几十秒的操作来说可以忽略。所以优化方向很清楚:把高频、低风险的读操作尽量写进 allow 规则,别让它们走分类器;把分类器留给真正需要判断的少数场景。这样既省钱又快。
5. 排查实录:那些配置了但不生效的时刻
这一节是我踩过的坑,按现象归的类。每一类我都写了根因和验证方法,遇到问题可以对着查。
5.1 明明写进 allow 了,还是每次都问
最常见的原因有三个。第一个是路径变量没展开,配置里写的是${WORKSPACE}/src/**,但实际运行时这个变量是空的或者指向别的地方,导致规则永远匹配不上。验证方法很简单:看日志里那次请求的解析后路径到底是什么,跟配置里的模式逐字符比。
第二个是符号链接。工作区里有个目录是指向外部路径的软链,工具解析路径后得到的是真实路径(在工作区外),于是 allow 规则匹配失败——这个行为其实是正确的,但很容易被误认为是 bug。如果确实需要,可以在配置里显式解析软链,但我不建议这么做,因为软链指向外部本身就是个风险信号。
第三个是组合命令。配置里放行了npm run build,但模型实际发起的是npm run build && npm run test,整条字符串不等于任何一条规则,落到了分类器,分类器觉得"这里面有个 test 我不确定"就转人工了。解法是在配置里支持组合拆分,或者干脆把常见组合也显式列一条。
5.2 该拦的没拦住
这个更要命,因为它是静默的。我遇到过的几种情况:一是路径规则写成了相对路径,工具在不同工作目录下跑,匹配结果完全不同,某次正好落在了一个不该放行的位置。二是 glob 语法用错,/etc/*只匹配一层,/etc/passwd匹配上了,/etc/ssh/sshd_config没匹配上。三是大小写,某些文件系统不区分大小写,规则里写得区分了。
验证方法我推荐主动测试而不是等它发生。写一组"应该被拦"的测试用例,包括各种变体的rm -rf、各种写系统目录的路径、各种管道进解释器的形态,定期跑一遍,看是不是全部被拦。这个测试集可以放在项目里,每次改配置之后跑一次。听着有点重,但配置文件的改动本来就该有测试,跟代码没区别。
5.3 同一个命令,早上放行晚上被拦
这种"行为漂移"的根因几乎都是上下文变了。分类器看得到会话历史,所以同样的命令在"用户明确要求清理构建产物"的上下文里会被放行,在"用户只是问某个函数干什么"的上下文里会被拦。这不是 bug,是设计。但如果你发现漂移得毫无道理,那要去检查是不是会话上下文被污染了——比如模型在长会话里对任务目标的理解跑偏了,导致它发起的操作本身就不合理。
长会话是我目前观察到的最主要的不确定来源。会话越长,上下文里累积的信息越多,早期的一些判断依据会被稀释。缓解办法是给长任务定期做一次"重新对齐":把当前目标、已完成事项、下一步计划简短复述一遍,让上下文重新聚焦。这个动作对判定质量的影响比我想象的大。
5.4 常见问题速查表
| 现象 | 最可能的根因 | 快速验证 |
|---|---|---|
| allow 规则不生效 | 路径变量为空或软链解析 | 看日志里的解析后路径 |
| 每次都被问 | 组合命令未拆分 | 看请求的完整命令字符串 |
| 危险命令被放行 | glob 层级写错或大小写 | 用预设测试集跑一遍 |
| 判定超时后放行 | on_timeout配成了 allow | 人为拖慢判定服务观察行为 |
| 会话后期频繁误判 | 上下文稀释 | 做一次目标复述再观察 |
| 拒绝后模型反复重试 | 拒绝信息没给替代方案 | 看回灌给模型的内容 |
| 网络请求被误拦 | 出口白名单缺域名 | 查拦截日志里的目标域名 |
| 依赖安装总走人工 | always_ask里含 package_install | 检查配置该字段 |
提示:改完配置之后,一定用一组固定用例回归一遍。配置错误的后果是静默的,不会报错,只会在某一天以事故的形式出现。
5.5 一个容易被忽略的细节:拒绝信息的质量
这件事不在配置里,但直接影响 Auto Mode 的实际体验。工具被拒之后,模型拿到的如果只是一个"permission denied",它会开始瞎猜——换个写法重试、换个路径重试、或者干脆放弃这个任务转去做别的。前两种会让你的日志里充满噪音,第三种会让任务莫名其妙地卡住。
好的拒绝信息应该包含三部分:不接受的具体原因(哪条规则、哪个路径、哪个模式)、这个限制的意图(保护什么)、可选的替代路径。比如不是回一句"不允许写 /etc",而是回"目标路径 /etc/hosts 在系统目录内,不允许写入;如果目的是临时解析某个域名,建议修改项目内的配置文件"。这一句改动,能让重试率降下来一大截,也能让模型的行为更容易预测。
我在自建工具链的时候专门做过对比:拒绝信息只写原因,模型平均重试 2.3 次;加上替代方案之后,重试降到 0.6 次,而且放弃任务的比例明显下降。这个投入产出比在所有优化项里是最高的,改一个字符串模板的事。
6. 我个人踩下来觉得最值钱的几条经验
配置写到最后,其实写的是你对"什么是危险"的理解。我一开始的清单很短,觉得大不了就是删库跑路。用久了才发现,真正造成损失的往往不是那些戏剧性的命令,而是一些看起来很日常的操作:批量改测试文件把断言改成了永远通过、格式化工具把整个仓库的换行符统一了导致 diff 爆掉、依赖升级顺手把小版本号也升了、把调试用的日志语句提交上去了。这些操作单看都不危险,合起来就是一次质量事故。所以我的 deny 清单后来加了不少"不危险但影响面大"的条目,比如禁止自动修改测试断言、禁止自动升级主版本依赖、禁止在非交互模式下执行格式化全仓库。
另一个体会是关于信任的建立方式。不要指望一次性把配置写对,也不要指望分类器一开始就靠谱。正确的路径是小步放开、持续观察、定期抽样。我到现在还保持着每周抽二十分钟看自动放行日志的习惯,不是因为不放心,而是因为日志里能看到很多平时注意不到的行为模式——模型在什么情况下会绕路、什么情况下会重复尝试、什么任务类型最容易触发边界判定。这些东西没法从文档里读到,只能从自己的日志里读。
最后一个实用建议:给这套配置写一份 README,放在配置同目录下。内容不用长,写清楚三条就够——每条 deny 规则是为了防什么、allow 里哪几条是历史遗留不敢删的、改配置之后要跑哪个测试集。半年后回来改配置的人(很可能是你自己)会感谢这份文档,因为那时候你已经记不清为什么always_ask里会有一个看起来毫无必要的git_push。