本地 AI 错别字审阅器:MacBERT、词典与文档编辑器插件
本文目录
中文校对工具常被误解成“把文章发给 AI,拿回一份改好的全文”。一个可控的编辑器插件更像审阅流程:尽量在本地分析文本,只标出疑似问题,由作者逐处确认,再把修改准确落回原文。这里以一个真实的 React/TypeScript 插件实现为样本,说明它的架构、能力边界和可以继续打磨的地方。文章讨论的是该实现的代码审查结果,不代表所有浏览器、文档产品或模型版本都具有相同行为。
两类检查:上下文模型与确定性词典
插件把检查拆成两条互补路径。上下文模型读取一小段文字,判断某个字或词是否可能应被另一个候选替换;手工维护的规则词典则处理已知固定搭配、易错词和项目约定。模型擅长发现上下文相关的可疑字符,词典则更适合表达明确、可解释的规则。
两条路径的结果需要统一成“原文位置、错误片段、建议片段、来源和解释”,再按位置合并、去重。不要把词典命中伪装成模型结论,也不要因为模型没有提出问题就声称全文绝对无误。拼写、语义事实、专有名词、数字和引用来源是不同类型的审阅任务,最好分开呈现。
浏览器内推理不等于完全离线
样本使用 Transformers.js 与 ONNX 模型,在用户设备上执行推理;待审文本不必上传到一个远端生成式模型。这对隐私和成本很有吸引力。不过,当前插件通过 CDN 动态加载推理运行库,再从插件自己的资源目录读取模型。因此,“推理在本地”是准确描述,“首次打开无需网络”则不准确:运行库、插件页面和模型资源都必须已经可取得,浏览器缓存也不等于可保证的离线安装。
模型文件会直接影响等待时间、内存和下载流量。样本同时包含 FP32 与量化模型,但当前加载路径选择 FP32,实际主模型约 452 MB;目录里的约 114 MB 量化文件并不会因为存在就自动被使用。切换量化权重前,需要在目标浏览器和硬件上评估准确率、加载速度、峰值内存和结果一致性。不要只凭文件更小就断言体验更好。
要做到真正可离线使用,需要把运行库、WASM/执行后端、模型配置、分词器和权重一起纳入可缓存的版本化资源,并设计 Service Worker 缓存更新、存储配额、清理和失败恢复。第一次下载应显示大小和进度;离线状态要能说明缺少哪项资源,而不是无限显示“加载中”。
分段推理和编辑器桥接
长文不能不加限制地一次性送入模型。样本按标点切成较短片段,最长约 128 个 JavaScript 字符单位,再交给分词器;模型侧也有最大 token 长度。这里的“字符单位”不等于用户看到的字数,中文扩展字符、组合字符和 emoji 都可能占多个 UTF-16 code unit。生产实现应统一使用编辑器定义的位置单位,并明确模型截断或跳过片段时的行为。
编辑器一般通过宿主桥接能力读取文档、遍历段落、展示侧栏和执行变更。桥接层应只传递完成当前任务所需的文本和位置;避免把整份含敏感内容的文档写入普通日志。长任务需要取消、进度、错误提示和文档版本检查,否则用户在等待期间继续编辑,旧结果就可能指向已经变化的内容。
代码审查看到的真实边界
对这个样本的实现检查发现了几项值得明确标出的限制:
- 当前“修正一处”把错误文本和建议文本交给服务层,再遍历文档寻找匹配文本,并不使用侧栏结果携带的精确位置。如果同一错词出现多次,点击后面的建议有机会改到前面第一次出现的位置。
- 服务层把文档结构展平成普通字符串后再去除部分 HTML 标签和实体。表格、换行、链接文字、特殊实体等结构可能让纯文本偏移与编辑器位置不再一一对应。
- “全部修正”连续修改后,后续建议若仍使用旧位置,前面文本长度变化就可能导致偏移。应逐项从后向前编辑,或每次基于当前文档版本重新定位并验证。
- 模型候选的原始 logit 差值阈值是工程启发式,不是概率,也不是经过校准的“正确率”。界面不应把它显示成百分比置信度。
- 某些片段发生异常时,当前处理会跳过该片段;用户需要知道检查是否部分失败,不能把“没有建议”误当成“检查成功且无问题”。
这些限制不抹去插件的价值,但意味着“逐条审阅”应当比“一键全部替换”更值得信任。对于可能改动事实、代码或专有名词的内容,保留作者确认尤其重要。
精确修正:范围、原文和版本必须一起校验
稳妥的建议对象至少携带文档版本、编辑器范围、原始片段和建议片段。执行前重新读取范围内容:只有当版本仍一致且范围文本与审阅时完全相同,才应用变更;否则提示作者重新检查。这段是适配具体编辑器事务 API 的设计示意,并非样本插件的现有代码:
type Suggestion = {
revision: string;
from: number;
to: number;
expectedText: string;
replacement: string;
};
function applySuggestion(editor: Editor, item: Suggestion) {
if (editor.revision !== item.revision) {
return { applied: false, reason: "document-changed" };
}
const actual = editor.getText(item.from, item.to);
if (actual !== item.expectedText) {
return { applied: false, reason: "range-mismatch" };
}
editor.replaceRange(item.from, item.to, item.replacement);
return { applied: true };
}
如果宿主没有稳定的版本号,可以记录文档内容哈希或局部上下文,但上下文匹配也可能有歧义,必须在确认唯一命中后才允许修改。批量应用时应以事务方式组织,按位置从后往前修改,并在结束后重新计算剩余建议。每个修改都应可撤销。
规则词典也需要治理
词典看似简单,却很容易从“准确补充”变成“到处误报”。每条规则最好带有稳定 ID、错误形式、建议形式、适用上下文、说明、来源和更新时间;规则变更要能审查与回滚。中文里同一个词可能在不同领域有不同含义,不能只凭子串相等就自动替换。专有名词、产品名、代码标识符和用户自定义词汇尤其需要白名单或“忽略本次/加入个人词典”的入口。
模型与词典命中重叠时,界面可以合并成一条建议并标出两个依据;如果两者建议冲突,应展示冲突而不是任意挑一个。评估时也要分别统计词典规则的精确率、模型建议的误报率、作者采纳率和撤销率。采纳率只是产品信号,受文档类型和用户习惯影响,不能直接替代语言质量评估。词典不应把用户正文静默上传,也不应默认把私人文档中的新词同步给其他人。
让模型响应不阻塞编辑体验
几十万参数或更大的模型初始化与推理可能占用主线程。可评估 Web Worker、ONNX Runtime 的代理 worker 或 WebGPU 后端,但要先确认编辑器宿主的 CSP、Worker、WASM、SharedArrayBuffer 和设备能力。WebGPU 的可用性依浏览器和硬件而异,必须有可工作的 WASM 回退;失败后要让用户继续编辑,而不是把插件卡死。
交互上可采用“先显示词典结果,再渐进补充模型结果”的方式,同时给出取消按钮、当前段落进度、模型加载状态和部分失败提示。长文按段落并发时要设置并发上限,避免多个推理任务同时复制模型输入、抢占内存。完成后应把建议稳定排序,并允许按错误类型筛选、忽略单项或加入个人词典。
测试要覆盖中文文本和文档变更
- 同一错误词在多个段落重复出现,分别点击后只修改对应范围。
- 审阅后用户插入、删除文本,再尝试旧建议时应拒绝并要求重审。
- 覆盖中文标点、换行、表格单元格、链接、HTML 实体、emoji 和组合字符的位置映射。
- 模拟模型文件缺失、CDN 不可达、WASM 初始化失败、浏览器内存不足和任务取消。
- 比较 FP32 与量化模型在代表性中文样本上的建议差异、加载时间和峰值内存。
- 确认日志、遥测、崩溃报告和网络面板没有意外上传正文或敏感片段。
- 文档内容为空、只有代码、含专有名词或规则词典误报时,允许作者忽略而不是强制改写。
模型与许可
样本采用中文 MacBERT 拼写纠错模型。使用或分发模型权重前,应逐项核对模型卡、权重来源、上游许可和目标发布方式,并在产品中保留必要的来源说明。模型许可与运行库许可是两个独立事项;即使许可证允许某种使用方式,也仍要评估权重体积、隐私说明、第三方资源供应链和用户设备负担。本文只链接上游模型卡,不复制模型权重。
小结
本地模型加规则词典,为文档提供了一条不依赖云端生成服务的校对路径;真正决定体验是否可靠的,往往不是模型名字,而是位置映射、文档版本、失败可见性和修改可撤销。审阅结果应是作者的候选建议,不是自动改写全文的授权。
官方资料
Live Notes
留言提交后会先进入审核,审核通过后对其他读者公开。