AC自动机等确定性字符串匹配模型适合敏感词过滤,而BERT或LLM因延迟高、资源重、误判多不适用;敏感词过滤是精确多模式匹配,非语义理解任务。

不能靠“语言学习思路”构建高性能关键词过滤算法——AC自动机、Trie树、DFA这些是确定性字符串匹配模型,不是NLP模型,不涉及词向量、上下文理解或统计学习。想用BERT或LLM做敏感词识别,延迟高、资源重、误判多,生产环境基本不可行。
为什么别把NLP那一套搬进敏感词过滤
敏感词过滤本质是精确多模式字符串匹配问题,不是语义理解任务。用户输入“密码123”,你要立刻命中"密码",而不是去推理它是否在讨论登录凭证。用Transformer类模型:
- 单次推理耗时通常 >50ms(CPU)或 >10ms(GPU),而AC自动机在纯Go实现下可压到
- 模型需加载GB级参数,而双数组Trie内存占用常
- 无法保证
"王八"和"王八蛋"这种重叠词全命中的确定性行为 - 热更新词库要重新训练/微调,而AC树只需
Build()一次新节点
真正该复用的“学习”是工程经验,不是AI模型
所谓“语言学习思路”,其实是把多年文本处理中踩过的坑总结成可复用的工程约束:
-
strings.TrimSpace()后判空——过滤掉""、"\x00"、"\t\n ",否则Build()会panic - 按长度升序 + 字典序排序词典——让
"user"和"username"共享前缀节点,减少内存碎片 - 所有字符串统一转
[]rune再遍历——避免[]byte切碎“王”字导致匹配失效 -
node.children每次访问前检查nil——子节点的子节点极易漏初始化,压测时才暴露
10万+词库必须换数据结构,别硬扛map[rune]*Node
开源库如github.com/BobuSumisu/aho-corasick默认用map[rune]*Node,10万词时构建时间超2s、GC频繁。这不是配置问题,是底层结构瓶颈:
立即学习“go语言免费学习笔记(深入)”;
- 改用双数组Trie(如
github.com/grepner/go-ahocorasick优化分支),内存降为1/10,构建快10倍 - 但注意:双数组不直接支持
rune,中文需预映射(如GBK字节→uint16ID),不能直接拿'中'当索引 - 若坚持用
rune版,至少把map[rune]*Node换成[65536]*Node稀疏数组(仅限ASCII场景)
并发匹配时State必须隔离,sync.Pool没用
AC自动机结构只读,但匹配过程中的游标状态(当前节点指针、已匹配长度、路径深度)必须每个查询独占:
- 错误写法:
matcher := pool.Get().(*Matcher); matcher.Search(text),且未重置内部current字段 - 正确做法:每次
Search()都从root开始;或传入干净的上下文结构体,含current *Node字段 -
sync.Pool对State复用意义不大——状态重置成本远低于分配开销,且易引发竞态
最难的从来不是建树,而是让每个rune走对路、每个map不panic、每个重叠起点都不被跳过。这些点线上一出问题,就是漏词或误杀,没有中间态。



















