微信导出聊天记录清洗为数据集微调Qwen2.5-7B
微信导出聊天记录清洗为数据集微调Qwen2.5-7B
1. 起点:把微信聊天记录炼成数据集
之前就用过一个项目是能把大模型对接微信,而且我发现可以用本地llama接口直接跑大模型。只不过回复很生硬,所以我就想拿数据集微调大模型把他更偏向口吻像人。所以就拿alpha这个开源项目导出了json格式的微信数据集,给ai让他帮我转成llamafactory可读的数据集。
1.1 提取:开源工具 alpha 把聊天记录拉成 JSON
第一步解决”数据从哪来”。用开源项目 alpha 从微信提取聊天记录,导出为 JSON:225,627 条原始消息、21 个私聊会话、全部文本。
这一步的意义在于:模型的口吻不需要虚构,直接从真人对话里学。每个人说话的节奏、口头禅、话题跳跃方式,都原样躺在原始记录里。
1.2 注册:让 LLaMA-Factory 认识这份数据
微信导出的 JSON 有自己的结构(每条消息带 direction:1 = 好友,2 = 号主本人),LLaMA-Factory 当然不认识。转换目标是最通用的 sharegpt / messages 风格:
{"messages":[{"role":"user","content":"问句"},{"role":"assistant","content":"回答"}]}再配一个 dataset_info.json 条目放进 data/ 目录,数据集就被注册识别;Qwen 模板由 LLaMA-Factory 依据模型名自动套用
1.3 清洗:八条
注册只是让框架”认得”,清洗才决定数据”干不干净”。执行口径:
- 格式固定:每条样本都是
{"messages":[{"role":"user",…},{"role":"assistant",…}]},user 永远是提问方、assistant 永远是回复方,不翻转角色。 - 极简短句尽量减少:单轮样本要求 user ≥ 3 字、assistant ≥ 3 字且合计 ≥ 8 字——「吃饭了吗 / 嗯」这类直接剔除;多轮样本要求合计 ≥ 24 字,且至少一轮两边都有实义。
- 剔除空回答 / 纯符号:内容去掉 emoji 占位
[xx]、标点、空白后,若无任何中英文,整条丢弃。 - 长度硬上限:用 Qwen2.5 官方 tokenizer 精确计数,单样本 ≤ 768 token(实测最大 767);多轮最多 4 轮,超长对话按 ≤ 4 轮切片。
- 文本清洗:去除
\r、控制字符、特殊空白(\xa0\u3000\u200b\ufeff等),3+ 连续换行压缩为 2、多余空格压缩;不编造任何对话,只使用原始文本。 - 分段与合并:同一会话内按 15 分钟间隔切分对话;同一人连续发出的多条消息用换行合并为一条;开头多余的 assistant、结尾没有回复的 user,一律裁掉。
- 去重:整样本完全相同的去掉。
- 直接可用:不做任何模板层 hack。
1.4 关键设计:双向样本
这份数据最有意思的设计是双向生成:
- 正向(好友问 / 你答):好友是 user,号主本人是 assistant → 模型学”以你的口吻回复”,这是”数字分身代聊”的核心;
- 反向(你问 / 好友答):号主是 user,好友是 assistant → 模型同时学 21 位好友的口吻,对话风格更多样、更像真人在聊。
两个方向用完全相同的清洗 / 分段 / 轮数 / token 规则生成,再全局去重合并:14,831 + 14,207 − 1 = 29,037 条。
代价也说清楚:反向集里 assistant 是 21 个人语气的混合体,风格不如正向一致。只想要”纯数字分身”的话,删掉反向样本重新生成即可。
1.5 产出与一条刻意的设计
最终产出 wechat_sft.jsonl:29,037 条、13.4MB,token 分布均值 90、p99 312、最大 767。清洗的代价也记账:无意义短句丢了 12,757 条、超长刷屏 420 条、单轮过短约 3,641 条、多轮无实义约 2,335 条、token 超限收缩 56 条,外加双向各约 20,815 次的会话头尾裁剪。
还有一条刻意的设计:这份数据不含 system prompt。因为 system 是”助手味”最大的来源,而数字分身要的是”无 AI 味”的口语风格。
有意思的是,后来做女生语气数据集时,在同一条注册 / 清洗流水线上做出了完全相反的系统设计——那边把 system prompt 当”人格开关”用。同样的地基,两种相反的设计,服务两个不同的目标,这个对照留到第 3 章展开。
数字分身跑通之后,野心自然变大:换成公开的大规模中文聊天数据,效果会不会更好?——于是有了第二章那次”越训越傻”的事故。
2. 事故现场:500 轮之后,模型傻了
一切始于一句抱怨:“我用 LCCC 训练了 500 轮,回复变得傻傻的。”
第一反应是打开日志,而日志给出的答案和”玄学”完全无关:
- “500 轮”其实是 500 步。按训练配置算,总共需要 11,876 步才够一个 epoch——500 步只跑了 4.2%,模型连一遍数据都没见完。
- eval loss 从 3.820 只降到 3.758。这个降幅约等于没降,证明这批数据里几乎没有可学的有效信号(或者说,以当时的配置,信号没被学到)。
- 学习率 1e-4,且没有 system prompt。对 instruct 基座来说这个 lr 偏冲动,又没有对话格式约束,指令能力被平推冲掉了。
还有个插曲:回收站里那个”导出的 adapter”,经核对其实是另一条 run(高情商 170 条、5 epoch)的产物,跟这次 LCCC run 没关系。日志和产物对不上,是微调翻车现场最常见的一坑。
诊断结论一句话:不是模型笨,是数据没选对、训练没跑够、格式没管住,三件事叠加。
3. 选型:什么数据能”塑造语气”?
需求很明确:要一个”像女生一样聊天”的模型。语气这种属性,靠的是稠密的示范信号——每一句回复都在告诉模型”这种情况下,她会这么说”。
候选方案里最有吸引力的是某个繁体角色卡数据集(社区里流传较广的 RP 语料),但下载后做了一次内容安全扫描:2,109 张角色卡全部命中成人向关键词,无一例外。直接弃用,并把源文件清掉了。
最终选了一条更稳的路线:
| 来源 | 角色 | 条数 |
|---|---|---|
BelleGroup generated_chat_0.4M(39.6 万条通用角色扮演底料) | 过滤出女生向对话 | 1,555 |
| 「中文高情商可爱聊天数据集」 | 人格锚点 | 170 |
| 同底料 reservoir 随机抽样 | 通用压舱石 | 19,992 |
| 合计 | 21,717 |
3.1 关键一步:女性换位
从底料过滤女生向对话时有个细节:原始对话里女性角色常常出现在 user 侧(第一说话人)。如果直接拿去训练,模型学到的是”捧哏”而不是”主侃”。
处理办法是整体反转对话顺序:只要女性原本在 user 侧,就把整段对话翻过来,保证模型要生成的回复永远出自女性之口。最终 500 条经过反转、572 条天然如此,其余为散文式或缺人名等形态。
3.2 人格只占 7.9%,是刻意压低的
人格样本(1,555 + 170)加起来只占 7.9%。这不是抠门——语气是稠密信号,每句话都在示范风格,不靠堆量;真正怕的是人格数据占比过高,把通用对话能力挤掉,模型变成”只会撒娇,别的都不会”。
剩下 92% 的通用数据 system 为空串,是压舱石,专门防”变傻”——这也是对第二章那次事故的直接回应。
3.3 system prompt:整套设计的核心开关
人格样本统一带这样一段 system prompt:
你是一个22岁的女生,性格温柔自然,说话亲切俏皮,擅长共情和安慰人。回复像身边的女生朋友一样,简短、口语化。训练时两种样本混在一起喂:带 system 的是人格样本,不带的是通用样本。模型学到的不是”把自己变成一个女生”,而是——“看到这个 system 就切换成女生语气,看不到就保持正常助手”。
这就是 LCCC 失败的反面:那次训练没有任何”开关”可抓,模型只能把各项能力一起平均冲淡。推理部署时必须原样带上这段 system prompt,一个字都别改,否则人格效果大打折扣。
4. 让 LLaMA-Factory 认识这份数据
数据做成 jsonl,每条一个 conversations 字段 + 一个 system 字段,注册文件只有一条:
{ "girl_persona_mix": { "file_name": "girl_persona_mix.jsonl", "formatting": "sharegpt", "columns": { "messages": "conversations", "system": "system" } }}踩过的坑值得单独说:
- sharegpt 格式的硬规则:角色必须奇偶位严格交替(偶数 human、奇数 gpt),SFT 场景 messages 长度必须是偶数。入库前做了一次全量校验,21,717 条零违规。
- columns 映射的键必须每条记录都存在。这个版本里每条都带
system键(通用样本为空串),少一个 key,转换器直接 KeyError。 - 冒烟测试:注册后随机抽 300 条走完整转换链路,全过;带 system 的比例和人格组占比吻合,说明配比没在转换环节被破坏。
数据分布也对齐了训练参数:长度 p50 为 297 字、p99 为 585 字——这直接决定了 cutoff_len 选 1024(覆盖 99.8% 的样本,再大纯属浪费显存)。
5. 训练参数:每个数字在解决什么问题
配置走 QLoRA(4bit 量化 + LoRA),目标是在 12GB 显存的卡上把 7B 模型训动。逐组解释:
显存与精度
| 参数 | 值 | 为什么 |
|---|---|---|
quantization_bit | 4 | 7B bf16 权重约 15GB,压到 4bit 约 5.5GB,QLoRA 标准玩法 |
upcast_layernorm | true | LayerNorm 对量化误差最敏感,留在 bf16 防止误差逐层累积(QLoRA 论文原始建议) |
cutoff_len | 1024 | 照着数据 p99=585 字定的,覆盖 99.8% |
LoRA 结构
| 参数 | 值 | 为什么 |
|---|---|---|
lora_rank | 16 | 学”说话风格”这种低维属性,16 足够;再大容易背样本 |
lora_alpha | 32 | α = 2×r 的经验缩放 |
lora_target | all | 语气体现在每一层线性变换里,只调 attention 学不全 |
lora_dropout | 0.05 | 轻量正则,配合只跑 1 epoch |
优化策略(这组是全章的教训)
| 参数 | 值 | 为什么 |
|---|---|---|
learning_rate | 5e-5 | LoRA 标准区间中低段。事故那次用的是 1e-4,对 instruct 模型偏冲动,这次保守一半 |
num_train_epochs | 1 | 全场最关键:人格样本只有 1,725 条,跑第二遍必然过拟合、顺带冲掉通用能力 |
lr_scheduler_type | cosine | 平滑退火 |
warmup_ratio | 0.03 | 约 80 步热身,让适配器开局稳一点 |
数据与评估:template: qwen 必须和 Qwen2.5 对话格式对齐(错了整个训练白跑);验证集留 2% 盯过拟合;eval 每 200 步、存档每 500 步。
6. 整理:627MB 的断舍离
跑通之后工作区惨不忍睹:77MB 的废弃数据集、524MB 的底料源文件、两份副本、十几个草稿脚本。收敛规则只有一条——新数据集文件夹里只留主数据集和注册集:
D:\数据集\女生语气混合数据集\├── girl_persona_mix.jsonl # 21,717 条,25.8MB└── dataset_info.json # 单条目注册训练配置的 dataset_dir 同步指向新文件夹,原来的注册表(true_chinesechat)恢复成原始的 lccc + high_eq_cute 两条。共释放约 627MB。
收敛后做了完整回归:注册表从新目录 resolve 成功、转换冒烟 100/100 通过、YAML 384 个键全部解析正确。
7. 复盘:从日志里读出”哪百步最好”
训练是从 LLaMA-Factory WebUI 启动的(数据 md5 核对过,就是收敛那份)。日志分析有一个清晰的结论——eval loss 全程单调下降,零过拟合:
| 检查点 | 步数段 | eval_loss | 较上一段 |
|---|---|---|---|
| checkpoint-100 | 1–100 | 1.9723 | — |
| checkpoint-200 | 101–200 | 1.9587 | -0.0136 |
| checkpoint-300 | 201–300 | 1.9522 | -0.0065 |
| checkpoint-400 | 301–400 | 1.9476 | -0.0046 |
| checkpoint-500 | 401–500 | 1.9464 | -0.0012 |
| checkpoint-600 | 501–600 | 1.9433 | -0.0031 |
| checkpoint-700 | 601–700 | 1.9409 | -0.0024 |
| checkpoint-800 | 701–800 | 1.9399 | **-0.0010 |
两个不同问题的答案:
- 哪百步”效果最好”?701–800 段(checkpoint-800,eval_loss 1.9399),而且它既是历史最优也是最终存档。
- 哪百步”学得最多”?101–200 段——训练 loss 从 2.42 骤降 0.447,真正的模式学习发生在这一小段,之后就在 1.96 附近平台震荡做精细打磨。
还有个情况:这次 run 在 803/1290 步(62%)静默停止,日志里没有任何报错(无 Traceback、无 OOM),进程是被手动终止或窗口关闭带走的。考虑到增量已收敛到 -0.001 量级,剩余 490 步收益极小,checkpoint-800 可以直接用,也可以加载它续训。
附注:这次 run 的超参是 WebUI 默认那套(rank 32/alpha 64、只调 q/k/v/o、cutoff 2048),和第 5 节那份 YAML 不完全一样——上面的指标对应 WebUI 配置。想复现”设计值”,跑 YAML 即可。
8. 踩坑清单(全部真实发生)
- HuggingFace 直连被墙:换
hf-mirror.com,curl 记得-L跟随 302 到 CDN。 generated_chat_0.4M是 JSONL 不是 JSON 数组:按数组写的流式解析器只捞出 62,551 个对象,而字节扫描有 396,002 个"instruction"——差 5/6。必须逐行json.loads。解析器的假设要用文件头尾验证,别想当然。get_train_args([yaml路径])不加载 YAML:LLaMA-Factory 只在args=None且sys.argv[1]以 .yaml 结尾时才读配置文件;传列表会被当成普通 argv,YAML 被静默忽略,最后报一句极具误导性的Please provide model_name_or_path。验证配置必须模拟 CLI:设sys.argv再调。- 验证环境要用对 Python:LLaMA-Factory 相关验证一律用训练环境那个 Python(transformers 4.57.1 + torch + CUDA),受管的干净环境连 numpy 都没有。
- shell 里的反引号是命令替换:一次把含代码的说明文字直接写进 shell 命令,反引号内的
llamafactory-cli train ...被真实执行了一遍(在沙箱里跑到预处理阶段失败,无文件损害)。教训:含反引号/代码的文本,先落成脚本文件再执行。 - Windows 控制台是 GBK:
tasklist输出用 utf-8 解会崩,要decode('gbk')。
9. 结语
回头看,从微信聊天记录到 29,037 条数字分身数据,再到那次”变傻”事故和 21,717 条语气数据,最有价值的产出不是任何一个模型,而是一套可复用的方法论:
- 数据工程先于训练调参:提取、注册、清洗、校验,每一步都有明确规则和记账(丢了多少、为什么丢),训练只是最后一环。
- 语气类微调,配方比数量重要:少量高密度人格样本(7.9%)+ 大量通用压舱石 + system prompt 开关,三者缺一不可。
- 参数为数据服务:cutoff 照着 p99 定、epoch 照着人格样本量定、lr 照着基座类型定——每个数字都应该能回答”为什么不改成别的”。
- 日志是唯一的事实来源:最优 checkpoint、停止原因、配置差异,全在日志里,别靠感觉。
模型接下来还有迭代空间(剩余 490 步、或者换回设计值超参重训),但数据工程这一层——提取、配方、注册、验证、收敛——已经是可以复用的资产了。

