Llama-Factory训练中文小说续写模型的实践心得
Llama-Factory训练中文小说续写模型的实践心得
夜深人静,键盘轻响。你正为一部玄幻小说卡文发愁——主角刚踏入秘境,剧情却断了线。如果有个“写作搭子”,能顺着你的笔触自然延展情节,会是怎样一种体验?这并非幻想,而是如今借助大语言模型微调技术即可实现的现实。
但问题来了:通用大模型写出来的续章,要么太现代、要么没韵味,风格完全不对味;自己从头训练一个专属模型?动辄几十GB显存、复杂的代码流程,让多数开发者望而却步。有没有一条更平滑的技术路径?
答案是肯定的。在过去几个月里,我尝试用 Llama-Factory 搭建了一个专精于中文武侠与玄幻小说续写的定制化模型。整个过程无需编写复杂训练脚本,仅靠可视化界面和几行配置,就在单张RTX 3090上完成了对 Baichuan2-7B 的高效微调。最终生成的内容不仅语义连贯,还能模仿出类似《雪中悍刀行》那种冷峻苍茫的文风。
这套方案的核心,正是 Llama-Factory + QLoRA 的黄金组合。它不是实验室里的理论玩具,而是一套真正能让中小团队或独立开发者快速落地AI创作能力的实用工具链。
为什么选择 Llama-Factory?
在接触这个框架之前,我也走过不少弯路。最初试图基于 Hugging Face Transformers 手动搭建微调流程:写数据加载器、定义训练循环、处理 tokenizer 对齐……每一步都容易踩坑,调试成本极高。更麻烦的是,换一个模型(比如从 LLaMA 切到 ChatGLM),几乎要重写一半逻辑。
直到遇见 Llama-Factory,才真正体会到什么叫“开箱即用”。
这个开源项目由国内团队维护,深度适配中文生态,支持包括 Qwen、Baichuan、ChatGLM、LLaMA 等数十种主流架构。它的设计理念很明确:把大模型微调变成一件普通人也能操作的事。
其核心优势体现在四个方面:
- 统一接口:无论底层是哪种模型,训练命令和配置基本一致;
- 多模式微调集成:全参数微调、LoRA、QLoRA 都已内置,切换只需改个参数;
- WebUI 可视化操作:通过浏览器就能完成数据导入、参数设置、启动训练和监控指标;
- 端到端闭环:从数据预处理到模型合并导出,全流程覆盖,省去大量胶水代码。
尤其对于中文小说这类小众但高价值的应用场景,这种一体化平台的价值尤为突出。
技术底座:LoRA 与 QLoRA 如何改变游戏规则?
传统全参数微调的问题在于“太重”——以 7B 参数模型为例,光是 optimizer state 就可能占用超过 80GB 显存,必须依赖 A100 集群才能跑起来。这对个体开发者来说几乎是不可逾越的门槛。
LoRA(Low-Rank Adaptation)的出现改变了这一点。它的思想非常巧妙:不直接更新原始权重矩阵 $ W $,而是在旁边引入两个低秩矩阵 $ A \in \mathbb{R}^{d \times r} $、$ B \in \mathbb{R}^{r \times k} $,其中 $ r \ll d,k $,通常取 8~64。前向传播变为:
$$
h = (W + BA)x
$$
训练时只更新 $ A $ 和 $ B $,冻结主干参数。这样一来,可训练参数数量从数十亿降到百万级,显存消耗大幅下降,且推理时仍能恢复完整计算图。
而 QLoRA 更进一步,在 LoRA 基础上加入了三项关键技术:
- 4-bit 量化:使用 NF4(NormalFloat4)格式压缩预训练权重,显存减少约 60%;
- 双重量化(Double Quantization):对 LoRA 适配器中的量化常数也进行压缩,节省额外内存;
- Paged Optimizers:利用 CUDA Unified Memory,当 GPU 显存不足时自动将优化器状态卸载至 CPU 内存,避免 OOM。
这三者结合,使得原本需要多张专业卡的任务,现在一张 RTX 3090(24GB)就能搞定。我在实际测试中,使用 Baichuan2-7B-Base 模型配合 QLoRA,峰值显存控制在 18GB 左右,训练稳定流畅。
| 微调方式 | 显存占用 | 可训练参数比例 | 是否适合消费级GPU |
|---|---|---|---|
| 全参数微调 | >80GB | 100% | ❌ |
| LoRA | ~20GB | ~0.5% | ⚠️(需大显存卡) |
| QLoRA | <24GB | ~0.5% | ✅ |
可以说,QLoRA 是当前性价比最高的微调范式,特别适合像中文小说续写这样资源敏感但需快速迭代的场景。
实战:如何用 Llama-Factory 训练一个会写古风小说的AI?
整个流程可以分为五个阶段,全部可通过 WebUI 或命令行完成。
1. 数据准备:构建“上下文 → 续写”样本对
关键是要模拟真实写作场景。我收集了约 5,000 段来自《斗破苍穹》《凡人修仙传》《剑来》等热门作品的连续段落,提取“前一段 + 后一段”的结构,并构造如下 JSONL 格式的数据:
{"instruction": "请续写以下小说段落", "input": "夜色如墨,山风呼啸。林间小道上,一道黑影疾驰而过……", "output": "他脚步轻盈,仿佛踏叶无痕。忽然,前方传来一阵铃声,清脆却透着诡异……"} Llama-Factory 默认会按照 Alpaca 模板将其拼接为:
Below is an instruction that describes a task. Write a response that appropriately completes the request. ### Instruction: 请续写以下小说段落 ### Input: 夜色如墨,山风呼啸。林间小道上,一道黑影疾驰而过…… ### Response: 他脚步轻盈,仿佛踏叶无痕。忽然,前方传来一阵铃声,清脆却透着诡异…… 如果你希望加入风格控制,还可以自定义模板,例如插入提示词:“请以金庸武侠风格续写”。
2. 模型选择与训练配置
我选择了 Baichuan2-7B-Base 作为基础模型,原因有三:
- 中文理解能力强;
- 开源且商用友好;
- 社区已有成熟适配方案。
启动训练的命令如下:
CUDA_VISIBLE_DEVICES=0 python src/train_bash.py \ --stage sft \ --do_train \ --model_name_or_path baichuan-inc/Baichuan2-7B-Base \ --dataset chinese_novel_demo \ --template baichuan2 \ --finetuning_type lora \ --lora_target W_pack \ --output_dir ./output/chinese_novel_lora \ --per_device_train_batch_size 1 \ --gradient_accumulation_steps 8 \ --lr_scheduler_type cosine \ --learning_rate 5e-5 \ --num_train_epochs 3.0 \ --fp16 \ --plot_loss \ --quantization_bit 4 \ --device_map auto 几个关键参数说明:
--quantization_bit 4:启用 4-bit 量化,开启 QLoRA;--lora_target W_pack:Baichuan 模型将 q/v/k 投影层融合为一个W_pack,需针对性注入;--gradient_accumulation_steps 8:虽 batch_size=1,但累积 8 步相当于全局 batch=8,保证梯度稳定性;--device_map auto:自动分片加载模型,支持显存不足时的 offload。
整个训练过程约耗时 6 小时(RTX 3090),loss 曲线平稳下降,未见震荡。
3. 模型合并与导出
训练完成后,使用内置工具将 LoRA 权重合并回原模型:
python src/export_model.py \ --model_name_or_path baichuan-inc/Baichuan2-7B-Base \ --adapter_name_or_path ./output/chinese_novel_lora \ --export_dir ./merged_model \ --export_quantization_bit 4 \ --export_device cuda 输出的是标准 HuggingFace 格式的模型目录,包含 config.json、tokenizer.model 和 pytorch_model.bin,可直接用于推理。
4. 推理测试:看看它会不会“写故事”
加载合并后的模型,输入一段开头:
“残阳如血,黄沙漫天。边关城楼上,一名白衣剑客负手而立……”
模型续写如下:
“他的目光遥望着远方的地平线,那里有一骑快马正疾驰而来。风吹动他的衣角,发出猎猎声响。他知道,那个人终于来了——十年之约,今日终须一战。”
语气沉郁、节奏紧凑,颇有几分古龙风味。再试一次,换成玄幻风:
“灵根被废,丹田尽毁。少年跪在宗门广场,任雨水冲刷脸庞……”
续写:
“但他眼中没有屈服,只有燃烧的怒火。那一夜,他在禁地深处挖出了一块刻满符文的石碑,传说那是上古炼气士留下的传承……”
逻辑通顺,情绪递进合理,甚至出现了“禁地”“符文”“炼气士”等典型设定词汇——说明模型已经学会了该类文本的表达范式。
5. 部署上线:打造一个小说续写API服务
最后一步是部署。我用 FastAPI 包装了一个简单的推理接口:
from transformers import AutoModelForCausalLM, AutoTokenizer import torch model = AutoModelForCausalLM.from_pretrained("./merged_model", device_map="auto") tokenizer = AutoTokenizer.from_pretrained("./merged_model") def generate(text, max_new_tokens=200): inputs = tokenizer(text, return_tensors="pt").to(model.device) outputs = model.generate( **inputs, max_new_tokens=max_new_tokens, do_sample=True, temperature=0.8, top_p=0.9 ) return tokenizer.decode(outputs[0], skip_special_tokens=True) 前端则用 Gradio 搭了个交互页面,供编辑试用。反馈相当积极:比起原来通用模型的“塑料感”输出,这个定制模型更像是“懂行”的写手。
遇到了哪些坑?又是怎么解决的?
任何项目都不会一帆风顺。以下是我在实践中总结的几点经验教训:
问题1:生成内容口语化严重,缺乏“古意”
初期训练后发现,模型喜欢用“然后”“所以”“我觉得”这类现代口语连接句式,破坏氛围。
解决方案:
- 在训练数据中剔除夹杂网络用语的段落;
- 在 prompt 中加入风格锚点,如“仿明代话本笔法”;
- 使用更高阶的模板机制,在输入中注入风格标记。
问题2:长文本连贯性差,写着写着就偏题
小说续写不同于问答,要求长时间维持角色设定和情节走向。
改进措施:
- 控制生成长度(建议不超过 200 token),避免失控;
- 引入滑动窗口机制,在后续生成中重复输入前文关键句;
- 后期可考虑接入 RAG,动态检索相关背景知识辅助生成。
问题3:小规模数据下容易过拟合
5,000 条样本看似不少,但对于 7B 模型仍是杯水车薪。
应对策略:
- 设置早停机制(early stopping),观察验证集 loss;
- 使用 dropout=0.05 进行正则化;
- epoch 数控制在 2~3 轮,避免反复扫同一数据。
设计背后的权衡思考
在整个过程中,有几个关键决策点值得深入探讨:
LoRA 秩(rank)选多大?
我对比了 r=32、64、128 三种设置:
r=32:显存友好,但风格捕捉能力弱,生成较平淡;r=64:平衡点,既能学习复杂语义,又不会过度拟合;r=128:性能略优,但显存逼近上限,训练变慢。
最终选定 r=64,并配合 alpha=128(即缩放比为 2),符合 LoRA 论文推荐的经验公式。
学习率该怎么设?
QLoRA 对学习率更敏感。尝试过 1e-4,结果 early divergence;降到 5e-5 后收敛平稳。最终采用 cosine 衰减,在第 2 个 epoch 末尾进入平台期,正好停止。
数据质量 vs 数量?
与其堆数量,不如提质量。我花三天时间人工清洗数据,删除语病明显、风格混杂的样本,虽然总量少了 30%,但训练效果反而提升显著。这也印证了一个观点:在领域微调中,干净、一致的数据比海量噪声更有价值。
写在最后:平民化AIGC的时代正在到来
回顾整个项目,最让我感慨的不是技术本身,而是它的“可及性”。几年前,训练一个大模型意味着组建团队、申请算力、熬代码;而现在,一个人、一台消费级显卡、一个开源框架,就能做出具备实用价值的垂直模型。
Llama-Factory 正是这一趋势的缩影。它降低了大模型微调的认知门槛和工程成本,让更多创作者、产品经理、独立开发者能够亲手打造属于自己的“AI写手”“AI编剧”“AI助手”。
未来,随着更多高质量中文语料的释放、微调算法的持续演进,我们或许会看到一批“风格化模型”涌现:专写悬疑的、擅长宫斗的、精通科幻的……每一个都像是某个作家的精神分身。
而这一切的起点,也许就是你现在打开终端,运行的那一行 train_bash.py。