去年在帮朋友调试一个聊天机器人项目时,我偶然发现一个有趣现象:当把训练数据换成某个特定人的对话记录后,模型输出的语句会不自觉地带上这个人的语言习惯。这让我萌生了一个想法——能不能用我自己的微信聊天记录,训练一个能模仿我说话风格的AI分身?
经过两个月的实践验证,我成功在单张RTX 3090显卡上实现了这个构想。整个过程涉及微信数据脱敏处理、大模型微调技术选型、显存优化等多个技术环节。最让我惊喜的是,最终得到的模型不仅能模仿我的口头禅,连常用的颜文字表情都学得有模有样。下面就把这个项目的完整实现路径分享给大家,特别适合想打造个人AI助理的开发者参考。
提示:本项目所有数据处理均在本地完成,微信记录导出后立即进行匿名化处理,确保隐私安全。建议使用小号或测试账号数据进行实验。
在对比了Hugging Face Transformers、DeepSpeed等主流框架后,我最终选择了LLaMA-Factory作为基础框架,主要基于三点考量:
由于需要处理微信桌面客户端的数据,我选择了腾讯云的CloudStudio作为开发环境。具体配置步骤如下:
bash复制# 创建Python 3.10虚拟环境
conda create -n wechat_ai python=3.10 -y
conda activate wechat_ai
# 安装LLaMA-Factory
git clone https://github.com/hiyouga/LLaMA-Factory.git
cd LLaMA-Factory
pip install -e .[metrics]
# 验证安装
llamafactory-cli version
这里有几个容易踩坑的地方:
pip uninstall protobuf经过测试多个开源工具,MemoTrace是唯一能完整保留对话上下文关系的工具。但由于微信版本限制,需要按以下步骤操作:
注意:实际操作中发现,MemoTrace对群聊支持较差,建议优先导出私聊记录。导出后的数据需要立即删除原始微信ID等敏感信息。
传统全参数微调(fine-tuning)需要更新模型所有参数,对于Qwen2.5-1.5B这样的模型(15亿参数),光是存储优化器状态就需要约24GB显存。而LoRA通过低秩分解,将参数量减少了100-1000倍。
具体实现上,我在LLaMA-Factory中配置了以下关键参数:
yaml复制lora_rank: 64 # 低秩矩阵的维度
lora_alpha: 32 # 缩放系数
target_modules: ["q_proj", "v_proj"] # 仅作用于注意力层的Q/V矩阵
这些参数的设置依据是:
QLoRA在LoRA基础上引入了4-bit量化,使得显存占用进一步降低。关键技术实现包括:
实测显示,QLoRA相比原始LoRA可节省约40%显存。以下是我的训练配置对比:
| 配置项 | LoRA | QLoRA |
|---|---|---|
| 显存占用 | 18GB | 10GB |
| 训练速度 | 22it/s | 18it/s |
| 最终loss值 | 1.23 | 1.31 |
虽然QLoRA略慢且loss稍高,但使得在消费级显卡上训练成为可能,这个trade-off非常值得。
为了保持对话风格的一致性,我制定了严格的数据筛选规则:
微信原始数据是线性对话流,需要转换为多轮对话格式。我开发了一个转换脚本,核心逻辑如下:
python复制def convert_to_sharegpt(messages, max_gap=600):
conversations = []
current_conv = []
prev_time = None
for msg in messages:
if prev_time and (msg['time'] - prev_time) > max_gap:
if len(current_conv) >= 2:
conversations.append({"conversations": current_conv})
current_conv = []
role = "user" if msg['is_outgoing'] else "assistant"
current_conv.append({"role": role, "content": msg['content']})
prev_time = msg['time']
return conversations
关键参数说明:
max_gap=600:超过10分钟间隔视为新对话为了提升模型鲁棒性,我采用了两种数据增强方法:
局部扰动:对15%的样本随机进行以下操作
风格混合:将5%的样本与其他风格对话混合,增强模型对不同语气的适应能力
处理后数据样例如下:
json复制{
"conversations": [
{
"role": "user",
"content": "今天那个需求文档你看了吗?( ̄▽ ̄*)"
},
{
"role": "assistant",
"content": "还没看完呢,下午开会时再说吧~"
}
]
}
对比了多个开源模型后,选择Qwen2.5-1.5B-Instruct的原因包括:
从ModelScope下载模型的命令:
bash复制python src/download_model.py \
--model_name_or_path qwen/Qwen1.5-1.5B-Instruct \
--cache_dir ./models
在LLaMA-Factory的train_qlora示例配置基础上,我做了以下关键修改:
yaml复制# 数据相关
dataset_dir: data/wechat
dataset: wechat_style
template: qwen2_wechat # 自定义模板
# 训练参数
per_device_train_batch_size: 4
gradient_accumulation_steps: 8
learning_rate: 1e-4
num_train_epochs: 3
max_length: 1024
# QLoRA配置
quantization_bit: 4
lora_rank: 64
lora_alpha: 32
特别说明几个参数的选择依据:
batch_size=4 配合 accumulation_steps=8 等效于32的batch,在显存和效果间取得平衡max_length=1024以保留长对话上下文在templates.py中添加了专属模板:
python复制qwen2_wechat = Template(
input_format="""<|im_start|>system
你正在模仿用户个人的微信聊天风格,请用随意、口语化的方式回复,可以使用颜文字表情。以下是历史对话:
{history}<|im_end|>
<|im_start|>user
{query}<|im_end|>
<|im_start|>assistant
""",
no_history_format="""<|im_start|>system
请用轻松自然的口吻回复,就像朋友间的微信聊天。<|im_end|>
<|im_start|>user
{query}<|im_end|>
<|im_start|>assistant
""",
)
这个模板的设计要点:
使用以下命令启动训练并监控:
bash复制CUDA_VISIBLE_DEVICES=0 python src/train_bash.py \
--stage sft \
--do_train \
--model_name_or_path ./models/Qwen1.5-1.5B-Instruct \
--dataset wechat_style \
--template qwen2_wechat \
--output_dir outputs/qwen_wechat \
--overwrite_cache \
--plot_loss
通过plot_loss选项可以实时观察loss曲线。我的训练过程显示:
使用FastAPI构建的高效后端服务:
python复制from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
class ChatRequest(BaseModel):
query: str
history: list[dict] = []
@app.post("/chat")
async def chat(request: ChatRequest):
formatted_input = format_message(request.query, request.history)
output = model.generate(formatted_input, max_new_tokens=256)
return {"response": output}
关键优化点:
基于Jinja2的模板设计要点:
html复制<div class="chat-container">
{% for item in history %}
<div class="msg {% if item.role == 'user' %}right{% else %}left{% endif %}">
{{ item.content }}
</div>
{% endfor %}
<form id="input-form">
<input type="text" name="query" placeholder="说点什么...">
<button type="submit">发送</button>
</form>
</div>
<script>
document.getElementById('input-form').addEventListener('submit', async (e) => {
e.preventDefault();
const query = e.target.query.value;
const response = await fetch('/chat', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({query, history})
});
const data = await response.json();
addMessage('assistant', data.response);
});
</script>
实现的功能细节:
使用uvicorn运行服务:
bash复制uvicorn app:app --host 0.0.0.0 --port 8000 --workers 2
测试时发现几个典型问题及解决方案:
| 问题现象 | 原因分析 | 解决方案 |
|---|---|---|
| 回复过于简短 | temperature设置太低 | 调整为0.9-1.2范围 |
| 偶尔出现重复回复 | 重复惩罚不足 | 设置repetition_penalty=1.2 |
| 长回复质量下降 | 上下文窗口限制 | 增大max_length到2048 |
使用50条保留测试集进行评估:
| 指标 | 原始模型 | 微调后 |
|---|---|---|
| 风格匹配度 | 32% | 78% |
| 颜文字使用频率 | 2% | 19% |
| 回复长度(中位数) | 28字 | 15字 |
| 语义连贯性(BLEU) | 0.42 | 0.38 |
虽然BLEU分数略有下降,但风格匹配度显著提升,更符合微信聊天的短句特征。
邀请5位常聊天的好友进行盲测:
特别有趣的是,AI生成的某些回复因为"太像本人"反而被误判为真实对话。
目前发现的改进空间:
一个特别实用的技巧是在system prompt中添加近期话题提示:
code复制最近常聊的话题包括:
- 周末打算去爬山
- 在学做红烧排骨
- 工作项目deadline是周五
经过多次实验,总结出以下几点经验:
对于想尝试类似项目的开发者,我的建议是从小规模开始:
最后分享一个实用脚本,用于清理微信导出的HTML格式记录:
python复制import re
from bs4 import BeautifulSoup
def clean_wechat_html(html_file):
with open(html_file, 'r', encoding='utf-8') as f:
soup = BeautifulSoup(f.read(), 'html.parser')
# 移除图片、表情等非文本内容
for img in soup.find_all('img'):
img.decompose()
# 提取纯文本对话
messages = []
for msg in soup.select('.message'):
sender = msg.select_one('.sender').text.strip()
content = msg.select_one('.content').text.strip()
messages.append({
'sender': sender,
'content': re.sub(r'\s+', ' ', content)
})
return messages