1. 为什么我会掉进“Agent 写正则”这个坑
说起来有点惭愧。上个月我接手一个老项目,后端的文件上传接口一直用一段“历史悠久”的正则做后缀校验。那段正则写得很抽象,缩进乱、分支多,没人敢动。新需求要加几种文件类型,我第一反应是“让 Agent 帮我写一段新正则,改一下不就完了”。当时我用的还是一个接入了通用大模型的 Agent 框架,顺手把需求一贴,几秒钟它就给我吐了一段看起来挺工整的表达式,还贴心地加了注释。
我差点直接复制进代码库。
好在最后时刻,我多留了一个心眼,把它丢进测试用例里跑了一圈。结果第一个用例就没过——一个合法的.jpg文件被拦下来了。接下来我又试了几个边界输入,发现问题远比想象中多。这一段经历让我彻底明白了一个道理:Agent 能写正则,但它写出来的正则和你脑子里的“规则”往往是两个世界的东西。
这也是我写下这篇博文的原因。如果你也在用 Agent 辅助写正则、做规则校验,或者你正准备把“规则”这件事交给 AI 去自动化,我会把这次踩坑的完整过程、背后的原理、以及我后来总结的避坑套路全部展开讲一讲。内容会涉及 Agent 交互、正则语法、规则评审、测试用例设计这几个方面,希望能帮你少走点弯路。
2. Agent 写正则的底层逻辑:它到底“懂”什么
2.1 Agent 生成正则的过程,本质上是在做“模式补全”
首先要搞清楚 Agent 写正则的原理。以当前大模型为底座的 Agent,生成正则的方式不是“推导”出来的,而是“预测”出来的。它会根据你的自然语言描述,从训练数据里寻找“最相似的问答对”,然后把最常见的正则写法作为输出。换句话说,它是在做模式补全,不是在执行逻辑推演。
这就带来一个很反直觉的结论:Agent 写得越流畅、注释越完整,越说明它见过类似的训练样本。而这种“见过”跟“理解你的业务规则”是两回事。你告诉它“要求文件名后缀只能是 png、jpg、jpeg、gif”,它能写出来;但你告诉它“这个系统在 Apache 2 环境下运行,文件名可能是中文、可能有空格、大小写不敏感”,它就未必能把所有约束都编码进正则里了。
在实际项目中,业务规则往往不只是“允许哪几种后缀”这么简单。它还可能包含:文件名长度限制、是否允许点号出现在中间、是否允许下划线、Windows 与 Linux 文件名兼容、URL 编码后的百分号字符、Unicode 文件名……这些规则叠加起来,单靠一段正则去承载,本身就是一个高风险设计。Agent 只负责生成“字面匹配规则”,但承载不了的隐含约束,它不会主动帮你识别。
2.2 正则的“可读性”是个陷阱
我还发现一个特别误导人的地方:Agent 写出来的正则,通常配了一堆注释,看起来特别“正规”。但这恰恰是问题所在。真实生产环境里的正则,经过几轮需求变更后,往往已经演变成“能跑就行”的状态。你让 Agent 重新生成,它会给你一份“从零开始”的干净版本,却完全丢失了原来正则里那些隐含的兼容逻辑。
举个最常见的例子。老正则里可能有[a-zA-Z0-9_\-]+这种字符类,为什么?不是因为它当时只想允许这些字符,而是因为早期某个版本有一个 bug,文件名为中文时上传会乱码,大家为了“省事”直接把中文禁了。这个背景没人写进注释。几个月后,你让 Agent 基于“当前需求描述”重写,它大概率不会保留这个限制,于是中文文件名开始进入流程,乱码问题卷土重来。
所以我在实际使用中得出一个教训:让 Agent 写正则之前,一定要先把你现有的正则、历史变更记录、以及所有的边界测试用例一起喂给它,而不是只给一份“简明需求”。否则你得到的不是“重写”,而是“失忆”。
2.3 Agent 的“自信”与真实错误率不匹配
还有一个很普遍的现象:Agent 生成的表达式,往往没有经过充分的测试就交给了你,而且它不会主动告诉你“这个正则可能需要更多边界测试”。你问它“这个正则对吗”,它更倾向于回答“对”,而不是“我测试了 100 个用例,其中 3 个没过”。
我把这种现象叫做“自信不匹配”。它源自语言模型的优化目标——生成流畅、合理、符合人类偏好的文本,而不是“生成绝对正确的代码”。所以你要把 Agent 当成一个“非常善于表达但需要严格验收”的协作者。它写出来的正则,只是第一版草稿,不是可交付的成品。这个心态不建立起来,后续所有步骤都容易翻车。
3. 翻车现场实录:Agent 写正则踩到的四个典型坑
3.1 黑名单思路:看着在拦截,形同虚设
我最先让 Agent 帮忙处理的,是接口里那段后缀校验。当时的现状是:后端本来有一个“不允许上传的可执行后缀列表”,比如php、jsp、exe、sh等。Agent 很快就写出了类似这样的逻辑:
# 这是一个“黑名单”风格的正则(示意,请勿直接使用) import re blocked_ext = re.compile(r"\.(php|jsp|exe|sh|asp|aspx)$", re.I) def check_filename(filename): return not blocked_ext.search(filename)表面看没问题,逻辑也简单:命中黑名单就拦截。但这里有个致命缺陷——黑名单永远列不全。攻击者不会只用php后缀,他可以用phtml、pht、php3、php5、PhP(如果服务器配置了多后缀解析,还会出现.jpg.php这种组合)。更麻烦的是,Apache 2 在某些版本配置下,可能会把foo.php.(末尾带点)或foo.php%00之类的内容交给处理器。服务端正则要应对这种变体,几乎是不可能穷举的。
正确做法是改白名单:只允许明确允许的后缀,其余一律拒绝。
# 白名单方案:只允许配置列表内的后缀 ALLOWED_EXT = {".png", ".jpg", ".jpeg", ".gif", ".webp", ".pdf"} def check_filename(filename): ext = Path(filename).suffix.lower() return ext in ALLOWED_EXT这个案例给我的启示特别深。Agent 写代码时,会按照“你让我拦截什么我就拦截什么”的逻辑执行,但它不会判断你的底层设计是不是安全的。如果规则本身设计错了,Agent 只是在帮你更高效地执行一个错误方案。所以让 Agent 写正则前,先确认“规则的思路本身是否成立”。
3.2 贪婪匹配:校验通过,但括号里全是问题
第二个坑出在一个看似人畜无害的“表单字段校验”上。当时要校验一个“用户标识”,规则是:下划线开头,后跟字母数字,总长度 5-20 位。Agent 给的正则是:
pattern = r"^_\w+$"我一看,就觉得不对。\w在 Python 的 re 模块里默认会匹配 Unicode 字母数字,包括中文、大写、全角数字。于是_张三、_ABC这类的输入也能通过校验。这里的问题不是“能不能匹配”,而是“匹配的边界和字符范围与业务规则不一致”。
更隐蔽的是另一个例子。我需要匹配一个“包含版本号的文件名”,比如report_v2_final.pdf这样的结构。Agent 给出的正则是r"report_v\d+_.*\.pdf"。这个正则在很多输入上都表现正常,但遇到report_v20241201_draft_v3_final_merged.pdf时,它也能匹配。表面上没问题,可如果你的意图是“只匹配 v 后面跟一个两位数的版本”,那贪婪匹配和多段下划线的组合就会让规则范围失控。
我后来用了一个笨办法:把所有历史文件名跑一遍,看看正则漏了什么、多吞了什么。结果发现有两类合法文件名被拦截,一类是因为大写字母(Windows 用户上传的文件名常带大写),另一类是中文文件名。这两类单看正则都能匹配,但组合起来就有问题。
其实正则的“正确性”不能靠看,只能靠“测试用例 + 输出比对”。这也是我后来反复强调的核心原则:没有测试用例验证的正则,都只是“待定草稿”。
3.3 灾难性回溯:规则没写错,服务被拖垮
这是最吓人的一个坑。度的把握,正则看似写对了,但在特定输入下,引擎会进入灾难性回溯(Catastrophic Backtracking),直接导致 CPU 飙升、请求卡死。
有一次,我需要写一个校验“数字和字母混合的短码”的正则,规则是“必须以字母开头,可以包含数字和字母,总长度 4-10 位”。Agent 给出了一个“看似更通用”的版本:
pattern = r"^([a-zA-Z0-9]+)+$"后面跟了一个$,乍看没什么问题。但如果完全没有长度限制的输入进来,比如用户传了一个 30 万个字符的字符串,正则引擎就需要反复回溯来尝试不同的匹配方式,性能急剧下降。这就是经典的嵌套量词导致的灾难性回溯。换成r"^[a-zA-Z0-9]{4,10}$"之后,性能问题立刻消失。
这个案例说明,Agent 生成正则时,常常为了“包容性”加上多余的量词分组,而这恰恰是性能隐患的来源。你不仅要看“规则是否匹配”,还要看“最坏情况下是否还能跑得动”。当然,多数情况下问题不会那么极端,但一旦出现线上卡顿,排查起来非常费劲。
3.4 字符编码与大小写:被忽略的隐性规则
最后一个高频坑,是 Agent 生成的正则对大小写和 Unicode 的处理与业务预期不一致。最常见的是用户名校验。规则要求“小写字母、数字、点号、下划线”,Agent 直接写了r"^[a-z0-9._]+$",看着没问题。但现实业务里,用户可能输入大写字母,或者输入全角小写字母(来自中文输入法),或者行尾带上不可见字符。后果就是一方面合法输入被拒绝,另一方面看起来“同形”的非法输入却能通过。
这时候我会用一个很简单的辅助方法:写一段超短脚本,把候选输入全部跑一遍,同时输出“输入内容 + 正则结果 + 实际字节码”,通过字节码一眼看出是否有隐藏字符。隐藏字符(比如零宽空格)是正则和肉眼都特别容易漏掉的东西。
import re pattern = r"^[a-z0-9._]+$" for name in ["test_user", "Test_User", "test", "test_user\u200b"]: result = bool(re.match(pattern, name)) print(repr(name), "->", result)输出会直接展示第四种输入带了一个\u200b,但你能看见的名称却是正常的。这种情况如果发生在登录、改名、路由匹配上,会很隐蔽。Agent 无法主动替你考虑“用户会不会输入全角字符”,它只能按照你描述的规则来写。
4. 实操记录:如何让 Agent 输出“可验收”的正则
4.1 把“规则”翻译成“测试用例”,再做反向提问
经过三轮翻车后,我开始改变用法。现在我的做法是:不让 Agent 直接写正则,而是先让它帮我把业务规则转换成一个测试用例清单。比如我给它一句话规则:“文件名后缀只能来自白名单,不区分大小写,且文件名不能以点结尾。”我会要求它输出至少二十组输入样例,覆盖合法、非法、边界、异常编码等场景。
有了测试用例,再让它写正则,然后直接用这些用例去验证它写出来的东西。如果 Agent 足够强,它会先拆分规则、再输出代码和测试脚本;如果 Agent 能力有限,至少我能拿着用例清单去人工写正则,然后做对比测试。这套流程,本质上是把“模糊的自然语言规则”转成“可以自动判定的用例契约”。
4.2 Prompt 工程:把需求描述“结构化”
我给 Agent 下指令时,不再说“帮我写个校验文件后缀的正则”,而是这样写:
场景:后端校验上传文件名,仅允许白名单内的后缀。 系统环境:Linux + Apache 2,文件名可能包含空格、中文、大写字母。 约束: 1. 采用白名单校验,不允许黑名单。 2. 后缀列表:.png, .jpg, .jpeg, .gif, .webp, .pdf 3. 后缀匹配不区分大小写。 4. 文件名不能为空,不能以点结尾,不允许路径分隔符(包含 / 和 \)。 5. 输出 Python3 代码(re 模块或 pathlib 均可),并在代码中自带上文约定的测试用例。 请先输出测试用例清单,再输出实现代码,并在代码里添加运行时自检。这样写的好处是:Agent 不再只根据“语义”来猜测需求,而是被约束在明确的规则边界里。它生成的正则即使第一版不能全部通过测试,预期也会更接近正确结果。而测试用例列表又会反过来帮我理解规则本身的漏洞——比如我发现“文件名不能以点结尾”这条其实很容易被忽略,而 Agent 自己会在输出里强调它。
4.3 多轮迭代:把 Agent 当成“需要验收的实习生”
我现在的习惯是,Agent 输出第一版正则后,我不会直接看代码,而是先跑测试用例。如果没过,我把失败的用例回贴给 Agent,让它自己修。这个过程可以重复两三轮。有一次,正是因为反复迭代,我发现了re.match与re.fullmatch之间的语义差异——re.match只从开头匹配,如果正则没有$,就会导致“前缀合法但整体非法”的输入通过校验。而 Agent 在第二轮迭代里主动指出这一点,并把代码改成了fullmatch。
迭代过程里,我会刻意加入“坏输入”案例——比如空字符串、超长字符串、包含换行符的字符串、包含百分号编码的字符串、包含空格的字符串。这些输入通常在业务里不容易被想到,却恰恰是正则翻车的高发区。每次迭代,我都记录失败用例数,直到归零,才算“初步可用”。
4.4 用工具“可视化”正则,再做人工审查
即使测试用例全过,我也不会马上上线。我建议你用支持正则可视化的工具(比如 regex101、regexr)把 Agent 生成的正则图形化展示一遍。这时候可以直观看到量词的作用范围、分组的嵌套结构、边界锚点的位置。
看图形的意义在于:你会发现很多正则写成一行时“看着对”,但拆解成图形后,量词的嵌套与分组关系可能跟你设想的不一样。比如([a-zA-Z0-9]+)+这种嵌套量词,图形化后会让你意识到它可能引发回溯风险。正则的“可读性”在图形化之后会大幅提升,人工审查的难度也会降低。
4.5 线上验证:不要只做单元测试
最后一步是回归真实请求。我会把新的校验函数接进一个灰度接口,提前几天采集线上的历史请求日志,把“曾经正常通过”的请求全部回放一遍,看看新正则是否会误伤。这一步特别重要,因为单元测试用例是我们构造出来的,而线上流量的复杂性永远超出想象。
有一次,我自认为测试用例已经覆盖得很全了,结果回放时发现大量文件名以数字开头、以多个点分隔的文件被拦截。原因是我给 Agent 的规则里没有体现“文件名可以包含多个点,且最后一个点后面的才是后缀”。这种“规则描述不完整”导致的问题,只有通过真实流量回放才能暴露出来。从那以后,我把“线上日志回放”写进了自己的验收清单。
5. 常见问题与排查技巧实录
为了让你以后排查起来省点力,我这里整理一份速查表。它来自我这几轮踩坑的总结,也结合了群里同行分享的常见问题。如果你在使用 Agent 生成正则时遇到相似情况,可以对照着查。
| 症状 | 可能的根因 | 排查思路 | 解决方向 |
|---|---|---|---|
| 合法输入被拒绝 | 字符范围过窄、未考虑大小写/中文/全角 | 用字节码打印输入,比对字符集 | 改用 Unicode 属性或扩大字符类 |
| 非法输入通过校验 | 未使用全文匹配(match vs fullmatch) | 检查是否漏了$或\Z | 改用fullmatch或补锚点 |
| 匹配结果与预期不符 | 贪婪匹配留下多余分支 | 图形化正则,观察分组 | 改懒惰匹配或固化分组 |
| 特定超长输入导致卡顿 | 嵌套量词引发灾难性回溯 | 用超长字符串做压测 | 重写为单层量词,增加长度限制 |
| 同一正则在不同语言行为不同 | 各语言正则引擎差异 | 分环境测试 | 锁定语言后针对性调整 |
| 规则描述正确但 Agent 输出偏差 | 需求描述缺乏边界样例 | 反推“测试用例先行” | 用用例清单约束 Agent |
| 上线后偶发误拦截 | 线上流量包含未预测输入 | 回放历史请求日志 | 用流量回放做回归 |
排查时我用得最多的工具是grep -P和 Python 的regex调试模式。尤其是 Linux 环境下,可以用grep -P快速验证某个表达式对不对,然后写一段批量脚本,把几十个候选输入一次性跑完。具体命令大致是这样:
# 快速验证:哪些文件名匹配白名单后缀 printf '%s\n' "a.PNG" "b.jpg" "c.php" "d.tar.gz" "e.pdf " \ | grep -iP '\.(png|jpe?g|gif|webp|pdf)$'注意grep默认按行处理,如果输入文件来自 Windows,行尾会带\r,这个隐藏字符经常会让正则的$匹配失败。遇到这种问题,先用sed 's/\r$//'清理再测试。
6. 比“写正则”更重要的:重新审视“规则”本身
踩完这些坑之后,我反而对“Agent 写正则”这件事有了新的理解。与其说问题出在 Agent 写不对,不如说问题出在我把“规则”想得太简单了。业务规则从来不是一个正则能承载的完整规范。规则里有“显性约束”,也有“隐性约束”;有“数据格式要求”,也有“安全底线要求”。
所以我现在的习惯是把规则拆成三层:第一层是“格式规则”,用正则和字符串方法处理,这部分可以交给 Agent 辅助生成;第二层是“业务规则”,比如某些文件名必须在特定目录下、某些用户不能上传特定类型,这层用显式代码判断,不要塞进正则;第三层是“安全规则”,比如白名单、路径穿越防护、文件内容嗅探,这层跟框架和中间件配置强相关,正则只能当辅助,不能当主力。
把这三层分清楚后,再去用 Agent,你会发现它的定位特别清晰:Agent 是一个能快速生成“格式规则草稿”的助手,而不是“业务决策者”。最终的验收责任永远在人这边。所以我在让 Agent 产出正则的时候,会坚持一条原则:可以有 Agent 的初稿,但必须有人去审、去测、去跑回归。一旦少了这步,任何看起来完美的正则都只是“未爆炸的坑”。
如果你正准备用 Agent 来自动化“规则”相关的工作,我的建议是从一个极小的规则开始试水,比如“只校验一个表单字段的后缀”,跑通这套“用例前置 + 迭代验收 + 流量回放”的流程,再逐步扩展到更复杂的场景。个人体会是,这个流程本身的价值,远远大于 Agent 帮你省下的那几分钟写代码的时间。一次线上故障的成本,基本够你手写几百条正则了。