1. 项目起源为什么我最终动手重构了CNSH的纠错内核先说清楚CNSH中文编辑器是什么。它是一个面向中文写作场景的开源编辑器项目主打本地优先、纯文本工作流没有云端同步、没有账号体系所有内容都以Markdown和纯文本的形式存放在本地目录里。我参与这个项目的前期维护时最大的痛点不是编辑器本身而是它的纠错能力——早期版本只能识别最基础的错别字遇到“登录”和“登陆”、“截止”和“截至”这种同音近义词就完全无能为力更别提标点误用、量词搭配、语序颠倒这类深层问题。很多用户反馈说用CNSH写长文时纠错模块基本是个摆设。所以从去年开始我着手对CNSH的纠错模块做一次彻底重构目标很明确做一套“完整纠错规则库”覆盖中文写作中最常见的高频错误。到了今年这套规则库迭代到了v2.0版本也就是这个项目的标题“CNSH中文编辑器·完整纠错规则库 v2.0”。它解决的问题非常具体怎么让一个纯本地、无网络依赖的编辑器具备接近专业校对软件的纠错能力同时保证误报率在可接受的范围内。这篇博文适合谁看一类是自己维护编辑器或写作工具、想在本地嵌入纠错能力的开发者另一类是对自然语言处理感兴趣、想了解中文纠错规则怎么落地到实际产品的读者。我会把这套规则库的设计思路、分类体系、匹配策略和踩坑经验全部摊开来讲清楚。整个v2.0版本我一共沉淀了400多条规则分成四大类字词级错误、标点符号错误、语法搭配错误、语义一致性错误。每条规则都包含触发条件、匹配模式、置信度、建议替换文本和适用场景说明。接下来我会逐一拆解这些内容。2. 规则库的整体设计思路与版本差异2.1 先做减法再做加法v2.0的核心设计原则在v1.0阶段我的做法是“能加多少规则就加多少”结果规则库膨胀到700多条看起来覆盖率很高实际用起来误报率惊人。比如“的、地、得”的区分规则我当时一股脑加了三十多种模板结果用户写“慢慢地走”时系统提示“慢地”疑似误用建议改成“慢的”完全说反了——因为“地”用在动词前做状语本来就是对的我的规则反而把正确用法当成了错误。这就是v2.0重构时我坚持的第一条原则每条规则必须有明确的触发上下文不能做成词级别的简单映射。错误纠正必须放在语境里判断否则就是在帮倒忙。第二原则是尽量降低规则之间的冲突。v1.0里很多规则来自不同渠道有的是网上爬的有的是从别家工具逆向出来的经常出现两条规则对同一段文字给出完全相反的判断。v2.0里我给每条规则加了“优先级”和“互斥组”两个字段当多条规则同时命中时由引擎根据优先级和上下文特征做仲裁。2.2 v2.0相比v1.0具体改了些什么从能力边界来看v1.0只能处理“字”级别的错误比如“针炙”提示“针灸”“再接再励”提示“再接再厉”。但v2.0把能力扩展到了“词”级别和“句”级别。举几个具体例子量词搭配能识别“一只马”“一件衣服穿在脚上”这种量词和名词不匹配的问题语序错误能识别“我吃饭了已经”这种状语后置的非常规表达标点全半角混用能识别中文字符中间出现英文逗号、英文句号等不规范用法语义一致性能识别“他在阳台上晒太阳温度计显示零下三十度”这种上下文明显的逻辑矛盾2.3 规则库的架构分层我把规则库拆成三层结构第一层是“候选召回层”。这一层只负责从文本中快速找到可能出错的片段要求是快、全宁可多召回也不能漏掉。实现上用了Trie前缀树和Aho-Corasick自动机做多模式匹配扫描一篇5000字的文章只需要几毫秒。第二层是“特征过滤层”。候选片段进入这一层后会结合周围的词性、句法特征做判断。比如连通词“的”后面接动词时大概率是状语结构用了“地”字这里就不能判错。特征过滤层主要靠规则里预先定义好的上下文模板来完成。第三层是“置信度评分层”。每条规则都有一个基础置信度特征过滤后还会根据上下文对置信度进行加减分。只有当最终得分超过阈值时系统才会提示用户。v2.0里把阈值从0.6提高到了0.75虽然提示条数减少了大约30%但有效纠正率反而提升了将近一倍——因为剩下的提示基本都能切中要害。3. 核心规则分类与详细拆解3.1 字词级纠错规则最常见也是最难做的一类字词级错误是最常见的中文写作问题但处理起来并不简单。我把这类规则细分成四种子类型。第一种是形近字错误比如“戊、戍、戌”三个字混用“已、己、巳”分不清“尴尬”写成“尶尬”。这类错误的特点是两个字长得像输入法或手写时容易搞混。规则设计上不能简单做映射表因为正确用法往往依赖词语搭配。比如“戍边”是驻守边疆“戌时”是晚上七点到九点同一个字在不同词语里的正确写法不同。所以我的做法是建立“词语-正确字形”对照表以词为单位做检查而不是单字映射。第二种是同音字错误包括“在”和“再”、“做”和“作”、“度”和“渡”这类高频混淆。我积累了一个约180组的同音混淆词表每组都带有例句和判断逻辑。比如“度”和“渡”核心区分逻辑是“渡”必须和水域相关且有“由此岸到彼岸”的语义所以“欢度春节”用“度”“渡过难关”这个比喻性用法虽然不涉及具体水域但因为“难关”带有“彼岸”隐喻也习惯上用“渡”。规则里我把这些例外情况单独列出来。第三种是繁体字或异体字误用。很多用户输入法切来切去会出现“發”和“髮”不分、“裡”和“裏”混用的情况。v2.0里我建了一个简繁对照表结合上下文判断应该用哪个字形。第四种是错别字中的“通假”陷阱。有些读者会把“蚍蜉撼树”写成“毗蜉撼树”但“蚍蜉”本来就是大蚂蚁的意思写成“毗蜉”就是错字。这类规则是纯粹的词典对照没有太多技术含量但要收集得足够全、足够细。3.2 标点符号规则从全角半角到中英文混排标点是中文文本里容易被忽略但影响很大的环节。v2.0里我做了五个维度的标点检查。全半角混用是重灾区尤其是中文字符中间夹着英文逗号、英文句号、英文括号的情况。比如“你好,世界”中间那个逗号就是半角规范写法应该是全角“”号。检测方法很简单如果中文字符前后紧邻着半角标点就触发提示。第二个维度是成对标点的配对检查。左右引号、左右括号必须成对出现且顺序不能颠倒。这个规则实现不复杂但要注意嵌套和引号内再引号的情况。我在写规则时用了一个栈结构来模拟配对过程比单纯的正则匹配可靠得多。第三个维度是标点使用位置检查。比如顿号“、”只能用在并列词项之间不能放在句末省略号应该是六连点“……”而不是三个点“...”破折号是“——”而不是连字符“-”连用。这些细节读者未必说得清楚但一眼看到就觉得“不专业”。第四个维度是中英文之间空格的处理。在中文排版规范里中文和英文之间通常要加一个空格但也不是所有场合都需要。规则里我要做的是判断“中文-英文-中文”这个结构是否有合理的分隔太紧了提示加空格太松了多个连续空格提示合并。第五个维度是标点与语气词的配合。比如“”和“”不能连用成“”规范写法是“”还是“”目前没有绝对统一但同一篇文章里应该保持一致。我把它做成了一致性检测规则不强制规定用哪种写法但要求全文统一。3.3 语法搭配规则量词、助词、介词的高频错误这一块是v2.0新增的重头戏。我重点处理了三类高频语法错误。量词使用错误。中文量词极其丰富“一头牛”“一匹马”“一只鸡”“一条鱼”各有各的搭配。外国学习者容易错其实母语者也经常搞混比如“一幅画”写成“一副画”的人并不少见。我建了一个“量词-名词”搭配表收录了300多组常见搭配。匹配时先找出数词和量词组合再检查后面紧跟的名词是否在搭配表里。助词“的、地、得”的使用错误。这个真的是老大难我单独为它写了三十多条细分规则。基本判断逻辑是定语标记用“的”状语标记用“地”补语标记用“得”。“开心地笑着”里“地”后面接动词“笑着”是补足动作方式用“地”正确。但“笑得很开心”里“得”后面接的程度补语“很开心”要说明程度用“得”正确。v2.0里我引入了基于词性的判断规则先判断标记的后置词的词性类别再决定是提示还是不提示。准确率大概在85%剩下的15%确实需要看具体语境。介词使用错误。比如“对于”和“对”的区别“关于”和“对于”的差异以及“被”字句、“把”字句的规范性。这些在正式写作中很容易出错。规则设计上我先识别出介词结构再检查介词和后面动词、名词的搭配关系是否在规则库中登记为错误或可疑。3.4 语义一致性规则浅层逻辑矛盾的自动识别这是v2.0的一个亮点也是目前误报率相对高的一类规则。所谓“语义一致性”本质上是检测文本内部的逻辑矛盾比如“外面下着倾盆大雨但地面干燥如初”“他两岁的时候已经大学毕业了”。这类错误对现在的NLP大模型来说并不难识别但CNSH是纯本地工具不能调用在线API所以我只能用规则库加轻量统计的方式来实现。我的方案分三步。第一步抽取数字类信息包括年龄、时间、尺寸、温度、价格等统一转换成数值形式。第二步检测同一实体在不同位置的数值描述是否一致。比如前面说“小明今年20岁”后面又说“小明在大学教了25年书”系统就会提示“年龄与教龄不符”。第三步检测常用的反向语义词对比如“下雨”和“干燥”、“上涨”和“下跌”等如果出现在同一个因果句里就提示可能的逻辑不一致。这一块对规则库的设计要求很高。我一开始只是简单地把矛盾词对压进表格里硬匹配结果误报满天飞。后来加入了“因果关系标记词”的判断比如“因为...所以...”“既然...就...”“虽然...但是...”只有在这些连接词出现时才对矛盾词做检查误报率才降到了可接受的范围。4. 规则引擎的实现细节与性能调优4.1 规则匹配的核心流程规则库只是“配方”真正让配方生效的是匹配引擎。CNSH的纠错引擎工作流程可以分成五个阶段。第一阶段是文本预处理。原始文本经过分词、词性标注、句子边界识别输出结构化数据。分词用的是jieba的C版本封装词性标注也是标准的北大词性标记集。预处理阶段还会把全角字符统一转成半角存储方便后续的规则匹配做正则处理。第二阶段是候选召回。把处理后的文本按字切分后送入Aho-Corasick自动机做多模式匹配。这一阶段只负责快速找出所有可能命中的规则ID不做过多的逻辑判断。Aho-Corasick的好处是一次扫描就能找出所有模式串的匹配结果时间复杂度是线性的非常适合做实时性要求高的编辑器场景。第三阶段是规则后续的上下文校验。每个匹配命中的规则内部定义了若干谓词检查比如“当前词左边第二个词是什么”“当前词在句子中的位置是否处于开头”“这个判断前面是否出现了否定词”等。引擎执行这些谓词如果任何一条不满足就放弃这条命中。第四阶段是置信度计算。基础置信度是规则定义时写死的根据历史误报率做了加权修正。上下文条件满足得越多置信度越高。比如“的、地、得”规则如果识别出“地”后面是动词短语且前面是副词置信度会从基础的0.7升到0.85可以直接提示用户。但如果前后是“开心”这类兼类词置信度就降到0.55不满足阈值不会提示。第五阶段是输出与展示。引擎把所有得分超过阈值的纠错建议输出给编辑器前端。前端把错误片段用红色波浪线标注并弹出建议修改文本。用户可以选择一键替换、忽略或加入个人词典。4.2 性能优化如何让纠错不拖慢打字编辑器场景对性能非常敏感用户边打字边出波浪线如果延迟超过200毫秒就明显影响体验了。v2.0里我做了几个关键优化。第一把规则库编译成二进制格式加载。不要每次启动都去解析JSON那样加载一个几MB的规则文件要花掉一两秒。我写了一个编译脚本在安装阶段把JSON规则库转换成紧凑的二进制格式启动时直接用mmap映射进内存冷启动加载耗时压缩到50毫秒以内。第二把规则按触发频率分类分桶存储。高频触发的规则放在L1缓存桶低频触发的放入L2桶。每次扫描先只查L1桶命中后再去L2桶扩充检查。这样正常写作场景下大部分规则检查都在L1桶内完成性能开销很小。第三增量检查。不要一篇文章每次都全文扫描。我实现的方案是仅对用户最近修改的句子做纠错其他句子保持上一次的结果缓存。怎么判断用户改了哪几个字符通过编辑器的文本变更回调把变更区间传进来然后对包含变更区间的句子重新做纠错就行了。这样每秒能支持五六次连续输入波浪线完全跟上用户打字节奏。第四并发优化。虽然规则匹配本身是纯CPU计算但C和嵌入的规则引擎之间跨语言调用开销不小。我在引擎层做了一次批量接口把一次文本扫描中涉及的规则调用全部打包成一次跨语言请求大幅减少了调用次数。4.3 规则覆盖率的验证方法规则库写完了怎么验证效果我建了一套纠错评测集包含1200个真实写作中收集的错误句子和修改后文本。每次改完规则库我都会在这套评测集上跑一遍计算两个核心指标准确率提示中真正是错误的比例和召回率真实错误中被正确提示出来的比例。v2.0在评测集上的表现是准确率91.2%召回率74.6%。召回率不高是正常的因为还有很多错误语义复杂单靠规则库覆盖不了。我给自己定的目标是优先保证准确率因为对编辑器用户来说频繁出现错误的纠错提示比不提示更让人崩溃。宁可少提示一些提示出来的结果要尽量准。评测集本身也是规则库的一部分我放在项目仓库的tests目录下每条评测数据都有原始句子、修正后句子和对应的规则ID。提交新规则时必须附带一条新的评测用例不然我不会合入主分支。5. 规则库的编写规范与实操指南5.1 一条完整规则长什么样我以一条具体的规则为例展示规则库的实际格式。规则采用YAML编写每条规则由元信息、触发条件、上下文条件、建议动作和优先级这几个部分组成。- id: SS_QM_001 category: punctuation name: 中文语境下的半角逗号 trigger: type: regex pattern: [\u4e00-\u9fff],[a-z\u4e00-\u9fff] context: - check: not_in_code_block - check: previous_char_is_cjk suggestion: confidence: 0.85 priority: 80 examples: - input: 今天天气不错,我们去公园吧 output: 今天天气不错我们去公园吧字段说明id规则唯一编号。前缀表示大类例如SS_表示标点类ZW_表示字词类YF_表示语法类YY_表示语义类。trigger规则触发条件。支持regex、literal、n_gram等多种类型。正则表达式要写得尽量精准避免大范围的.*匹配。context上下文校验条件支持条件组合。引擎按顺序执行任何一条失败则规则不触发。suggestion建议的修改文本。confidence基础置信度0到1实际使用中会动态调整。priority优先级数值。当多条规则同时命中时高优先级优先。examples测试用例。加入规则时必须附带。我建议所有规则都写成这样结构化的格式不要用几十行代码去写死。结构化有个好处规则和引擎解耦其他人不需要理解引擎代码也能看懂规则含义甚至能直接写新规则。5.2 怎么评估一条规则是否合格每条新规则上线前我会跑三个检查。第一是正例检查。找出所有应该触发的情况确认规则都能触发。比如新写了一条例句错误规则就拿20个包含该错误的例句测试。任何一个没有触发就要回头检查。第二是负例检查。找出所有不应该触发的情况确认规则不会误报。还用例句错误规则举例我会拿20个正确使用该字词的句子确认规则不报错。第三是边界场景检查。主要测试否定句、条件句、反问句等特殊句式下规则的稳健性。很多规则在陈述句中工作良好一遇到反问句就崩溃。实际操作中我用一个简单的回归脚本批量执行所有检查一次跑完输出报告对比上一次的结果。谁要是动了某个基础规则这个脚本一跑就知道影响范围。5.3 规则的优先级冲突处理两条规则同时命中同一位置怎么处理在v2.0中我设计了两个机制。第一个是优先级。规则里有priority字段数字越大优先级越高。引擎先执行高优先级规则如果高优先级规则给出明确建议就忽略低优先级规则的结果。第二个是互斥组。如果两条规则从语义上就不可能同时正确就把它们放进同一个互斥组里。例如“的”用法的某个上下文模板和“地”用法的某个上下文模板在某些边界文本中可能同时命中但显然不能同时给出两个相反的建议。我在引擎里实现了互斥组的仲裁逻辑同一互斥组中取置信度最高的一条作为最终输出。如果想避免这个冲突还有一种办法是调整规则里的上下文条件。比如把“的”规则里的某个触发条件写得更加严格使它与“地”规则的触发条件在边界情况下不会重合。但这个做法容易防了今天漏了明天不如直接写明互斥关系。5.4 规则库的版本管理与发布v2.0的规则库是跟随CNSH编辑器主项目一起发布的但作为独立子模块维护。我用的是Git子模块方式CNSH主仓库引用规则库仓库的某个版本标签。每次发布时规则库仓库打一个tag比如v2.0.0、v2.0.1并且记录一份CHANGELOG。规则库的变更是迭代式的我强烈建议不要攒几个月才发布一个大版本。每次合入新规则前先做完整回归测试确认不会引入新误报然后直接发一个小版本。CNSH的用户只要更新编辑器就能自动拉到最新规则库不需要单独下载。从v2.0开始我还在规则库里增加了一个metadata文件记录当前版本号、生效日期、规则总数、各类别分布情况。编辑器启动时读取这个文件在“关于”页面展示当前规则库版本信息方便用户在反馈时附上版本号。6. 常见问题与排查技巧实录6.1 规则不生效从头排查的五步流程开发过程中最常遇到的问题是“我写了条规则为什么就是不触发”。排查时我按下面五步走基本能定位问题。第一步确认规则文件被正确编译。v2.0里规则是编译成二进制后加载的如果你改了YAML但忘了重新编译那跑的仍然是旧规则。排查命令是看启动日志里加载的规则版本号。第二步确认规则确实被加载。在引擎里加一行调试日志输出加载的规则ID和总数。如果总数和你本地数量对不上说明编译或打包环节丢了文件。第三步确认触发条件的正则表达式写得正确。很多建议在正则测试网站上是对的放到引擎里就不行。原因可能是引擎用了RE2语法不支持反向断言等特性。我在项目里特意写了正则语法兼容性说明排查时先对照这个说明检查。第四步确认上下文条件是否太严格。有时候规则本身没问题但上下文条件一个都不满足。比如要求“左边第二个词是动词”实际文本里左边第二个词是副词就永远触发不了。我写了一个调试模式能输出每条上下文条件的真实执行结果一步到位定位问题。第五步确认置信度是否达到阈值。如果规则触发但得分只有0.7而阈值是0.75那也不会弹提示。这在“的、地、得”规则里最常见因为这类规则基础置信度比较低。6.2 高误报率的根因分析如果说规则不触发让人烦躁那高误报率会直接导致用户关闭整个纠错功能更严重。我自己踩过的坑主要有三类。第一类是规则触发条件写得太宽。一开始我在识别“在”和“再”错误的时候正则写成了在(?!意|于|场|线)这种排除格式。意思是“前面有‘在’但后面不是‘意、于、场、线’时触发”。结果“现在”“所在”“内在”这些常用词全部被误判。后来我做了系统的白名单把所有包含“在”且不可能是“再”的常用词全部列出来才把误报率压下来。第二类是规则之间互相打架。还是“的、地、得”那组老问题“开心地笑”和“开心得笑起来”很容易被不同规则同时命中。我在互斥组里解决了这个问题。第三类是没考虑否定与肯否叠加。“他不是不会来”这句话在语义上是“他会来”但简单的规则会把“不是”和“不会”拆开理解导致完全反向的建议。v2.0里我引入了一个否定词检查层凡是涉及语义判断的规则必须先经过这个检查层统计句子中有几个否定词偶数否定等于肯定。6.3 性能卡顿的定位手段性能问题在长文档场景下特别明显。你写了一两万字每次键盘输入都触发全文扫描必然卡。我的经验是先用profile工具定位瓶颈不要瞎优化。在CNSH项目里我用的是Perf和FlameGraph来生成CPU火焰图。跑一篇文章的纠错流程然后查看火焰图如果看到大部分时间消耗在规则的正则匹配上就说明模式串太长或者有灾难性回溯。如果消耗在Aho-Corasick构建或扫描那就要检查模式串数量是否过多、匹配逻辑是否低效。还有一个容易被忽略的瓶颈是分词。分词库虽然被优化过但在超长文本上反复分词还是有开销。我的优化方案是只对变化句子的前后各一个句子的范围做分词不整篇重复。改完后实测性能提升了三倍多。6.4 用户反馈驱动的规则库演进规则库迭代不是闭门造车真实用户反馈是最有价值的素材。我在CNSH里加了一个“反馈纠错”按钮用户点击某个波浪线提示时可以选择“正确”“错误”“不确定”三个选项数据自动写入本地日志。我在收集统计时只做匿名分析不涉及任何用户内容上传或网络传输。这些反馈数据给我带来了很多意外收获。比如“度”和“渡”的规则原本我以为判断逻辑已经够细了后来发现“度假”“渡口”这些词因为在语料中高频出现混着用也能被理解所以它们不应该作为“错误”报告而应该作为“存疑”级别的提示。我把规则库增加了“提示级别”维度错误、存疑、风格建议三级。用户看到“存疑”级别时可以按一下忽略也不会有太大的打扰感。7. 我对CNSH纠错规则库v2.0的几点总结性思考项目做到v2.0我最大的体会是中文纠错规则库本质上是把一个巨大的“中文使用经验集”显式编码成机器可判断的规则它的上限不在规则引擎多快、多准而在于编写规则的人对中文本身的理解有多深。v2.0的规则库目前有400多条规则覆盖了字词、标点、语法和浅层语义四个维度。和商用产品相比它肯定还有不少差距——商用工具是集成了大模型和千万级语料训练出来的纯规则方案在理解力和泛化性上确实不够看。但规则库也有它不可替代的优势完全没有网络依赖数据不会离开本地用户可以自己审查每一条规则并修改拥有完全的可解释性。对于想自己动手搭建纠错能力的朋友我建议从自己写作时最容易犯的错误入手先建一个几十条规则的小库跑通流程再逐步扩充。不要一上来就想做一个大而全的系统那样很容易在误报和漏报的泥潭里挣扎。规则库的核心价值不在数量在于每条规则都是经过真实场景验证过的“确定知识”。这个项目后续我还会继续推进重点方向有两个一是增加对口语化文本和网络用语的处理能力——这部分规则的确定性低但年轻人写作时经常遇到二是尝试引入一个本地小模型作为规则库的补充判定层用来处理那些拿不准的“存疑”级别提示。如果你也对这个方向感兴趣欢迎多交流这条路确实还有很大的提升空间。