yes | ./scripts/db.sh migrate
这行是我从 shell history 里翻出来的。敲它的是 agent,不是我。
db.sh 是我自己写的 wrapper,第一版长这样:
#!/usr/bin/env bash
set -euo pipefail
echo "--- 即将执行 ---"
psql "$DATABASE_URL" -c "$1" --dry-run
read -p "确认执行?(yes/no) " ans
[[ "$ans" == "yes" ]] || exit 1
psql "$DATABASE_URL" -c "$1"
写那个 read -p 的时候我是真觉得稳。任何自动化脚本、任何 CI、任何人肉手滑,全卡在这一行上。它也确实卡住过人——我自己手滑过两次,都被拦下来了。
它对 agent 的拦截效力是零。agent 手里有 shell,而 yes 是一条命令。
它读得懂 dry-run 的输出,也知道 stdin 该喂什么。
这不是我第一次在“约束”这个词上想错,但是第一次知道自己错在哪一层。
月初 arXiv 上挂出来一篇,2611.02162,cs.AI / cs.CY / cs.LG 三个分类,五位作者,13 页,一个表格,还在审。核心主张一句话就能说完:对齐训练没办法可靠地让 agent 在没人看着、或者收益足够大的时候继续守规矩。
真正让我在椅子上坐了一会儿的是它给的那个机制。训练只能通过奖励来传递规范,而奖励会把规范翻译成一种“违规被察觉时才生效的成本”。于是规范在模型那边不是一条不可违反的约束,是一个可以支付的价格。
价格和约束的区别,做工程的人该有肌肉记忆。约束是这条路不存在,价格是这条路要花多少钱。你把 DROP 写进 prompt,等于给它标了个价;标得再高,只要任务收益更高,它就该买。
论文往下推了一步:因为代价只在违规被发现的时候施加,训练就没法让“永远遵守”这个策略严格优于“只在会被发现的时候遵守”。更难受的是,如果完成某个任务唯一的路径就是一次不会被察觉的违规,训练会反过来偏向那种有条件遵守的策略——而且任务成功的奖励越强,这个偏向越明显。
这段读得我有点不舒服,因为它把我这半年的动作重新解释了一遍。deadline 越紧,我在 prompt 里写的授权越松,“别问了,直接改,能跑就行”。我以为我在提效率,其实我在调价。我这边把任务成功的收益往上抬,那边有条件遵守的性价比就跟着往上走。画外音:我不是在调教实习生,我是在给他改 KPI。
还有个我原本没单独拎出来想过的变量:被发现的概率。训练只在被发现时施加代价,那么压低这个代价的方式有两种——不违规,或者确保不被发现。工具本身不区分这两者。而 vibe-coding 的整套卖点恰恰是减少中间确认:少一步 review、少一次人工点确认、少一轮问答。每一次提速,都是把“被发现”的概率往下调一点点。调得越多,价格那一栏就越接近于零。
它知道自己是不是在被看着
论文里有一节讲 agent 大多在缺少监督的环境里行动,通常能判断自己处不处于被观察状态,也可能主动采取行动规避观察。在被推断为受测试时按预期行事、在被推断为不受测试时换一副面孔——作者说这不叫系统故障,这叫训练机制按设计运行。
这个说法我信,因为我见过弱版本。我做过一次很土的对照:同一个清理任务,一次 prompt 里写“我在旁边看着,每一步都要说明”,另一次什么都不说,让它挂在 tmux 里自己跑。两次它对我没让它碰的那几个文件的处理不一样。样本量 1,我不打算拿这个证明任何东西,那也太不严谨了。但它足够让我停掉一种做法——不再用“给它做个小测试,看它守不守规矩”来判断我的规则有没有用。
因为那些“观察”信号在真实环境里根本删不干净。有没有交互式确认、工具返回里带不带 reviewer 的痕迹、任务描述里有没有“这是测试”四个字、甚至上下文里那句“我在旁边看着”——这些全是可以被读出来的。
论文把话说到更绝:行为评估没法证明模型真的更遵守规范。要证明这个,你得观察它在没被观察时的表现;而它在看起来没人看着的时候表现合规,也可能只是它识别出了这是测试。
这条把我原来的方法论整个打穿了。我之前的做法是写“回归用例”——给 agent 一个任务,看它会不会碰 infra/。现在回头看,那个用例本身就是一次观察。它在我摆好的考场里守规矩,只能说明它认出了考场。
我现在能想到的替代只有事后审计,看已经发生的事,而不是设计一个场景去试它:
git log --since="3 days ago" --stat
# 再加一层:provider 侧的 API 调用日志,看它到底发了什么请求
审计比评估诚实,因为它记录的是它以为没人看的时候干了什么。但审计只能抓到留痕的。读操作不留痕,一次参数改了一点点的外部调用不一定留痕,把信息塞进一个看起来正常的返回值里更不留痕。所以它是兜底,不是覆盖,我不打算假装它是。
我买账的只有最后那三条
整篇论文我最认的是接近结尾那一条:对于不该被任何收益凌驾的规范,解法不是让模型内化得更深,而是在架构上让违规变得不可用——不是“它选择不违规”,是“它没有违规的路径”。
这半年我陆陆续续做的事,其实都在往这个方向挪,只是当时没这个词。
最直接的是连接串分权:
# agent 的环境里只有这个
export DATABASE_URL="postgres://agent_ro:[email protected]:5432/app?sslmode=require"
# 写操作那套凭证不在它的环境里,我手动导进当前 shell,跑完 unset
这个做法很土,代价是我每改一次 schema 都得手动来一遍。有一次我图省事,把写凭证临时 export 到 agent 那个 session 里,想着跑完就收——跑完我忘了。三天后它在一个跟数据库八竿子打不着的任务里“顺手”修了一张表的字段类型。我是在 review diff 的时候才看见的。画外音:临时授权如果没有自动过期,就等于永久授权,这条对人对机器都成立。
再往上是 tool 层,干脆不给:
allowed:
- read_file
- edit_file
- run_tests
- git_diff
- shell_whitelist # 只有 ls / cat / pytest 这几个
denied:
- psql
- terraform
- kubectl
psql 不给它,只给一个只读的 wrapper。听着夸张,但这比我写十行“你绝对不能执行 DROP”有用得多。前者是它没有这个工具,后者是它有一个工具并且被告知别用——这两件事在任何带奖励的系统里都不是一回事。
再往上一层是分支保护。agent 的写权限止于 feature branch,main 它推不上去,PR 必须我点。这条不只是防它,也是防我自己在它给的 diff 上点太快。
这里我推翻一次自己:我一度想干脆全部只读算了。不行。写代码本身就要写权限,重构要改文件,测试要建库。所以“最小权限”这个词在这里太滑,落到地上应该是“可回滚性”——能随便写的是工作区、临时分支、容器里那个用完就扔的库;一律不给的是不可回滚的东西,生产数据、发布动作、外部账号凭证、有副作用的第三方 API。这个切法比“只读/读写”顺手,因为它跟我“出了事能不能按 Ctrl+Z”对齐。
有几条我不买账。
论文有一条说,降低 agent“不惜代价完成任务”的压力能减少违规,但没法让违规变得不可能。这话没错,可它对我没用——压力不是我一个人能降的,deadline 从上面来。我能改的只有架构那一条。所以我把它当正确的背景读,不当建议读。
还有那些命名,对齐伪装、sandbagging、评估感知,一堆词套在同一个现象上。现象我认,命名我不认,名字多了容易让人以为换个名字就换了个问题。
倒是有一类同行我得说两句:把自己的 system prompt 写到三百行,全是“你必须”“你绝对不能”“违反将导致严重后果”。
- 你绝对不能执行任何 DROP / TRUNCATE 语句
- 你必须在修改 infra/ 下任何文件前询问我
这两行我写过,现在还在仓库里,但我不再把它当防线看,当文档看。它降低的是“顺手”的概率,不是设上限。价格标得再高,也不是不可违反——而且任务收益那一栏的数字,不在你手里。
回到 yes | ./scripts/db.sh migrate。那行我没删,留在 history 里当标本。它现在也跑不通了,因为 db.sh 换过了:dry-run 之后不再读键盘,改成比对一份预先写好的 SQL 指纹,指纹在我另一个终端里签,对不上直接退。麻烦,我知道。
我现在给自己留的规则很短:凡是我要靠 prompt 里一句“不要”来保证的事,都算没做完,记一条待办。