用户输入型号 AB-12,资料里写着 AB-12。肉眼看起来只是字宽不同,精确匹配却可能把它们当成两串完全不同的字符。
这时先换Embedding模型,未必找对了故障位置。RAG——先检索资料再生成回答——不仅要理解语义,也要把型号、条款号和文档标识符接到正确记录上。适当的文字正规化能减少漏检,但“可以放在同一个搜索篮子里”不等于“已经证明是同一个业务对象”。
本篇只解释这个边界:怎样让字符变体更好搜,同时不把原始证据与对象身份一起改掉。
AI原创概念插画:两只象牙托盘分别放着月纹与日纹的红色蛾形摆件,中央琥珀色玻璃装置里呈现结合两种纹样的蛾形,后方是木雕图样柜。它比喻搜索视图可以汇合、原件仍应分别保存;不是正规化算法、真实光学效果或产品截图。
先区分:看起来一样、字符等价、业务相同
电脑比较字符串,不是先看它们在屏幕上的样子,而是比较其编码表示。一个带重音的字母,既可能是单个预组合字符,也可能是基础字母后面跟着组合重音。两种表示可以具有Unicode定义的规范等价关系,却不能直接靠原始字符串比较得到相等。[1][2]
Unicode正规化(normalization,也常译作规范化)提供统一表示的规则。其中最值得先认识的是:
- NFC:做规范分解,再按规则组合;用于把规范等价的表示归到同一形式。它不是“把所有相似字符都合并”。
- NFKC:在分解时还处理兼容映射,再做规范组合;会额外折叠一些字宽、圈号、上下标等区别。[1]
这两种形式都不替你决定产品编号的业务规则。Unicode中存在兼容等价,不代表某厂商允许两个编号互换;反过来,业务上确认的旧型号别名,也未必能用Unicode变换得到。
所以要把三层判断分开:视觉相似是展示现象,正规化等价是字符规则,业务同一性则需要目录、登记表或其他可信映射。
一次本地字符串实验:NFKC既能补桥,也会抹去区别
下面是本篇实际运行的精简实验。它只调用Python标准库,比较人工指定的字符串,没有运行模型、向量库或Elasticsearch,也没有测量RAG准确率。
import unicodedata as u
pairs = [
("café", "cafe\u0301"),
("AB-12", "AB-12"),
("①", "1"),
("m²", "m2"),
("A", "\u0410"), # 后者是西里尔字母,不是拉丁字母
("Ab", "ab"),
]
for a, b in pairs:
print(a == b,
u.normalize("NFC", a) == u.normalize("NFC", b),
u.normalize("NFKC", a) == u.normalize("NFKC", b))实际输出中,每行依次是“原串相等、NFC后相等、NFKC后相等”:
False True True
False False True
False False True
False False True
False False False
False False False完整检查还断言了两种正规化的比较结果。本地环境是Python 3.13.5、Unicode数据库15.1.0;引用的Python说明页面属于3.14系列,不能把文档版本当成本机版本。
结果值得分开读:
字宽变体可能接上了。 AB-12经过NFKC得到AB-12。如果业务已明确这只是录入格式差异,就能把这种形式作为检索别名,让用户不必猜文档用了哪种字宽。
兼容折叠也会丢信息。 ①和1在NFKC下归为相同字符串,m²和m2也一样。如果前者表示章节圈号、后者表示另一个登记编号,或者原文中的上标参与数学含义,覆盖原文就可能损坏证据。Unicode规范明确警告:NFKC与NFKD不能盲目应用到任意文本,兼容折叠可能移除对语义重要的区别。[1]
看起来像,不保证能正规化成一样。 本例的拉丁A与西里尔А仍不同。NFKC也没有把Ab改成ab。大小写折叠、OCR纠错、跨文字系统的相似字处理是另外的策略,不应统统藏在一个叫“清洗”的函数里。
一份型号手册该怎样进RAG
以下是未执行的教学场景,型号、记录和规则均为虚构。
设备目录登记了一个稳定对象device-042,正式型号是AB-12。导入手册时,封面文字却被录入为全角AB-12。我们不让模型自行宣布“这肯定是同一个型号”,而是按明确的数据规则分开存放:
{
"object_id": "device-042",
"model_original": "AB-12",
"model_search_alias": "AB-12",
"alias_rule": "model-width-policy-v1",
"document_id": "manual-042-rev-c"
}这些字段是本文建议的应用设计,不是某个框架的统一协议。
处理过程可以是:
- 保留原件与原值。 原始手册、抽取文本及出处定位都保留;检索字段不是原文的唯一副本。
- 明确字段规则。 只对业务允许的型号字段生成兼容形式;若规则仅允许字宽差异,不应因为实现方便就无条件接受所有NFKC折叠。可以限制输入字符集、核对变化类型,或采用经审查的窄范围映射。
- 两侧用同一规则。 入库时生成什么别名,查询解析出的型号就用相同版本的规则处理。不要一侧做NFC、另一侧做NFKC,也不要只有查询做小写转换。
- 别名查回稳定对象。 候选必须再连接到登记目录中的对象;不能直接把别名当全局主键。目录、厂商或产品系列等作用域也要参与判断。
- 引用仍回到原件。 回答可以说明搜索使用了别名,但展示引文时应保留原始写法和来源;没有确认关系时,报告“找到可能相关的型号变体”,而不是给出确定结论。
尤其要处理碰撞:不同原值可能得到相同别名。此时不是“清洗完成后随便留一份”,而是保留候选集合,检查它们是否本就映射到同一稳定对象。若是不同对象,必须补充厂商、系列等条件或停止自动合并。
正规化规则更新也不只是换一个函数。需要让旧索引与新查询保持一致,检查碰撞变化,再选择重建相关字段或成对切换策略;本篇不提供线上迁移效果保证。
“精确字段”也可能已经被变换过
不要看到搜索字段叫keyword,就默认它保存的是未改动字符串。
Elasticsearch当前官方说明中,keyword字段可配置normalizer,其分析链保证产生一个token;该normalizer既在索引前应用,也会在文档列举的match、term查询情境中应用。文档示例把lowercase和asciifolding组合,让BÀR与bar可按归一后的形式匹配。[3]
这支持的是一个具体机制:所谓精确匹配,可能是在变换后的键上精确,而不是对原始字节精确。 文档中的配置不是NFKC实现,本文也没有在Elasticsearch执行该示例;不能把ASCII折叠、小写转换与Unicode兼容正规化当成同一种操作。
对RAG来说,建议分别设置有明确用途的字段:原始标识用于审计和回查,经允许的别名用于扩大召回,稳定对象ID用于后续连接。是否区分大小写、是否忽略重音、是否接受圈号折叠,都应成为可审查的规则,而不是由字段名字暗示。
什么情况下先查这一层
如果用户给了明确型号、错误码或条款号,系统却“语义上搜出很多相似资料、就是找不到那一条”,值得先对照输入与索引的字符表示和字段配置。尤其是在扫描识别、跨语言录入、网页复制与多系统导入之后。
但不要据此把所有漏检归咎于Unicode。资料未入库、权限过滤、分词配置、候选预算和版本条件都可能造成相同表象。向量模型是否能跨这些写法召回,也必须针对实际模型与语料测量,不能从本地字符串比较推断。
可复用检查清单
- 原始字节、抽取原文与出处是否仍可回查?
- 该字段允许哪些等价关系,依据是什么?账号、签名、文件路径等字段是否被误套用同一清洗规则?
- 入库和查询是否采用相同规则、相同作用域及可记录的版本?
- 测试是否同时包含“应该合并”和“绝不能合并”的样例?
- 是否覆盖组合重音、全半角、圈号、上下标、大小写与跨文字系统相似字符?
- 多个原值落入同一别名时,是否报告碰撞而非悄悄覆盖?
- 最终对象是否通过可信目录确认,而非让模型根据外观猜测?
- 引文、审计与回查是否仍指向原件,召回收益是否另做了真实评测?
给搜索增加一条可解释的别名,和改写事实,是两件事。 好的正规化策略不是让所有字符越来越像,而是清楚说明:哪些差别可以在这个字段上忽略,哪些差别必须留下。
来源与阅读范围
本文由AI创作。实际研究截止时间见文末;产品机制仅按本轮读取的官方资料说明,不声称完成任何模型或检索系统基准测试。
- Unicode UAX #15,Revision 57:固定Unicode 17.0.0文档,日期2025-07-30。实际阅读第1.1、1.2节及相关示例,支持规范/兼容等价、四种形式与兼容折叠风险;不是把该历史固定版冒充当前最新版。
- Python
unicodedata官方文档:3.14系列滚动页面,未锁定补丁版本或独立原始发布日期。实际阅读normalize、unidata_version及相关说明;本地运行环境另行记录在实验段。 - Elasticsearch
normalizer官方参考:动态文档,未锁定产品版本或核实独立发布日期。实际阅读单token保证、索引/查询应用及小写与ASCII折叠示例;仅据此解释字段变换,不声称配置了Unicode NFKC。
检索截止:2026-10-23T17:40:13.825742+00:00。


留下你的想法 0
欢迎提问、补充和不同观点。评论通过审核后会公开显示。