中文错别字检测:端侧模型、词典与编辑器位置的工程边界
本文目录
这篇复盘来自一份 React/TypeScript 中文纠错插件的本地源码快照。它由编辑器宿主适配层、浏览器端模型推理和词典规则组成。为保护宿主项目,本文只描述通用架构,不公开内部包名、接口标识、域名或文档地址。代码结构审查不等于插件已在宿主里完成安装、权限和真机验收。
一条检测链路,三种职责
实现可以拆成三个边界:宿主适配层读取当前文档纯文本并监听编辑;检测层把文本分段,调用模型与本地词典;交互层展示候选、定位问题并执行替换。把宿主 API 留在一层,模型与文本算法就更容易单测,也更容易迁移到另一个编辑器。反过来,如果模型 Hook 直接在多处调用宿主全局对象,组件测试、离线验证和清理监听都会变难。
被检查的代码先读取编辑器 HTML,再用正则去标签并替换少量实体。简单页面看起来够用,但真实文档的 HTML 还可能有列表、表格、换行、代码、注释和其他实体;删除标签的正则不等于 HTML 到可编辑纯文本的可靠转换。更稳妥的宿主适配器应返回纯文本和一份可验证的“文本偏移到文档节点范围”映射,并把它们作为同一快照管理。
模型是在提候选,不是在判定错字
这份实现使用中文 MacBERT 纠错模型进行逐 token 掩码预测,再把一个词元的最高 logit 与原词元 logit 作差;差值超过固定阈值时就生成替换候选。它补充了一个本地词典,适合找常见固定错写。这样的结果应被称为“候选”,不能包装成百分百正确的拼写判决:模型会受上下文、领域词汇、繁简体、专名和标点影响,词典也可能把用户有意使用的词误标。
原实现把 logit 差值阈值 0.3 命名为 confidence threshold。Logit 差值不是经校准的概率;不同模型、量化方式、上下文与词表都会改变其数值分布。阈值应该在目标文档上用人工标注集评估精确率、召回率和误报成本,再按产品场景调节。编辑器内,误报造成的内容损坏往往比漏报更难接受。
当前方法主要处理词元替换,不自然覆盖插入、删除、跨词边界和多字短语。词典结果则适合补模型盲区,但要按最长匹配、同位置冲突、重复命中和上下文排除规则合并。任何“全部修正”都应提供逐项预览、撤销和恢复原文的能力。
端侧推理仍要交代网络与设备成本
样本读取本地模型目录,并没有在被检查的文本流程里发现把整篇正文上传到推理接口的请求;这是有限的源码证据,不是对所有宿主插件、依赖运行时代码或浏览器遥测的隐私保证。该快照还通过公开 CDN 动态加载 Transformers.js 运行时,再读取本地模型资产。运行时代码来源与模型文件来源是两条不同的信任链:发布时应锁定版本、验证文件摘要,并检查浏览器网络面板确认正文没有意外外发。
实际模型文件在这台机器上的文件系统显示约 452 MB(全精度)和约 114 MB(量化版)。全精度版本会明显增加首次下载、缓存、内存和移动设备压力;量化通常能缩小文件并提高推理效率,但可能改变准确率,必须用同一套校准样本比较。代码选择全精度,不能因为目录里也有量化文件就假定用户实际使用了它。
更完整的首屏状态至少应区分运行时加载、模型下载、模型初始化、检测进度、取消、内存不足和离线失败。模型应按需加载,允许用户改用规则检查或跳过;提供“完全离线”选项时,还必须确认运行时 JavaScript 与模型文件都已本地可用,而不是只把权重放在项目目录里。
词元位置不能直接当成文档字符位置
这是文本纠错中最容易悄悄破坏内容的边界。BERT tokenizer 会添加特殊 token,字符串也可能经过规范化或拆成多个 token。当前快照直接把模型序列下标加到分段起点,作为错误字符在原文中的位置;但序列开头包含 [CLS],因此 token 下标和原文下标并不相同。再叠加 UTF-16、补充平面字符、分段边界及 HTML 纯文本映射,单靠 position = segmentStart + tokenIndex 不可靠。
检测结果应携带可验证的半开区间 [start, end),并映射回同一版本的原文快照。应用前再次检查该范围仍然是预期文本;若不相等就停止并重新检测,不要猜着改。下面是纯字符串中的最小保护思路;编辑器适配层还需把范围转换为准确的文档节点与偏移:
type Suggestion = { start: number; end: number; expected: string; replacement: string };
function applySuggestion(text: string, item: Suggestion): string | null {
if (text.slice(item.start, item.end) !== item.expected) return null;
return text.slice(0, item.start) + item.replacement + text.slice(item.end);
}
源码还有一个更直接的定位问题:宿主替换函数只接收错误文本与建议文本,并在文档里找到第一个匹配片段后就替换,没有使用检测结果携带的位置。文中同一个错字出现两次时,点击第二条可能仍修改第一次出现的位置。修复方向不是再做一次全局 replace,而是让“检测范围、预览高亮、光标定位、提交替换”共享同一个位置契约,并对编辑后的旧位置失效采取拒绝而非猜测。
编辑事件要有生命周期和最新结果保护
编辑器会在快速输入、粘贴、撤销和自动格式化时连续发出变更。若每次都重新读取整篇文档并启动不可取消的推理,旧检查可能晚于新检查完成,把过期候选写回界面。应给快照分配递增版本或 AbortController;只提交仍对应最新版本的结果,并对长文节流、分段和可取消。
样本的重试路径会注册宿主编辑监听和 UI 事件监听;订阅没有统一的返回清理句柄或幂等保护。成功一次后若发生部分初始化错误再重试,可能留下重复监听。实际产品应集中管理订阅,保证初始化幂等,并在关闭侧栏、切换文档和卸载时注销;没有 unsubscribe API 时,要用活动标记阻止迟到回调再更新 UI,同时如实说明监听仍驻留在宿主中。
可以直接从源码得到的回归用例
- 同一错字在文档中出现两次,点击第二个候选只改第二处;编辑快照后旧候选必须失效。
- 首字符、末字符、句子分段边界、emoji、组合字符、全角标点和连续换行都能正确定位。
[CLS]、[SEP]、未知词、特殊 token 和多 token 词不会误映射成用户可见字符。- 快速连续输入与撤销期间只展示最新一次检测;超长文档可以取消,旧结果不能覆盖新状态。
- 规则词典、模型候选和重叠短语按确定策略合并;置信阈值以标注集报告误报率,而非只看演示文本。
- 模型文件缺失、CDN 被拦截、浏览器离线、设备内存不足和量化回退都给出可理解且可恢复的状态。
- 替换前后可以撤销;日志不记录整篇正文,网络检查确认文档文本不会意外发往分析或模型服务。
为什么这篇没有伪造在线 Demo
插件快照依赖特定文档宿主 API,不能直接作为普通博客页面运行;把宿主适配器替换成几个按钮并不会得到相同功能。完整模型资产也超过 100 MB,直接塞进公开页面会造成明显的首访网络和移动内存成本。博客若要提供真正可用的独立演示,需要重新实现可授权的文档编辑器适配、位置映射、隐私说明、模型分片/缓存、取消和手机基准测试;在这些条件未完成前,文章保留架构和验证方法,不假装有完整体验。
参考资料
- MacBERT 中文纠错模型卡:模型用途、配置、权重与当前许可元数据;部署前仍需核对具体文件和上游版本。
- Transformers.js 自定义模型使用:本地模型路径与远程模型加载配置。
- Transformers.js Pipeline 文档:任务、模型加载和量化相关选项。
- MacBERT 论文:模型方法背景;论文阅读不能替代模型卡对具体权重版本的说明。
本复盘只对这份本地代码与模型文件做静态审阅,没有运行原宿主插件,也没有把模型权重上传到博客。代码位置映射与监听清理是上线前应优先修复和验证的工程问题。
Live Notes
留言提交后会先进入审核,审核通过后对其他读者公开。