用分步提示词拆解复杂正则:让模型先解释语义,再给等价改写

直接说结论:让模型一次输出「解释 + 改写」往往两头都不精,把任务拆成「先逐段翻译语义、再约束等价条件、最后给出可验证的候选」三步,能显著提高复杂正则改写任务的可用性。下面用我实际踩过的例子,把每一步的提示词构造和坑都拆开讲。

先让模型解释语义,但必须约束解释的粒度

正则表达式的难点从来不是语法本身,而是嵌套分组、回溯行为和零宽断言叠加之后产生的「隐性语义」。如果直接让模型「解释这段正则」,它大概率会给你一段人话翻译,但丢失关键信息——比如捕获组编号、贪婪与懒惰的边界、回溯的触发条件。

我常用的第一步提示词模板是这样的:

请逐段解释下面这个正则表达式。要求:
1. 按「从左到右」的顺序,把每个 token 或 token 组合拆开;
2. 对每个分组、量词、断言,说明它匹配的字符串范围,以及它不匹配什么;
3. 明确指出每个捕获组的编号和它可能捕获的内容;
4. 指出可能触发灾难性回溯的位置,并解释原因;
5. 不要给出改写,只做语义解释。

正则:<PASTE_HERE>

这里的关键约束是「不要给出改写」。为什么?因为一旦模型在解释阶段就开始尝试改写,它的注意力会被「怎么改」带偏,解释部分就会出现为了迁就改写而模糊化的倾向。我试过让 GPT-4 和 Claude 3.5 同时做「解释 + 改写」和「只解释」,前者在解释嵌套量词时漏掉回溯风险的概率明显更高。

还有一个容易被忽略的点:要求模型说明「它不匹配什么」。这是正则语义里最容易被 LLM 忽略的部分。比如 ^(\d{1,3})(?:,\d{3})*$ 这个匹配千分位数字的正则,模型解释时通常会正确说出它匹配 1,234,但如果你不追问,它很少主动告诉你 12,34 不匹配、,123 不匹配、1234 不匹配。而这些「不匹配」的边界,恰恰是后续改写时判断等价性的核心依据。

改写阶段:把「等价」拆成可验证的约束条件

拿到语义解释之后,第二步才是让模型给出等价改写。但这里有一个致命陷阱:LLM 对「等价」的理解是语义层面的模糊等价,不是形式语言层面的严格等价。它可能会认为 \d+[0-9]+ 等价(对 ASCII 来说确实等价),也可能会认为 (?:...)(...) 等价(捕获行为完全不同)。

所以第二步提示词的重点不是「给出等价改写」,而是先列出等价性约束清单,再让模型在约束下生成候选

基于上面的语义解释,请给出 2 个等价改写版本。

等价性要求(不可违背):
1. 匹配的字符串集合必须完全一致,包括所有边界情况;
2. 捕获组的数量、编号、嵌套关系必须保持不变;
3. 如果原正则依赖回溯行为(如回溯控制动词、原子组、占有量词),改写版本必须保持相同的回溯语义;
4. 零宽断言(lookahead/lookbehind)的生效位置和方向不得改变;
5. 匹配性能不得显著劣于原表达式;
6. 如果原正则使用了特定引擎特性(如 PCRE 的 \K、递归 (?R)、条件分支 (?(1)...)),改写版本必须兼容目标引擎。

对每个改写版本,请:
a. 给出正则表达式;
b. 用表格列出与原版在「匹配范围」「捕获组行为」「性能特征」三个维度上的差异;
c. 指出改写版本可能引入的新风险(如果有)。

这里「捕获组编号不变」这条约束非常重要。我见过很多次模型把 (a)(b) 改写成 (?:a)(b) 然后告诉你这是等价改写——从匹配结果看确实等价,但只要下游代码用 $1 引用了第一个捕获组,行为就完全变了。实战里这种改写造成的 bug 极难排查,因为正则测试工具通常只显示整体匹配,不显示捕获组详情。

另外一个技巧是:要求模型用表格输出差异,而不是让它自己判断「是否等价」。让模型自证等价基本等于没有验证——它会倾向于说「这两个表达式是等价的」然后附上一段看似合理的论证。但如果强制它列出三个维度的差异,它反而会暴露出一些自己没意识到的行为差异,你作为人类可以快速判断哪些差异是不可接受的。

第三步:用对抗性测试用例验证等价性

前两步拿到候选改写之后,不能直接信任。第三步是让模型自己生成测试用例集,并且刻意构造边界情况

请为以下两个正则表达式生成一组对抗性测试用例:

