我,能不能把这次的教训做成一道硬性护栏?以后 AI 再这么干,就直接拦住它。
AI,可以考虑,但你想用的办法有个问题。
它说,等你看见结果再拦,已经来不及了。
这就是这次折腾的起点。
我原本只是想解决一个很具体的问题,AI 干活的时候,有些事情明明已经吃过亏了,为什么下次还是会犯?
难道每次都得重新提醒它一遍?
#一、一个 2 GB 的包,最后长到了 15 GB
先说事情是怎么发生的。
AI 当时在后台跑了一条搜索命令,想从一堆文件里找点东西。
结果这条命令撞上了一个不太友好的情况,它把二进制文件也当成普通文本来搜索。偏偏其中有些文件特别大,搜索命中之后,整坨内容就被往外吐。
如果你不熟悉这些术语,也没关系。你可以把它想成,本来只是想在一摞书里找一句话,结果有人不光找到了,还把整本书复印了一遍。
而且不止一本。
等我发现不对劲时,已经有一个文件膨胀到了 13.6 GB。
原本只有 2.1 GB 的自包含程序包,硬生生被撑到了 15 GB 左右。
删掉那个异常文件,包才恢复原样。
这件事当然可以记下来。以后每次搜索前,都提醒 AI 一句,「注意,不要让搜索命令把大块二进制内容打印出来。」
听起来挺合理,对吧?
但我转念一想,既然这次已经知道哪里危险了,为什么还要等 AI 下次想起来?
能不能直接立一条规矩,让它根本没机会再犯?
#二、我想装一道护栏,AI 却说第一种办法不行
我把这个想法交给 AI,能不能做成一个硬性钩子,只要搜索输出超过 100 KB,就自动拦截?
这里的「钩子」,你可以理解成一个自动检查员。
AI 每次准备执行某种操作时,检查员先出来看一眼。发现不合规,就不让它继续。
这听起来不难,问题却出在一个很小的细节上。
**检查员什么时候出来?**可以在命令执行之前检查;也有一种,可以等命令执行之后再检查。
如果等执行之后才检查,确实能知道输出有多大。
可问题是,13.6 GB 的文件已经写出来了。
你现在才跑过去说「哎呀,输出太大了」,就像火已经烧起来了,你才开始检查电线有没有问题。
而如果在执行之前检查,输出又还没有产生,拿什么来判断它有没有超过 100 KB?
这就是我第一个没想到的地方。
我原本想的是,先看结果,超标就拦。
但这个位置根本没有办法提前看到结果。
怎么办?
AI 给出的方向是,既然还看不到结果,那就先看看它准备怎么做。
这一下,思路就变了。
#三、别等它闯祸,先看它准备干什么
回头看那条惹祸的命令,其实危险信号早就摆在明面上了。
它要递归搜索,也就是从当前目录一路往下翻,子目录里的文件也不放过。
与此同时,它没有告诉搜索工具应该跳过二进制文件。
这两个条件凑在一起,就像让一个人去整栋楼里找某个字,却没告诉他哪些房间不能进,也没规定找到之后最多能搬多少东西出来。
既然危险能从命令本身看出来,就没必要等到输出产生之后再判断。
于是,我把原来的要求换了个说法,
只要是递归搜索,又没有排除二进制文件,就先拦下来。
这里不需要 AI 预测会产生多少数据,也不需要真的跑一次看看会不会出事。检查员只需要看它准备做什么。
对普通人来说,这个思路其实很好理解。
我们平时立规矩,不也是这样吗?
与其等孩子把水杯打碎,再教育他端杯子要小心,不如先把容易碰倒的东西挪开,或者给杯子找个稳当的位置。
有些风险,根本不值得等它发生。
#四、护栏也会犯傻,它差点把好人拦了
不过,事情还没完。
我让 AI 把这个检查规则做出来。第一版看起来挺像那么回事,测试时却发现了一个漏洞。
比如下面这条命令,
bash -c "grep -rn foo ."
不用看懂每个字。你只需要知道,它的意思是让另一个命令解释并执行里面的搜索操作。
问题出在检查员身上。
它只会在整条命令里找一些特定的字符,看看有没有能让输出变少的参数。
偏偏这里的 -c 是外层命令的参数,并不是里面那个搜索工具的参数。
可检查员不认识这个区别。它看见了 -c,就自作聪明地认为,有保护措施,可以放行。
当时的现场:扫描器把 `-c` 当成了保护措施

