内容过滤与替换引擎怎么做?一次词表匹配的工程复盘
一、一篇稿子过滤要二十三秒年初我们把过滤这一环拖出来单独压过一轮一篇一千八百字的稿子走完五轮不同的处理策略要四点七秒因为每一轮都是拿两万六千条词表逐条去做字符串替换一轮一次全量扫描。我们一天要生成四百篇左右光过滤就吃掉二十多分钟的 CPU接在生成接口后面的那一段响应时间的九十五分位从两秒出头涨到了九秒。更麻烦的是命中率并不高。两万六千条词表里实际会在稿子里出现的不足八百条也就是说绝大多数的扫描是空转。可我们又不敢把这些词筛掉因为下一次生成换个题材命中集合就完全变了。思路走到这里其实已经很清楚需要的是让一次扫描同时匹配所有模式而不是让所有模式轮流扫一遍文本。二、难点不在匹配在匹配之前和之后把这件事拆成三段看。第一段是匹配也就是在文本里找出所有命中词的位置这一段有成熟的算法可以借。第二段是归一化用户写的稿子里同一个词可能有全角半角两种写法、简体繁体两种字形、中间还夹着看不见的零宽字符不先折叠就一定会漏。第三段是还原折叠之后文本的位置变了替换必须回到原文的偏移上去改否则改出来的是一份被清洗过标点的稿子而不是作者写的那篇。最容易被低估的是第三段。我们第一版实现直接在折叠后的文本上做替换结果交付的稿子里作者习惯用的全角括号全变成了半角数字之间的空格也没了。客户提了一句话说你们这不是过滤是重写。从那一版之后我们才明白归一化只用于判断绝不能用于输出判断用的文本和输出的文本必须是两份。三、正则、分词还是自动机第一条路是把词表合并成一条正则让引擎去做一趟扫描。这条路写起来很省事两万六千条词拼一条正则也不过几兆的字符串代价是词表里难免有重叠的模式正则引擎在这种输入上容易退化成回溯我们实测过一版某些词表上单篇耗时反而涨到七秒。第二条路是先分词再查表用词典匹配的思路逐个词表决。它的好处是能拿到词边界判断一词多义时更准代价是分词器对网络新词和变形写法不友好我们的词表里恰好有很多这类词分词器会把它们切碎命中率掉得厉害。第三条路是构建自动机把所有词条编成前缀树再补上失败指针文本只扫一遍边走边跳一次扫描就能拿到全部命中位置。我们最后选了第三条理由很直接词表规模固定、匹配只需一次、更新频率不高这三点恰好是自动机的舒适区而它的构建开销可以通过复用摊薄。四、把匹配逻辑固化进过滤层过滤层属于 AI智能媒体助理词表加载和文本扫描各占一个模块彼此只通过一个命中结果列表通信。扫描模块不关心命中之后怎么办它只负责给出一串三元组词条编号、起始位置、结束位置。策略模块拿到这串三元组再按配置决定每一处是该替换、该打码、该整段丢掉还是只记下来送人工复核。策略做成可配置之后同一份词表能服务不同的产线。对外发布的稿子走严格策略命中即替换并打标内部试写的草稿走宽松策略只打标不替换让人工去看图片描述这类短文本走折中策略命中就整段丢掉因为短文本里留着一处打码痕迹比删掉更难看。四条策略共用一套匹配结果切换只改一个配置项。五、前缀树、失败指针与偏移映射自动机的节点结构很简单每个节点存一张子节点表、一个失败指针、一份输出词条列表。两万六千条词条展开后大约十八万个节点构建一次耗时一点二秒这个开销放在服务启动时做一次可以接受但绝不能放在每次请求里。所以词表是常驻内存的构建后的根节点被一个原子引用持有。替换的落脚点是偏移映射。折叠文本的每一个字符都记下它在原文里的下标命中位置在折叠文本上算出来之后回头查映射表就能换算成原文的起止下标替换只在原文的这一段执行。这样原文的标点、空格、全角符号全都原样保留只有命中词本身被改动作者看到的就是自己被改过的句子而不是一份被重新排版的稿子。六、三个坑分别踩在语义、热更新和变体上第一个坑是替换之后句子读不通。有一批稿子里出现了一处人名替换词把它换掉之后后半句的指代全部悬空整段话成了病句。根因是命中即改完全不看前后文。改法是给每一处命中取前后各十二字的上下文窗口与一份白名单比对窗口里出现白名单词汇就跳过不替换只打标送人工。同时给整段丢弃策略加了一条段落长度阈值段内汉字少于三十个不执行丢弃免得把文章掏成骨架。第二个坑是词表热更新不生效。运营改了词表前台跑的结果还是老样子非得重启服务。根因是我们把前缀树建在了模块级常量上进程起来之后就再没重建过。改法是引入词表版本号更新时先在后台把新树建好建完再用一次原子赋值换掉根节点引用正在跑的请求还握着老根节点跑完自然释放。匹配结果里也带上版本号出了问题能追到是用哪一版词表判的。第三个坑是变体绕过和一词多义误伤这两个问题总是同时出现。变体那半边把词里的一个汉字换成同音的另一个字、或者在全角环境下写字母逐字比对就漏了误伤那半边一个词在有的语境里是敏感词在别的语境里是普通名词一刀切换掉会把正常的句子改坏。改法是两头各做一半匹配前先折叠统一大小写、全角转半角、繁体转简体、剔除零宽字符折叠出问题只影响判断不影响输出判不准的词进复核队列由人工决定要不要加入替换词对绝不自动替换。七、这层管不到的地方图片和视频里的文字它管不到。我们的过滤只吃纯文本图片上的水印文字、视频字幕里的口播内容都要另外走识别链路识别出来再送进这一层。所以对外说明里我们从不说内容全部过了一遍只能说文本部分过了哪几条策略这个边界必须讲清楚否则出了事就是我们的责任。谐音替代和拆字这类写法也覆盖不了。词表是穷举的写法是无限的今天补了一条明天就有人换个相近的说法绕过去。我们的应对是把词表约束同时写进生成侧的提示词里让模型在生成阶段就避开这些表达而不是全靠事后来抓两层叠加之后漏网的比例降了下来但仍然不是零。改写之后的稿子好不好读机器判不了。打散句式、降低连接词密度、重组段落这几步只能让形式上不像模板读起来自不自然得靠人工抽检我们目前每批抽一成发现问题就回滚到那一版的规则。抽检这件事没有捷径谁偷懒谁承担后果。八、小结生成前的词表约束和生成后的替换改写是 AI智能媒体助理 在这一层留的两个入口。前者把明显不能出现的表达挡在提示词里后者负责兜住漏网的两层合起来之后单篇过滤的耗时从四点七秒降到零点四秒词表从两万六千条加到三万四千条耗时几乎没有变化因为自动机的扫描成本跟词表规模基本无关。如果你也在自己写过滤我的建议是先把判断文本和输出文本分开这一条看着简单却是后面所有替换逻辑能不能站住的前提。判断用的那一份可以随便折叠清洗输出用的那一份必须逐字保留两者之间只靠一张偏移映射表连接谁搞混了改出来的稿子就不再是作者写的那一篇。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →