晚上九点,骑手老王接到一单外卖:“西虹市炒旦”。他盯着屏幕愣了好几秒——这到底是道什么菜?好在系统背后藏着一位“人工智能食堂大妈”,她一眼就断定:顾客想点的其实是菜单上的“西红柿炒蛋”。大妈凭什么这么笃定?靠的是两样本事:第一,掰着手指数一数,从“西虹市炒旦”到“西红柿炒蛋”,最少要改动几个字;第二,把两边菜名都切成“西红、红柿、柿炒”这样的两字小片段,数一数两堆片段有多少是重合的。前一种本事,行话叫编辑距离;后一种本事,行话叫 n-gram 重叠。你每一次在搜索框里的“手滑”,搜索引擎都在用这两样本事默默读懂你的心思。这篇文章,我们就跟着这位食堂大妈,把拼写纠错的原理、工程实现和踩坑经验一次讲透,让你也能为自家产品装上“读心术”。

岗位关联

对搜索推荐算法工程师而言,拼写纠错是查询理解的“第一道安检门”,直接决定用户搜索体验与业务指标;啃下这块硬骨头,等于掌握了搜索链路中性价比极高的优化杠杆。

目录
  1. 可视化演示
  2. 纠错问题概述
  3. 编辑距离算法
  4. n-gram重叠
  5. 候选集生成
  6. 相似度评分
  7. 工程优化
  8. 实践与陷阱
  9. AI 应用场景
  10. Python 演示
  11. 常见误区
  12. 面试题

可视化演示

通过编辑距离DP表、n-gram重叠匹配和噪声信道模型,直观展示拼写纠错的核心算法流程。

本节配有交互动效(进入视口后自动加载,约 3KB)。

编辑距离动态规划 将拼错的词通过插入/删除/替换操作转为正确词 每步操作代价为1,求最少操作次数 d(i,j) = min(d(i-1,j)+1, d(i,j-1)+1, d(i-1,j-1)+cost) DP表逐格填充,右下角即为编辑距离

纠错问题概述

你有没有过这种经历:搜“苹果手ji”,结果页第一屏全是手机壳,而你明明只打错了一个字母?食堂大妈的日常也是这样——每天都有客人把“西红柿”写成“西虹市”、把“宫保鸡丁”写成“工保鸡丁”。在搜索引擎的世界里,错别字查询的比例高达一成左右,如果不处理,用户要么看到一堆无关结果,要么干脆跳出页面,流量就白白流失了。

那么,拼写纠错到底是什么?简单说,就是系统先判断“这句话里有没有写错的词”,再从词典里召回一批“看起来可能是你想写的词”,最后给这些候选词打分排序,选出最合理的那一个展示给用户。整个过程可以拆成三步:检错、候选、评分,就像食堂大妈先发现“这道菜名不对劲”,然后翻出几道相近的菜,最后凭经验锁定最像的那一道。

围绕这三步,业界形成了两大主流方法。一类是从“字形改动”出发,计算两个词之间最少要变几个字,这就是编辑距离(Edit Distance);另一类是从“局部片段”出发,把词切成小块再看重合度,这就是 n-gram 重叠。两者各有各的脾气,也常常配合使用——就像大妈既会逐字核对菜单,也会凭“这片菜叶子眼熟”来速记。

回到我们的比喻:编辑距离是“逐字抠”,n-gram 是“看片段”。理解了两者的分工与配合,拼写纠错这道菜,你就已经知道该从哪下筷子了。

编辑距离算法

食堂大妈拿到“西虹市炒旦”后,心里开始默算:“'西'对'西'没问题;'虹'得改成'红';'市'得改成'柿';至于'炒旦'嘛……'旦'要改'蛋'。”她数的不是哪个字像,而是一共需要动几次。这种思路落到计算机里,就是编辑距离:把一个字符串变成另一个字符串,最少需要多少次单字符操作,每次操作限定为插入、删除、替换之一。

为什么只允许这三种操作?因为这是人类手写和输入法出错的最小单元:漏打一个字是删除,多打一个字是插入,打错字形或读音相近的字就是替换。任何复杂的改动都能拆解成这三样的组合,所以把“最少操作次数”当作两个词的距离,既直观又统一。

