TwilightRain | 别再提醒 AI 小心了,给它装道护栏

别再提醒 AI 小心了,给它装道护栏

约 5867 字 · 阅读约 15 分钟

我,能不能把这次的教训做成一道硬性护栏?以后 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
2
UserPromptSubmit  →  router/hook.js        (路由)
PreToolUse → pretool-gate/hook.js (闸口,匹配 Bash|Write|Edit)

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 哪些事情不能做。

我想把那些真正重要的规矩立起来,让它们在该起作用的时候自动起作用。

规矩不是写得越多越好。真正让人省心的,是该拦的时候拦得住,该放的时候放得过。

护栏立住了,人才有余力去做更值得做的事。

从事故到闸口
09-26

一条后台 grep 写出 13.6 GB,包从 2.1 GB 撑到 15 GB

09-29

墓碑表述拦截钩子开始测试,补上拒绝理由

10-02

把已有的几个预处理钩子合并成一个闸口

10-08

grep 形状判据落地;用 4,938 条历史命令做回归,发现过拟合风险

10-08 之后

量出长度信号的预测力只有 1.87 倍,阈值定在 2000 字符

评论