
简介基于Python的微信聊天记录专属聊天机器人项目面向毕业设计、课程设计及个人项目开发覆盖从微信聊天数据导出、解密清洗到模型微调、本地部署的完整链路。压缩包共8个文件核心为3个Python脚本分别负责数据预处理、聊天记录解密与模型接口调用另含README开发文档、许可文件以及3张运行效果截图包括alpaca-lora-ui界面示例整体仅151KB轻量却结构清晰。目前已有278人学习下载。源码经过严格测试可直接参考并延申用户可导入自己的微信聊天记录完成数据清洗与模型训练快速复现专属对话机器人开发文档还帮助理解指令微调、模型调用的关键流程便于在毕业设计或课程设计中二次开发。这份资源以极小的体量提供了一套从数据到模型的可运行方案适合希望低成本上手大模型应用的学生和开发者。1. 用微信聊天记录训练专属机器人先搞定脏数据再谈模型拿微信聊天记录训练一个“说话像你”的机器人这个想法听起来很酷但真正动手的人大多死在第一步微信的本地聊天数据库是加密的导出来的文本又全是语音、表情包、撤回消息的碎片连一句完整的话都凑不齐。这个基于 Python 的 chat4u 项目把整个链路拆成了三块——decrypt.py解密数据库、prepare_data.py构造训练语料、llama4openai-api.py封装成 API 服务中间还带了一份完整的docs开发文档和 alpaca-lora 微调界面截图。它适合毕业设计、课程设计也适合想给自己做一个“说话风格复刻”机器人的开发者。整条链路跑通后你拿到的不只是一个模型而是一套从私有数据到对话服务的完整工程方法。2. 微信聊天记录解密decrypt.py的原理与实操2.1 为什么聊天记录不是直接能读的 SQLite微信 Android 端的聊天记录存储在一个名为EnMicroMsg.db的 SQLite 数据库中但这个库是加密过的后缀名虽然叫.db直接用 SQLite 工具打开只会得到一堆乱码或「file is not a database」的报错。加密算法是 AES-192-CBC密钥由手机的 IMEI 和微信账号的uin共同派生数据库文件头部还写入了随机的salt每次安装微信都可能不同。这就是为什么网上很多教程说“直接拷贝数据库文件再打开”行不通。decrypt.py做的事情本质上就是把这个加密数据库还原成明文 SQLite。它先读数据库文件头的固定偏移位置取出salt再用 IMEI 和 uin 计算出 7 位密钥前缀经过 1024 次 HMAC 迭代后得到真正的 AES 密钥最后逐页解密。注意解密后的库不是直接就能用需要重建 SQLite 页结构否则sqlite3打开后依然会报malformed。import hashlib import hmac from Crypto.Cipher import AES def decrypt_db(db_path, imei, uin, out_path): with open(db_path, rb) as f: db_data f.read() # 微信加密库的 salt 固定存储在文件偏移 0x3C 处长度 16 字节 salt db_data[0x3C:0x3C 16] # 密钥前缀 md5(imei uin) 的前 7 位这是微信公开资料里一致的推导方式 md5 hashlib.md5((imei uin).encode()).hexdigest() key md5[:7].encode() # 用盐和密钥做 1024 次 PBKDF2 派生得到 24 字节的 AES-192 会话密钥 derived_key hashlib.pbkdf2_hmac(sha1, key, salt, 1024, 24) # 微信使用 AES-192-CBCIV 固定为 16 个零字节 cipher AES.new(derived_key, AES.MODE_CBC, bytes(16)) # 加密库按页加密每页 4096 字节解密后写入新文件 plain_data bytearray() for i in range(0, len(db_data) - len(db_data) % 16, 4096): page db_data[i:i 4096] plain_data.extend(cipher.decrypt(page)) with open(out_path, wb) as f: f.write(bytes(plain_data))这段代码的关键点有三个。第一salt的偏移位置0x3C是微信数据库结构的固定约定不同版本微信可能会有微调项目文档docs里建议先检查文件头魔数再决定偏移。第二pbkdf2_hmac的迭代次数1024是微信老版本的标准值如果解密后 SQLite 结构依然不对优先怀疑这个参数。第三解密后的文件只是一个“明文页集合”务必用PRAGMA integrity_check;验证完整性再导入到 SQLite 中正常查询。2.2 获取 IMEI 与 uin 的实操路径解密的前提是拿到 IMEI 和 uin这一步对新手最不友好。IMEI 是手机硬件串号安卓系统可以通过adb shell读取也可以在设置里查看uin 是微信账号的加密标识存在微信的配置文件system_config_prefs.xml或 SharedPreferences 里通常是一串纯数字。最稳妥的做法是用 adb 从安卓设备里把/data/data/com.tencent.mm/shared_prefs/目录整个拉出来然后用文本编辑器打开找uin字段。adb shell su -c cat /data/data/com.tencent.mm/shared_prefs/system_config_prefs.xml system_config.xml因为涉及 root 权限这一步在不同手机上表现差异很大。小米、一加等品牌自带 root 开关还好部分手机必须刷第三方 recovery。项目文档里明确说推荐用模拟器或备用机操作不要拿主力机折腾。如果 adb 拉取失败常见原因是权限不足可以改用adb shell run-as com.tencent.mm绕过但这个方案对微信版本要求苛刻。拿到 IMEI 和 uin 后把两个值拼起来存成key.txt后续decrypt.py直接读取避免每次手敲。2.3 解密后的消息表结构与导出策略解密后的 SQLite 库里聊天记录主要落在message表关键字段有msgId、talker聊天对象标识、content消息内容、isSend0 代表接收1 代表发送、createTime毫秒时间戳。但content字段并不是纯文本语音消息存的是voice开头的 XML 结构图片消息存的是缩略图路径表情包是emoji标签这些都要过滤掉。我一般会写一个 SQL 把有效文本一次性捞出过滤条件集中在三块只保留isSend为 0 或 1 的普通文本消息、排除content里带符号的 XML 片段、排除长度小于 2 的无效消息。导出成 JSON 或者 CSV 都行但建议保留createTime做排序后面构造训练语料时对话的先后顺序直接决定机器人能不能学会上下文衔接。SELECT talker, isSend, content, createTime FROM message WHERE content NOT LIKE %% AND length(content) 1 AND type 1 ORDER BY createTime ASC;type 1是文本消息的类型标识这是排查乱入消息最有效的过滤条件。导出的数据里通常还有大量系统通知、微信运动、服务号推送这些杂音会在训练阶段放大成回答语气的偏移。理想的数据源是「和某个熟悉的人的单聊记录」而不是群聊——群聊的消息顺序是交错的isSend字段的归属经常错位训练出来的机器人会精神分裂。单聊记录建议导出最近半年以上少于 500 条有效对话的训练结果基本是复读机效果。3. 语料构造与指令微调格式prepare_data.py的转换逻辑3.1 聊天记录到 Alpaca 格式的映射prepare_data.py的核心任务是把上一步导出的 JSON/CSV 聊天记录转换成一个可以喂给模型做指令微调的数据集。项目用的是 Alpaca 格式每条样本有三个字段instruction指令、input输入、output输出。聊天机器人场景下instruction描述角色和任务input是用户最近一句话output是期望模型生成的回复。这里有一个很多人踩过的坑不能把每一条消息都当成独立样本必须按「上一条消息 → 下一条消息」配对。我和项目作者的处理方式一致——用发送方翻转来切分对话轮次isSend1的消息作为output紧邻的isSend0消息作为input如果连续多条都是对方发的就取最后一条作为input避免指令过碎。import json def build_alpaca_dataset(records, speaker_a, speaker_b): dataset [] prev_msg None for rec in records: # rec: {sender: a|b, text: ...} if prev_msg is None: prev_msg rec continue # 只有上一条是对方、本条是自己时才构成一组问答对 if prev_msg[sender] ! rec[sender]: instruction ( f你正在模仿{speaker_b}和{speaker_a}聊天。 f请根据{speaker_a}的发言用{speaker_b}的语气回复 f不要解释直接输出回复内容。 ) dataset.append({ instruction: instruction, input: prev_msg[text], output: rec[text] }) prev_msg rec # 过滤掉超长样本防止训练时截断导致标签错位 filtered [d for d in dataset if 0 len(d[input]) 200 and 0 len(d[output]) 500] return filtered if __name__ __main__: raw json.load(open(chat_records.json, encodingutf-8)) alpaca_data build_alpaca_dataset(raw, 我, 好友) json.dump(alpaca_data, open(train_alpaca.json, w, encodingutf-8), ensure_asciiFalse, indent2)这段代码里有两个参数值得细说。200和500是输入/输出的最大长度上限单位是字符之所以不直接用 token 数是因为中文字符在主流中文 tokenizer 下平均 1 个字符约等于 0.6~1 个 token200 个字符的input对 7B 模型来说已经足够触发上下文窗口压力再长就会在训练时被截断造成loss异常波动。speaker_a和speaker_b在人名替换上我一般会保留微信备注名而不是真实姓名这样模型学到的语气更贴近日常聊天习惯。3.2 样本去重与角色平衡聊天记录里高频出现的句子——比如「哈哈哈」「嗯嗯」「好的」——会在训练集中占据压倒性比例导致模型把所有回复都往短句上收敛。prepare_data.py里做了两个处理第一对完全相同的input和output组合做去重把重复样本的计数权重压低第二限制单条消息的回复长度下限过滤掉只有一两个字的回应保证模型学到的是「有内容的对话」而不是「语气词复读」。角色平衡这个问题容易被忽视。如果你的聊天记录里 80% 都是你主动发消息构造出的样本对里output基本全是对方的回复模型最后学会的是「替你说话」而不是「模仿你」。真正想让机器人「像你」需要保证作为output的消息里你自己的发言占比不低于 40%。所以我在导出数据时会把isSend字段统计出来直接看双方占比比例失衡的话就换个聊天对象或者调整过滤逻辑把长对话切细。3.3 Alpaca 格式与 ChatML 格式的取舍chat4u 的docs文档里提到指令微调数据除了 Alpaca 格式还可以转成 ChatML 格式。区别在于 Alpaca 把角色信息写在instruction字段里ChatML 则用|im_start|system等特殊 token 分隔角色。Alpaca-lora-ui 默认支持 Alpaca 格式直接用prepare_data.py输出的 JSON 就能训练省去一层转换。但如果后面要接 vLLM 或者 FastChat 这类推理框架ChatML 的兼容性更好。我自己的习惯是保留 Alpaca 格式做训练训练完成后在推理阶段用模板重新拼接角色提示。因为 LoRA 微调的本质是让模型在特定前缀下学会特定风格训练和推理用同一套模板效果最稳定。如果训练用 Alpaca、推理用 ChatML模板不一致模型的表现会明显退化这个是很多人训练完发现「模型变笨了」的隐藏原因之一。4. LoRA 微调与 API 封装从alpaca-lora-ui到llama4openai-api.py4.1 为什么选 LoRA 而不是全量微调基于微信聊天记录训练专属机器人数据量通常在几万条以内这个规模做全量微调7B 模型也容易过拟合更别说消费级显卡根本装不下 14GB 以上的完整参数。LoRALow-Rank Adaptation的思路是冻结原始模型权重只训练注入的低秩矩阵参数量只有原来的 0.1%~1%显存占用大幅降低。项目里用的是 alpaca-lora-ui一个基于 Gradio 的 Web 界面可以在浏览器里配置训练参数、启动训练、查看 loss 曲线。微调基座的选择上我见过用 LLaMA 原版跑的也见过用中文基座跑的。chat4u 的目录结构里alpaca-lora-ui.jpg截图中看到的基座是 LLaMA 系模型文档里明确说中文场景建议换用中文语料预训练的基座否则模型对口语化中文的还原度很粗糙。常见做法是直接用llama.cpp量化的 LLaMA 中文版或者用 ChatGLM 系列的 LoRA 方案但 chat4u 的llama4openai-api.py是为 OpenAI 兼容接口设计的基座模型只要能通过 llama.cpp 或者 transformers 加载就行。4.2 alpaca-lora-ui 的训练参数设置用 alpaca-lora-ui 训练前需要把prepare_data.py产出的train_alpaca.json放到alpaca-lora-ui项目指定的数据目录然后在界面里填参数。我常用的参数组合如下表它兼顾了显存压力和生成质量特别适合 24GB 显存的显卡。参数名推荐值说明lora_r8低秩矩阵的秩越大模型能力越强但过拟合风险上升lora_alpha16LoRA 缩放系数一般设置为lora_r的两倍lora_dropout0.05防止小数据集过拟合不建议设成 0learning_rate3e-4微调学习率比预训练低一个数量级batch_size4显存不足时降到 2但收敛速度会明显变慢num_epochs3聊天记录数据量小3 轮足够多了必过拟合cutoff_len512输入序列最大长度超过会被截断val_set_size0.1划分 10% 作为验证集观察 val loss 判断是否过拟合训练时重点关注验证集 loss。如果 val loss 在 epoch 2 之后开始反弹但 train loss 还在下降这是过拟合的典型信号应该立即停止训练把lora_r调小或者把lora_dropout调大。千万不要盲目堆 epoch聊天记录这种重复句式多的数据集经常在 epoch 2 就进入过拟合区间。我自己用过一轮 5 epoch 的训练最后模型几乎把「嗯嗯」当成了万能回复。4.3 用llama4openai-api.py把模型封装成服务训练完成后模型产物是一个 LoRA 权重目录还需要和一个固定的基座模型合并才能提供服务。chat4u 项目用llama4openai-api.py把这套流程封装成了一个 OpenAI 兼容的 API 服务前端不管接聊天机器人 Web 页面还是桌面客户端只用openai这个 SDK 就能调用。脚本内部加载基座模型 → 加载 LoRA 权重 → 挂载到 FastAPI 服务 → 暴露/v1/chat/completions接口。from fastapi import FastAPI from pydantic import BaseModel app FastAPI() class ChatRequest(BaseModel): messages: list max_tokens: int 256 temperature: float 0.7 top_p: float 0.9 class ChatResponse(BaseModel): choices: list app.post(/v1/chat/completions, response_modelChatResponse) async def chat_completions(req: ChatRequest): # messages 是 OpenAI 格式的消息列表取最后一条 user 内容作为输入 user_text req.messages[-1][content] # 在 prompt 前拼接指令模板让模型进入「模仿好友语气」的状态 prompt ( 你正在模仿一位好友的聊天语气。 请用简短、口语化、带有个人习惯用词的方式回复。\n f对方说{user_text}\n 你的回复 ) # 这里调用底层推理函数 generate_reply(prompt, req.max_tokens, req.temperature, req.top_p) reply generate_reply(prompt, req.max_tokens, req.temperature, req.top_p) return {choices: [{message: {role: assistant, content: reply}}]}这段代码的逻辑很直接。messages是标准 OpenAI 格式user_text取最后一条用户消息拼上项目自定义的角色提示词。temperature和top_p是关键推理参数temperature控制随机性聊天场景 0.7 左右比较合适太高容易答非所问太低会变成复读机top_p是核采样0.9 在保持多样性和可控性之间比较均衡。如果要模拟「这个人说话很冲」的风格把temperature稍微降到 0.5 再配top_p 0.8效果会比纯调 prompt 更明显。5. 避坑清单训练专属机器人最容易翻车的五条记录5.1 现象解密后 SQLite 打开报file is not a database第一次跑decrypt.py解密完的文件拖进 SQLite Browser 直接报错。原因是微信新版把加密库的页大小从 4096 改成了 8196 或 1024代码里写死的分页长度对新版本失效。解决方式是把解密循环里的4096改成先探测页大小从文件头偏移 0x10 处读取两个字节的页大小字段然后动态传入。5.2 现象训练时 loss 直接变成nan模型训练到第二个 epochloss 突然变成nan训练进程直接崩溃。常见原因有两个数据里有空字符串或纯空格样本tokenizer把它们编码成空序列学习率设置太高梯度爆炸。我的排查顺序是先检查train_alpaca.json里有没有或 这种样本过滤掉之后把learning_rate从 3e-4 降到 1e-4重新跑一轮。绝大多数nan都是数据问题不是模型问题。5.3 现象模型输出的回复全是「嗯」「好的」「哈哈」微调完第一次测试模型就像进入了省电模式所有回答都变成两个字的语气词。原因不是模型没学会而是数据集的output里有大量短回复模型发现「用最少 token 完成任务」的规律后开始偷懒。解决方法是重新过一遍prepare_data.py把output长度小于 5 个字符的样本全部过滤掉或者只保留那些output长度大于input长度的对话对让模型被迫输出有信息量的内容。5.4 现象多轮对话上下文断裂机器人答非所问API 封装完成后连续聊几轮机器人就开始不接上下文了上一句说「吃饭没」下一句回「我昨天看电影了」。原因在于llama4openai-api.py的generate_reply函数只取了messages的最后一条没有把历史消息拼进 prompt。LoRA 模型本身是有上下文窗口的但脚本没把历史传给模型。我改的方式是拼 prompt 时把messages里最后 6 轮历史全部带进去用换行分隔模型就能维持住对话连贯性。5.5 现象同一个问题每次回答差别极大语气不像同一个人temperature设置在 1.0 以上时模型每次输出都像换了个人。微信聊天的风格固化程度比通用问答高得多需要更低温度。我一般把temperature固定在 0.6~0.7 区间top_p设 0.85。如果依然不稳定就在 prompt 里加一句「保持和上一轮一致的语气」实测对风格稳定性帮助明显。这个问题的根源是 LoRA 权重对风格的学习强度不够排查时优先看训练数据的output多样性不要一上来就调推理参数。6. 验证模型效果用留存对话做回测比看 loss 曲线更可靠训练完别急着上服务先做一个简单的离线验证。我通常会把聊天记录按时间切成两份最新 10% 不参与训练单独留作测试集。测试时把input喂给模型把生成结果和真实的output做对比重点看两个方面第一语义上是否接得上第二语气词的分布是否和本人习惯接近。loss 曲线再好看也替代不了这种「用没说过的对话做盲测」的方式它能直接暴露模型是学会了风格还是死记硬背了训练样本。import json from openai import OpenAI client OpenAI(base_urlhttp://localhost:8000/v1, api_keyEMPTY) with open(test_alpaca.json, encodingutf-8) as f: test_data json.load(f) hit 0 total 0 for sample in test_data[:30]: resp client.chat.completions.create( modelchat4u-lora, messages[ {role: system, content: sample[instruction]}, {role: user, content: sample[input]} ], max_tokens128, temperature0.65, top_p0.85, ) pred resp.choices[0].message.content real sample[output] # 判断标准真实回复是否出现在预测文本里或者预测文本长度是否接近真实回复 overlap len(set(pred) set(real)) / max(len(set(real)), 1) is_acceptable overlap 0.4 or len(pred) len(real) * 0.6 if is_acceptable: hit 1 total 1 print(f输入: {sample[input]}) print(f真实: {real}) print(f预测: {pred}) print(---) print(f覆盖率: {hit/total:.0%})这个回测脚本的逻辑不复杂核心是overlap这个指标它在字符级别上计算预测回复和真实回复的重复度。聊天场景里两个人对同一句话的回复往往有 30%~50% 的字符重合率比如「哈哈」和「哈哈哈」的重合度就很高。如果覆盖率低于 60%说明模型基本没学到对方的话术优先回去补训练数据而不是继续调推理参数。max_tokens设 128 是为了限制回复长度避免模型把「模仿」做成了「扩写」。验证完毕后还有一个容易忽略的步骤把生成回复里的低质量输出人工打分挑出 3~5 条典型的错误案例回到prepare_data.py重新检查对应样本的instruction写得好不好。比如「不要解释直接输出回复内容」这句指令如果测试时模型频繁出现「好的我明白了」这样的开头说明指令里的约束词力度不够需要改成「禁止输出任何解释性文字」。从那以后我每次微调完都会强制走一遍完整回测盯完 30 组真实对比再决定要不要上服务——这个习惯帮我避开了至少三次「看着 loss 挺美、一聊就翻车」的事故。希望帮到你。本文还有配套的精品资源点击获取