恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
一个客户裂成两个身份:形近字归一化越界污染白名单,4/4 复现后我拆掉了整条自动纠错链
首页
资讯中心
/
一个客户裂成两个身份:形近字归一化越界污染白名单,4/4 复现后我拆掉了整条自动纠错链
一个客户裂成两个身份:形近字归一化越界污染白名单,4/4 复现后我拆掉了整条自动纠错链
发布时间:2026/9/28 4:40:41
我们这套微信自动回复工具里有一块很小、平时没人注意的代码:客户名归一化。它的职责说起来只有一句话——OCR 把客户名片上的「李文宇」读成「李文字」时,把误读纠回真名。上线三个月没出过事,出事是在一个周四的晚上:值班同事反馈「有个客户的消息永远不回」。排查下去的结果是,同一个人的消息被系统当成两个人轮着说,而会话切换判据每轮都判定「这个人切走了」,于是永久漏回。更难看的是第二个反馈:一个老板投诉说系统把「李女士」自动改成了「李先生」。当天夜里我们复盘时发现,这两件事的根子在一条链上——纠错结果被写进了白名单。一周之后,整条自动纠错链被拆掉,只留下一个收得很紧的保守版本。这篇文章把排查过程、错误判据为什么看起来总是对的、以及防线怎么重新摆位置讲清楚。## 1. 背景:为什么会有自动纠错这件事### 1.1 名字要过三道工序才成为判据客户给我们发消息时,系统需要先回答「这是名单里的谁」。数据链路是这样跑的:text名片截图 / 聊天窗口截图 │ ▼ OCR 引擎(tesseract + 自定义字符白名单) │ 原始识别文本,含误读 ▼ normalize_name() ← 形近字归一化,翻车主体 │ ▼ whitelist 精确 / 模糊匹配 │ ▼ 会话状态机(判断“这个客户切走了 / 切回来了”)形近字问题的来源是汉字本身:宇与字、己与已、未与末、氏与氐,OCR 置信度掉到 0.85 以下时读错很常见。而「名字是不是名单里那个人」又是一个硬判定——判对了正常回复,判错了要么冷场要么串聊。所以我们顺理成章地加了归一化这一步:把疑似误读纠正回真名,让匹配能命中。### 1.2 白名单:身份信息的来源白名单是一份本地 JSON,同时充当两件事的基准:哪些人算「我们的客户」,以及会话状态机的身份键。rustpub struct WhitelistEntry { /// 客户登记的真实姓名,纠错的目标基准 pub real_name: String, /// 微信 wxid,比名字更稳定的身份锚点 pub wxid: String, /// 备注来源: manual(人工录入) / learned(系统学习) pub source: Source,}设计约定很朴素:real_name只能由人工修改,系统的纠错只是「识别阶段的一次查询」,不改名单。现在回头看,这条约定被「让它自动回复」这个交互按钮轻易绕过去了——这是后面所有事的起点。另一个三个月没人注意这道工序的原因:归一化出错的代价不体现在报错日志里。纠错了名字,系统不抛异常,它只是「信心十足地匹配到了另一个人」。监控只看回复率和延迟,聚合指标一路健康,直到事故那晚才头一回变红。这是排查教会我的头一课:失败模式是静默的,就一定会等到人来反馈才暴露。## 2. 翻车一:等长形近字表把「李女士」改成了「李先生」### 2.1 一次数据重建里的巧合字表起初是人工整理的形近对。后来有一次白名单数据重建,为了省事,我们写了一段脚本从名单里「自动发现」候选纠错对:pythondef whitelist_reconstruct(old_path: str, new_path: str): old = json.load(open(old_path)) new = {} for e in old["entries"]: key = e["real_name"] # 把同一个人的多条记录折叠成一条 new[key] = e # 顺带“学习”:两两比对,发现只差一个字且都在 2~4 字内的, # 视为形近对,自动写入 homoglyph.json names = list(new.keys()) for a in names: for b in names: if a != b and one_char_diff(a, b) and same_len(a, b): homoglyph_pairs.add((b, a)) # b 视作 a 的误读 json.dump(...)问题出在one_char_diff + same_len这对判据上。名单里存在「张女士」「张先生」这样的称呼型条目——登记客户本人时用了「李女士」,客户配偶又登记了「李先生」,这在销售团队里非常常见。它们等长、只差一个字,被脚本当成形近对收了进去。女和宇、李和季这类真形近对混在里面,肉眼核对 700 多条字表时根本看不出来。### 2.2 「自动纠错」变成了「自动混淆」翻车后的归一化函数是这样工作的:pythondef load_homoglyph(path="config/homoglyph.json"): raw = json.load(open(path)) table = {}