我最早是在做 AI 自动写作工具时发现问题的模型产出的段落读起来完全顺畅结构规整连语气都像那么回事但一旦你去较真核对它提到的论文标题、统计数据、年份出处就会开始冒冷汗。有一次它引用了某篇“研究”我按图索骥去查作者对不上期刊对不上连是否真实存在都存疑。更离谱的是我追问它为什么这么写它还会立刻编出一套听起来更合理的解释来圆场。那一刻我意识到一个挺瘆人的事实LLM 骗起人来是连自己都一起骗的。所以后来在我的内容生成流程里我坚持加了一道“可信度闸门”而这道闸门的第一版实现用的不是另一个大模型来做交叉审核也不是复杂的知识图谱校验就是几行正则表达式。1. 当 LLM 开始“骗”自己问题藏在哪儿1.1 LLM 幻觉并不只在问答环节出没很多人对 LLM 的幻觉印象还停留在“你问它一个知识性问题它答错”这个阶段但真实应用里更麻烦的是长文生成场景。无论是自动写产品介绍、行业研报、科普文章还是学术辅助材料模型都会在长达几千字的输出中“平滑地”滑入虚构状态。为什么说平滑因为 LLM 本质上是在预测下一个 token它在生成第 1000 个 token 时并不知道自己在第 300 个 token 处写下了“这项技术诞生于 2019 年”。如果后面某个段落需要再次提到时间、机构、数量它就可能顺着新的概率分布写出另一个不冲突、但实际上跟前文矛盾的数字。这种自相矛盾往往藏得很深人快速阅读时根本反应不过来因为每个句子单独拎出来都通顺只有合在一起对比才会发现数字对不上。我在测试中做过一个很典型的实验让模型写一段 800 字的产品介绍要求提到“研发周期用了 18 个月”。生成结果里后文某处写的是“团队在近两年的研发中”。单看这句没问题但你把它和前文一对照就会发现“18 个月”和“近两年”有出入。这种问题不会让整段内容突然“很假”但会让细心的读者慢慢失去信任积累到一定程度整篇内容的可信度就塌了。1.2 为什么模型会一本正经地胡说把 LLM 拟人化成“故意撒谎”其实不准确。它更像一个极其擅长联想和补全的续写器所有输出都基于训练中学到的统计规律和模式匹配并没有一个独立的事实核查机制在后台逐句验证自己说了什么。它说“某某研究指出”不是因为真的检索过那篇研究而是因为在类似语境下“发表过的研究”是最常见的下一段内容形态。这就是标题里说的“自欺”模型在生成时并不维护一个“外部世界状态表”它没有真正记住自己已经生成过的每一个断言。它的注意力窗口看到的是前面的 token而不是前面 token 对应的“世界事实”。所以一旦某个事实细节不在显式上下文里反复出现它就很容易顺着更高概率的方向写最后生产出漂亮的、合理的、但可能是虚构的内容。我见过不少同行第一次做内容生成应用时把绝大部分精力都花在 prompt 调优上期待只要写清楚“你必须输出真实信息”模型就能乖乖遵守。实际上这种约束的效果非常有限。模型没有能力意识到自己即将写出的内容与真实世界不符就像一个人没法在梦中靠意志力随时踢自己一脚醒过来。所以需要在输出侧加一道与生成模型完全无关的检查机制这正是“闸门”的价值。1.3 两种幻觉必须分开看如果不做区分很容易把“用正则拦幻觉”理解成试图用正则判断所有内容真假那显然会翻车。实际工程里我把幻觉分成两类第一类是“语义级幻觉”特点是内容听上去合理但事实错误例如把某公司的创立年份搞错或者编造一个根本不存在的合作案例。这类问题需要外部知识库、检索增强生成RAG或者人工复核才能解决正则基本无能为力。第二类是“结构级幻觉”或“表达级幻觉”特点是内容内部的格式、编号、数字、术语前后不一致。这类问题有一个共同点它们都在文本表面留下了可被检测的痕迹。比如引用了“[3]”但全文压根没有“[1]”前文说“共有四项原则”实际只列出三项年份写着“2035 年”出现在一篇常识性非科幻文章里。这些痕迹是 LLM 在“自欺”过程中露出的马脚也是正则真正能发挥作用的地方。所以我的闸门策略从一开始就很明确不试图判断“事实对不对”只判断“输出文本内部是否自洽、格式是否规范、是否存在表面破绽”。这个思路让正则的作用边界一下子清晰起来也让我不会盲目期待它能解决所有问题。2. 为什么是“几行正则”——闸门方案的选型逻辑2.1 先想清楚为什么不用模型审模型很多人会问现在 LLM API 这么方便为什么不让 GPT 去审 GPT 的输出听起来很优雅用 A 模型审 B 模型A 模型可以理解语义能识别出错误。我在实际项目中试过这个方案效果确实有但它有非常现实的三座大山。第一是成本。一篇 2000 字的文章用模型做一次完整审核要消耗的 token 可能是原文的两到三倍。如果每天生成几百篇这笔开销会让应用很难规模化。第二是时延。用户点完“生成”已经等了十几秒再追加一轮几十秒的模型互审体验基本没法用。第三是“同源盲区”。如果两个模型在训练数据、对齐方式上高度相似它们很可能共享同一种错误认知模式A 模型会认为 B 模型编造的那篇“研究”同样合理双方在错误的认知上达成一致等于审了个寂寞。正则表达式就没这些毛病。它不依赖模型不烧 token执行时间通常是微秒到毫秒级。更重要的是它是“确定性”的。同样一段文本输入进去每一次检查结果都完全一致。这种确定性在内容生产的质量控制环节里非常宝贵因为你可以建立一个稳定可回归的规则库每次模型迭代后都能跑同一套校验清楚知道新增了哪些错误。2.2 正则能拦住的幻觉都有哪些共性我逐渐总结出一个规律能被正则有效拦截的幻觉通常满足“文本表层有显式标记”。就像一个人吹牛说“我上个月去了 15 个国家旅行”你不需要跟着他核实机票只要把他前后说的行程细节摆在一起就能看出破绽。规则能抓住的正是这些摆在一起的破绽。举几个我实际用来做规则的例子引用编号不连续、声称数量与实际枚举数量不一致、日期超出合理范围、同一段落里同一数据出现两个版本、绝对化表述密度过高、该成对出现的符号没有闭合。这些模式通通可以通过正则或基于正则的轻量逻辑扫描出来。有一种情况特别典型当模型要引用文献时常常会“创造”参考文献。它会给正文加上 [1]、[2] 这样的上标然后在文末列一个文献表。盗版也有认真的时候它会真的把编号排好但如果让它改写一段内容后内部的编号可能会错位比如正文引用跳到 [4]文献表里却没有第 4 条或者在正文中只看到 [1] 和 [3]缺了 [2]。这类问题一旦出现用正则提取编号集合、比对连续性一个函数就能搞定。2.3 “拦截”与“可解释”的平衡在 AI 生成内容的质量管控中最怕的是模型输出一个结果审核流程告诉你“不合格”却说不清为什么不合格。这种黑盒式拒绝在开发调试阶段是灾难你只能一次次猜改 prompt、调温度、又生成一遍然后再撞运气。正则闸门的好处是每条规则都自带明确的原因。校验器命中某条规则后可以直接返回规则编号和失败片段例如RULE_DATE_YEAR_OUT_OF_RANGE: 2037-04-11。内容运营团队拿到这个结果可以立刻判断是模型真写错了还是规则需要放宽。这种“可解释性”在 AI 应用里极其稀缺也是我后来坚持把所有质量日志都按规则维度统计的原因。你能清楚看到哪类错误占大头从而决定是把 prompt 调得更细还是直接把某段业务数据塞进 RAG 上下文。2.4 适用边界正则解决不了什么必须得承认正则的能力边界很窄。它不能做情感判断不能判断观点对不对不能查证新发布的事件更不能判断一段话是否违背某个行业规范里微妙的描述。比如药品广告里禁用词很多是语义层面的像“安全无副作用”它文本上看着没问题但可能违规这需要专门的合规词表加语义建模。所以更务实的做法是把正规定位成内容可信度防线中最廉价、最前置的一层。它像小区门口的保安先拦下明显形迹可疑的人至于每个人包里装的到底是什么还需要后续的安检和人工判断去管。先做好这一层整个系统的容错率就会高很多。我在实践中看到很多团队一上来就追求大而全的事实验证系统结果半年还没上线反而是先上几行正则闸门的项目第二天就发现了一批可复现的模型输出问题。3. 给 AI 文章上一道闸门规则实现与接入方式3.1 校验器整体设计我倾向把所有检查规则实现为一个纯函数集合输入是一段文本输出是一份校验报告。这样设计的好处是规则之间互不依赖后续新增规则只需要加一个函数并注册到规则列表里。先看骨架代码。import re from dataclasses import dataclass, field from typing import List, Callable dataclass class CheckResult: rule_id: str passed: bool message: str matched_text: str CheckFunc Callable[[str], List[CheckResult]] class ContentGuard: def __init__(self): self._rules: List[CheckFunc] [] def register(self, func: CheckFunc): self._rules.append(func) def run(self, text: str) - List[CheckResult]: results [] for rule in self._rules: try: results.extend(rule(text)) except Exception as e: results.append(CheckResult( rule_idINTERNAL_ERROR, passedFalse, messagefrule execution failed: {e}, matched_text )) return results每条规则返回一个结果列表方便一条规则在文本中多处命中时输出多条记录。注册机制让整个校验器变成可插拔结构。对于只在内部使用的小工具这看起来有点过度设计但一旦规则数量超过 10 条这种结构能帮你省下大量调试时间。3.2 第一批规则日期、年份和数字一致性最早值得加的就是年份边界和日期格式规则。LLM 生成内容时经常年份错乱尤其是涉及“近年来”“某年某月”的表述。一个参考规则是提取文章中的四位数并判断它是否在一个合理的年份区间内。YEAR_PATTERN re.compile(r(?!\d)(1[5-9]\d{2}|20[0-4]\d)(?!\d)) SUSPECT_YEAR_CANDIDATE re.compile(r\b(18[0-9]{2}|2[1-9][0-9]{2}|[3-9][0-9]{3})\b) def check_year_range(text: str) - List[CheckResult]: results [] for m in SUSPECT_YEAR_CANDIDATE.finditer(text): year int(m.group()) results.append(CheckResult( rule_idRULE_YEAR_OUT_OF_RANGE, passedFalse, messagefyear {year} is out of reasonable range, matched_textm.group() )) return results但光有年份范围还不够。我发现更隐蔽的问题是“数字前后不一致”。比如模型前一段写“用户量达到 1.2 亿”第三段又说“接近 1.5 亿人使用”。这时不能靠单一正则直接判断真假但你可以做一个比较保守的检查把同段或相邻段落中的带有单位的大数字提取出来如果两个数字的表达结构相同且数值不相等就标记为“数字疑似前后不一致”交给人工判断。这个方法误报率不低所以我在设计上只把它当作提示性规则而不是拦截规则。3.3 第二批规则引用编号与格式完整性学术风、研报风的内容必须处理文献引用编号。模型生成时非常容易出现引用编号断裂。检查逻辑一句话就能说清提取所有方括号编号排序后从 1 开始检查是否存在缺失。REF_PATTERN re.compile(r\[(?:bibcite\s)?(\d{1,3})\], re.IGNORECASE) def check_reference_continuity(text: str) - List[CheckResult]: nums [int(n) for n in REF_PATTERN.findall(text)] if not nums: return [] present set(nums) missing [] for i in range(1, max(nums) 1): if i not in present: missing.append(i) if missing: return [CheckResult( rule_idRULE_REF_MISSING, passedFalse, messagefmissing reference numbers: {missing}, matched_textstr(missing) )] return []这套规则同样适用于检查“图编号”“表编号”“步骤编号”。只要内容是以编号形式组织的模型就可能在某些位置漏编号或跳号。正则提取出编号全量后做一次序列完整性检查几乎零成本。我见过一份案例模型在润色一篇技术文档时把一个三级标题下的步骤序号从“1、2、3”改成了“1、2、4”审稿人肉眼扫过去真的不容易发现但这种错误会让文档的严谨性大打折扣。3.4 第三批规则绝对化表达与自相矛盾理性、严谨的文章通常不会堆砌“毫无疑问”“众所周知”“绝对不可能”这类绝对化表达。倒不是说这些词一出现就一定是错的而是当模型不确定内容真实性时它有概率生成更笃定的措辞来补偿心虚感。我在规则库里做了一版“夸张语气检测”命中后不直接判死而是记录可疑。ABS_WORDS [ 毫无疑问, 毋庸置疑, 绝对不可能, 100%正确, 众所周知, 全世界都知道, 史上最强, 彻底解决 ] ABS_PATTERN re.compile(|.join(map(re.escape, ABS_WORDS))) def check_absolute_words(text: str) - List[CheckResult]: results [] for m in ABS_PATTERN.finditer(text): results.append(CheckResult( rule_idRULE_ABSOLUTE_WORD, passedFalse, messageabsolute expression detected, matched_textm.group() )) return results自相矛盾的检测稍微高级一点。最简单的一种可以做“肯定/否定模式”的启发式分析把包含“必须”“需要”“应该”“禁止”“不能”等词的句子抽出来做一次很浅的共现分析。如果同一个动作在前文说“必须启用 A 方案”后文又说“不应启用 A 方案”就可能撞到规则上。这类规则更像是简单 NLP 管道正则负责初步提取后续逻辑做集合比较属于几行内能完成但又不至于简陋的折中方案。3.5 接入生成管线的示例有了规则集剩下最关键的是在生成流程中把闸门放到正确位置。我的推荐位置有两处第一处是流式生成结束、拿到完整文本之后第二处是如果内容后续还有“改写/翻译”环节那么每次大变动后都跑一次校验。import json from typing import Dict def generate_with_guard(model, prompt: str, guard: ContentGuard, max_retries: int 3) - Dict: last_results [] for attempt in range(max_retries): raw model.generate(prompt) results guard.run(raw) failed [r for r in results if not r.passed] if not failed: return { status: ok, text: raw, attempt: attempt 1, results: [] } last_results failed # 把检测到的问题回灌给 prompt要求模型针对性修正 feedback build_feedback(failed) prompt prompt \n\n请修正以下质量问题不要改动其他内容\n feedback return { status: failed_after_retries, text: , attempt: max_retries, results: last_results } def build_feedback(results: List[CheckResult]) - str: lines [] for r in results: lines.append(f- 规则 {r.rule_id}: {r.message}命中片段{r.matched_text}) return \n.join(lines)用大模型自动修正需要小心修正轮次可能引入新问题所以修正后的文本必须重新跑一遍校验不能直接信任模型“我知道错了”的回应。如果重试多次仍然失败我倾向于把文本打回“需要人工审校”队列而不是无限次让模型自攻自守。每增加一轮重试成本和时延都在涨内容质量却没有指数级提升。3.6 规则触发后的降级策略一道闸门不只是“拦住/放行”两种状态。内容生产场景不同处理方式也完全不同。我在系统里把规则分成了“拦截级”和“提示级”。拦截级规则针对的是“一旦出现内容肯定不可用”的问题比如明显的文献编号错乱、年份写成 2099 年、药品类内容出现明确违禁词。这类命中会强制触发改写或人工处理。提示级规则则针对“可能有问题但不绝对”的问题比如数字不一致可疑、绝对化表达较多。这类命中只把结果记入质量标签不阻断发布流程但运营人员在后台能看到标注。这套分级背后是对误报的妥协。正则本身很傻如果所有规则都设置成硬拦截业务会被误伤搞得寸步难行。分级机制让你可以把精确度高的规则设置成自动拦截把精确度一般的规则设置成人工辅助提示这样既保留了正则闸门的效率又不会因为它的“目光短浅”伤害正常内容。4. 我踩过的坑与排查实录4.1 正则误伤比漏检更让人头疼上线第一周我就被一个误报案例教训了。规则里有一条“检测引用编号缺失”结果有一篇完全正常的文章因为某处引用编号写成了“[1-3]”正则把它解析成单独的 1 和 3判断缺少了 2。其实作者的意思是从 1 到 3 连续引用。后来我在解析前增加了对连字符区间的预处理先把[1-3]展开成[1][2][3]问题才消停。另一个高频误伤来自“年份范围检测”。一篇讲 AI 发展史的文章从 1950 年代开始写我这里设的候选年份下限是 1800当时觉得足够宽松结果文章里一句“这本书的原型最早可追溯到 1748 年”直接触发告警。这条文章引用的是真实历史文献不是模型幻觉。后来我把年份规则从“拦截级”降成了“提示级”并且允许在规则说明里备注置信度避免了正常内容被冤枉。4.2 性能问题比想象中来得早本来以为正则就是毫秒级的事不会造成性能瓶颈直到某一天我把所有规则都跑在一个超长文档上单篇耗时冲到了将近一秒。事后我翻规则才发现问题出在几条正则写得太贪婪尤其是有几条使用了.*?加re.DOTALL的组合导致回溯路径爆炸。排查时我先把每条规则拿出来单独计时定位到耗时规则后尝试三种优化一是尽量缩小字符类范围不要用.匹配所有内容明确写成[^。]或[\u4e00-\u9fa5]之类二是增加正则前面的锚点或否定字符类让不匹配的文本快速失败三是把单条复杂正则拆成多个简单正则先用最快的方式做初筛只有初筛通过才进入开销较大的逻辑。优化之后同样一篇长文档的完整校验时间降到了 50 毫秒以内这个量级已经可以毫无压力地放在每次生成回调里。4.3 中文场景下的特殊问题英文场景里单词有天然空格分隔很多正则写起来顺手但中文处理是另一回事。比如“1.2亿”和“1.2 亿”模型有时输出带空格有时不带。如果正则写得太死就会漏检或误判。我的做法是在进入规则系统前先做一层简单的标准化统一把数字和中文之间可能的空格去掉或者在做数量提取时显式允许\s*。中文标点也是一个坑。很多规则我在刚开始时没有考虑全角标点导致“和“这种引号无法配对检查。对于成对符号我用的是先归一再匹配的策略把全角左右引号统一映射到 ASCII 引号的替身再做栈式配对判断。这虽然超出了几步正则的范畴但本质上仍然属于轻量级文本检查没有引入重型 NLP 依赖。4.4 排查流程速查表当你接到一次“闸门误报”时别急着改正则按下面这个顺序走会高效很多。步骤操作目的1记录触发规则的完整文本片段拿到可复现的最小样例2单独跑该条规则确认是否稳定复现排除文本预处理或并发问题3判断是“规则写错”还是“业务要求变化”决定改正则还是改规则配置4修改后把历史语料批量回放一遍确认没有引入新的误杀5更新对应规则的版本号和说明保证质量日志可追溯我在实际项目中经常遇到有人直接在线上服务器把某条规则注释掉结果三天后团队都不知道这条规则当初为什么加。质量规则库也是代码资产需要版本管理、变更记录和回归测试。哪怕只是几行正则也应该像业务代码一样对待。5. 从闸门到护栏稳定性的下一步5.1 正则之外的“二次闸门”光靠正则不能覆盖所有幻觉问题所以我在后续版本中把闸门从“单层”扩展成“多层”。正则闸门是第一层负责发现格式、编号、数量和基础一致性等问题。第二层叫“实体一致性检查”本质上是一个很轻的词表映射表把文章中的公司名、人名、产品名提取出来然后对比它们在全文中的写法是否完全一致。模型可能在第一段写“OpenAI”第五段变成“Open AI”初看没问题但在专业文章中是明显的错误。第二层可以再叠加“关键数据白名单”机制。如果你的业务中某些参数是绝对不允许写错的比如某款产品的发布时间、某个核心性能指标就提前把它们写成键值对形式在生成文本中扫描这些关键词附近的内容如果数值跟白名单不一致直接告警。这个思路和正则没关系但执行的壳仍然复用前面的规则引擎实施成本很低效果却立竿见影。5.2 高价值内容建议接入人工抽检即便自动化闸门做得再完善对于发布后不可撤回的高价值内容我仍然坚持保留人工抽检环节。正则能帮你把模型明显的“自欺”痕迹挡在门外但它无法替代人对语义正确性的判断。实际操作中可以在校验报告里给每篇文章打分得分低的内容直接进入“高风险待审池”由人工优先处理。得分高的内容随机抽取 5% 到 10% 做抽检。这样一来人工不用看完全部内容只需要盯着机器觉得可疑的部分效率提升非常明显。团队里内容运营同学反馈以前看 AI 文章总觉得心里没底现在有了标注他们在审核时能快速定位到具体段落不用全文找茬。5.3 对模型提示词的约束要一起做最后我想提醒一个看似无关、但实际影响很大的点正则闸门在排查问题上有效但你别忘了同时优化生成侧的 prompt。质量检查做得再好也不如模型在一开始就少犯错。我在 prompt 里明确要求“每个引用编号必须连续”“避免使用未经证实的绝对化表述”“多处提到同一统计数字时必须保持一致”配合输出侧的正则校验生成质量会好非常多。最让我感慨的是几行看起来“很低级”的正则居然能在 AI 时代继续发挥这么大作用。它不懂语义不智能但它稳定、可解释、零成本。在所有人都把目光投向更大模型、更强推理时用规则守住内容的最后一道底线反而成了最冷静也最可靠的一环。我现在的工具链里这个正则闸门仍然保持着一个朴素的地位先拦截所有一眼就能看穿的错误再把剩余的问题交给更聪明的系统。踩过几次坑之后我越来越觉得做 AI 应用不能只追求模型的“上限”还要认真设计系统的“下限”。如果哪一天你也被 LLM 的流畅输出惊艳到然后在核对一个细节时后背发凉不妨也花十几分钟写几行正则给内容上一道可信度闸门。它不一定能让你高枕无忧但至少能让你在给读者看到之前少一些心惊胆战。