同事在群里甩来一句:「你那 AI 助手是不是傻了,一直嚷嚷着要备份,就是不往下走。」

那是给七亿批量远程管理工具加 AI 助手那阵子的事。工具是个 Windows 上的 WinForms 程序,给一台 Linux 主机配了个「逐步执行」的运维助手——AI 一次只吐一条命令,客户端拿去服务器上跑,把真实回显喂回去,它再想下一条。那天想让它帮着把 SOCKS5 搭起来,用的是 3proxy。

卡就卡在第一步。

它一遍遍地回「还没搭好,先把配置备份一下」,然后把这条命令抛出来:

cp -a /etc/3proxy.cfg /etc/3proxy.cfg.bak

抛完就没动静了。步骤数不涨,那条命令也没往外发。用户在那头等,以为它在思考,其实它每一轮都在提同一条被拦下的命令。

我第一反应是模型抽风了。换台机器,换个说法,重开一轮对话——一模一样。后来切到本地看客户端日志,才发现每条 cp … .bak 都被打了「已拦截」。是我自己在七亿批量远程管理工具里写的代码在拦,跟模型没关系。

回头查那段过滤逻辑,根因很清楚,也有点丢人。

之前吃过一个亏:AI 干活爱来回翻烧饼——改了一半觉得不对,又把备份盖回去,再改,再盖。于是我们加了条规矩,防它乱执行「回滚」命令。写得图省事:

if (cmd.Contains("cp") && cmd.Contains(".bak")) { block(); }

看着挺合理。可它把两种完全相反的操作一起拦了:

  • 真回滚:cp /etc/3proxy.cfg.bak /etc/3proxy.cfg——从备份恢复,源是备份文件;
  • 改前备份:cp -a /etc/3proxy.cfg /etc/3proxy.cfg.bak——先存一份,源是正式配置。

后一种是干活的正路,被这条规则一并当成了回滚。它每次想动手,第一步「先备份」就被拦,于是永远迈不出去。

改法不复杂:判据从「命令里有没有 .bak」,收窄成「源是不是备份文件」——也就是只看 cp 的第一个参数里带不带 .bak。源是 .bak(恢复)拦,源是正式文件(备份)放行。

// 只拦「从备份恢复」:源路径里含 .bak
var src = FirstArg(cmd);   // cp 的源
if (cmd.StartsWith("cp") && src.Contains(".bak")) { block(); }

改完再跑,它一步接一步往下走,账号密码、监听端口、白名单,一条条铺过去,没再卡。

回头想,这事绕的地方不在代码,在「一条规则该拦多宽」。当初写的时候我只想着「别让它回滚」,脑子里根本没有「清白的备份命令长什么样」,一刀切下去,最先被误伤的恰恰是它每一步都要走的那步。防回滚的规则,顺手把备份也防了。

后来补的几条规则,我尽量都走同一个思路:先想清楚「要拦的这件事,具体长什么样」,再把匹配收窄到那个形状,而不是拿一个宽泛的特征去到处套。宽泛的匹配不止会漏,也会误杀——像那条 pkill 把自己也杀了的坑,是另一种宽;这条不算别致,就是拦得太粗。

那天顺手翻了下这东西的历史,一天里从 V95 改到 V113,十几个版本,全是这种边边角角的修补堆出来的。这类助手,看介绍觉得是「接个模型就完事」,真做起来,一大半精力花在「怎么把它框住」——逐步执行怎么走、有哪些限制,我在之前那篇里提过;这套七亿批量远程管理工具到底怎么用,官网教程讲得比我这样零散说清楚。

同事后来又问了一次,我说修好了。他回了个「哦」。也是,谁关心你那条判断收窄了几个字符,能往下走就行。