这就好比学校门口的保安只认校服,不看是谁。坏人穿着校服进来了,他还觉得自己检查得很认真。
更麻烦的是,这种误判并不只会发生在这一条命令里。别的程序也可能有同样的参数,检查员却把它们误认成了搜索工具的保护措施。
我让 AI 继续修。
这次不能只看整句话里有没有某个字符,而得先弄清楚,这些参数究竟属于谁?
简单说,就是先把一整串命令分清楚,再检查真正需要检查的那一段。
这也是我越来越在意的一件事,AI 能很快做出一个看起来合理的东西,但「看起来能用」和「真的能挡住问题」是两回事。
如果我只是让它写完就算了,这个护栏可能从第一天起就在悄悄漏风。
#五、还有一种聪明办法,我也没敢用
后来又想到一个更直接的办法。
既然输出太大,那能不能干脆给输出加个上限?最多只能吐出 100 KB,超过就停止。
AI 确实有办法尝试改写即将执行的命令,把这个限制塞进去。
听起来是不是更省心?
但继续研究后,发现这种改写方式和原有的权限检查之间存在冲突。为了让改写生效,可能不得不改变原本的权限处理方式,甚至让一些命令反复弹出确认框。
这就有点得不偿失了。
我想解决的是一个危险操作,不能为了拦住它,顺手把日常工作也变成一场不停点确认的游戏。
所以最后选择了更克制的办法,检查员只负责两件事。
发现危险,就拦下来,并说清楚原因。
没发现危险,就不多管闲事,让原来的权限机制继续工作。
护栏的目的不是接管整条路,而是在最容易出事的地方挡一下。
#六、只说「不行」,AI 可能会换个姿势继续撞墙
做到这里,我又想到一个问题。
如果 AI 执行命令时突然被拦下来,却什么解释也没有,它会怎么想?
它未必知道自己违反了哪条规矩。
它可能觉得工具出了故障,于是换一种写法再试一次。还是不行?那就再换一种。
最后,本来只需要改一行命令的事情,变成了反复试错,白白消耗时间和上下文。
这让我意识到,一个真正好用的护栏,不能只有红灯,还得有路标。
所以我要求拦截时必须说清楚两件事,
为什么被拦,以及接下来应该怎么做。
比如,不只是告诉 AI「递归搜索不允许执行」,还要告诉它应该排除二进制文件,或者换一种更合适的搜索方式。
这样,拦截才不只是阻止错误,也是在帮助 AI 找到正确的路。
对我这个不打算从头研究软件工程的人来说,这一点尤其重要。
三类墓碑,分着说
后来这个理由还分成了三类,因为三类墓碑不是一回事:
| 类型 | 在干什么 |
|---|---|
| 自称被取代 | 在给本文判死刑 |
| 指向继任件 | 在把读者支走 |
| 索引表格标记 | 在状态列里记流水账 |
分着说,agent 才不用从一条总则里自己推。
我不需要自己把每个技术细节都写出来,但我得知道,如果它被拦住了,下一步应该往哪里走。
否则,所谓自动化,不过是把原来的麻烦换了个地方。
#七、我发现,光记住规矩还不够
这次折腾的另一个部分,其实来自更早的一条教训。
我不希望重要的决策文档写着写着,就变成这样,
「本文已废弃,请参阅新版。」
「本文已被替代,请以其他文档为准。」
「本文已迁移至新位置。」
这种话偶尔出现当然没问题。可如果一份文档本来就是用来说明当前决策的,里面却只剩下各种「别看我,看别处」,读者就得沿着一串链接追来追去,才能找到真正有用的内容。
所以我立了一条规矩,如果旧决策已经被推翻,就直接修改原文,让它清清楚楚地写着现在采用的决定,而不是到处留下「本文已经过时」的提示。
我以为把这条规矩写好就行了。
后来才发现,AI 还是可能钻空子。
我禁止它写「已废弃」,它可以改成「已被新版替代」;我再补几种说法,它还可以继续换词。
这让我明白,如果一条规矩只禁止几个具体词语,那它检查的就只是用词,不是问题本身。
我真正不想看到的,不是某几个词,而是整份文档只顾着把读者往外推,却没有把当前有效的决定讲清楚。
于是,检查规则也要跟着改变。不能只看某个词有没有出现,还得看这些话放在一起,到底在做什么。
当然,这并不意味着 AI 从此就不可能绕过规则。它只是让护栏更贴近真正想解决的问题,而不是让它跟我玩同义词接龙。
#八、规矩多了,能不能别让每条规矩都单独站岗?
做到这里,检查规则已经不止一条了。
有的检查危险搜索,有的检查过长的内联命令,还有的检查文档写入时有没有违反约定。
如果每增加一条规矩,就增加一套独立的入口,维护起来很快就会乱。
于是我又让 AI 帮忙,把原来分散的几个检查入口整理到一起。
你可以把它想成一个门卫室,不同的检查员各自负责不同的问题,但大家从统一的入口工作。
合并之后,注册点只剩两个
1 | |
pretool-gate 内部挂着五个独立的判定核心:表情符号、通用闸口、grep 危险、内联命令危险、提示词卫生。每个核心都不碰 IO,纯粹是输入到输出的判定函数,这样才好写单元测试。整个钩子目录里,每个核心文件旁边都躺着一个同名的 .test.js。
检查员之间不需要互相知道太多事情。你给它一份待检查的内容,它给出判断。这样的设计更容易单独测试,也更容易在某条规则出问题时定位原因。
这一步没有什么神奇的技术突破。
它只是让我少维护几个分散的入口,后面想调整某条规矩时,也不必把整个系统翻一遍。
我越来越觉得,AI 辅助编程真正省心的地方,不一定是让它一次写出多少代码,而是让那些以后还会改动的东西,尽量保持简单。
#九、拿四千多条旧记录测试,真的能证明它有用吗?
护栏写好了,总得测试。
我手里正好有一份历史记录,包含本机以前执行过的 4,938 条命令。
于是,我让 AI 拿这些记录跑一遍,看看新规则会拦住什么,又会放过什么。
结果测试着测试着,我自己先提出了一个问题,
这些历史命令,真的能代表 AI 在没有护栏时会犯的错吗?
想想看,这些命令大多是在已经有规矩、已经受过提醒的情况下产生的。
它们本来就可能比较规矩。
就像一个班的学生已经学过交通安全,再拿他们平时过马路的记录,证明新装的红绿灯很有效,似乎不太公平。
因为你没有观察过他们在没有红绿灯、也没有人提醒时会怎么做。
这批历史记录当然仍然有用。它能帮助我发现新规则有没有误伤以前正常的操作,也能验证某些已知问题是否得到解决。
但它不能单独证明,面对所有未知情况,这套护栏都有效。
我不想因为测试数字看起来漂亮,就给自己一种虚假的安全感。
所以,我又让 AI 分析了不同特征与命令失败之间的关系。
其中一个结果是,单看命令有多长,对失败的预测能力并没有想象中那么强。长命令确实可能更容易出问题,但不能因此把所有长命令都拦下来。
最后,我把特别长的内联命令阈值设在 2,000 个字符。
这不是说 1,999 个字符一定安全,2,001 个字符就一定危险。这个数字只是一个实用的提醒线,如果一整段程序长到这个程度,还被塞在一条命令里,那么把它单独写成脚本,往往更容易检查、修改和重跑。
这也是我希望 AI 帮我做的事,不只是把规则写出来,还要尽量说明规则为什么这么定,哪些地方有依据,哪些地方只是谨慎的取舍。
#十、我知道它还有漏洞,但我不想假装没有
这套护栏并不完美。
它没有真正理解整套命令的意思,只是用一些规则检查命令的结构。
所以,有时你只是想打印一段包含危险命令的文字,它也可能误以为你要真的执行那个命令,把你拦下来。
这确实烦人。
但对于这次特定的风险,我愿意接受偶尔一次误拦,也不愿意再让一条命令悄悄制造出 13.6 GB 的垃圾文件。
这不是放之四海而皆准的答案。如果误拦会造成严重后果,或者规则覆盖的操作范围更大,就应该重新权衡。
还有一个更值得认真对待的问题,不同的规矩可能互相冲突。
比如,一条规则要求检查某类文档,另一条规则却明确规定某些目录不在检查范围内。两条规则各自看起来都有道理,放在一起却打了架。
这时候,我没有让 AI 自己偷偷挑一条执行,而是把冲突记录下来,留给人决定。
因为我想要的是一个能帮我守规矩的助手,不是一个擅自替我修改规矩的裁判。
哪些事情必须拦,哪些事情可以通融,最终仍然应该由人来决定。
#十一、普通人用 AI,究竟能从这件事里学到什么?
回头看这次折腾,我写了钩子,补了测试,改了规则,还处理了好几个之前没想到的边界问题。
听起来好像很像一个软件工程项目。
但对我来说,真正重要的不是这些技术名词。
最初,我只是不想让 AI 再犯同一个错误。
后来我发现,与其每次都在提示词里追加一段提醒,不如想办法把最重要的要求变成它执行任务时必须经过的检查。
与其让 AI 记住「不要这样做」,不如让它在准备这样做的时候就收到拦截和解释。
与其把所有可能出错的情况都写成长篇大论,不如先找出最值得防范的风险,把它们变成简单、明确、能测试的规则。
当然,也不是所有事情都值得做成钩子。
有些要求只需要提醒,有些需要写进项目说明,有些则值得变成自动检查。关键不是把每条规矩都变成程序,而是想清楚,哪种错误值得花力气提前挡住,哪种错误只需要在发生后总结经验。
AI 可以帮你写代码、设计检查方式、补充测试案例,甚至提出你一开始根本没想到的问题。
但它写出来的东西是不是符合你的目的,测试有没有遗漏,遇到两条规矩打架时应该听谁的,这些仍然需要你来判断。
你不必一开始就懂所有技术。
你只需要能把问题说清楚,能追问「为什么」,能在看起来完成的时候再多检查一步。
这大概就是我理解的氛围编程,不是让 AI 替你承担全部思考,而是让你可以把自己的判断,逐渐变成真正能用的东西。
我不想每天背着一大串提示词,反复提醒 AI 哪些事情不能做。
我想把那些真正重要的规矩立起来,让它们在该起作用的时候自动起作用。
规矩不是写得越多越好。真正让人省心的,是该拦的时候拦得住,该放的时候放得过。
护栏立住了,人才有余力去做更值得做的事。
一条后台 grep 写出 13.6 GB,包从 2.1 GB 撑到 15 GB
墓碑表述拦截钩子开始测试,补上拒绝理由
把已有的几个预处理钩子合并成一个闸口
grep 形状判据落地;用 4,938 条历史命令做回归,发现过拟合风险
量出长度信号的预测力只有 1.87 倍,阈值定在 2000 字符
评论