首页 / 资讯中心 / 文章详情

构建文档校审工具:规则引擎与语言模型的实战经验

构建文档校审工具:规则引擎与语言模型的实战经验 ★ FEATURED ARTICLE
写代码的人校文档这事儿听起来有点跨界但只要你写过几百页技术文档或者替别人排过稿子就会明白文档校审早就不只是编辑的体力活了。两年前我做了个叫 AiCheck 的小工具专门处理文档里的错别字、标点、格式、术语一致性和语义别扭的问题用规则引擎加上语言模型一起管。结果它在我手里断断续续维护了两年直到最近停在了“暂不更新”这个状态。今天写这篇文章不光是交代 AiCheck 为什么停更更重要的是把这两年里踩过的坑、调过的参数、反复纠结过的架构选择一次讲清楚。如果你打算自己做一个文档校审工具或者正在用类似工具下面这些内容应该能帮你省下不少试错的时间。1. 项目整体设计和拆解思路1.1 文档校审到底在审什么很多人以为校审就是“挑错别字”实际操作过就会知道一份几十页的文档放在面前问题从来不是一个维度而是被拆成几类完全不同性质的东西。第一类是硬错误包括错字、别字、重复字、漏字。比如“配置”写成“配署”“部署”写成“布署”这类错误靠人眼扫非常容易漏但用规则扫几乎是秒杀。第二类是格式规范问题。全角空格、半角标点、中文和英文之间有没有空格、标题编号是不是连续这些虽然不影响理解但在正式交付的文档里属于很掉档次的问题。格式问题靠人工检查最痛苦因为几十页文档里几千个字符肉眼去数全角半角看到最后眼睛都是花的。第三类是术语一致性问题。同一份方案里一会儿叫“数据源”一会儿叫“数据来源”有的地方写“客户”有的地方写“甲方”。这种前后不一致的问题规则层可以做基础匹配但要覆盖所有变体就得用模糊匹配或者轻量模型。第四类是语义层问题。句子语序不当、成分残缺、逻辑前后矛盾这部分规则完全无能为力必须交给语言模型处理。但也正因为是模型误报率是最高的后面要花大量精力去控制。我当时把这些问题分成两个大桶规则桶和模型桶。规则桶只处理“有一就是一”的确定性问题模型桶处理“靠感觉判断”的开放性问题。这个分法在后来维护中起到了决定性作用因为它让每次改动的风险边界变得非常清晰改规则不会影响模型调模型也不会牵扯规则。1.2 为什么先上规则引擎再上模型做这个项目之前我犹豫过一个问题要不要直接用一个现成的大模型去评判文档质量把整篇文档丢进去让它把有问题的地方标出来听起来一步到位。但我很快就打消了这个念头。原因有三点。第一是稳定性和可解释性。规则引擎对同样的文本每次输出的结果完全一样用户可以预期到什么会被报、什么不会被报。模型则不同同一个句子换个说法输出可能就变了用户没法建立“报错直觉”。对于工具型产品确定性和透明感非常重要。第二是速度。规则层跑一篇几百页的文档耗时几乎可以忽略不计。模型推理则要占用大量计算资源尤其是中文长文本分句、拼接、推理一篇下来可能几十秒甚至更久。第三是定位。我真正想要的是“可执行的报告”不是“大模型的一句话总结”。用户需要知道问题在第几页、什么错、怎么改。规则引擎天然适合输出结构化结果模型更适合做候选筛选而不是最终裁决。所以最后定的架构是规则层先扫扫完把可疑句段送给模型层做二次判断。模型层只处理那些规则确实拿不准的句子。这样做下来模型请求量少了至少一个数量级成本可控精度也没降低太多。1.3 明确不做的事比做哪些功能更重要这个项目早期最大的问题是范围失控。有同事问我能不能顺便检查图片有没有缺失能不能检查文档版本号是否正确甚至有人希望它直接帮忙润色所有行文风格。我通通没做。现在回头看这个坚持太重要了。文档校审工具一旦开始做润色、做排版、做图表检查实际上就变成了一个完整的排版系统涉及图片、样式、模板、输出格式等等一连串问题根本不是一个能靠业余时间维护的项目。AiCheck 最终只聚焦在“文本层面的可执行检查”范围定得窄才能在两年的业余维护周期里撑住。2. 核心模块拆解与实现要点2.1 文档解析层先分清电子文档和扫描件所有校审都建立在“能拿到可检索文本”的基础上。这一步看着简单实际坑最多。如果你是处理 docx 格式需要按段落、表格、页眉页脚分别抽取文本同时保留每个文本块在文档里的位置信息。位置信息非常重要它决定了后面报告能不能做到“点击跳转到原文”。我用的是一个专门处理 docx 的 Python 解析库遍历文档对象把段落块和表格单元格剥离出来统一输出成带序号的文本块。PDF 的情况比较特殊需要区分数字版 PDF 和扫描版 PDF。数字版 PDF 可以直接抽取文本层扫描版则只有图像必须走 OCR。当时我加了一重判断逻辑先抽文本如果发现整个文档抽出来的文字少于一个阈值比如一页平均不足 20 个字符就判定它可能是个扫描件自动转到 OCR 流程。这里有一个我踩过的坑不要在解析阶段做任何文本清洗比如不要提前删空格、不要统一大小写。因为清洗会改变字符偏移量后面报错就没办法定位到原文。解析层输出的文本应该尽量保持原样把清洗操作放在具体规则内部做局部处理。2.2 规则引擎把校审规范变成可执行的配置规则引擎的核心是“把规范文本化把规则配置化”。我没有写死规则代码而是用结构化配置文件描述每一条规则。下面是一个简化的规则示例rules: - name: 重复句末标点 pattern: [。]{2,} severity: error message: 检测到连续句末标点 suggestion: 删除多余标点只保留一个 - name: 全角空格 pattern: \u3000 severity: warning message: 检测到全角空格 suggestion: 替换为普通空格 - name: 中文与英文之间缺少空格 pattern: [一-鿿][A-Za-z] severity: warning message: 中文与英文相邻处建议增加空格 suggestion: 在中文和英文之间添加一个空格每条规则有四个关键字段正则模式、错误等级、提示文案、修改建议。错误等级我分了 error、warning、info 三级error 表示必须改warning 表示强烈建议info 只做提示。正则表达式是规则层的主力但有几个问题需要特别留意。中文的正则和英文不太一样比如“汉字”的范围是\u4e00-\u9fff但扩展区汉字在这个范围之外如果文档里出现生僻字直接匹配会漏掉。还有就是转义问题全角标点和半角标点长得像处理不好容易误判。除了正则术语一致性检查我用的是“标准词表 编辑距离”的方式。类似“数据源”和“数据来源”这种先维护一个标准术语表每个标准词配几个常见变体然后对文档里的候选词做模糊匹配距离超过阈值就认为可能不一致。这个方案看着简单实际效果相当好因为术语不一致在文档里普遍存在而编辑距离能抓到大部分漏网的变体。2.3 语义模型层只处理规则管不了的句子规则层跑完剩下的句子就是“格式正确但读起来别扭”的那种。比如“由于系统升级后导致接口无法调用”这种句子规则查不出来但人读起来明显觉得累赘。这类问题我统一交给预训练语言模型去做语法错误检测。具体做法是先用规则层的可疑句段作为候选再把这些句段按窗口切分窗口大小大概是 256 个 token相邻窗口之间保留 50 个 token 的 overlap确保前后文的衔接信息不丢失。然后模型对每个窗口输出一个错误标签和置信度分数。一个模型推理的简化流程大概是下面这个样子的def semantic_filter(sentences, model, threshold0.85): results [] for sentence in sentences: predictions model.analyze(sentence) for p in predictions: if p.score threshold and p.error_type in { 语序不当, 搭配不当, 成分残缺, }: results.append({ start: p.start, end: p.end, message: p.message, suggestion: p.suggestion, confidence: p.score, }) return results阈值为什么定 0.85 而不是 0.8 或者 0.9这是用一份约 500 句的标注测试集跑出来的结果。阈值低时漏报少但误报多阈值高时误报少但漏报多。当时我对比了 0.75、0.80、0.85、0.90 四档0.85 是一个相对均衡的点误报能控制在一页文档大约两三条同事愿意看又不至于吵到不可容忍。2.4 结果输出一句一句给意见而不是甩一个总结校审工具的最终产物是一份报告。我一开始输出的是一段自然语言总结比如“本文档共发现 20 个问题以格式问题为主”结果同事反馈说根本没法用因为不知道问题在哪。后来我把报告改成结构化输出。每条检查结果包含问题类型错别字 / 格式 / 术语 / 语义严重级别error / warning / info原文引用内容出现位置第几段、第几行修改建议忽略按钮支持用户标记为误报报告先输出成 JSON 中间格式再渲染成 HTML。这样做的好处是后续想接 CI 管道或者团队协作平台JSON 格式可以直接被其他系统消费不需要再改逻辑。我还加了一个“忽略清单”功能。有些词本身是特殊的比如产品名、代码变量名校审工具不懂这些会反复报。用户可以把这些词加入忽略清单之后的扫描就不再提示。3. 实操过程从零搭一个能用的校审工具3.1 数据准备第一份标注集是怎么来的做校审工具最忌讳的是没有测试集就开干。没有测试集你就没法判断改动是好是坏。我当时的做法是从自己过去一年写过的技术文档里收集了 300 多个段落挨个手工标注出问题类型。这批标注工作花了大概一整个周末很枯燥但后面的每一次调整几乎都离不开它。同样一条规则改完正则之后跑一遍测试集立刻就知道漏报和误报变化了多少。标注集不需要很大300 段足够支撑一个 MVP。关键是覆盖类型要全错别字、格式、术语、语义各占一部分不然调优的时候容易顾此失彼。3.2 第一版能跑起来的 MVP第一版 AiCheck 非常简单就是一条扫描主循环加一组合规则的实现。from dataclasses import dataclass import re dataclass class Rule: name: str pattern: str severity: str message: str suggestion: str def scan(text: str, rules: list[Rule]): hits [] for rule in rules: pattern re.compile(rule.pattern) for match in pattern.finditer(text): hits.append({ rule: rule.name, start: match.start(), end: match.end(), matched: match.group(), severity: rule.severity, message: rule.message, suggestion: rule.suggestion, }) return hits这段代码跑起来特别快但第一次跑真实文档的时候我被结果淹没了。原因很简单规则写得太粗糙比如“中文与英文之间缺少空格”这条规则在代码块里几乎每行都触发而文档里贴了很多代码根本不应该检查。所以后来我加了一个“跳过区域”机制凡是识别为代码块、行内引用、URL 的内容规则层直接跳过。3.3 准确率和召回率怎么调才不会变成玄学提到调参我强烈建议先搞清楚所谓准确率和召回率在这个场景里的含义不然很容易跟着感觉走。我建了一个大约 100 页的标准测试文档集分两类统计一类是“埋进去的错误有没有被查出来”这叫召回另一类是“报出来的问题里有多少是真的问题”这叫准确度。每一次规则调整、每一次模型阈值变化都要跑一遍这套测试集盯住这两个数的变化。调参过程中有一个坑值得大写加粗不能只盯着准确率。有一段时期我把阈值调得很高报告的准确率到了 95%但漏了一大堆语义问题。同事反馈“报告看着很准但没报出来的问题也不少”。后来我改成同时记录漏报率凡是埋在测试集里没被查出来的错误都会单独列一个列表每次调整后对照它自查。阈值不是越高越好也不是越低越好而是要找到准确率和召回率的交叉点。比如下面这张我当时的调试记录表阈值准确率召回率误报条数/百页0.7572%91%270.8078%87%190.8586%82%90.9091%71%50.80 和 0.85 之间其实是最难抉择的。我最后还是选了 0.85理由是误报多的后果更严重如果文档作者每次跑完都要处理一堆无效反馈很快就会失去对工具的信任这个损失比偶尔漏掉一两个问题大得多。3.4 接入团队流程别让工具堵住提交工具做出来之后怎么让大家用起来是另一个门槛。我试着把 AiCheck 接入过 Git pre-commit 钩子但很快就发现这行不通。因为文档这类文件经常在提交前还没有定稿直接阻断提交会导致团队怨声载道。最后采用的方案是接入 CI 流程但只生成报告不阻断构建。每次提交触发一次检查报告自动出现在流水线的工件里评审人在看合并请求时顺手就能看到。旁边再跟一个微信链接的方式固然有效但报告本身要足够直观大家才愿意看。下面是当时用的一个简化命令aicheck check doc/manual.docx \ --rulesettech-writer \ --outputreport.html跑完以后会输出一个 HTML 报告。如果发现问题报告里会直接列出问题清单没问题时显示零问题。整个过程不阻塞任何流程自动完成大家也慢慢接受它作为评审环节的一部分。4. 常见问题与排查实录4.1 误报多到没人看怎么办这是校审工具最大的信任危机。我遇到过连续几周误报率都压不下去的阶段同事直接开玩笑说“这东西只会刷屏”。解决办法是三层过滤。第一层做区域跳过代码块、链接、引用不过规则。第二层做上下文判断比如“中英文之间没有空格”这个规则在英文是专有名词时往往不适用那就加一个例外词表。第三层做模型置信度校准把模型输出的低分结果直接不展示。误报这东西不存在彻底消灭只能逐步压缩同时要不断收集用户的“忽略”操作记录。当时我在报告页面前端埋了点击“标记为误报”的按钮把误报样本回收到一个 JSON 文件里隔一段时间分析一次把高频误报的场景做针对处理。4.2 中英混排和长文档处理难度完全不同技术文档里中英混排是常态。中文和英文之间要不要留空格本身就是一个让规则很难拿捏的问题。我当时用的规则是“中文字符和英文字符相邻且中间没有空格”就报警但这个规则对“Windows系统”这类常见写法非常不友好。最后只能加大量例外遇到常见产品名、操作系统名、专有名词一律不报。长文档的问题主要是性能。规则层还好模型层在超过 100 页的文档上会明显变慢。后来我加了并行处理把文档按章节切块分配到多个工作进程里跑速度提升了三倍多。另一个方案是只对规则层标记的可疑句段跑模型而不是整篇跑这个优化让速度和成本都大幅改善。4.3 同一条问题被规则层和模型层各报一次这种情况比想象中频繁。比如一个句子里的“部署”写成“布署”规则层会报错别字模型层如果也识别出“搭配不当”那么报告里就会出现两条重复意见。解决办法是按文本位置去重。每条检查结果都记录 start 和 end 偏移量当两条结果的区间有重叠时只保留等级更高的一条。这个去重逻辑看着简单但我当时漏做了导致用户反复吐槽“同一个问题报两遍有点弱智”。实现去重之后报告的观感好了很多。4.4 排查问题速查表现象可能原因处理方式报告中同一错误出现两次规则层与模型层未按位置去重增加文本区间重叠判断保留高等级结果扫描版 PDF 没有被检查文本抽取层没有走 OCR 流程增加“文本过少自动转 OCR”的判断逻辑文档跑得越来越慢未切分章节、模型层处理全量文本只对规则可疑句段推理并启用并行切块代码块里反复误报规则层缺少区域跳过机制增加代码块、URL、行内引用的跳过检测某些规则失效正则表达式匹配范围不够补充汉字扩展区、全角标点变体等边界用户点了“忽略”还是反复报忽略清单没有区分上下文忽略清单支持按规则、按词、按上下文三种粒度4.5 两个让我少走弯路的好习惯第一个习惯是黄金回归测试集。不管规则改动多少、模型阈值如何调整每次发布前都跑一遍那 100 页的标准文档集保证老问题不会因为新改动而复发。这个习惯救了我非常多次。第二个习惯是保存误报样本。每一条被用户标记为误报的反馈我都留在了样本库里。几个月之后回看会发现很多误报集中在少数几个场景上比如产品名、缩写词、代码变量。针对这些高频场景做专项优化一次就能把误报率压下来一大截。5. 为什么暂不更新以及这个选择背后的成本账5.1 维护一个小工具的真实成本很多人觉得个人工具做完功能就能放着跑其实不是这么回事。AiCheck 到后期每周平均要处理十来个 issue其中一半是“规则误报”四分之一是“新场景检查需求”四分之一是想集成到新流程里的请求。每一条误报走查都要翻原文、调规则、跑回归。每一条新场景需求都要考虑要不要做、做了会不会引入新的误报。到后面我算过一笔账哪怕每周只花一个晚上维护它一个月下来相当于要挤掉四到六个小时的私人时间。工作忙起来以后这些时间会变得非常奢侈。停更不是突然做的决定。在我连着三周打开 issue 列表却只能批量标记“暂缓处理”的时候我就知道这个项目进入了不适合继续频繁更新的阶段。5.2 把项目切到低维护模式停更不代表把项目扔掉。我做了几件事让它维持在“可用但不再演进”的状态。第一把 README 改清楚写明白项目的成熟度是“已应用在一部分内部文档场景暂不承诺后续更新”。这能大幅减少新用户的无谓提问也让老用户对响应速度有预期。第二冻结所有新功能开发只处理阻断性问题比如解析层崩溃、模型接口调用失败这类必须修的事。第三把设计文档和规则文件整理好。如果后面有时间我可以随时恢复开发如果有人接手也不会因为代码没人看得懂而劝退。低维护模式的好处是项目不再持续消耗你的精力但它仍然是可用的资产。这个状态比彻底删除或者闭源要好得多。5.3 送给想自己折腾校审工具的人如果你现在也在考虑做一个文档校审工具我有几条实在的建议。不要从模型入手。先把 50 条规则打磨好哪怕只是错别字、空格、标点这三个方向。规则处理的是 60% 以上的高频问题而且规则好维护、好解释。不要做“什么文档都能审”的通用工具。每个团队的文档规范完全不同工具最好绑定一套规范去设计。通用化意味着你要维护无数种例外复杂度会把你拖垮。最后一条工具的边界等于规范的边界。没有稳定的团队写作规范校审工具永远只会变成一个误报制造机。先有规范后有工具顺序不能反。回看这两年我这个 AiCheck 最让我满意的一点是它把文档检查从“靠人盯”变成了“自动化提示”把有限的注意力留给真正需要判断的内容。停更之后它还会继续跑也会继续在老项目里帮忙找出那些重复出现的低级错误。如果哪天工作节奏缓和下来我不排除重新打开规则文件继续折腾。但在那之前我现在这样写下来把这些经验和教训留给后来者可能就是它最好的归宿。
阅读完成 · 觉得有帮助?
咨询建站