简介:一个可直接基于自有语料训练的中文聊天机器人项目,以 Python 为核心,同时提供 TensorFlow 2.x 与 PyTorch 两个主流框架的版本。项目内置 seq2seq、SeqGAN、分布式 seq2seq 等模型路线,适合算法学习者、服务端开发者在智能客服、在线问答、闲聊机器人等场景中快速落地和二次开发。压缩包共 85 个文件,大小约 37.94MB,以 18 个 Python 脚本为核心,并搭配 20 个 js、15 个 css、5 个 html 构成可视化与交互前端,另有 vocab 词表、data 示例数据和 md 使用说明。目录按 Tensorflow2.0、SeqGANchatbot、Distribute_seq2seqchatbot、Chatbot_pytorch 等模块拆分,方便按需调用;工程在 1.0 版本中重新整合了整体结构,使用更统一。目前已有 1069 人浏览学习。拿到后可直接替换自己的语料进行训练,也能借鉴 FAQ 问答与闲聊的无缝切换机制搭建更自然的对话服务;项目还规划了基于 Transformer 的预训练模型接入方向,并提供依托 horovod 的分布式训练配置,支持 batch_size 训练模式。对照多个版本可清晰理解生成式对话模型从基础到分布式训练的演进,节省从零搭建和调参的时间。
1. 为什么“能自己训练”的中文聊天机器人,落点其实是微调而不是从零训练
一个可以自己训练的中文聊天机器人,听上去像是要把一堆语料喂给某个程序,然后它就学会用你想要的口吻回答问题了。这正是标题背后真正的需求:用 Python 代码和自有数据,训练出一个领域、语气、边界都由自己控制的中文对话模型。但真动起手来你会发现,“训练”这个词的门道比想象中多:它不等于把文本丢给模型,而是“语料整理 → 基底模型选择 → 训练参数调整 → 部署验证”一整套工程。下面我以 Python 生态里最常见的开源微调工具链为例,把这条路完整走一遍。运维、后端、算法新人照着做能跑通第一版;有模型基础的人,重点看参数设置和验证方法。
2. 训练前想清楚:为什么是“微调+LoRA”,而不是“从零训练”
2.1 从零训练为什么撑不起来:语料量级和算力账
一笔账先算在前面。所谓“自己的语料”,绝大多数情况下是几百到几万条对话记录。假设你整理出 1 万条、平均每条 100 字,也才约 100 万字,换算成 token 只有数百万。这个量级离从零预训练模型的需求差着两三个数量级——一个 1B 参数级别的 Transformer,预训练语料通常要数十亿 token;7B 模型的预训练语料更是以万亿 token 计。把这一小把数据反复喂给模型,网络大概率会“背答案”而不是“学规律”,遇到问法一换就会崩。
另一个问题是算力。即便抛开数据量,从零训练一个 10B 量级的模型,单卡训练周期以月为单位,多卡集群又不是个人项目能随手获得的。而开源社区里已经有一批中文理解能力相当强的底座模型,它们完成了耗资巨大的预训练,语言能力、常识、推理能力都是现成的。你要做的不是教模型学中文,而是在它已有的能力上做“行为修正”:让它知道在你的业务场景里,什么问题该用什么语气答、答到什么详细程度。
所以,这个标题下真正普遍的做法是:选择一个开源中文底座模型,整理自有语料,在其上进行微调。这也解释了为什么你在搜索时会看到大量“lora训练”“模型训练”“llama factory”相关的结果——它们讨论的都是同一条技术路线。
2.2 用 LoRA 控制显存消耗:全参微调与 QLoRA 的账
那为什么不直接全参数微调?算一笔显存账就清楚了。以一个 7B 参数的模型为例,bf16 权重本身约占 14GB;训练时还要额外保存梯度、优化器状态(AdamW 的动量和方差),这些加起来很容易超过 100GB,这不是一张消费级显卡能承受的。
LoRA 的思路是冻结原模型全部权重,只在每层 Transformer 中注入两个低秩矩阵 A 和 B,只训练这两个矩阵。7B 模型可训练参数只有几百万到千余万,显存压力骤减。再配合 4-bit NF4 量化(即 QLoRA),把冻结权重压缩到约 3.5GB,整卡 16GB 就能训练 7B 模型,24GB 的 4090 则跑得相当从容。
| 方案 | 可训练参数量 | 所需显存(7B 模型估算) | 适用场景 |
|---|---|---|---|
| 全参数微调 | 约 6.9B | 100GB 以上 | 有数据中心级资源,不适用于个人项目 |
| LoRA(bf16 基座) | 1000 万以内 | 约 24GB | 单卡 4090 可跑,效果稳定 |
| QLoRA(4-bit 基座) | 1000 万以内 | 约 16GB | 单卡 3060/3080 可跑,个人首选 |
2.3 中文底座模型怎么选:从 Qwen2.5 系列起步
底座模型的选择直接决定微调效果的上限。个人微调中文聊天机器人,目前社区里最省心的选择是 Qwen2.5 系列:中文预训练语料占比高,分词器对中文友好,Instruct 版本自带一套清晰的对话模板,微调时可以少处理很多对齐问题。
如果你的语料场景更接近严肃客服,ChatGLM 系列也可以作为备选。不过要注意各模型的许可证条款,商用场景需逐一确认。如果你只有一张低显存卡(如 8GB),可以考虑 Qwen2.5-3B 或更小的 1.5B 版本,训练速度更快,但对复杂业务语义的理解会弱一些。选择时可以先拿几个典型问题在小模型上试跑,再决定是否升级到 7B。
3. 用自己的语料跑通第一轮 LoRA 训练
3.1 训练环境的最小配置与 Python 依赖安装
训练环境是卡住新手的第一道坎。硬件上,推荐 NVIDIA 显卡,显存 16GB 以上可以流畅跑 7B QLoRA;12GB 也能跑,但要控制序列长度和 batch size;8GB 建议改用 3B 模型。CPU 只能做推理,训练效率低到难以接受。
最近常有朋友问 python 安装教程,这里给出我的固定建议:不管 Windows 还是 Linux,用 Anaconda 管理 Python 环境,别用系统自带的 Python,否则后面 PyTorch 和 CUDA 版本冲突时会很痛苦。如果你日常在 VS Code 里写代码,记得装好 Python 扩展后,把解释器切换到创建的虚拟环境。
conda create -n chatbot python=3.10 -y conda activate chatbot pip install torch --index-url https://download.pytorch.org/whl/cu121 pip install transformers datasets peft trl accelerate pip install llama-factory说明:torch 安装命令中的cu121表示 CUDA 12.1 对应的版本;驱动较新的机器也能兼容。后面四个库分别是模型加载、数据集处理、LoRA 实现、训练器,llama-factory是封装好的微调平台,内部已经依赖了前面这些组件。若下载慢,可在 pip 后追加-i参数指向国内 PyPI 镜像;模型权重下载同样可以设置镜像源。
3.2 把日常语料整理成 SFT 的 instruction/input/output 格式
微调用的语料有固定的组织格式。当前主流工具链统一采用“指令、输入、输出”三段式结构:instruction放系统指令或角色设定,input是用户提问,output是期望回答。这种格式的好处是训练模板统一,换工具时不用重写数据。
{"instruction": "你是某电商平台的客服助手,回答要简短、礼貌、口语化", "input": "我下单后多久发货?", "output": "您好,默认48小时内发出哦~"} {"instruction": "你是某电商平台的客服助手,回答要简短、礼貌、口语化", "input": "发货后可以修改地址吗?", "output": "发货后就改不了了,建议您下单前仔细核对地址。"}LLaMA Factory 需要把数据集注册到data/dataset_info.json中,文件结构与上面的字段一一对应:
{ "my_chat": { "file_name": "my_chat.json", "columns": { "prompt": "instruction", "query": "input", "response": "output" } } }注意同一个instruction不要反复出现一千次,模型会误以为这是必须原样复述的内容;指令变化太少,等于在教模型“所有问题都用同一套话回答”。另外数据量最少也要几百条,低于这个量级训练基本无意义。更多时候,质量比数量更重要——后面第 5.3 节会讲怎么用少量种子数据扩充语料。
3.3 清洗语料的几个必须写的过滤规则
语料进入训练前,至少要过一遍清洗脚本。坏数据对训练结果的污染远大于数据量不足:一条带 HTML 标签的回答,可能让模型在真实场景里突然吐出“<br>”;一条重复出现十次的样本,会让模型对某个问题产生固定输出。下面这个脚本覆盖了最基本的四类过滤。
import re import json def clean_text(text: str) -> str: # 去掉 HTML 标签和多余空白 text = re.sub(r"<[^>]+>", "", text) text = re.sub(r"\s+", " ", text).strip() return text def is_valid_pair(inp: str, out: str) -> bool: # 过滤空样本、过短样本、复读机样本 if len(inp) < 4 or len(out) < 2: return False if out.count(out[:10]) > 3: return False return True with open("raw.json", "r", encoding="utf-8") as f: lines = [json.loads(line) for line in f if line.strip()] seen = set() deduped = [] for item in lines: inp = clean_text(item["input"]) out = clean_text(item["output"]) if not is_valid_pair(inp, out): continue key = (inp[:20], out[:20]) if key in seen: continue seen.add(key) deduped.append({"instruction": item["instruction"], "input": inp, "output": out}) with open("my_chat.json", "w", encoding="utf-8") as f: for item in deduped: f.write(json.dumps(item, ensure_ascii=False) + "\n")clean_text处理明显的非对话噪声;is_valid_pair里的复读检测专门对付“嗯嗯嗯嗯”“好的好的好的”这类敷衍样本;seen集合通过前 20 个字符做轻量去重,避免同一问题在数据集中占比过高。清洗完再人工抽查 50 条,确认没有格式错位,就可以注册到dataset_info.json里了。
3.4 用 LLaMA Factory 跑起第一轮微调:命令与参数选择
LLaMA Factory 是当前训练环境里最顺手的微调平台:数据注册、LoRA 训练、模型导出、推理验证都可以用命令行完成。训练 7B 模型的最小命令如下:
llamafactory-cli train \ --model_name_or_path Qwen/Qwen2.5-7B-Instruct \ --dataset my_chat \ --template qwen \ --finetuning_type lora \ --quantization_bit 4 \ --output_dir ./output/chatbot \ --num_train_epochs 3 \ --learning_rate 2e-4 \ --lora_rank 8 \ --lora_alpha 16 \ --per_device_train_batch_size 1 \ --gradient_accumulation_steps 8 \ --max_seq_length 1024 \ --logging_steps 10 \ --save_steps 200template要按底座模型的对话模板填,Qwen 就是qwen,换 ChatGLM 则改为chatglm。quantization_bit 4表示用 NF4 量化基座,显存不足时优先检查这一步。output_dir是 LoRA 权重的输出目录。
下面这几个参数值得单独列出来:
| 参数 | 推荐初始值 | 调参方向 |
|---|---|---|
learning_rate | 2e-4 | 数据量少可降到 1e-4,防止过拟合 |
num_train_epochs | 3 | 语料少于 1000 条时升到 5-8 |
lora_rank | 8 | 领域专业性强可升至 16,但显存随之上涨 |
lora_alpha | 16 | 一般保持为 rank 的 2 倍 |
max_seq_length | 1024 | 回答过长才需要加大,越大越耗显存 |
gradient_accumulation_steps | 8 | 显存不够就把 batch 再拆细 |
训练时终端会滚动输出 loss。正常情况 loss 应该缓慢下降,最终落在 0.5-1.5 区间;如果 loss 从一开始就趋近于 0,说明数据里有和基座原分布严重重复的文本;如果 loss 反复震荡不降,优先检查语料里是否存在大量相互矛盾的问答对。训练中途 OOM,分两步处理:先降per_device_train_batch_size到 1,再降max_seq_length。
4. 把训练好的模型拉起来对话
4.1 导出合并后的模型,避免运行时依赖 adapter
训练产出的是 LoRA adapter 权重,体积只有几百 MB。推理时有两种用法:一种是运行时加载 adapter,灵活但容易踩 dtype 不一致的坑;另一种是直接把 LoRA 权重合并进底座模型,导出一个完整的模型目录,我一般推荐后者,省心且方便部署。导出命令如下:
llamafactory-cli export \ --model_name_or_path Qwen/Qwen2.5-7B-Instruct \ --adapter_name_or_path ./output/chatbot \ --template qwen \ --finetuning_type lora \ --export_dir ./exported_modelexport_dir就是合并后的模型目录,里面有config.json、model.safetensors、tokenizer等文件。导出完成后,可以先加载这个目录做一个 10 分钟的冒烟测试,确认回复风格符合预期再进入服务化阶段。合并后的模型体积约 15GB(7B bf16),如果磁盘吃紧,也可以在导出时追加--export_quantization_bit 4,直接把导出结果量化成 int4。
4.2 写一个带 system prompt 的本地对话脚本
对话脚本的核心是正确组装 messages 列表并传给底座模型。LLaMA Factory 的template qwen在训练时即按 Qwen 的 ChatML 格式拼接,所以推理时也要保持一致:用system消息固定角色,把历史对话放进history,最后追加当前问题。
from transformers import AutoModelForCausalLM, AutoTokenizer model_dir = "./exported_model" tokenizer = AutoTokenizer.from_pretrained(model_dir, trust_remote_code=True) model = AutoModelForCausalLM.from_pretrained( model_dir, torch_dtype="auto", device_map="auto" ) system_prompt = "你是某电商平台的客服助手,回答简短、礼貌、口语化。" history = [] while True: user_input = input("你: ").strip() if user_input in ("exit", "quit"): break messages = [{"role": "system", "content": system_prompt}] for h in history[-4:]: messages.append({"role": "user", "content": h[0]}) messages.append({"role": "assistant", "content": h[1]}) messages.append({"role": "user", "content": user_input}) inputs = tokenizer.apply_chat_template( messages, add_generation_prompt=True, return_tensors="pt" ) outputs = model.generate( inputs.to(model.device), max_new_tokens=512, temperature=0.7, top_p=0.9, repetition_penalty=1.05, ) reply = tokenizer.decode(outputs[0][inputs.shape[-1]:], skip_special_tokens=True) print("机器人:", reply.strip()) history.append((user_input, reply.strip()))history[-4:]只保留最近四轮对话,防止上下文超出max_seq_length。temperature越低回复越保守,客服场景建议 0.7 左右,闲聊场景可以提升到 0.9。repetition_penalty大于 1 可以抑制复读,太低则可能让回答变得啰嗦。如果发现模型只回几个字,把max_new_tokens调大;如果回答不相关,优先怀疑 system prompt 和训练时的角色设定不一致。
4.3 用 FastAPI 包装成 HTTP 服务,供其他系统调用
本地脚本只能自己玩,要接入实际业务,得把它包成一个 HTTP 接口。FastAPI 是当前最简单的方式:请求体传消息和历史记录,接口返回回复文本。这样 QQ 聊天机器人、网页客服、企业微信机器人等场景,只需要让网关把收到的消息 POST 到这个接口,再把返回值发回对话窗口即可。
from fastapi import FastAPI from pydantic import BaseModel app = FastAPI() class ChatRequest(BaseModel): message: str history: list[list[str]] = [] @app.post("/chat") def chat(req: ChatRequest): messages = [{"role": "system", "content": system_prompt}] for h in req.history[-4:]: messages.append({"role": "user", "content": h[0]}) messages.append({"role": "assistant", "content": h[1]}) messages.append({"role": "user", "content": req.message}) inputs = tokenizer.apply_chat_template( messages, add_generation_prompt=True, return_tensors="pt" ) outputs = model.generate( inputs.to(model.device), max_new_tokens=512, temperature=0.7, top_p=0.9, repetition_penalty=1.05, ) reply = tokenizer.decode(outputs[0][inputs.shape[-1]:], skip_special_tokens=True) return {"reply": reply.strip()}启动服务后,用curl -X POST http://localhost:8000/chat -H "Content-Type: application/json" -d '{"message": "今天能发货吗", "history": []}'验证即可。注意两个生产环境必须处理的问题:一是接口没有任何鉴权,不能暴露在公网;二是一次请求触发一次完整推理,并发太高会 OOM,可以在前面加一个简单的排队或改用更专业的推理框架加速。首次请求会触发模型加载,响应时间较长,上线前最好在启动脚本里做一次预热请求。
5. 训练后必做的验证与语料迭代技巧
5.1 训练后先做这 5 个验证,别急着拿去用
训练完成不等于可以上线。拿同一批固定问题去测训练前后的模型,你会发现很多隐藏问题只有在对比时才会暴露。
第一,验证训练语料本身能否被准确回复,这是最基础的“记住了”测试。第二,换几种说法问同一个问题,比如把“多久发货”改成“发货要等几天”,考察模型是否理解语义而不是死记硬背。第三,测一个和业务无关的常识问题,比如“中国的首都是哪里”,如果在微调后答错了,说明出现了灾难性遗忘,训练过度了。第四,连续追问五轮,观察回复是否出现重复短句或逻辑断裂,LoRA 训练不足时最容易出现这种症状。第五,把用户可能用来“越狱”的敏感输入跑一遍,确认模型没有被自己的语料带偏。
这几项验证建议写成一个evaluate_case.py脚本固化在仓库里,每次训练后统一执行。
5.2 语料太少时的三个补救办法:混合指令、调低学习率、轮次递减
自有语料不足 500 条时,直接训练会很快过拟合。常见的补救方案有三个。
第一个是混合指令数据:在my_chat之外混入 30% 左右的通用指令数据(比如 Alpaca 中文版的子集),让模型在学业务的同时保留通用对话能力,这比只拿少量业务语料硬练稳得多。第二个是调低学习率:语料越少学习率越低,1e-4 甚至 5e-5 都可以试,虽然收敛慢一点,但不容易把底座模型原有的参数冲坏。第三个是轮次递减:先以 5 个 epoch 跑一版,观察验证集 loss;如果第 3 轮之后 loss 就开始回升,说明已经过拟合,回退到第 3 轮的 checkpoint 即可。
5.3 把一条 FAQ 扩写成一组多样化对话
语料扩充是提升效果投入产出比最高的环节。以一条 FAQ 为例——“多久发货?答:默认48小时内发出”——不要只放原句,而是扩写成一组语料:把问题换成口语说法“今天下单明天能到吗”,换一种回答方式“一般两个工作日左右发,急的话可以备注加急”,再加一条多轮对话“在吗?在的请问有什么可以帮您?我想问下发货时间”。这样模型学到的是“用户问法多样但意图一致”的映射,而不是一个固定的 QA 对。
每次扩写后重新训练,再跑 5.1 的验证脚本,看“未见过的说法”这一栏的通过率有没有提升。这个“扩充语料 → 重新训练 → 跑验证脚本”的循环,就是所谓“自己训练”真正迭代起来的样子。把每次训练的数据版本和验证结果一起提交进 Git,比单靠记忆靠谱得多。
本文还有配套的精品资源,点击获取