用正则表达式构建大模型输出可信度闸门

发布时间:2026/9/5 22:26:37
用正则表达式构建大模型输出可信度闸门 我在整理一份技术选型文档时让大模型代笔它写了一版看起来很有说服力的初稿连版本号、发布年份、资源链接都像模像样。复制给同事后没几分钟对方就来问文档里那个工具真的存在吗我去翻了官方仓库整段引用链都是模型编出来的。更麻烦的是我把它生成的错误结论原样丢回去做追问它没有纠正自己反而顺着错误往下圆。这就是 LLM 一个非常隐蔽的坑表面自信但内部没有事实校验模块。它会基于自己前一轮生成的文本继续二次推断错误不仅不会被发现还会被强化最后形成一篇“自己骗自己”的内容。后来我给自己所有 AI 生成流程加了一道关卡——用正则表达式做可信度闸门在内容送出去之前先过一遍筛子。这个方案不依赖复杂算法成本极低效果却意外地稳定拦截了大量会让作者尴尬的“伪精确表达”。如果你也在做 AI 写作辅助、RAG 问答、Agent 自动生成报告或者只是拿大模型当编辑工具这篇文章应该能给你一个立刻能落地的防护思路。1. LLM 为什么会在内容里“骗自己”1.1 上下文污染它把你给的错当成既定事实LLM 本质上是文本概率模型它没有“这句话是我上一步猜的”这种元认知。你丢给它一段含有错误的对话历史它不会像人一样产生警觉反而会认为“这是用户提供的事实背景”所有后续推论都会建立在这个错误前提上。我在测试里遇到过非常典型的情况先让模型写一段产品介绍它随口说“该模块支持 Kafka 3.2 之后的所有版本”然后我在下一条消息里追问“那它是否兼容 3.6 的 Consumer API ”模型立刻给出了一大段分析讨论 3.6 的兼容细节。可问题是 3.6 这个版本本身就是它上一轮胡诌出来的。它无法区分“自己生成的假设”和“外部提供的资料”因为对模型来说这两者都是上下文窗口里的 token。这种机制造成的后果很直接一个错误不是孤立出现的它会被后续所有相关回答引用、扩展、包装成更“可信”的论述。只要源头没被卡住错误就会像雪球一样越滚越大。1.2 训练语料里的自我强化错误被重复就成了刻板印象现在的大模型训练流程里有一个很现实的问题新一代模型会拿上一代模型或同代模型生成的文本当训练语料。如果这批语料里混入了大量“看起来合理但实际错误”的内容模型在训练时学到的不只是一个错误事实而是学到了一整套“用流畅句式包装虚假信息”的生成风格。这个现象背后是研究者反复提到的模型坍缩风险。简单说当模型生成的数据不断被机器筛选、加权、重新训练小概率的统计数据会被放大少见的异常表达会被抹平最后剩下的往往是同类错误的平均值。模型会越来越“自信”因为它的输出概率分布被训练得越来越单一、平滑但那个概率中心对应的内容未必真实。对我们这些使用者来说这意味着一个残酷的现实错误不是偶发噪声而是模型的统计倾向。如果你完全信任输出不设任何人工或程序化防线错误会以极高的一致性反复出现。1.3 长链路任务里的误差接力Agent 类应用比单轮问答更危险。模型先把任务拆成子步骤再为每个子步骤生成结果然后把结果拼装成最终答案。这中间任何一步出现幻觉后面所有步骤都会基于错误结果继续推理。我见过一个比较典型的案例让 Agent 做一个竞品分析报告第一步要“生成竞品调研大纲”模型列了一个文件夹结构。第二步它去填充内容时把大纲里一个虚构的竞品名字当成了真实存在的公司为此专门编造了市场份额和产品定位数据。最后一步做总结时它甚至根据这个不存在的竞品得出了“该领域已经进入红海竞争”的结论。这类问题特别难发现因为最终输出看起来结构完整、逻辑连贯每一步单独检查都像“那么回事”。可一旦你把中间步骤全部摊开比对就会发现错误在很早就埋下了。这也是为什么不能只在最后人工看一眼而要尽量在生成链路的输出端加一道自动化闸门。1.4 提示词只能缓解症状治不了根有人会问那我直接在提示词里写“请确保所有数据都有可靠来源”“不要编造内容”行不行实测下来这类指令能减少一些夸张表述但无法解决根本问题。因为模型输出的是概率分布不是逻辑推理结果。你在提示词里反复强调“要诚实”等价于把包含“诚实”语义的 token 在概率上略微上调并不会触发任何外部事实校验机制。更麻烦的是如果模型在生成过程中“觉得”前面某个说法很有道理它会倾向于补全一个符合上下文的论证。提示词约束的是宏观风格管不了微观层面的错误引用、错误版本号和虚构数字。所以现实情况是大模型负责产出的弹性我们负责加一道刚性闸门两者各有分工。2. 给输出加闸门为什么我选了正则而不是“再请一个 LLM 把关”2.1 AI 幻觉留下的往往是“形式痕迹”很多人以为 AI 幻觉是纯粹的语义问题只能靠语义模型去抓。但在实际内容生产中最容易让人社死的错误其实是高度模式化的表达。我整理了手头近百篇生成内容发现一个规律编造统计数据时模型几乎必然用类似句式“XX% 的用户认为”“超过 XX 的企业表示”“绝大多数受访者选择”。这些句式不是不能用可问题在于当文本缺少来源标记时它们恰好是最容易误导读者的结构。编造论文引用时情况也一样模型最爱写“[12] 表明……”然后正文里却没有任何参考文献表或者它会在正文中生成“[1]”但到文末根本没有对应的 1 号条目。这些错误并不是纯语义层面的“这句话内容有误”而是结构层面的“这句话留下了可被追踪的可疑痕迹”。正则正好擅长捕捉这类痕迹它不需要理解“62.4% 是否真实”只需要判断“一个精确到小数点的比例后面附近有没有数据来源标记”。如果连来源都不存在那这句话就该被标出来人工复核。进一步说正则能做的是给内容加仪式感让文本主动暴露它自己该有的证据结构。没有证据结构的华丽断言就是高危险信号。2.2 正则闸门的核心优势确定性、低成本、可审计对比方案是想让“另一个 LLM 当裁判”给生成的文章逐条打分。从直觉上这可行但真落地有四个麻烦。第一是成本。一篇文章生成后再调用一次大模型评判token 费用直接翻倍。第二是不确定性。同一段文字同一个 prompt你让大模型评两次结果可能有波动尤其当阈值卡在中间地带。第三是一致性问题。评审模型可能被原文的权威口气带偏产生一种“跟随性幻觉”即它也觉得文章写得好因为文本太流畅了。第四是可解释性差评审模型说“这个段落不可信”但它不会告诉你具体原因是缺引用还是数据来源存疑。正则没有这些问题。同一段文本跑一百次结果完全一致。每次命中都能定位到具体规则、具体前后文、具体字符位置。如果客户或审核方问“为什么标记这条”你可以直接给出规则名和匹配片段。这种可审计性在生产环境里极其重要。2.3 先想清楚边界它管形式不负责验证事实我必须强调不要把正则闸门的定位搞成“事实核查器”。正则能做的是识别三类问题格式硬错误比如引用了不存在的参考文献编号、日期格式不合法危险句式比如无来源的百分比声称、绝对化断言语结构可疑比如正文提到了“最新版本”却缺少版本发布说明的配套信息。正则做不了的事情也一样清晰它无法判断某个 API 在真实世界里是否存在无法判断一段符合语法的陈述是否违背外部事实。比如模型写“某公司在2025年6月发布了财报”句子结构无懈可击正则不会拦截除非你额外引入数据库或检索工具。因此最佳定位是把闸门当作“过滤漏斗”而不是“真相审判官”。先让正则把所有高风险、低风险、需要人工留意的内容筛出来再由人或其他事实校验模块处理。这个思路能让整个系统既高效又不至于误杀太多。判别层次正则适合典型处理方式格式错误引用标记缺失、日期不合法适合直接拦截或发出硬警告危险句式比例无来源、绝对化过度表述适合标记为 review提示人工确认语义错误全文通顺但事实错误不适合留给外部检索/事实核验模块上下文矛盾同一论述前后冲突弱适合先做模式匹配再配合人工判断有了这个边界后面的规则库设计就不会跑偏。3. 动手写第一版规则库拦截 AI 最常“说谎”的几种模式3.1 规则库骨架规则、命中、动作我的实现非常简单没有引入复杂框架。先把规则拆成两层第一层用正则定位候选片段第二层写两行业务判断决定是否真的标记。正则负责“找得到”业务判断负责“判得准”。先看骨架import re class TrustIssue: def __init__(self, rule_name, risk_level, snippet): self.rule_name rule_name self.risk_level risk_level # block 或 review self.snippet snippet def __repr__(self): return fTrustIssue rule{self.rule_name} risk{self.risk_level} snippet{self.snippet[:30]}... def locate(pattern, text, width80): 返回每个命中的上下文片段 results [] for m in pattern.finditer(text): start max(0, m.start() - width) end min(len(text), m.end() width) snippet text[start:end].replace(\n, ) results.append((m.start(), m.end(), snippet)) return results每条规则本质上是“把可疑点从长文本里捞出来”后续所有命中都会汇总成审计报告。我用的是 Python 标准库 re不需要额外依赖任何环境都能直接跑。3.2 第一类规则百分比声明缺少来源标记编造数据是最常见、也最容易人设崩塌的幻觉类型。我的思路不是“见到百分比就报警”而是用正则定位百分比再看该百分比前后有没有来源短语。source_patterns [ r据[^。]*(?:统计|调查|报告), r数据来源[:], r根据[^。]*(?:机构|报告|研究|平台), r(?:Statista|Gartner|IDC|艾瑞|QuestMobile) ] compiled_sources [re.compile(p) for p in source_patterns] def audit_unverified_percent(text): findings [] # 匹配百分比数字排除“100%”这种明显的完整量 percent_pat re.compile(r(?!\d)(\d{1,3}(?:\.\d)?)\s?%) for pos, endpos, ctx in locate(percent_pat, text, width150): # 上下文区域内如果出现来源词则放过 if any(p.search(ctx) for p in compiled_sources): continue # 上下文里出现“约/大概/预计/估算”这类表述时降低冲突 if re.search(r(约|大概|预计|估算|超过?), ctx) and len(ctx) 120: # 仍然值得关注只是风险等级降低 findings.append(TrustIssue(percent_without_source, review, ctx)) continue # 没有来源也没有模糊限定词直接提升风险 findings.append(TrustIssue(percent_without_source, block, ctx)) return findings为什么这样的设计更稳如果一篇文章写“根据 QuestMobile 报告62.4% 的用户选择……”那这句是安全的不需要打扰作者。如果一篇文档满屏都是“95% 的开发者认为”却连一个“据分析机构统计”都找不到那大概率是模型在自由发挥值得直接拦截。这里的核心逻辑叫作“用上下文履约检查替代关键词匹配”正则负责缩小范围上下文检查负责下结论。实际跑下来这条规则能捞出一大批“AI 味很浓但完全不可考证”的句子。3.3 第二类规则版本号、日期与“最新状态”的模糊声明AI 生成技术文档时疯狂爱写“最新版已经更新到 v2.3.0”。如果它生成时用的是旧版本知识库这个断言就是错误的而且会误导读者选错依赖版本。这种论述的逃逸速度很快因为形态千变万化。我给出一个保守做法把“时间状语 版本规格”锁定起来。latest_versions [ r(目前|当前|最新|现在|截止至?今?) r[^。\n]{0,30}? r(版本|框架|工具|库|更新) r[^。\n]{0,20}? r(?:是|为|已到|升级至|更新到|来到了) r\s*[vV]?(\d(?:\.\d){1,4}) ] latest_version_pat re.compile(|.join(latest_versions)) def audit_latest_claims(text): findings [] for pos, endpos, ctx in locate(latest_version_pat, text, width120): # 如果周围已经出现明确的版本比较语义我们仍然提示但降级为 review findings.append(TrustIssue(latest_version_claim, review, ctx)) return findings这条规则不适合做成 block因为人类作者也可能会写“当前版本是 3.1.2”并且完全正确。但它非常适合做“强制人工确认”凡是这种句式哪怕内容可能没问题也要确保背后有官方更新日志支撑。尤其在自动化生成内容聚合站、文档站这种场景没有经过确认的“最新”描述会对 SEO 和用户体验造成双重伤害。3.4 第三类规则同段内绝对化表述与模糊限定词并存模型偶尔会在同一句话里产生一种“逻辑分裂”的观感典型的症状是先用无条件断言再用退化限定词找补。例如“绝对可以保证 100% 兼容不过在部分环境下可能存在兼容问题。”这种句子常常出现因为模型从一个推理分支切到了另一个分支却没有意识到两者彼此冲突。直接做严格的矛盾检测很难正则更合适的方式是寻找“绝对化/高置信词”与“模糊限定词”在近距离内频繁组合的情况abs_words r(绝对|百分之百|必然|毫无例外|完全肯定|一定不会|绝对没有) hedge_words r(可能|或许|有些情况下|部分场景|偶尔|不一定|例外) def audit_absolute_hedge(text): findings [] # 两个词在200字之内同处一个论述段落时作为可疑矛盾 pattern re.compile( f({abs_words}).{{0,200}}?({hedge_words}), re.S ) for pos, endpos, ctx in locate(pattern, text, width150): findings.append(TrustIssue(absolute_hedge_conflict, review, ctx)) return findings这条规则的误报率比前两条高因为有些文章会刻意使用“绝对不行但某些场景有例外”这种合理的让步论述。但正因如此它的风险等级我设成 review而不是 block。它真正的作用是让审核人员一眼看到那些“建模时左右互搏”的段落节省整篇通读的时间。3.5 第四类规则正文引用标记与文末参考文献脱节AI 非常擅长生成看起来专业的伪引用。它会在正文里写 [12]但文章结尾根本没有参考文献列表。这时候用正则直接定位两边的引用编号做集合差是一个极其干净可靠的方法。def audit_broken_references(text): # 正文行内引用例如 [1] [2] [12] inline_refs re.findall(r\[(\d{1,3})\](?\s*[。、]), text) # 文末参考文献列表行首的编号例如 1. 或 [1] 或 [1] list_refs re.findall(r(?:^|\n)\s*\[?(\d{1,3})\]?[.、\s], text) inline_set set(map(int, inline_refs)) list_set set(map(int, list_refs)) missing_inline inline_set - list_set missing_list list_set - inline_set findings [] for ref in sorted(missing_inline): findings.append(TrustIssue(freference_{ref}_not_in_list, review, f正文引用了 [{ref}]但文末找不到该编号)) for ref in sorted(missing_list): findings.append(TrustIssue(freference_{ref}_not_inlined, review, f文末有 [{ref}]但正文中从未引用)) return findings这个判断极其直观没有对应条目的引用就是“僵尸引用”。我在验证集里跑过很多次AI 生成的文章对这个规则的命中率高得惊人尤其是让它写综述类内容时几乎每篇都会有一两个编造的引用编号。它不属于“语义判断”而是结构完整性检查正则在这里发挥的效力完全是确定性的。4. 接入生成流水线从单次筛查到自动化闭环4.1 闸门放在生成管线的哪个位置很多人的第一个直觉是“内容生成之后执行下一步动作之前放一道钩子”。这个方向对但我更建议具体理解一下位置差异。如果你的应用是纯聊天机器人每次输出都直接展示给用户那在流式输出阶段做近乎实时的筛查就不现实。更好的做法是把闸门放在“完整生成结束但尚未呈现最终答复”的时机。有一种技巧是让流式结果先到一个临时缓冲区只有当生成完毕、进入缓冲区的全文被审计通过后才统一推送给用户端。虽然会增加显性延迟但保证了内容在到达用户前已经过检。另一种常见场景是批量生成比如每天晚上定时用 Agent 生成一批SEO文章或商品描述。这里闸门应该放在批量落库之前先把所有文章写入一个称为“待审核”的表单状态跑完规则库后命中高危规则的直接进入“审核驳回”状态命中低危规则的进入“人工复核队列”没问题的才正式发布。def generate_with_gate(generate_func, prompt, audit_funcs): # 第一阶段用模型生成原始文本 raw_text generate_func(prompt) # 第二阶段所有审计规则执行聚合 all_issues [] for audit in audit_funcs: all_issues.extend(audit(raw_text)) # 第三阶段根据风险级别决策 block_issues [i for i in all_issues if i.risk_level block] review_issues [i for i in all_issues if i.risk_level review] decision { status: pass, block_issues: block_issues, review_issues: review_issues, } if block_issues: decision[status] block elif review_issues: decision[status] review return raw_text, decision这个函数是通用模板可以根据业务改造成 REST API 或内部任务队列。要点在于审计结果永远和原文绑定返回不要只返回一个“是否通过”的布尔值。因为后续需要人工查看命中的上下文、决定是修订还是采纳。4.2 分档处理硬拦截、标记提醒、人工复审在实践中不同内容场景对错误的容忍度完全不同。面向用户的 C 端文案尽量硬拦截高风险项内部工作流则可以容忍一部分。我把处理策略分成三档block硬拦截命中引用缺失、恶意格式错误、生成内容包含无效 API 引用等。输出不会进入正式渠道而是原样退回给生成方或请求方。review标记提醒命中“无来源百分比声明”“最新版本断言”“绝对化表述”等。内容可以正常发布但系统在管理后台或编辑器里给出一个醒目的提示条。低置信提醒优先级最低比如某些绝对化和限定词组合的上下文很短不一定是真矛盾只会出现在日志里供运营人员定期查看。通过这种分级我们不需要把正则闸门做成“一言堂”而是让它在不同场景下扮演不同角色。对一个个人博客编辑器来说它可以是温柔的校对员对自动化金融摘要平台来说它可以是强硬的合规检查员。4.3 审计日志让每条规则都处于可迭代状态如果只是跑一把筛子积累不了任何经验。我强烈建议把每次命中记录下来尤其是人工之后判定为“确实有问题”还是“误报”的结果这会直接决定下个版本规则怎么写。我在实际项目里使用的日志格式很简单每条命中记录一个 JSON{ text_id: doc_204812, generated_at: 2025-06-21T10:05:00Z, rule: percent_without_source, risk_level: review, is_suspicious: true, human_verdict: null, snippet: 95% 的企业已经将大模型服务接入生产环境 }人工审核完之后把human_verdict更新为true或false。这样积累两周你就可以统计每条规则在生产数据里的命中准确率。规则准确率 人工判定为误报的比例。当发现某条规则命中一百次有八十次都是无害的那就该放松阈值或者直接关掉。这不是凭感觉调正则而是靠真实数据做回归。5. 实测效果和一些容易翻车的地方5.1 在一组典型文本上的测试结果为了演示这个闸门的效果我拿一组模拟 AI 生成内容的文本做了测试。测试样本存在的问题闸门动作样本A“62.4% 的开发者认为该方案的性能优于传统方法。”无任何来源标记比例精确高危block样本B“目前该框架最新版本已更新到 v4.2.0支持……”没有给出更新日志依据版本状态存疑review样本C“绝对没有任何兼容问题但某些老版本环境可能有例外。”绝对化断言 模糊限定词并存review样本D“Karpathy 在他的博客中明确说过这种架构……”这句话看起来自然但引用无法定位无命中样本E正文出现 [7] 引用文末却没有任何编号 7 的条目僵尸引用block前面的 A、B、C 三类问题都能被规则库准确找出来。D 类问题的本质是“声称某个人说过某句话”正则管不了需要靠检索外部资料来核。E 类则属于结构性问题落地到生产环境里几乎零误报。5.2 正则库真正容易踩的坑误杀和漏网我刚开始把规则做得特别激进比如用正则识别所有没有来源的百分比结果把很多正常文本也拦截了。有的文档上一段已经写了“根据 Gartner 报告”下一段只是延续数据讨论不再重述来源也被我的规则捞出来。后来我把阈值从“前后 80 字符内缺少来源词”调整成“前后 200 字符内缺少来源词且排除明显的延续性分析段”误报率立刻降了很多。正则规则不是越严格越好关键是先定义“什么样的上下文可以被视为有证据支撑”。另一个非常实际的坑是全角半角不统一。中文文本里可能有一半百分号是半角“%”另一半是全角“”数字也可能会夹着空格。如果不做规范化正则会漏掉一大片。建议进入审计前先执行一个标准化函数import unicodedata def normalize_text_for_audit(text): # 统一把全角字符转半角顺便把连续空格压缩成一个 normalized unicodedata.normalize(NFKC, text) normalized re.sub(r\s, , normalized) return normalized这个函数解决了我很多无谓漏报让正则规则长得更“干净”。另外还要特别小心正则里如果用了大量非贪婪量词.*?在超长文本上的性能会变得很差所以最好把locate函数里的上下文窗口限制在 80 到 200 字符之间避免 re