原版:<ORIGINAL>
候选改写:<CANDIDATE>

测试用例要求:
1. 至少 10 个正向用例(两个表达式都应该匹配);
2. 至少 10 个负向用例(两个表达式都应该不匹配);
3. 其中至少 5 个用例要针对零宽断言的边界情况;
4. 至少 3 个用例要包含换行符、Unicode 字符、以及可能导致回溯爆炸的长字符串;
5. 对每个用例,给出「原版匹配结果」「候选版匹配结果」「两者是否一致」三列;
6. 如果发现不一致,不要尝试修复,直接报告不一致的用例和原因。

这个步骤的价值在于,LLM 生成的测试用例往往比你自己想的更刁钻。因为它「知道」正则的哪些角落容易出问题,只是默认不会主动暴露。当你用对抗性提示词逼它构造边界用例时,它经常能找出一些真实的等价性缺陷。

我在实际项目中用这个流程处理过一个长约 200 字符的 PCRE 正则(包含嵌套断言、条件分支和原子组),第一版「直接让模型改写」的结果在 15 个手工测试用例里挂了 4 个;换成三步流程后,候选改写通过了全部手工用例,并且模型自己生成的对抗用例里发现了一个我手工没测到的捕获组编号偏移问题。

一个完整的分步提示词模板

把上面三步整合起来,可以直接复制使用的模板长这样:

你是一个正则表达式分析专家。请按照以下步骤处理我提供的正则表达式,每步之间用分隔符隔开。

【第一步:语义解释】
逐段解释正则表达式,包括:
- 每个 token 的作用和匹配范围
- 每个捕获组的编号和可能内容
- 零宽断言的生效位置和方向
- 可能触发回溯爆炸的位置
- 该表达式明确不匹配的边界情况
不要给出任何改写建议。

【第二步:等价改写】
基于第一步的解释,给出 2 个等价改写版本。必须满足:
- 匹配字符串集合完全一致
- 捕获组数量、编号、嵌套关系不变
- 回溯语义不变
- 兼容 <TARGET_ENGINE> 引擎
对每个版本,用表格列出匹配范围、捕获组行为、性能特征的差异。

【第三步:对抗验证】
为原版和每个改写版本生成对抗性测试用例,覆盖:
- 正向和负向用例各至少 10 个
- 零宽断言边界至少 5 个
- 包含换行、Unicode、长字符串的用例至少 3 个
报告每个用例在两个版本上的匹配结果是否一致。

正则表达式:<PASTE_HERE>

这个模板我建议不要一次性丢给模型,而是分三次对话轮次执行。原因是:如果一次性给三步,模型在生成第一步解释时就已经「预见」了后面要改写,解释的客观性会受影响。分开执行,每一步的输出质量都更可控。

常见问题

问:为什么不让模型一次性完成解释 + 改写?省一轮对话不好吗?

不好。模型在同一个生成序列里同时处理两个目标时,会倾向于在解释阶段就为改写铺路,导致解释不够客观。实测分步执行后,模型对回溯风险和边界情况的识别完整度明显提高。多花一轮对话的成本,远低于上线后正则 bug 的排查成本。

问:这个流程对简单的正则也有必要吗?

没必要。长度 50 字符以内、没有嵌套量词和零宽断言的正则,直接让模型改写通常不会出问题。这个流程的价值在复杂正则——尤其是包含多个捕获组、嵌套断言、或者你怀疑有回溯风险的表达式。判断标准很简单:如果你自己一眼看不出这个正则的所有边界行为,就走分步流程。

问:模型生成的测试用例可信吗?它会不会为了「证明等价」而故意避开会暴露差异的用例?

有这个倾向,所以提示词里要明确加上「如果发现不一致,不要尝试修复,直接报告」以及「刻意构造边界情况」。另外,模型生成的测试用例只适合作为第一轮筛选用,最终上线前还是得用真实的业务数据跑一遍。对抗性用例的作用是把候选改写里明显的等价性缺陷提前暴露出来,不是替代真实测试。

问:目标引擎对改写结果影响大吗?

非常大。JavaScript 的 RegExp 不支持 lookbehind(直到 ES2018 才支持,且 Safari 到 16.4 才补齐)、不支持原子组(直到 ES2024 提案才进入 Stage 4,实际支持仍不完整);PCRE 有 \K、递归、条件分支;RE2 直接不支持回溯和大部分零宽断言。给模型的约束里必须明确目标引擎,否则它默认按 PCRE 语义理解,改出来的表达式在 JavaScript 或 RE2 里可能直接报错或行为完全不对。