AI 写的正则总在边界上翻车:样本、规则、反例这三步到底该怎么排
数据截至 2026-07。文中示例以 Python 的
re为准并已实跑验证;各语言正则实现对锚定、Unicode 属性、命名捕获的支持差异较大且随版本变化,文中不写具体版本号,请以你所用运行时的官方最新说明为准,并在本地跑一个最小用例确认。
这个环节最常见的归错因是:正则不对就怪模型能力不行,于是换个更强的模型再问一遍。真正的病根通常在输入侧——你给的是一段自然语言规则描述,没给样本,没给反例,模型只能按它对”你大概想匹配什么”的猜测去补全边界。 正则是一种边界极度密集的表达:一个 + 和 * 的差别、一个字符类里加不加连字符、锚不锚定行首行尾,都会让同一段描述展开成完全不同的语言。人写正则的时候,这些边界是在盯着真实样本一行行试出来的;你把样本拿走只留描述,等于要求模型凭空替你做本该由数据决定的决策。它会做,而且会做得很像那么回事,这恰恰是最难发现的失败。
站内有两篇相邻的文章,分工是这样的:AI 写的正则把服务卡死 讲的是正则已经写出来、上了线之后引发灾难性回溯的故障定位与止损;怎么给 AI 定验收标准 讲的是通用意义上把”做完了”变成可判定条件的方法论。本篇夹在这两者中间,只管正则从提出需求到合入代码这一段工程环节:哪一步 AI 能替你干、哪一步必须人来定、写完之后拿什么动作确认它真的对。
一、先分清:这个环节里哪一步是人的活
把”写一条正则”拆开,其实是四件事,AI 在其中的可用度差别很大。
第一件是定义匹配目标:从什么样的文本里、抠出什么、允许多松、绝不能误伤什么。这一步无法外包。模型不知道你的日志里混着两代格式,不知道你这条正则是做告警过滤还是做数据清洗——前者宁可多报也不能漏,后者宁可漏也不能改错数据。松紧方向是业务决策,人必须先拍。
第二件是提供样本:正例、反例、边界例。人只需要出原材料,不需要出结论,但材料质量决定后面三步的天花板。绝大多数返工都堵在这里。
第三件是把样本和规则翻译成模式。这一步 AI 确实强,尤其在你已经给了样本的前提下。它熟悉转义细节、字符类简写、分组与命名捕获的写法,比人手敲更快也更少低级错误。让它同时给出三到四个候选并说明各自宽严差异,比让它给一个”最佳答案”有用。
第四件是验证。必须机器跑、人看结果,不能是模型自述。让模型解释”这条正则会匹配 abc 但不会匹配 abcd”,它给的是按语义直觉做的复述,而不是把模式当状态机跑了一遍的结果——写模式和推演模式在它那里是同一种生成动作,你没法从语气上区分哪次是真推对了。
一句话概括这一节:方向和样本是人的活,写法是 AI 的活,结论是运行时的活。 谁越界谁出事。
二、样本在前,规则在后:输入该怎么组织
顺序真的有影响。先扔一句”我要匹配手机号”,模型会立刻输出一个它见过无数遍的通用模式;你再补样本,它往往只在原模式上打补丁,越补越长。反过来先给一组样本、再说明规则倾向,它会围绕样本归纳,产出的模式紧凑得多,也更容易读懂。
样本至少包含三类,缺一类就等着返工:
- 正例:必须匹配上的,覆盖你已知的所有格式变体。日志里有新旧两代格式,两代都要给,不要只给手边最近那批。
- 反例:长得像但绝不能匹配的。这是最容易被省略、也最值钱的一类。你要抓订单号,那些同样十几位数字的流水号、时间戳、身份编号都得作为反例给出来。没有反例,模型只能往宽了写,因为宽只会让正例更容易过。
- 边界例:空串、只有分隔符、超长串、前后带空白、大小写混合、全角半角混排、跨行。中文场景另备一条含全角标点、一条含不可见字符的样本。
规则描述放在样本之后,用来限定样本表达不出来的东西:是否锚定整串、是否要捕获分组、区不区分大小写、是不是在多行模式下用、目标运行时是什么。不同实现对断言、命名捕获、Unicode 属性的支持不一样,运行时不说明白,它会默认给你一个某种语言风格的写法。
再加一句**“宁可漏还是宁可多”**,往往比十行描述都管用——它给了模型一个在样本没覆盖到的空白区域上做决策的方向。
写完让它一并交出两样:模式本身,以及它认为这条模式会拒绝的三个典型输入。后者不是拿来信的,是拿来对照的——如果它列出的拒绝样本里有你的正例,说明你俩对目标的理解已经岔了,这时候回去改需求描述比改正则划算。
三、常见现象与判别表
写出来之后跑一轮,多数问题会落在下面几类里。这张表按”看到什么 → 大概率是什么原因 → 怎么确认 → 怎么处置”来用。
| 现象 | 大概率成因 | 怎么验证 | 处置动作 |
|---|---|---|---|
| 正例大部分能过,个别过不了 | 样本覆盖不全,模型没见过那个格式变体 | 把失败的那条单独喂进去逐段截断测试,定位是哪一节卡住 | 把这条加入正例集重新生成,不要手工在模式上打补丁 |
| 反例也被匹配上了 | 模式过宽,通常是缺锚定或字符类范围太大 | 用只含反例的集合跑一遍,统计误匹配率 | 补锚定、收窄字符类;把反例写进用例文件长期保留 |
| 匹配上了但捕获的内容多一截或少一截 | 贪婪与非贪婪用错,或分组边界划错 | 打印每个分组的 span 起止位置,而不是只看整体是否命中 | 改量词贪婪性或加明确的定界字符 |
| 本地过、线上不过 | 运行时或语言实现不同,转义与特性支持有差异 | 在目标运行时里跑同一组用例,而不是在本机的另一种语言里 | 换成两种实现都支持的写法,避开偏门特性 |
| 含中文时行为诡异 | 编码问题或字符类被按字节理解 | 打印输入的编码与长度,确认不是读入阶段就已经错了 | 先修编码再谈正则,顺序反了会一直修错地方 |
| 单条输入让匹配长时间不返回 | 嵌套量词导致回溯爆炸 | 用逐步加长的构造串测耗时增长曲线 | 改写模式或加匹配上限,走灾难性回溯那条排查路径 |
| 用例全绿但线上仍漏抓 | 用例是模型自己造的,与真实数据分布脱节 | 拉一批真实数据抽样人工核对 | 用真实样本替换合成样本重跑 |
最后一行是最隐蔽的。让模型顺手把测试用例也生成了,它造的用例往往和它写的正则出自同一套假设,两边一致地错——这就是自证。用例的原材料必须来自真实数据,哪怕只有二十条。这也是测试假通过那一类问题在正则场景下的具体表现。
四、验收动作:拿反例过一遍
验收只有一个硬标准:一个可重复执行的脚本,输入是正例集和反例集,输出是通过或失败,失败时能指出是哪一条。 不能是你在终端里手工试几下觉得没问题。
最小可用的做法是把样本落成两个文本文件,一行一条,然后跑一段几十行的校验:
import re, sys
PATTERN = r"\d{4}-\d{2}-\d{2}" # 换成你要验收的模式,锚定交给下面的开关
WHOLE = True # True=要求整串匹配,False=允许命中子串
rx = re.compile(PATTERN)
hit = rx.fullmatch if WHOLE else rx.search
def load(path):
with open(path, encoding="utf-8") as f:
return [ln.rstrip("\n") for ln in f if ln.strip()]
fails = []
for s in load("positive.txt"):
if not hit(s):
fails.append(("漏匹配", s))
for s in load("negative.txt"):
if hit(s):
fails.append(("误匹配", s))
for kind, s in fails:
print(f"{kind}: {s!r}")
print(f"failed={len(fails)}")
sys.exit(1 if fails else 0)
几个细节:用 rstrip("\n") 而不是 strip(),尾随空格本身可能就是你要测的边界,被顺手洗掉就测不出来了。load() 里那句 if ln.strip() 是用来跳过文件末尾空行的,代价是空串这条边界例没法靠文本文件表达——真要测空串,得在脚本里显式补一句,比如把 "" 直接塞进对应的集合里判一次,并且先想清楚空串在你的语义下算正例还是反例。打印用 {s!r},不可见字符和全角空格会以转义形式显现,否则你会盯着两条看起来一模一样的字符串怀疑人生。把”整串匹配还是子串命中”提成一个显式开关,而不是靠在模式里加 ^ $ 来表达,这一条值得单独说:Python 里 $ 除了匹配串尾,也匹配串尾换行符之前的位置。拿 ^\d{4}-\d{2}-\d{2}$ 配 search() 去测 "2026-07-29\n",结果是命中;同一条输入换成不带锚定的模式配 fullmatch(),结果是不命中。你的样本文件如果有一行末尾多了个换行或回车没洗干净,前一种写法会悄悄放行,后一种会当场报出来。这行为在不同语言的实现里并不一致,所以更该把语义写在代码里、别藏在模式里——换个运行时时,你一眼能看见自己当初要的是哪种。
脚本以退出码表达结果,可以直接挂进 CI。样本文件读入前用 wc -l positive.txt 确认条数没被编码或换行符问题截掉(wc -l 数的是换行符个数,末行没换行会少算一条,对不上的时候先看是不是这个);跨平台协作用 git config core.autocrlf 确认换行策略,Windows 上生成的样本带回车符会让本该匹配的行全线失败,报错里完全看不出来。
每修一次正则,跑的是全量用例,不是新加的那条。 正则改动极容易顾此失彼,收窄一个字符类顺手就废掉三条旧正例。这是它和普通函数最不一样的地方:普通函数改一处影响一处,正则改一处影响整个语言。
有条件就把校验接进代码审查流程,让改动正则的提交必须带上用例文件的同步改动,做法可参考AI 代码评审工具怎么选里把硬性检查前置到提交环节的那部分。
五、什么时候别再折腾了
正则是个非常容易陷进去的东西。你会觉得”再调一版就好了”,然后一个下午没了。给自己设几条明确的止损线:
止损点一:改到第四版还在正例反例之间来回摆。 这说明目标本身不是正则能干净表达的——通常是因为你要匹配的东西依赖上下文、依赖计数、依赖嵌套结构。常规正则处理不了任意深度的嵌套配对(个别实现带递归或平衡组这类扩展,但写出来的东西更没人敢接手,而且换个运行时就废),也处理不了”第三个字段”这种位置语义,硬凑出来的模式即使当下能过也没人敢维护。换成先分割再判断的普通代码,几行搞定,可读可测。
止损点二:模式长度超出一屏,或者你已经看不懂自己在维护什么。 长度本身不是罪,但一条没人能读的正则等于一个没人敢改的黑箱。正确动作是拆:拆成两三条小正则加几行判断,或者拆成”粗筛正则 + 精确校验函数”两层。粗筛快速淘汰不相关文本,精确校验用普通代码写,边界清晰、能打日志、能单步调。
止损点三:耗时随输入长度非线性增长。 串长翻倍而耗时翻了十倍以上,就别再优化模式,直接走改写加上限那条路。
回滚点怎么定:合入前保留旧实现并用配置开关切换,上线后先影子运行一段——新旧两条路都跑,只记录差异不改变行为,差异集合稳定为空再切主路。成本不高,但能挡住绝大多数”用例全绿、真实数据翻车”。场景不允许影子运行,至少保证回滚是改一行配置而不是重新发版。
换条路的判断依据:要处理的是结构化格式(JSON、XML、HTML、CSV、URL、邮件地址、日期时间),几乎总有现成解析库比正则更合适。用正则解析结构化格式是经典的把简单问题做复杂——库会替你处理转义、嵌套、编码、时区这些你根本没打算处理的东西。模型对这类需求往往顺着你的问法直接给正则,因为你问的就是”正则怎么写”。把问法改成”这个需求用什么方式实现最稳”,答案会不一样。
六、避坑清单
坑一:让模型同时生成正则和它的测试用例。 为什么会踩:省事,一次对话拿到全套。但两边出自同一套假设,模型一旦误解需求,正则和用例会一致地错,全绿反而给了你虚假的安全感。 怎么避:正例反例必须来自真实数据或人工构造,模型只能补充边界例,不能提供主体样本。
坑二:信模型对正则行为的口头解释。 为什么会踩:它的解释读起来笃定、逻辑自洽,但那是按语义直觉复述,不是执行有限状态机,遇到多层量词嵌套、断言、贪婪回退时错得很自然。 怎么避:任何”这条正则会不会匹配 X”的结论一律以运行时输出为准。这属于大模型幻觉在细粒度技术判断上的典型表现。
坑三:没说明目标运行时。 为什么会踩:对话里没提语言,模型按最常见的那套语法写,可能带上某实现独有的特性;本地又恰好用同一种语言测,一路绿灯,换到线上运行时才炸。 怎么避:需求第一句就写清运行时,验收脚本必须在目标运行时里跑,不能拿另一种语言代跑。
坑四:把用户输入直接拼进正则。
为什么会踩:“支持自定义关键词搜索”是很自然的需求,拼字符串是最直接的实现。但输入里的元字符会改变模式语义,轻则结果莫名其妙,重则构造出回溯爆炸的模式把服务卡死。
怎么避:外部片段进入正则前必须转义,Python 里是 re.escape(),其他语言有对应函数。能用普通字符串包含判断就别上正则。
坑五:忽略中文与全角字符。
为什么会踩:样本沿用英文场景的习惯,\w 这类简写在不同实现里对中文的含义不一样;全角逗号、全角空格、零宽字符在肉眼上和半角毫无区别。
怎么避:样本集固定放两条中文用例、一条全角标点用例,脚本打印用可见转义形式,别让肉眼当裁判。
坑六:改完只跑新加的那条用例。 为什么会踩:改动看着很局部,就收窄了一个字符类而已。但正则没有局部性,一处修改会重写整个匹配语言。 怎么避:把校验做成一条命令,改一次跑一次,接进 CI 让它成为不需要记的动作。
收束:合入前自检清单
AI 在正则上的贡献主要在第三步——把已经想清楚的边界翻译成紧凑的模式。前面的目标定义和样本准备、后面的运行时验证,都得你自己扛。这三步排对了,模型的产出质量会有肉眼可见的变化;排错了,换多强的模型也只是在原地换个姿势猜。
合入前对着过一遍:
- 需求里写清了目标运行时,以及宁可漏还是宁可多。
- 正例覆盖所有已知格式变体,包括旧格式。
- 反例里有”长得像但不能匹配”的真实数据。
- 边界例覆盖空串、前后空白、超长串、中文与全角、大小写混合。
- 有一个可重复执行、以退出码表达结果的校验脚本,且已挂进 CI。
- 用长度递增的构造串测过耗时增长,确认不是非线性。
- 模式在一屏以内,或已拆成粗筛加精确校验两层。
- 上线路径能一行配置回滚,或已安排影子运行比对。
八条里缺两条就别急着合。正则出问题时通常不给你清晰的报错,而是安静地漏掉一部分数据——那种账,往往要到很久以后才结。