简介:面向开发者与研究人员的文本纠错项目实战包,基于ChatGLM3-6B大模型与Pycorrector开源工具构建,适用于文本编辑、校对、翻译等场景,可显著提升文本质量并降低人工校对成本。压缩包共62个文件,约27.33MB,以Python脚本、模型配置JSON、运行脚本Shell、项目说明Markdown及效果示意图为主,其中23个py文件涵盖数据处理、模型训练、纠错接口与Gradio演示等功能模块,便于直接复用与二次开发。已有429人学习下载。通过完整项目源码与流程教程,读者可掌握从数据处理、模型选择、训练评估到系统实现的各个环节,并了解ChatGLM3-6B工作原理与Pycorrector库安装配置、接口调用方法。该实战项目不仅提供了可直接运行的代码,还给出了清晰的目录结构与启动脚本,适合希望快速落地中文文本纠错系统的开发者参考。
1. 文本纠错这种项目,为什么值得把一个完整源码包从头拆到尾
做文本纠错最怕的不是模型不准,而是你以为准了,一上线就被真实用户拿长句、口语、专有名词打脸。这份资源把两条技术路线装进同一个项目里:Pycorrector 做规则与模型的基线纠错,ChatGLM3-6B 提供大模型语义层面的修复能力,而且从数据处理、LoRA 微调、API 封装到 Gradio 界面全都配齐了。适合三类人:想快速搭出可演示纠错服务的前后端开发,准备在本地部署 6B 级模型做业务验证的算法工程师,以及需要一套能跑通的完整项目当论文或毕设基座的在校学生。它不是玩具 Demo,是能拆开单独复用的工程包,值得动手跑一遍。
2. Pycorrector 基线纠错:安装、接口封装与 Gunicorn 部署
2.1 为什么先要 Pycorrector,而不是直接把文本丢给 ChatGLM3-6B
大模型纠错有个天然弱点:它会把「没错的字」也改掉。比如用户输入「我明天去深圳」,模型可能把「深圳」改成「沈阳」,这种错误在真实业务里不可接受。Pycorrector 这类工具走的是混淆词表、语言模型打分和纠错候选排序的路子,它对常见错别字的召回率稳定,且不会做过度修改。
这份资源里的设计思路很清晰:Pycorrector 作为第一道闸门,先把高频、规则可判定的错误消掉;改不动、拿不准的再交给 ChatGLM3-6B。双层结构既省大模型的 API 调用成本,又降低了误改率。我在自己的项目里也坚持这个原则——能用小模型解决的,绝对不惊动大模型。
2.2 服务端:app_config.py 如何把模型选择、阈值和日志集中管起来
打开pycorrector_server目录,先看app_config.py,它是整个纠错服务的配置中心。资源里把可调参数都集中到这个文件里,避免改业务代码时误碰模型加载逻辑。一个典型配置长这样:
# app_config.py import os BASE_DIR = os.path.dirname(os.path.abspath(__file__)) # 模型选择:macbert / conv_seq2seq / chatglm3-6b CORRECTOR_MODEL = "macbert" # Pycorrector 参数 PYCORRECTOR_CONFIG = { "model_name": "shibing624/macbert4csc", "device": "cuda:0", "batch_size": 32, "max_length": 128, # 置信度阈值,低于此值的候选不替换 "threshold": 0.5 } # ChatGLM3-6B 接入配置 CHATGLM3_CONFIG = { "api_url": "http://127.0.0.1:8000/v1/chat/completions", "model_name": "chatglm3-6b", "temperature": 0.1, "max_tokens": 256, "prompt_template": "prompts.py" } # 全局开关:是否启用大模型兜底 ENABLE_CHATGLM3 = True LOG_CONFIG = { "log_file": os.path.join(BASE_DIR, "logs", "corrector.log"), "max_bytes": 10 * 1024 * 1024, "backup_count": 3 }这段配置的逻辑说三点。第一,CORRECTOR_MODEL决定 Pycorrector 加载哪个基座模型,这里用macbert4csc是中文纠错常用选择,体积小、效果好,普通显卡就能跑。第二,threshold是一个关键参数,Pycorrector 对每个位置会产出多个候选词并打分,只有分数超过 0.5 的才替换,调低它纠错更激进,调高更保守,实际业务里我通常从 0.5 开始调。第三,ENABLE_CHATGLM3是总开关,想快速验证 Pycorrector 单跑效果时直接置 False,不用注释代码。
2.3 用 Gunicorn 起服务:workers、timeout 和并发这几个数怎么定
资源里的app_server.py是用 Flask 写的 Web 服务,gunicorn.py是它的生产部署配置。直接跑python app_server.py只是开发模式,上线要用 Gunicorn 多进程扛并发,这是很多新手容易跳过的步骤。
# gunicorn.py import multiprocessing bind = "0.0.0.0:8080" workers = multiprocessing.cpu_count() * 2 + 1 threads = 4 timeout = 120 keepalive = 5 max_requests = 1000 max_requests_jitter = 100 worker_class = "gthread" graceful_timeout = 30 # 日志 accesslog = "./logs/access.log" errorlog = "./logs/error.log" loglevel = "info"这里有几个参数需要特别解释。worker_class = "gthread"表示每个 worker 里再开 4 个线程,适合 Pycorrector 这种推理时释放 GIL 的场景;timeout = 120必须给足,因为 macbert 首次加载模型可能要几十秒,默认 30 秒会导致 worker 被误杀;max_requests = 1000是防内存泄漏的保命设置——模型推理服务跑久了显存会碎,定期重启 worker 比手动清理省心。启动命令是gunicorn -c gunicorn.py app_server:app。
3. ChatGLM3-6B 接入:download_model 脚本、显存规划与本地 API 伪装
3.1 为什么用 ChatGLM3-6B 而不是随便一个开源大模型
选 ChatGLM3-6B 当主力模型有几个现实原因:它原生支持中文,Prompt 理解能力强,不需要额外翻译或调整;6B 参数规模对显存要求相对宽松,消费级显卡能跑推理;同时它的 LoRA 微调生态成熟,社区案例多,出了问题容易搜到解决方案。相比直接调云端大模型 API,本地部署意味着数据不出内网,这对处理敏感文本的团队是硬需求。
但 6B 模型不是万能的,它对超长文本的处理能力有限,超过 2048 token 时上下文会截断,纠错能力断崖式下降。所以这份资源里把它定位在「兜底」而不是「主力」,前面先过 Pycorrector,这层只在疑难句子上发力,正好避开它的短板。
3.2 模型下载与本地推理:显存不够时怎么降级
资源根目录有个download_model.py,作用是自动拉取 ChatGLM3-6B 的预训练权重。模型文件合计超过 12GB,直接放进项目目录会导致仓库臃肿,所以脚本里通常做成可跳过或断点续传。常见的执行方式是:
# 先确认磁盘空间,模型权重解压后约 13GB df -h /opt/models # 执行下载脚本,中途断网可重新跑,支持续传 python download_model.py --output_dir /opt/models/chatglm3-6b下载完成后,推理阶段需要考虑显存。ChatGLM3-6B 在 FP16 精度下加载约需 13GB 显存,如果你只有 8GB 显卡,就必须上量化。资源里没有直接给出量化脚本,但openai_api_demo里保留了模型加载入口,常见做法是改torch_dtype为torch.float16,再配合bitsandbytes做 4bit 量化:
# openai_api_demo/utils.py import torch from transformers import AutoModel, AutoTokenizer def load_model(model_path: str, quantize_4bit: bool = True): tokenizer = AutoTokenizer.from_pretrained(model_path, trust_remote_code=True) if quantize_4bit: # 4bit 量化后显存占用可压到 6GB 左右 from transformers import BitsAndBytesConfig quantization_config = BitsAndBytesConfig( load_in_4bit=True, bnb_4bit_compute_dtype=torch.float16, bnb_4bit_quant_type="nf4" ) model = AutoModel.from_pretrained( model_path, trust_remote_code=True, quantization_config=quantization_config, device_map="auto" ) else: model = AutoModel.from_pretrained( model_path, trust_remote_code=True, torch_dtype=torch.float16, device_map="auto" ) return model, tokenizer这段代码的关键在trust_remote_code=True——ChatGLM3 的模型定义文件没有完全走 Transformers 标准结构,必须允许加载远程代码才能跑起来,很多新手在这里报错就是漏了这个参数。另外device_map="auto"会让模型自动分散到所有可用显存设备上,多卡机器能直接用上。
3.3 把 openai_api_demo 改成本地 API:一个请求一个响应,逻辑就这么简单
openai_api_demo目录里的api_server.py本质是把模型服务包装成 OpenAI 兼容的接口,方便前后端用统一格式调用。下面这段是经过简化的核心路由:
# api_server.py from flask import Flask, request, jsonify from utils import load_model app = Flask(__name__) model, tokenizer = load_model("/opt/models/chatglm3-6b", quantize_4bit=True) @app.route("/v1/chat/completions", methods=["POST"]) def chat_completions(): data = request.get_json() messages = data.get("messages", []) # 拼接对话上下文 prompt = "" for msg in messages: role = msg.get("role", "user") content = msg.get("content", "") prompt += f"{role}: {content}\n" # 用模型生成纠错结果 inputs = tokenizer.encode(prompt, return_tensors="pt") outputs = model.generate( inputs, max_new_tokens=256, do_sample=False, temperature=0.1 ) response = tokenizer.decode(outputs[0], skip_special_tokens=True) return jsonify({ "choices": [{"message": {"role": "assistant", "content": response}}] }) if __name__ == "__main__": app.run(host="0.0.0.0", port=8000)注意do_sample=False和temperature=0.1这两个参数。文本纠错任务要的是稳定输出,不是创意生成,所以关闭采样、降低随机性。我在实际调试时会把max_new_tokens限制在 256,因为普通句子的纠错结果不会太长,给太多生成空间反而容易让模型自由发挥、加废话。
4. LoRA 微调:train_large_v2 数据格式、超参与损失曲线的读法
4.1 微调数据格式:为什么是 JSON 而不是纯 TXT
只看train_large_v2.json这个文件名,很多新手会以为它是普通的{"原句": "纠错句"}配对。打开看会发现每条数据有多个字段,这是为了适配 ChatGLM3 的对话式微调格式。网上有些教程拿纯文本让模型硬学输入输出关系,效果很差,因为大模型的预训练任务是预测下一个 token,不是做序列到序列翻译,只有把纠错任务包装成对话格式,模型才更容易理解。
典型的一条训练数据长这样:
{ "instruction": "请纠正下面句子中的错别字,只输出纠正后的句子。", "input": "我今天吃了个萍果,味道很好。", "output": "我今天吃了个苹果,味道很好。" }资源里还带了一个get_data.ipynb,作用是把你手里的原始语料清洗并转换成上述格式。转换逻辑一般包括:剔除过长句子(超过 128 字直接丢弃)、过滤纯英文或数字串、保证输入和输出的长度差分不超过 20%。这些细节直接决定微调效果,不能跳过。
我在第一版微调时没做长度过滤,结果训练集里混进了几条 800 字的「长文纠错」,LoRA 训练直接爆显存,损失曲线一度飙到 NaN。
4.2 微调脚本与超参:LoRA 参数怎么定才不白训
ChatGLM3-6B 全参数微调需要至少 48GB 显存,普通用户根本跑不动。LoRA 把可训练参数压缩到约 1-3%,8GB 显存就能微调。资源里的训练流程是基于 HuggingFace 的 PEFT 库做的,核心超参数如下:
# lora_config.py from peft import LoraConfig lora_config = LoraConfig( r=8, lora_alpha=16, lora_dropout=0.05, bias="none", task_type="CAUSAL_LM", target_modules=["query_key_value"] )几个参数的含义要说明白。r=8是低秩矩阵的秩,太小模型学不住规律,太大显存扛不住,8 是多数场景的平衡点;lora_alpha=16控制微调权重对原模型的影响强度,一般设成r的两倍,效果比较稳;target_modules=["query_key_value"]指定只微调注意力层的这三个矩阵,这是 ChatGLM3 微调常用的目标模块组合,顺手把它的线性层也绑进去但效果不一定更好。
训练阶段还要配合几个关键参数,数据批次大小、学习率和梯度累积步数。我常用的组合是:
python train_chatglm3_lora.py \ --model_path /opt/models/chatglm3-6b \ --train_file dataset/train_large_v2.json \ --output_dir output/chatglm3-lora \ --num_train_epochs 3 \ --per_device_train_batch_size 4 \ --gradient_accumulation_steps 4 \ --learning_rate 1e-4 \ --max_seq_length 256 \ --logging_steps 10 \ --save_steps 500这里batch_size=4配合gradient_accumulation_steps=4,等效批大小是 16。显存小就调小 batch_size,同时增大 accumulation 步数,保证等效批大小不变,这样训练稳定性不会受影响。学习率 1e-4 是 LoRA 微调的典型起点,我试过 5e-5,收敛慢但更稳妥;也试过 3e-4,训练后期损失震荡明显,容易把已有权重冲坏。
4.3 看 training_loss.png 判断训练是否正常
资源里放了一张training_loss.png,这是微调过程中保存的训练损失曲线截图。拿它当参照物,你能判断自己训练是不是正常的。一张健康的损失曲线有三个特征:前 200 步损失从 1.2 左右快速降到 0.5 以下,这对应模型在快速学习最常见的错别字模式;中段在 0.3 附近缓慢下降,曲线有小幅波动但整体趋势向下;接近结束时曲线基本走平,没有突然拉升或出现 NaN 尖峰。
如果损失曲线一直高位震荡,先排查数据质量:是不是有大量「输入等于输出」的样本,模型学到的是恒等映射;如果出现 NaN,基本是学习率太大导致梯度爆炸,先把学习率降一个量级再重跑。曲线走平不代表微调没效果,LoRA 本身改动的是低秩子空间,损失降到 0.3 左右已经能明显改善错别字纠正效果,继续训练反而可能过拟合训练集里的特定句式。
5. 常见问题与排查:安装失败、显存溢出、模型加载慢、乱改词
5.1 Pycorrector 安装失败,多半是依赖冲突而不是网络问题
现象:执行install.sh时报ERROR: pip's dependency resolver,或者pycorrector装完导入时提示缺kenlm、pypinyin等模块。
原因:Pycorrector 依赖的组件跟 Python 3.8+ 存在兼容性问题,最常见的是kenlm需要编译安装,而本机没有 C++ 编译环境。另外它跟新版transformers的 API 有出入,版本互相踩踏。
解决:先建一个干净的虚拟环境,指定 Python 3.8 或 3.9,再装 kenlm 的预编译 wheel 包。我在项目里是这么处理的:
# 创建干净环境 conda create -n corrector python=3.9 -y conda activate corrector # 先装 kenlm,避免 Pycorrector 安装时编译卡住 pip install https://github.com/kpu/kenlm/archive/master.zip # 再装 Pycorrector 和 Flask 依赖 pip install pycorrector flask gunicorn pip install -r requirements.txt装完后用python -c "import pycorrector; print('ok')"做冒烟测试。如果导入还报错,就看报错信息里缺少哪个模块,单独装那个模块,不要整包重装。
5.2 ChatGLM3-6B 推理时 CUDA Out of Memory
现象:调用本地 API 时报CUDA out of memory,进程直接挂掉,别的服务也被拖垮。
原因:显存规划不合理。6B 模型 FP16 加载约 13GB,输入序列超过 1024 token 时激活值会额外吃掉 2-3GB,加上 Pycorrector 也占着显存,8GB 显卡必然爆掉。
解决:给 Pycorrector 和 ChatGLM3 分配不同的显卡,或者强制 ChatGLM3 使用 CPU 推理。更有性价比的方案是上 4bit 量化,前述代码已给出。另外把max_length限制到 512,输入超长时先截断再送模型,大多数业务纠错句子不会超过这个长度。我在部署时还会给进程加一个显存检测:
import torch if not torch.cuda.is_available(): raise RuntimeError("CUDA 不可用,请检查驱动") elif torch.cuda.get_device_properties(0).total_memory < 8 * 1024**3: print("警告:显存小于 8GB,建议开启 4bit 量化")5.3 模型加载要 50 秒,接口第一次请求超时
现象:Gunicorn 的timeout=30导致 worker 被 kill,日志显示Worker timed out。
原因:模型在 worker 进程启动后才加载,而 Flask 是懒加载模式,第一次请求触发模型加载,耗时超过 Gunicorn 的超时阈值,worker 被误杀后模型又得重新加载,形成死循环。
解决:在app_server.py的模块顶层就初始化模型,让加载动作发生在 worker 创建期间;同时把 Gunicorn 的timeout调到 120 秒以上。两者配合,既提前加载又给足缓冲。也可以改用lazy-app模式或preload_app=True,但最简单还是模块级加载。
5.4 大模型纠错「过度发挥」,把正确文本给改了
现象:输入「这本书很薄」,输出变成「这本书很厚」,语义完全翻转。
原因:大模型的生成式纠错不是「只改错字」,而是「重新理解并输出一句话」。它可能根据上下文脑补出不同的表达,尤其在do_sample=True时随机性更强。Pycorrector 的候选分数对这种情况无能为力,因为它根本不会被触发——这句话没有错别字。
解决:答案在text_correct_utils.py里,推理前做一个「差异检查」:先比较输入和输出,如果字符级别差异超过 30%,直接丢弃大模型结果,回退 Pycorrector 的候选。这个阈值不是拍脑袋定的——中文错别字纠错通常是单字或双字级别的局部替换,整体改动超过三成,基本就是模型在自由发挥。代码如下:
def is_reasonable_correction(src: str, tgt: str, max_diff_ratio: float = 0.3) -> bool: diff_count = sum(1 for s, t in zip(src, tgt) if s != t) diff_ratio = diff_count / max(len(src), len(tgt)) return diff_ratio <= max_diff_ratio值得注意的是,如果模型输出的句子和输入完全一样,说明模型没有理解任务或 Prompt 写得不到位,此时要检查prompts.py里的系统提示词是否足够明确。
6. 最终校验:用一份 50 句的测试集,把整个纠错链路跑通并量化收益
6.1 准备 50 句「带标注」的测试集
一份可以量化的测试集,每行是一条 JSON:{"src": "原句", "tgt": "标注后的正确句子"},并且刻意混入三种场景:常见错别字、形近字、无语境错误。从dataset/test目录里能看出资源的做法,它会把text_correct.json按 8:2 拆成训练和测试两部分,确保测试句不在训练集里出现。我习惯再补 10 句全对句子,专门检测「误改率」。
6.2 两个指标:纠错正确率与误改率
# 指标计算脚本 # precision = 正确改出的错误个数 / 模型改动的字符总数 # recall = 正确改出的错误个数 / 测试集中的真实错误总数 # F1 = 2 * precision * recall / (precision + recall) # 误改率 = 原本正确被改成错的字符数 / 测试集中正确字符总数跑完 50 句后,先看 F1 再看误改率。F1 反映纠错能力,误改率反映风险。Pycorrector 单跑的 F1 通常在 0.6-0.7,加上 ChatGLM3 兜底后 F1 能到 0.75-0.82,而误改率从 3% 降到 1% 以内,这个「补强不添乱」的效果就是本项目双引擎设计的核心价值。
6.3 上线前的一个习惯
从那以后我每次部署这类大模型功能前,都强制走一遍「50 句测试集 + 误改率统计」的流程,不让模型直接面对真实流量,而是让它在标注集面前先交代清楚自己几斤几两。这一套流程花费不到十分钟,却能在正式环境帮你挡掉绝大多数「看起来高级、用起来翻车」的尴尬。希望这份资源和你亲手调试的经验,能帮你在文本纠错这个方向上少走几段弯路。
本文还有配套的精品资源,点击获取