
简介面向NLP开发者和文本处理研究者的一份中文文本纠错实战源码包以ChatGLM3-6B大模型与Pycorrector开源库为核心完整覆盖纠错原理、模型选型、数据处理、训练评估到系统实现等环节适合用于论文复现或工程落地。压缩包共62个文件大小27.33MB主要包含23个Python脚本数据预处理、模型训练、API接口、17个pyc编译文件、9张架构与训练曲线图、4个shell启动脚本、2个JSON数据集、1个说明文档及1个notebook示例并带Gradio演示与OpenAI接口示例便于按需调用。项目内置pycorrector_server和app_server服务目录可快速启动纠错服务README也提供了流程引导。目前已有429人学习下载对于希望掌握大模型中文纠错全流程、并获取可运行参考的读者是一份高性价比的优质项目资料。1. 文本纠错为什么要拿 Pycorrector 和 ChatGLM3-6B 搭配先回答三个问题文本纠错这个任务看起来很简单——把“我门明天去公园”改成“我们明天去公园”但真上手做过的工程师都清楚难点不在“看出来错”而在“怎么改才不改错”。你拿纯规则写混淆集到处都是漏网之鱼你单靠大模型硬改它敢把没什么错的句子给你改得面目全非。这个标题里出现的 ChatGLM3-6B Pycorrector恰好是我这两年做文本预处理流水线时验证过的一套组合Pycorrector 负责用混淆集和语言模型快速召回“可疑位置”ChatGLM3-6B 负责对“这里到底是不是错、该怎么改”做语义层面的最终裁决。它不是什么新奇研究但真能同时解决 OCR 识别文本、语音转写、用户评论里最常见的两类问题漏纠和误纠。这篇文章不讲论文只讲我实际搭这套系统的流程、参数和踩过的坑。适合谁看做 NLP 算法落地的、在做文本清洗管线的、以及拿到这份源码但不确定怎么改造成自己业务的读者——下面按标题一步步拆开。2. 让 Pycorrector 和 ChatGLM3-6B 各司其职候选生成与语义裁决的分工逻辑2.1 Pycorrector 到底在纠什么混淆集、语言模型与候选召回先搞懂 Pycorrector 这工具吃了什么饭。它的底层核心是“混淆集 N-gram 语言模型”或“混淆集 微调后的序列标注模型”老版本里你甚至不需要 GPU 就能跑通。它的工作模式是先把输入句子按字切开对每个字检查是否命中了音近字、形近字、同音字的混淆词典再结合上下文计算一个“怀疑分数”分数超过阈值就认为这个位置出错然后从混淆词典里挑候选词替换再用语言模型算一下替换后的句子概率如果概率更高就采纳。这里有个容易被新手忽略的点Pycorrector 不是真正“理解”句子它是在做统计层面的“可疑度”排名。它特别擅长处理“晚知到”这种音近错误“知”和“道”音近和“厄运来临时”这种少一笔多一笔的形近错误换字狠且快。但它的短板也很明显——如果上下文语义需要更长距离的推理比如“我看到他在窗边看风景”被错写成“我看到他在床边看风景”混淆集一眼看不出问题语言模型也可能觉得“床边看风景”概率没那么低就放过去了。这时候就需要一个能真正读句子的模型登台。2.2 ChatGLM3-6B 介入的位置把“音近字”变成“语义相关性”判断ChatGLM3-6B 在文本纠错里干的活和你平时让它做问答不是一回事。你给它一句话直接问“这句话有错吗改一下”它大概率会改出一堆“更通顺”但其实没必要的替词比如把“今天天气很好”改成“今日天气晴朗”。所以这个模型不能放在第一层“一刀切”而要放在一个非常克制的角色上只对 Pycorrector 标记出来的可疑位置做语义校验。我的做法是这样的把原句和可疑字符位置一并塞给 ChatGLM3-6B让它只回答两种东西——这个位置有没有错如果错了给出你认定的正确字词。指令里写死“不要修改其他内容”“不确定就回答‘无需修改’”。这样做的原理是大模型在短上下文里做“局部修复”比做“全句重写”稳定得多。ChatGLM3-6B 的 6B 参数规模足够让它在语义相邻词语之间建立联系比如“河边”和“湖边”到底符不符合整个句子的动作逻辑这是传统混淆集做不到的。顺带说一句我试过直接拿大模型做全文纠错效果惨不忍睹误闹率能到百分之三十以上所以奉劝一步到位的心态放一放。2.3 两种协作架构对比串行 pipeline 还是并行投票在拿到这个标题的时候我第一反应就是这个项目里最有价值的其实是 Pycorrector 和 ChatGLM3-6B 的组合方式。就我试过的方案有两种靠谱的架构架构流程优点缺点适用场景串行 pipelinePycorrector 先召回候选错误 → ChatGLM3-6B 对候选项做逐点裁决逻辑清晰、延迟可控、改动局部性强如果 Pycorrector 漏召回大模型也看不到漏掉的地方大多数日常文本错误以音形近为主并行投票Pycorrector 和 ChatGLM3-6B 各自独立产出纠错结果再按规则合并比如两方重叠就采纳只有一方改且有高置信度也采纳召回率更高两边互补延迟翻倍且并发处理时模型加载更吃显存错误类型杂、宁可多算不能漏纠的正式场景我个人优先建议串行 pipeline。原因很简单你手里的源码大概率也是这个思路跑通之后你会发现百分之八十的修正都来自 PycorrectorChatGLM3-6B 只在少数位置介入所以整体速度依然可控。等串行跑顺了再考虑并行兜底。补充一点选型层面的理由为什么不是直接用 ELECTRA 或者是更大参数的模型因为 Pycorrector 自己也支持微调 BERT 做纠错但我对比下来在真实业务文本里微调模型需要一个和业务分布接近的训练数据集否则在 OOV词表外词上照样拉胯而 ChatGLM3-6B 是通用对话模型它对中文语感更“宽”从零样本理解能力上占了便宜。另一个理由更实在——这个项目名称把两个开源名摆在一起本身意味着它不是一个需要你从零训练的全新采集流程而是在低成本上做拼装这种拼装恰恰最适合业务线临时接一个纠错需求的时候。3. 把项目源码跑起来从零搭一个本地纠错服务3.1 环境与模型权重最小依赖安装和目录规划拿到这份“项目源码流程教程”的压缩包不要直接双击运行。我建议按下面这套目录来放文件因为 Pycorrector 的缓存目录、ChatGLM3-6B 的模型目录、临时输出目录如果不分开后面调试起来会想骂人。text-correction/ ├── models/ # 本地模型权重目录 │ ├── chatglm3-6b/ # ChatGLM3-6B 权重和 tokenizer │ └── pycorrector_cache/ # pycorrector 运行缓存 ├── data/ │ ├── input_samples.txt # 待纠错文本一行一条 │ └── output_results.jsonl # 结果输出JSONL 格式 ├── scripts/ │ ├── run_correct.py # 单条纠错脚本 │ └── batch_correct.py # 批处理脚本 ├── logs/ # 运行日志和错误追溯 └── requirements.txt关于环境配置这里有一个非常容易翻车的点Pycorrector 早期版本依赖 TensorFlow 1.x而 ChatGLM3-6B 需要 PyTorch 2.x两者塞在同一个环境里会有致命的版本冲突。我建议用 conda 拆两个环境再通过 HTTP 服务或文件落盘做两个环节的连接。不过如果你拿到的源码是较新版本Pycorrector 已经提供了不依赖 TensorFlow 的pycorrector轻量版那就可以放心用单环境。conda create -n textfix python3.9 conda activate textfix pip install pycorrector transformers torch jieba这里的pycorrector是传统混淆集版本transformers用来加载 ChatGLM3-6Bjieba是 Pycorrector 分词时依赖的库。装完之后先跑一句python -c from pycorrector import Corrector; print(ok)确认没有缺底层依赖再继续。3.2 单条纠错的完整代码流程Pycorrector 召回 GLM 校验这一步是整篇文章的核心也是你要在源码里扒出来的那个关键函数。我给你一个可直接复制的简化版逻辑覆盖了完整流程同时去掉了和业务无关的花哨展示。# scripts/run_correct.py # -*- coding: utf-8 -*- import json import re import torch from transformers import AutoModel, AutoTokenizer # ---------- 第 1 步加载 Pycorrector ---------- from pycorrector import Corrector py_corrector Corrector() def detect_with_pycorrector(text): 调用 Pycorrector 做召回返回每个疑似错误的位置和候选项。 correct 方法返回 (修正后的文本, 错误列表) 错误列表元素为 (original_word, corrected_word, position_index) corrected_text, errors py_corrector.correct(text) return corrected_text, errors # ---------- 第 2 步加载 ChatGLM3-6B ---------- model_path ./models/chatglm3-6b tokenizer AutoTokenizer.from_pretrained( model_path, trust_remote_codeTrue ) model AutoModel.from_pretrained( model_path, trust_remote_codeTrue, device_mapcuda, torch_dtypetorch.bfloat16 # 显著省显存对 6B 模型推荐 ).eval() # ---------- 第 3 步语义校验函数 ---------- def check_with_glm(text, original_word, cand_word, position): 让 ChatGLM3-6B 判断给定位置是保持原样还是改成候选项。 只允许模型输出‘不改’或一个具体字词。 prompt f你是一个严谨的中文文本校对助手。 原文{text} 在第 {position} 个字符处字符{original_word}纠错模块建议改成“{cand_word}”。 请根据上下文语义判断该处是否真的出错 - 如果确实错误只输出正确字符 - 如果原字符是正确的只输出“不改”。 不要输出任何解释。 inputs tokenizer(prompt, return_tensorspt).to(cuda) with torch.no_grad(): outputs model.generate( inputs.input_ids, max_new_tokens8, temperature0.2, # 低温度减少随机替词 top_k10, repetition_penalty1.2, do_sampleTrue ) answer tokenizer.decode(outputs[0][inputs.input_ids.shape[1]:], skip_special_tokensTrue) answer re.sub(r[^\u4e00-\u9fa5a-zA-Z0-9。、], , answer) return answer.strip() # ---------- 第 4 步完整纠错函数 ---------- def smart_correct(text): corrected_text, errors detect_with_pycorrector(text) if not errors: return corrected_text, errors final_chars list(corrected_text) # 注意这里的 position 是 pycorrector 返回的字符索引直接映射到字符串列表 for original_word, cand_word, pos in errors: if pos len(final_chars): continue # 先看这个位置在修正后的文本里是否还等于原词 if final_chars[pos] ! original_word: continue verdict check_with_glm(corrected_text, original_word, cand_word, pos) # 模型输出“不改”就还原否则替换成模型给出的答案 if verdict and verdict ! 不改: final_chars[pos] verdict else: final_chars[pos] original_word # 回退到原词防止被误改 final_text .join(final_chars) return final_text, errors if __name__ __main__: sample input(输入待纠错句子) result, errs smart_correct(sample) print(原始句子, sample) print(纠错结果, result) print(召回错误, errs)代码逻辑并不复杂但有几个参数值得说明白。第 1 步py_corrector.correct()返回的 errors 列表里pos是基于字符的索引不是标点跳过后的索引所以你拿它直接切 final_chars 时一定要先做越界检查。第 3 步里的temperature0.2非常关键我见过有人在大模型纠错里把温度开到 0.8结果同一个错误分两次跑出两个不同答案这类随机性在纠错任务里必须压低。top_k10则是控制候选词范围避免模型联想到八竿子打不着的字。repetition_penalty其实在这个场景里作用不大但保留无害。3.3 批量推理与结果结构化存储输入输出设计与异常捕获单条能跑通之后真正的业务问题是一次给你几百上千条文本必须批处理。这里最大的坑是 ChatGLM3-6B 的推理速度——单条在 V100 上大约 1.5 秒批量不等于无脑 for 循环。我下面写的方案是折中不做动态 padding 的大 batch而是每批只处理一条但要加缓存如果 Pycorrector 这轮没有召回任何错误就完全跳过 GLM 调用这能省下约 80% 的推理时间。# scripts/batch_correct.py import json import time from run_correct import smart_correct def load_samples(filepath): 读取纯文本按行拆分空行和纯标点行直接跳过 samples [] with open(filepath, r, encodingutf-8) as f: for line in f: line line.strip() if not line or len(line) 2: continue samples.append(line) return samples def save_result(line_num, original, corrected, errors, duration): 落盘为 JSONL保留原始文本便于回溯 with open(data/output_results.jsonl, a, encodingutf-8) as f: record { line: line_num, original: original, corrected: corrected, errors: [ {orig: w[0], cand: w[1], pos: w[2]} for w in errors ], duration_sec: round(duration, 4), } f.write(json.dumps(record, ensure_asciiFalse) \n) def main(): samples load_samples(data/input_samples.txt) failed_count 0 start_all time.time() for idx, text in enumerate(samples, start1): try: t0 time.time() corrected, errors smart_correct(text) save_result(idx, text, corrected, errors, time.time() - t0) print(f[{idx}/{len(samples)}] 完成耗时 {time.time() - t0:.2f}s) except Exception as e: failed_count 1 # 单条失败绝不能中断全批记录原始内容后继续 with open(logs/failed_lines.txt, a, encodingutf-8) as err_f: err_f.write(f{idx}\t{text}\t{repr(e)}\n) print(f全部完成总计耗时 {time.time() - start_all:.2f}s失败 {failed_count} 条) if __name__ __main__: main()这个脚本有两个细节很重要。第一save_result里落了original字段这不是多余——你后面要做人工抽检对比没有原始文本根本没法评估纠错是对是错。第二try...except要包住整条处理逻辑而不是只包住模型调用部分因为 Pycorrector 在纯英文句子或混合语言文本上偶尔会抛编码异常这种失败记录到failed_lines.txt后不打断整体流程才叫工程化。一个经验数值对一万条短文本这个 pipeline 大概跑半小时到一小时GPU 是 16G 的卡如果你在 CPU 上跑模型那不好意思我劝你先把 GLM 相关的组件注释掉先验证 Pycorrector 层面的输出是否符合预期。4. 纠错效果调优与避坑实录从阈值到显存的三条血泪经验4.1 Pycorrector 误报太高threshold 和 freq 的联动调整第一个需要动手调的参数是 Pycorrector 的误报阈值。现象是什么一句话里标出了四五个可疑位置人工一眼扫过去全是对的。原因也很直接Pycorrector 的默认混淆集覆盖了大量“日常口语里可接受”的变体。比如“吉他”和“遥琴”这种同义替换混淆集里写成音近但实际上在语境里并不算错再比如地名用字“志”和“治”两个都可能是正确写法Pycorrector 拿语言模型算概率时觉得“治”的概率更高就硬改了。解决方式是这样Corrector()初始化时可以传入自定义混淆集路径或自定义语言模型分数权重。我一般会在初始化后做一步“白名单锁定”——常见品牌词、专业术语、人名逐个加入自定义不纠错词典。在 Pycorrector 里可以通过加载自定义custom_dict来解决示例代码如下。from pycorrector import Corrector from pycorrector.config import config # 加载自建词典里面是噪声词和不纠词 config.custom_word_dict data/my_custom_dict.txt # my_custom_dict.txt 每行格式词语 频次 # 星界武器 100 # 卡芙卡 100 py_corrector Corrector( language_model_pathconfig.language_model_path, word_freq_pathconfig.word_freq_path, custom_word_freq_pathconfig.custom_word_dict )这里custom_word_freq_path传入的频次会直接影响语言模型算概率的基线如果不想让它纠某些词把这个词的频次调高就行。我建议初始跑完整数据后把输出结果里所有被改动的句子拉出来看一遍找出那些“强改但明显错误”的词一次性加入词典迭代三轮之后误报率能压到很低。4.2 ChatGLM3-6B 在低显存机器上的推理翻车量化与批处理第二个坑基本人手一份显存不足。ChatGLM3-6B 原始 FP16 权重大概占了 12GB加上推理时的 activation16GB 的卡勉强能跑单条稍微留点别的进程就 OOM。这个项目的标题里既然写“附源码”大概率源码里的加载方式是原始from_pretrained你在低配机器上直接复现就会死在这里。解决办法很成熟加载时加load_in_4bitTrue。但要小心这一步不是白捡的。4bit 量化之后模型在判断歧义位置时准确率会有小幅下降尤其是那些本来就模棱两可的“能否换成同义词”的判断。我的经验是如果你的显存有 16GB优先用 8bit 量化如果只有 8GB才上 4bit。另外还有一招是优化输入长度Prompt 不要传整篇长文只需要传错误位置前后各 10 个字符这能大幅压缩 attention 计算量同时效果几乎不损。# 低显存版加载方式 model AutoModel.from_pretrained( model_path, trust_remote_codeTrue, device_mapauto, load_in_4bitTrue, # 8GB 显存可用不然就是 OOM bnb_4bit_compute_dtypetorch.bfloat16, bnb_4bit_quant_typenf4 ) # 注意load_in_4bit 需要配合 transformers4.30 和 bitsandbytes 包如果你确实没有 GPU 环境那蛮力模式就不建议碰大模型了不过源码里应该能抽离出 Pycorrector 纯 CPU 的纠错流程先跑起来看结果格式再决定要不要上大模型校验这也是一种渐进式落地。真正做业务验证的时候拿一个 GPU 跑一次全量比在 CPU 上裸跑三天更能说明问题。4.3 纠错结果反被“改错”语义校验的置信度门槛怎么设第三个坑最阴间大模型把原本正确的字“纠正”成另一个通顺但原意跑偏的字。比如原句“我喜欢吃扇贝”模型觉得“扇贝”应该是“生蚝”这就属于过度发挥。我的排查经验是先看 Prompt 有没有让模型“保持原意”的约束再看温度有没有像我前面说的那样压低到 0.2 以下。很多“项目源码”默认给的 temperature 是 0.7这是写对话任务的习惯不是写纠错任务的。即便这些都调好了仍然会遇到模型缺乏上下文导致的错判。我的做法是引入一个“二次确认”环节——如果 ChatGLM3-6B 给了一个不同于 Pycorrector 候选项的答案比如 Pycorrector本文还有配套的精品资源点击获取