直接暴力枚举所有改动方案是不可能的——“西红柿炒蛋”和“西虹市炒旦”之间可能的操作序列多到爆炸。聪明的做法是动态规划:把大问题拆成小问题,用一张表格记录“某前缀变成另一前缀”的最小代价。定义 $d_{i,j}$ 表示把字符串 $a$ 的前 $i$ 个字符变成字符串 $b$ 的前 $j$ 个字符所需的最少操作次数,那么递推关系如下:

从“西虹市”到“西红柿”,查表得到距离是 2,和食堂大妈的心算完全一致。动态规划表看似复杂,其实就像大妈在心里列了一张“菜名对照账本”,一格一格记下所有前缀的最优改法,最后只取最角落那个数字。

di,j=min⁡{di−1,j+1删除 aidi,j−1+1插入 bjdi−1,j−1+[ai≠bj]匹配或替换\displaystyle d_{i,j} = \min\begin{cases}d_{i-1,j} + 1 & \text{删除 } a_i\\d_{i,j-1} + 1 & \text{插入 } b_j\\d_{i-1,j-1} + [a_i \neq b_j] & \text{匹配或替换}\end{cases}
这条公式的意思是:要得到“前 i 个字符 vs 前 j 个字符”的最小代价,只需要看三种“最后一步”谁更便宜——删掉 $a_i$、插入 $b_j$,或者让 $a_i$ 与 $b_j$ 相遇(相同不花钱,不同花 1 块钱)。填完整个表格,右下角就是答案。
  1. 定义状态$d_{i,j}$ 表示把 $a$ 的前 $i$ 个字符变成 $b$ 的前 $j$ 个字符的最少操作次数,表格尺寸为 $(m+1) \times (n+1)$。
    di,j=min⁡(⋯ )d_{i,j} = \min(\cdots)
  2. 初始化边界当一方为空串时,只能靠删除或插入补齐,因此 $d_{i,0} = i$(全删)、$d_{0,j} = j$(全插)。
    di,0=i,d0,j=jd_{i,0} = i, \quad d_{0,j} = j
  3. 删除分支删除 $a$ 的第 $i$ 个字符后,问题退化为“$a$ 的前 $i-1$ 个字符变为 $b$ 的前 $j$ 个字符”,代价加 1。
    di−1,j+1d_{i-1,j} + 1
  4. 插入分支在 $a$ 末尾插入一个 $b_j$,那么前面的 $a$ 只要负责变成 $b$ 的前 $j-1$ 个字符,代价加 1。
    di,j−1+1d_{i,j-1} + 1
  5. 替换分支让 $a_i$ 与 $b_j$ “同归于尽”,相同则代价 0,不同则代价 1,用艾弗森括号 $[a_i \neq b_j]$ 表达。
    di−1,j−1+[ai≠bj]d_{i-1,j-1} + [a_i \neq b_j]
  6. 取三者最小值三种最后一步中选代价最小的方案,保证每一步都在“省钱”。
    min⁡{di−1,j+1, di,j−1+1, di−1,j−1+[ai≠bj]}\min\{d_{i-1,j}+1,\ d_{i,j-1}+1,\ d_{i-1,j-1}+[a_i \neq b_j]\}
  7. 最终答案表格右下角 $d_{m,n}$ 就是整个字符串之间的最小编辑距离。
    dm,nd_{m,n}

n-gram重叠

不过,大妈也有犯难的时候。比如客人把“哈根达斯”写成“哈跟达丝”,逐字改起来要动三处,算编辑距离也不算小;可要论“看起来像不像”,这两个词几乎一模一样。这时大妈换了思路:不数改动次数了,而是把菜名切成“哈跟、跟达、达丝”这样的小块,再和菜单上的“哈根、根达、达斯”摆在一起,看看两堆小片段有多少是重合的。切成小块看重合,就是 n-gram 重叠。

n-gram 里的 n 表示每次取几个字符连成一“片”。中文里最常用的是 n=2,也就是二元片段(bigram):“西红柿”切成“西红、红柿”,“西红市”切成“西红、红市”。单个字太泛,三个字以上的片段又太苛刻——只要写错一个字,整片就对不上了。切成小片段后,怎么量化两堆片段的相似程度呢?最经典的是 Jaccard 相似度,它把片段当成集合,求“交集大小 ÷ 并集大小”:

回到比喻中,编辑距离像“数着算”,n-gram 重叠像“看着拼”。“哈跟达丝”虽然改字多,但片段重合度极高,n-gram 能轻松抓住这种“形近”的直觉,弥补编辑距离对局部相似不敏感的短板。

J(s,t)=∣N(s)∩N(t)∣∣N(s)∪N(t)∣\displaystyle J(s,t) = \frac{|N(s) \cap N(t)|}{|N(s) \cup N(t)|}
可以把 $N(s)$ 想成“从这道菜身上切下来的全部食材片”,两份菜名越像,共有的食材片就越多;Jaccard 值越接近 1,说明两堆片段几乎完全重合。
  1. 切分片段$N(s)$ 表示把字符串 $s$ 连续地切成长度恰好为 $n$ 的所有子串组成的集合。
    N(s)={s[i..i+n−1]∣0≤i≤∣s∣−n}N(s) = \{s[i..i+n-1] \mid 0 \le i \le |s| - n\}
  2. 求交集统计两个集合共有的片段数量,这是“相似”的直接证据。
    ∣N(s)∩N(t)∣|N(s) \cap N(t)|
  3. 求并集把两边所有不重复的片段合并起来,作为“总食材池”。
    ∣N(s)∪N(t)∣|N(s) \cup N(t)|
  4. 计算比值共有片段占总池子的比例,就是 Jaccard 相似度。
    ∣N(s)∩N(t)∣∣N(s)∪N(t)∣\frac{|N(s) \cap N(t)|}{|N(s) \cup N(t)|}
  5. 解读结果值域为 $0 \le J(s,t) \le 1$,越接近 1 越相似;“西红市”与“西红柿”的 bigram 集合并集为 3,交集为 1,所以 $J = 1/3$。
    J(西红市,西红柿)=13J(\text{西红市},\text{西红柿}) = \frac{1}{3}

候选集生成

食堂后厨有一千道菜,大妈不可能把每道菜都拎出来跟客人的错字比一遍。她真正的底气在于后厨的分类架:每一种两字食材片(比如“西红”)都固定放在一个贴着标签的盒子里,盒子里放着所有包含“西红”的菜名。客人报出“西红”,她直接冲到“西红”盒子前,把里面的菜名全抓出来。这个“食材片 → 菜名列表”的对应关系,在计算机里就叫 n-gram 倒排索引——它是候选集生成的加速器。

为什么要专门做这一步?因为词典可能有几十万甚至上亿个词,逐词计算编辑距离或者 Jaccard,一次查询就要做百万次比较,绝对扛不住。倒排索引把“从词查片段”变成“从片段查词”,查询时只需三步:先把用户输入的错词切成 n-gram 片段;再拿每个片段去倒排索引里查对应的倒排列表(posting list);最后把多个列表合并起来,就得到一份候选词集合。整个过程就像大妈报出“西红、红市”,分类架上两个盒子里的菜名一合并,候选名单立刻到手。

候选集不能来者不拒,否则又回到“百万比较”的老路。于是需要剪枝策略:只保留与查询至少有 1 个公共片段的词(通常要求更多),控制候选规模在几十到几百的量级;同时限制单个倒排列表的扫描长度,避免“西红柿”这种超热门片段拖垮性能。召回与效率之间是一对天平:剪枝太狠会漏掉正确答案,剪得太松则评分阶段变慢。实战中常把倒排列表按词频排序,优先处理高价值候选。

所以,倒排索引就是食堂后厨那面分类架——没有它,大妈只能一道菜一道菜地问;有了它,错词一出口,候选菜名已经在手里了。这也是为什么拼写纠错通常被安排在“倒排索引与查询处理”这个环节里:先靠索引快速召回,再靠精确打分定夺。

相似度评分

候选名单拉回来了,可能有三五道菜都沾边:西红柿、西红市、番茄……大妈得给它们一一打分,才能给出最靠谱的建议。打分需要两把尺子同时量:一把是“改动次数”(编辑距离),一把是“片段重合”(n-gram 重叠)。可两把尺子的刻度不一样——编辑距离是“越小越好”,n-gram 是“越大越好”,而且数值范围也完全不同,不能直接相加。

第一步,把编辑距离归一化到 0 到 1 之间,让它也变成“越大越像”。做法是用 1 减去“距离除以两个词中较长的长度”:距离为 0 时相似度为 1,距离占满整个长度时接近 0。第二步,给两把尺子各设一个权重,让系统决定更信“逐字抠”还是“看片段”。把两者加权融合,就得到一个综合评分:

打分这一步,相当于大妈在心里给候选菜逐一贴“靠谱指数”。只有把编辑距离和 n-gram 这两路信号都照顾到,再设好及格线,推荐出去的纠错结果才不会闹出“用户要西红市,你推了个红市炒蛋”的笑话。

score(q,c)=α⋅(1−d(q,c)max⁡(∣q∣,∣c∣))+(1−α)⋅J(q,c)\displaystyle \text{score}(q,c) = \alpha \cdot \left(1 - \frac{d(q,c)}{\max(|q|,|c|)}\right) + (1-\alpha) \cdot J(q,c)
这个公式像大妈心里的“综合印象分”:前半部分看“改几个字”(字越像分越高),后半部分看“片段重多少”(片越像分越高),$\alpha$ 就是她对两种判断的信任配比。
  1. 归一化编辑距离把原始距离 $d(q,c)$ 除以较长串长度 $\max(|q|,|c|)$,得到 0 到 1 的“距离占比”,再用 1 减去它,变成“相似占比”。
    1−d(q,c)max⁡(∣q∣,∣c∣)1 - \frac{d(q,c)}{\max(|q|,|c|)}
  2. 设置权重$\alpha$ 表示编辑距离信号所占权重,$1-\alpha$ 表示 n-gram 信号所占权重,两者相加为 1。
    α+(1−α)=1\alpha + (1-\alpha) = 1
  3. 加权相加两路相似度按权重融合成最终得分,分数越高代表候选越可信。
    score(q,c)=α⋅similarityed+(1−α)⋅J(q,c)\text{score}(q,c) = \alpha \cdot \text{similarity}_{ed} + (1-\alpha) \cdot J(q,c)
  4. 阈值过滤设定阈值 $\theta$,分数低于 $\theta$ 的候选直接丢弃,避免把八竿子打不着的词推荐给用户。
    score(q,c)≥θ\text{score}(q,c) \ge \theta
  5. 排序输出对剩余候选按分数降序排列,取前 $k$ 个作为纠错建议或自动改写结果。
    top-k{arg⁡max⁡cscore(q,c)}\text{top-}k\{\arg\max_c \text{score}(q,c)\}

工程优化

中午饭点一到,订单像雪片一样飞来,食堂大妈光靠手算肯定扛不住。真实搜索引擎的流量比食堂高峰还要恐怖:每秒有成千上万个查询涌进来,其中不少还带着错字。这时,纠错系统的工程优化就成了胜负手,后厨流水线必须做到三件事。

第一,离线构建索引。所有词典词的 n-gram 倒排索引是在服务启动前就构建好的,线上只做“查表”不做“建表”。构建时还能顺便统计每个词的热度(词频),把热门词放在倒排列表头部,这样合并候选时优先命中高价值词,既快又准。第二,缓存加速。绝大多数错词是重复的,比如“西虹市”每天被上千人搜索,把它的纠错结果缓存起来,命中缓存就直接返回,连查表都省了。缓存通常用 LRU(最近最少使用)策略,只留最热门的那些,避免内存被拖垮。

第三,性能分析与横向扩展。线上要监控两个核心指标:P99 延迟和候选集规模。如果发现某个 n-gram 的倒排列表特别长,说明它太“泛”了(比如单字“的”),需要剪枝或拆分;如果单机扛不住,就按 n-gram 的哈希值把索引分片到多台机器上,每个查询并行访问多个分片再合并结果。整个链路从“检错”到“打分”都要控制在毫秒级,才能保证搜索体验不被拖后腿。

后厨流水线的本质,就是把“现算”变成“预存 + 快查”:索引是准备好的食材架,缓存是热菜保温柜,分片是多个窗口同时出餐。拼写纠错做到这一步,才算真正从“能跑”进化到“能扛”。

实践与陷阱

再熟练的大妈也有失手的时候:客人点的是新菜,菜单上根本没有;或者客人故意写了个游戏里的“西红柿炒蛋”召唤梗,大妈却一本正经地改成了家常菜。拼写纠错的落地过程中,这类“好心办坏事”的陷阱比比皆是。

第一个要注意的是中英文差异。英文天然按空格分词,“appple“ 的编辑距离对着词典算就行;中文没有空格,直接拿整句和整篇词典比不现实,通常要把查询按字切分再做 n-gram,或者引入拼音、字形等额外的相似信号。第二个是热度信号不可或缺。编辑距离最小的词不一定是对的——用户搜”tb“,距离上最近的可能是某个生僻缩写,但 99% 的人想找的是”淘宝“。所以评分时一定要叠加词频、点击率等热度特征,让系统像大妈一样优先推荐”最常被点的菜”。

第三个大坑是误把纠错当成直接改写。很多搜索框会把“西虹市”悄悄改成“西红柿”再检索,但自动改写有风险:品牌词、人名、新词都可能被误伤。稳妥的做法是默认展示“您是不是想找:西红柿”的提示条,只有对高置信度、低风险的结果才自动改写。最后,效果好不好不能只看离线指标,还要看端到端评估:纠错是否真正提升了点击率、转化率?有没有伤害原本正确的长尾查询?这需要用真实搜索日志做 A/B 测试来回答。

要做好效果调优,最有效的武器就是搜索日志:把用户“输入错词 → 点击正确结果”的轨迹挖出来,就是一份天然的错词对照表,可以持续扩充词典和热度统计。大妈也会把每天“猜错”的订单记在小本本上——日志就是那个小本本,让系统越用越准,越用越懂人心。

AI 应用场景

场景一:电商搜索“手滑”自动纠错

阶段:查询理解(Query Understanding)

解决的问题:用户输入“苹果手ji壳”这类带错字的查询时,倒排索引召回不到任何商品,搜索结果页一片空白。传统方案只能原样检索,用户流失严重;AI 纠错在检索前把查询改写成“苹果手机壳”,让召回恢复正常。

具体案例:某电商团队通过搜索日志发现“手机壳”是高频错词,于是用商品词表构建 n-gram 倒排索引,用户输入先检错,再对候选计算“编辑距离 + n-gram”加权评分,命中高置信度结果时展示“您是不是想找:苹果手机壳”提示条。实验两周,搜索点击率提升约 2.3%,空结果率下降四成。

场景二:OCR/ASR 文本后处理纠错

阶段:文本后处理与归一化

解决的问题:快递面单、发票证件经过 OCR(光学字符识别),或语音转写(ASR)后,会产生大量形近字、同音字错误,直接入库会污染业务数据。

具体案例:某物流系统把“顺风快递”识别成“顺凤快递”,后处理模块用编辑距离将识别文本与标准地址库逐条比对,当距离 ≤ 1 且 n-gram 重叠度超过阈值时自动修正;同时用词频信号防止把正确的品牌名改错。上线后地址解析准确率从 91% 提升到 98%。

场景三:客服机器人意图识别前的文本清洗

阶段:语料预处理

解决的问题:用户语音输入“我想退欢手机”,转写结果里的错字会让意图分类模型“懵住”,进而给出答非所问的回复。

具体案例:某电商客服机器人对转写文本先做拼写纠错——将“退欢”与业务词典比对,编辑距离为 1、bigram 重叠度高,改写为“退换”后再送入意图模型,意图识别准确率提升 6 个百分点,机器人首次解决率显著改善。

Python 演示

python64 行
# Spelling correction demo: edit distance + n-gram overlapdef edit_distance(a, b):    m, n = len(a), len(b)    dp = [[0] * (n + 1) for _ in range(m + 1)]    for i in range(m + 1):        dp[i][0] = i    for j in range(n + 1):        dp[0][j] = j    for i in range(1, m + 1):        for j in range(1, n + 1):            cost = 0 if a[i - 1] == b[j - 1] else 1            dp[i][j] = min(                dp[i - 1][j] + 1,                dp[i][j - 1] + 1,                dp[i - 1][j - 1] + cost            )    return dp[m][n]  def ngrams(word, n=2):    return set(word[i:i + n] for i in range(len(word) - n + 1))  def jaccard(query, cand, n=2):    gq, gc = ngrams(query, n), ngrams(cand, n)    if not gq or not gc:        return 0.0    return len(gq & gc) / len(gq | gc)  def build_inverted_index(words, n=2):    index = {}    for w in words:        for g in ngrams(w, n):            index.setdefault(g, []).append(w)    return index  def generate_candidates(query, index, n=2, top_k=8):    scores = {}    for g in ngrams(query, n):        for w in index.get(g, []):            scores[w] = scores.get(w, 0) + 1    return sorted(scores, key=lambda x: -scores[x])[:top_k]  def correct(query, words, index, n=2, alpha=0.6, threshold=0.5):    candidates = generate_candidates(query, index, n)    results = []    for w in candidates:        norm_ed = 1.0 - edit_distance(query, w) / max(len(query), len(w))        score = alpha * norm_ed + (1 - alpha) * jaccard(query, w, n)        if score >= threshold:            results.append((w, round(score, 3)))    return sorted(results, key=lambda x: -x[1])  if __name__ == "__main__":    words = ["tomato", "potato", "tomorrow", "tobacco", "tonight"]    index = build_inverted_index(words)    print("edit_distance(tomato, tomoto):", edit_distance("tomato", "tomoto"))    print("jaccard(tomato, tomoto):", jaccard("tomato", "tomoto"))    print("candidates for 'tomot':", generate_candidates("tomot", index))    print("best correction:", correct("tomot", words, index))
目的:完整实现拼写纠错的四个核心组件——编辑距离动态规划、n-gram 切分、Jaccard 相似度、倒排索引候选召回,最后用加权融合打分给出纠错建议。
  1. dp = [[0] * (n + 1) for _ in range(m + 1)]创建一张 (m+1) 行 (n+1) 列的动态规划表,用于记录所有前缀变换的最小代价。
  2. dp[i][0] = i边界初始化:把长度为 i 的串变成空串,只能逐个删除,代价是 i。
  3. cost = 0 if a[i - 1] == b[j - 1] else 1当前两个字符相同则替换代价为 0,不同则为 1。
  4. dp[i][j] = min(...)从删除、插入、替换三种方案里挑出代价最小的,这是编辑距离的核心递推。
  5. set(word[i:i + n] for i in range(len(word) - n + 1))把词切成 n 个字符一组的连续片段,并用集合去重。
  6. len(gq & gc) / len(gq | gc)交集大小除以并集大小,一行代码算出 Jaccard 相似度。
  7. index.setdefault(g, []).append(w)每个 n-gram 片段后面挂上包含它的词,离线构建倒排索引。
  8. for w in index.get(g, [])用查询切出的片段反查倒排索引,无需全词典扫描即可召回候选。
  9. norm_ed = 1.0 - edit_distance(...) / max(len(query), len(w))把编辑距离归一化成 0-1 相似度,便于和 Jaccard 融合。
  10. alpha * norm_ed + (1 - alpha) * jaccard(...)编辑距离与 n-gram 两路信号加权融合,得到综合评分。

常见误区

误区“编辑距离最小的那个词一定是对的,直接替换用户输入就行。比如用户搜 'tb',距离最小的候选就该顶上去。”

正解编辑距离只刻画“字形”上的接近程度,完全不考虑这个词在真实世界中有多常见。用户搜 “tb“,距离最近的可能是某个生僻缩写,但绝大多数人想找的是”淘宝”。实战中必须叠加词频、点击率等热度信号,就像食堂大妈优先推荐最常被点的菜,而不是菜单上字形最近的菜。

为何常见:教材示例里的词典很小,且只演示形近词,容易让人误以为“距离”是唯一标准,忽略了真实世界中词的流行度差异。

误区“n-gram 窗口设得越大,切出的片段越独特,匹配就越精确。”

正解n 过大会让片段过于“挑剔”——只要错一个字,整片就对不上,召回率断崖式下跌;n 过小则片段太泛(比如单字),候选噪声巨大。中文一般取 n=2(bigram),英文可取 2 到 3,必须在“召回得多”与“匹配得准”之间找平衡。

为何常见:直觉上“更长的片段更独特”是对的,但忽略了拼写纠错的场景本身是“查询已经写错了”,过于严格的匹配反而匹配不上。

误区“英文按空格分词做编辑距离,中文加个分词器不就行了?”

正解中文没有天然空格,分词本身就可能出错,分词错误会直接污染纠错。中文通常直接把查询按字切分做 n-gram,把“拼音近似”“字形近似”作为额外信号;即便要分词,也应基于搜索日志定制词典,而不是套用通用分词工具。

为何常见:很多开源工具和教程都基于英文设计,接口上“加个 jieba”似乎就能跑,实际到了中文场景却水土不服。

误区“检测到错词后直接改成正确词再检索,效果一定更好,用户感觉不到违和。”

正解自动改写有真实风险:品牌词、人名、新造词都可能被误改,改错了反而把用户带偏。业界普遍做法是对高置信度、低风险的结果才自动改写,其余场景展示“您是不是想找:XXX”的提示条,把选择权还给用户。

为何常见:搜索引擎宣传的“丝滑纠错体验”让人只看到自动改写的爽快,忽略了背后大量线上 A/B 实验和风险控制。

误区“离线把纠错准确率做到 99%,线上效果肯定没问题。”

正解纠错的最终价值是提升点击率、成交率等业务指标。离线准确率高,可能只是因为测试集里的错词都很典型;真实查询里的大量长尾错词、新词、故意玩梗,离线根本覆盖不到。必须用真实日志做端到端 A/B 测试,观察“改了之后用户点不点”。

为何常见:离线评测容易构造、数字好看,线上收益却难归因,人们倾向于用离线数字代替线上结论。

面试题

Q1. 在经典编辑距离算法中,下列哪一项不属于三种基本编辑操作?

单选题 · easy

  • A.插入一个字符
  • B.删除一个字符
  • C.替换一个字符
  • D.交换两个字符← 正确答案

答案:D。经典编辑距离(莱文斯坦距离)只包含插入、删除、替换三种操作;“交换相邻字符”是 Damerau-Levenshtein 距离才引入的。就像食堂大妈改菜名时,一次只能加一个字、减一个字或改一个字,不能把两个字对调算一次操作。A、B、C 都是基本操作,D 不是。

Q2. 已知“西红市”与“西红柿”逐字符比较:西→西相同,虹→红不同,市→柿不同,则两者的编辑距离是?

单选题 · easy

  • A.编辑距离为 0
  • B.编辑距离为 1
  • C.编辑距离为 2← 正确答案
  • D.编辑距离为 3

答案:C。三个位置中两个字符不同,每个不同的位置需要一次替换,代价各为 1,所以最小编辑距离是 2。首字符“西”完全匹配,不需要任何操作。选 3 是误以为每个位置都要算一次操作;选 0 或 1 则忽略了实际不同的两个位置。

Q3. 在中文拼写纠错中,最常用的 n-gram 窗口大小通常是多少?

单选题 · mid

  • A.n = 1(一元)
  • B.n = 2(二元)← 正确答案
  • C.n = 4(四元)
  • D.n = 8(八元)

答案:B。中文纠错常用 bigram(n=2),既能捕捉相邻字符的搭配信息,又对单字错写有较好容忍度。n=1 过于宽松、噪声大;n=4 及以上过于严格,只要错一个字,切出的片段就可能完全不重叠,召回率大幅下降。

Q4. 编辑距离递推式中,替换分支"d[i-1][j-1] + [a_i ≠ b_j]"里的 [a_i ≠ b_j] 表示什么?

单选题 · mid

  • A.两字符不同时取 1,相同时取 0← 正确答案
  • B.两字符相同时取 1,不同时取 0
  • C.恒为 1 的惩罚项
  • D.恒为 0 的惩罚项

答案:A。这是艾弗森括号:条件成立记 1,不成立记 0。所以两字符相同时替换代价为 0,不同时代价为 1。这正对应“菜名相同就不用花钱改,不同才扣一次机会”。B 把逻辑弄反了;C、D 把替换代价固定为常量,无法体现“相同则不花钱”的特性。

Q5. 基于 n-gram 的候选集生成中,核心的检索索引结构是?

单选题 · mid

  • A.倒排索引(含倒排列表)← 正确答案
  • B.单链表
  • C.完全二叉树
  • D.斐波那契堆

答案:A。以每个 n-gram 片段作为索引项,后接包含该片段的所有词条,这种“片段 → 词列表”的结构就是倒排索引。查询时拿查询的所有片段去查表、合并列表,即可快速召回候选。单链表、完全二叉树、斐波那契堆都不支持从片段反查到包含它的词这一需求。

Q6. 已知“西红市”的 bigram 集合为 {西红,红市},“西红柿”的 bigram 集合为 {西红,红柿},两者的 Jaccard 相似度是?

单选题 · hard

  • A.1/4
  • B.1/3← 正确答案
  • C.1/2
  • D.2/3

答案:B。交集只有“西红”一个片段,并集为 {西红,红市,红柿} 共 3 个片段,交集除以并集得 1/3。A 把并集误算成 4;C 恰好是 Dice 系数的结果(分子取 2 倍交集);D 是分子分母都算错的常见错误。

Q7. 归一化编辑距离"1 - d/ max(|q|,|c|)"的主要作用是什么?

单选题 · hard

  • A.把距离换算成 0-1 的相似度便于与其他信号融合← 正确答案
  • B.把相似度放大到大于 1 的区间
  • C.消除对词典规模的依赖
  • D.将结果转换为和为 1 的概率分布

答案:A。原始编辑距离受字符串长度影响大,长串的距离天然更大,无法直接与 n-gram 相似度比较。归一化后取值范围落在 0 到 1 之间,方便与 Jaccard 等信号加权融合。B 方向反了;C 与词典规模无关;D 归一化并不保证所有候选分数和为 1,谈不上概率分布。

Q8. 一次完整的拼写纠错流程通常包含哪些环节?

多选题 · mid

  • A.检错(判断查询是否写错)← 正确答案
  • B.候选生成(召回可能的正确词)← 正确答案
  • C.相似度评分与排序← 正确答案
  • D.直接删除用户查询日志

答案:ABC。检错、候选、评分是纠错三环节,缺一不可:先判断有没有错,再召回候选,最后打分排序给出建议。D 与流程无关,而且查询日志恰恰是挖掘高频错词、验证纠错效果的重要资产,应该保留并利用,而不是删除。

Q9. 在实际搜索引擎拼写纠错工程中,下列哪些优化手段是常用的?

多选题 · hard

  • A.对热门纠错结果做缓存← 正确答案
  • B.融合词频与点击率信号← 正确答案
  • C.对低频长查询直接忽略
  • D.用倒排索引加速候选召回← 正确答案

答案:ABD。缓存热门纠错结果可以大幅降低重复查询的延迟;词频与点击率信号能保证“改得对”;倒排索引是候选召回的核心加速手段。C 直接忽略低频长查询会损害长尾用户体验,正确做法是分级处理,而不是一刀切忽略。

Q10. 关于编辑距离与 n-gram 重叠的关系,下列说法正确的是?

多选题 · mid

  • A.编辑距离关注字符级最少改动次数← 正确答案
  • B.n-gram 关注局部片段重合比例← 正确答案
  • C.两者可以加权融合使用← 正确答案
  • D.两者在数学上完全等价

答案:ABC。编辑距离是全局视角的“最少改动次数”,n-gram 是局部视角的“片段重合度”,两者各有盲区——前者对位置敏感,后者忽略长程结构,所以实操中常按权重融合互补。D 说完全等价是错的,它们衡量相似性的方式截然不同。