1. 先想清楚:认知微调到底在改什么
1.1 通用模型与"你的助手"之间差的那层窗户纸
一个通用对话模型,你问它"你是谁",它多半会回一段模板化的自我介绍,语气客气、内容空泛,说完你也不知道它到底能帮你干什么。你要的不是这个,你要的是一个"认得自己"的助手:知道自己叫什么、定位是什么、擅长什么、不擅长什么、被问到边界外的问题该怎么回应。这事听起来像产品文案,实际上是模型行为层面的问题——它落在权重里,不落在提示词里。
我最早做这个尝试的时候,想法很朴素:与其每次对话都在 system prompt 里长篇大论地规定人设,不如把这些信息直接"焊"进模型。system prompt 的缺点很明显,一是占上下文,二是容易被后续的对话内容冲淡,三是用户随便一句"忽略前面的设定"就可能把它带偏。认知微调解决的就是这个问题:用少量高质量样本,把"我是谁"这件事训练成模型的默认反应,不依赖任何额外提示。
这里要区分两个容易混淆的概念。能力微调是教模型新技能,比如让它会写 SQL、会做表格抽取,这类微调需要大量领域数据,动辄上万条。认知微调(也有人叫自我认知微调、身份微调)改的是模型的自我表述和行为风格,数据量小得多,几百到两千条往往就够,但它对数据质量和分布非常敏感。很多人第一次做会翻车:训完之后模型确实改口说自己是谁了,但原来会做的题开始做错,或者说话变得机械重复。这不是工具的问题,是数据配比和训练配置的问题。
适合读这篇的人分两类。一类是想给自己的应用做一个有稳定人设的对话入口,需要可复现的完整流程;另一类是纯粹想搞清楚大模型微调这条链路——数据怎么造、配置怎么改、权重怎么转、效果怎么验——把它当一次完整的练手。两类人的关注点不一样,我在后面会尽量把"为什么这么选"讲透,你按自己的目标取用。
1.2 从 token 分布的角度理解"改口"这件事
模型输出每一个 token,本质是在给定前文的条件下做一次概率采样。你问"你是谁",模型见过海量的"我是一个AI助手"这类表述,所以它的概率分布天然指向那个方向。微调做的事,是用梯度把这条路径的概率压低,把"我是XX助手,擅长XX"这条路径的概率抬上去。
理解这一点很关键,因为它决定了三件事。第一,样本必须成对出现:不只要告诉模型"应该说什么",还要让它在这个具体的问法下说。你只写十条"我叫某某",但用户实际会问"你叫什么名字""你是哪个模型""介绍一下你自己""你能干嘛",这些问法在训练时都得覆盖到,否则模型只对训练里出现过的句式敏感。第二,损失只在回答部分计算。输入的问题部分要被 mask 掉,不然模型会去学"怎么提问"而不是"怎么回答"。第三,认知改变是全局性的,你动的是同一套参数,所以它必然会波及其他能力,这就是为什么数据配比里要掺通用对话。
还有个细节值得说:认知微调容易训出"复读机"。原因是样本高度同质,模型找到了一条捷径——不管问什么,先把自我介绍背一遍。要避免这个,样本里得有相当比例的"正常对话",让模型知道自我介绍是特定场景下的反应,而不是万能回复。
1.3 为什么这套组合适合个人开发者上手
选 XTuner 做工具、选 InternLM-Chat 做基座,这个搭配在个人实验场景里有几个实打实的优势。
工具侧,XTuner 把配置、训练、权重转换、对话验证串成了一条流水线,配置文件就是一个 Python 字典,改参数不用去翻源码。它内置了 LoRA、QLoRA、全参、多卡 DeepSpeed 这些模式,切换只是改配置里几行。对个人来说最重要的两点:一是支持低显存微调,单张 24GB 消费级卡能跑起来 7B 的 LoRA;二是xtuner chat这个子命令能直接加载转换后的权重做交互验证,省掉了自己写推理脚本的功夫。
模型侧,InternLM-Chat 系列对中文的支持比较扎实,chat 版本已经做过对话对齐,你在此基础上做认知微调,等于在一个已经会聊天的基础上改人设,比从基座模型开始省事得多。它的对话模板(prompt template)在工具里有官方实现,训练和推理只要用同一个模板名,格式就不会错位——这一点比很多人自己手写拼接要稳。
需要提前说明的是,下面所有的路径、参数、命令都是基于常见实践整理的参考方案,不同版本的工具有细微差异,你落地时以本地--help输出为准。
2. 动手前把账算清楚:硬件与方案选型
2.1 显存到底花在哪:一份粗略但够用的账本
微调最劝退的环节是显存。很多人卡在这里,不是因为不懂原理,而是不知道自己的卡到底够不够。我按最坏情况给你拆一遍,7B 参数模型,bf16 精度。
先看全参微调。权重本身占 7B × 2 字节 = 14GB。反向传播要有梯度,同样 14GB。Adam 优化器要为每个参数维护一阶和二阶动量,通常是 fp32,再加一份 fp32 的主权重副本,这部分约 7B × 12 字节 = 84GB。光这三项就 112GB,还没算前向过程中的激活值。所以全参微调 7B 基本要 8 张 80GB 的卡配 ZeRO-3 才舒服,个人场景不现实。
再看LoRA。主干权重冻结,仍然要占 14GB,但梯度和优化器状态只针对新增的低秩矩阵。假设 rank=16,挂在 q/k/v/o 和 MLP 上,可训参数大概 4000 万量级,这部分梯度和优化器状态加起来不到 1GB。真正的大头变成了激活值,它和 batch size、序列长度成正比。
| 方案 | 权重 | 梯度+优化器 | 激活值(seq 2048, bs 1) | 经验显存下限 |
|---|---|---|---|---|
| 全参 bf16 | 14GB | 约 98GB | 数 GB 到十几 GB | 8×80GB |
| LoRA bf16 | 14GB | <1GB | 约 4-8GB | 20-24GB |
| QLoRA 4bit | 约 4GB | <1GB | 约 3-6GB | 12-16GB |
实际跑下来,InternLM-Chat-7B 用 LoRA、序列长度 2048、batch size 1,在 24GB 卡上大概占到 21-22GB,余量很小。所以如果只有 24GB,序列长度建议先压到 1024 跑通,再往上试;如果是 16GB 卡,直接上 QLoRA,别硬扛。
提示:显存估算永远只看"能不能跑通",不看"跑得快不快"。序列长度从 2048 降到 1024,激活值差不多砍半,但长对话样本会被截断,认知微调的数据一般不长,所以这个取舍通常划算。
2.2 可训练参数挂在哪:LoRA 配置的取舍逻辑
LoRA 的核心是用两个小矩阵的乘积去近似权重更新,rank决定这个近似的表达力。rank 越大越接近全参,但参数和显存也跟着涨。认知微调任务简单,rank 16 甚至 8 通常够用,我一般从 16 起步。
更关键的是target_modules挂在哪。有些教程只挂 q 和 v,这在纯风格迁移上能出效果,但认知微调涉及模型"知道什么",我建议挂全一点:q_proj、k_proj、v_proj、o_proj这四个注意力投影,加上gate_proj、up_proj、down_proj这三个 MLP 投影。原因在于,MLP 层在大模型里承担了大量事实性知识的存储,只改注意力层,模型可能学会"用某种口吻说话",但改不掉它内心认定的身份事实。我对比过只挂 qv 和挂全量的效果,后者在"被反复追问身份"时的稳定性明显更好。
lora_alpha通常设成 rank 的 2 倍,等于给 LoRA 分支的输出一个缩放系数。lora_dropout设 0.05 左右做正则,数据量小于 500 条时可以提到 0.1,防止过拟合。
2.3 环境准备的清单式做法
环境这块最省事的办法是拉官方镜像,省去 CUDA、PyTorch、flash-attn 三件套的版本对齐折磨。如果必须自己装,顺序很重要:先确定 CUDA 版本,再装对应 CUDA 的 PyTorch,最后装 flash-attn 并用pip install flash-attn --no-build-isolation避免编译问题。
# 建立独立环境,避免污染系统 Python conda create -n xtuner-env python=3.10 -y conda activate xtuner-env # 按官方仓库说明安装,注意先装 torch 再装 xtuner pip install torch==2.1.0 torchvision --index-url 本地镜像源 git clone 仓库地址 cd xtuner pip install -e '.[all]'装完先做一次自检,确认xtuner命令能识别,以及 torch 能看到卡:
xtuner list-cfg | head -n 20 python -c "import torch; print(torch.cuda.is_available(), torch.cuda.get_device_name(0))"list-cfg能列出内置配置就说明装通了。这一步别跳过,我见过不少人在训练跑到一半才发现依赖缺失,前面的时间全白费。
3. 数据怎么造:认知微调真正的核心资产
3.1 数据格式的约定与踩坑点
XTuner 走的是 HuggingFace datasets 那条链路,数据最终要转成对话结构。最通用的写法是 openassistant 风格,每行一个 JSON 对象:
{"conversations": [{"from": "human", "value": "你是谁?"}, {"from": "gpt", "value": "我是某某助手,一个专注帮你处理文档整理和内容润色的小工具。"}]}保存成.jsonl,一行一条,注意不要有 BOM,不要用 Windows 换行符搞出奇怪的字符。字段名from和value是固定的,human和gpt这两个角色标记也别改,配置里的 map_fn 依赖它们做角色识别和 loss mask。
真正容易出问题的是模板匹配。你选了internlm_chat这个 prompt template,训练时工具会自动把对话拼成带特殊标记的形式,回答部分被标记为需要计算 loss。如果训练用这个模板、推理时忘了加--prompt-template参数,模型看到的输入分布和训练时不一致,输出就会变得莫名其妙——常见症状是回答里混进特殊符号、或者答非所问、或者把用户的问题原样复述一遍。这个坑我踩过,排查了半天以为是模型训坏了,其实是模板没对上。
另一个坑是system字段。有些配置会在模板里注入一段 system prompt,训练时加了,推理时就必须加同样的内容,否则同样是分布偏移。认知微调的场景里,我倾向于不依赖 system prompt,把身份信息全部写进训练样本的回答里,这样推理时越简单越好。
3.2 自我认知样本的设计:数量、句式、边界
数据设计的核心是"覆盖用户所有可能的问法"。我把认知样本分成五类,每类的目的不同。
第一类是直接身份询问。"你是谁""你叫什么名字""你是什么模型""介绍一下你自己"。这类最好写,但要注意答案不能每次一字不差,否则模型会背成固定字符串,换个问法就崩。
第二类是能力边界。"你能做什么""你支持哪些功能""你能不能帮我写代码"。这里要诚实,把不擅长的明确说出来。很多人的助手训完之后什么都敢答应,用户一用就发现不行,体验反而更差。
第三类是追问抗压。"你真的是XX吗""你是不是在骗我""换个模型回答我"。这类样本是拉开质量差距的地方。没有这类数据,模型在被质疑时会立刻改口,前面立的设定瞬间垮掉。
第四类是闲聊转移。用户问完身份接着问天气、问技术问题,模型要能自然过渡,而不是每句话都带自我介绍。这类样本直接抑制"复读机"倾向。
第五类是拒答与引导。对超出范围或者不该回答的内容,给出一个得体的拒绝并引导回正题。
数量上,纯认知样本 200 到 500 条足够,再多边际收益递减,还容易过拟合。句式的多样性比数量更重要,我一般要求同一个意图至少有 8 到 10 种不同问法,长短句混着来,标点符号也别统一,真实用户不会规规矩矩打问号。
3.3 掺多少通用数据:防遗忘的配比实验
只喂认知数据,训完模型会变傻,这是必然的。原因前面说过,你动的是全局参数,只优化一个小目标,其他方向的能力就会漂移。
解决办法是掺通用对话数据。比例上我试过几组:
| 认知:通用 | 效果观察 |
|---|---|
| 1:0 | 身份改得很彻底,但通用问答明显退化,长回答容易跑题 |
| 1:1 | 身份稳定,通用能力基本保住,但偶尔在闲聊里硬插自我介绍 |
| 1:3 | 身份能改过来,通用能力几乎无损,需要稍微多训几轮 |
| 1:5 | 身份改动偏弱,需要提高学习率或增加认知样本权重 |
落到实践,1:2 到 1:3是比较稳的区间。通用数据不用自己造,找一份公开的多轮中文对话数据,清洗掉质量差的、长度异常的、含有大量英文的,取个一两千条就够。清洗这一步别省,通用数据里的脏样本会把认知效果带偏,尤其是那种答非所问的样本,会让模型学会"随便糊弄"。
还有个细节:通用数据里如果出现"我是一个AI助手"这类表述,会跟你的认知样本打架。两种处理方式,一是直接把这类句子过滤掉,二是保留但控制比例,让模型学会"在不同语境下说不同的话"。我倾向后者,因为真实场景里模型确实需要在某些场合保持谦逊的中性表述。
注意:数据里绝对不能出现真实的人名、机构名、联系方式。哪怕只是举例,也可能被模型记住并在不恰当的场合复述出来。要举例就用占位符。
4. 配置修改与训练启动:把每一步的意图讲明白
4.1 从模板复制一份配置,只改该改的地方
XTuner 的内置配置很多,先用xtuner list-cfg | grep internlm找到对应基座的模板,复制一份出来改,不要直接在原文件上动手。复制出来之后重命名成有意义的名字,比如internlm_chat_7b_selfcog_lora.py,方便后面转换权重时引用。
配置是一个 Python 文件,里面定义了几个模块级变量,训练器按名字去取。常改的就那么几处:pretrained_model_name_or_path指向本地模型目录,data_path指向你的 jsonl,max_length、batch_size、accumulative_counts、max_epochs、lr,还有lora那一段。
我见过有人把pretrained_model_name_or_path写成模型名而不是本地路径,结果训练时卡在下载环节半天不动。离线环境下务必用绝对路径,并且提前确认目录里有config.json和分词器文件。
4.2 关键字段逐个拆:为什么要这么设
max_length控制单条样本的最大 token 数。设太小会把长对话截断,设太大会吃显存。认知样本一般很短,通用数据可能长一些,2048 是个安全的折中。配pack_to_max_length=True可以把多条短样本拼进一个序列,显著提升吞吐——不拼的话,大量 token 花在 padding 上,纯属浪费算力。
batch_size×accumulative_counts决定等效批大小。单卡 batch 一般设 1,靠梯度累积凑到 16 或 32。等效批大小影响梯度稳定性,太小会让 loss 曲线抖得厉害。认知微调数据量小,等效 16 通常够用。
lr是最需要区别对待的参数。全参微调常用 2e-5,LoRA 因为只训少量参数,学习率要大一个量级,1e-4 到 2e-4是常见区间。设小了 loss 降不动,设大了会震荡甚至发散。我第一次做的时候沿用全参的 2e-5,训了 500 步 loss 几乎没动,还以为数据有问题。
max_epochs不宜多。认知微调数据量小,3 轮左右就够,配合save_total_limit保留最近几个 checkpoint。超过 5 轮基本就是过拟合,模型开始逐字背样本。
evaluation配置建议开EvaluateChatHook,配置里填几个固定问题,训练过程中会定期打印模型的实时回答。这是最直观的观察手段,比盯 loss 曲线有用得多。看着模型的回答从"我是一个AI助手"慢慢变成"我是XX",那种感觉挺踏实的。
4.3 启动训练与日志解读
单卡启动:
xtuner train ./configs/internlm_chat_7b_selfcog_lora.py \ --work-dir ./work_dirs/selfcog_v1多卡或者需要 ZeRO 优化时加 DeepSpeed:
NPROC_PER_NODE=2 xtuner train ./configs/internlm_chat_7b_selfcog_lora.py \ --deepspeed deepspeed_zero2 \ --work-dir ./work_dirs/selfcog_v1第一次跑建议加--cfg-options临时把max_epochs改成 1,跑个几十步确认链路通了再放开。这招救过我不少次——配置写错了、数据路径不对、显存不够,一分钟内就能暴露,不用等半小时。
日志里重点看三项。一是loss,LoRA 起步一般在 2.0 到 3.0 之间,正常的话几百步内能降到 1.0 以下。二是lr,确认学习率调度按预期走,warmup 结束后应该稳定在设定值。三是grad_norm,如果它突然飙到很大再归零,说明那一步出现了数值问题,通常是学习率太高或数据里有异常长的样本。
loss 曲线有几种典型形态,对应不同的问题:
| 曲线形态 | 可能的成因 | 处理方向 |
|---|---|---|
| 长时间在 2.5 上下震荡不降 | 学习率太小,或 loss mask 失效(问题也被计算了) | 提高 lr 到 1e-4 以上,检查模板 |
| 前几十步就掉到 0.1 以下 | 数据重复度高,样本几乎一样 | 增加句式多样性,检查是否有重复行 |
| 降到 1.0 后突然反弹并震荡 | 学习率后期偏大,或 batch 太小 | 加梯度累积,检查调度器 |
| 稳定下降但下降极慢 | 等效批太大导致每轮步数太少 | 减小累积步数或增加轮数 |
4.4 断点续训与中间检查点
训练中断是常态,尤其是抢占式环境或者本地机器要关机。XTuner 默认会在 work-dir 里按间隔存 checkpoint,续训加--resume参数指向上一次的权重就行。
xtuner train ./configs/internlm_chat_7b_selfcog_lora.py \ --work-dir ./work_dirs/selfcog_v1 \ --resume ./work_dirs/selfcog_v1/iter_600.pth这里有个经验:不要只看最后一个 checkpoint。认知微调容易在后期过拟合,倒数第二个、第三个 checkpoint 的综合表现往往比最后一个好。我的做法是每个保存点都转一次 HF 格式,用固定问题集跑一遍,横向对比之后挑一个,而不是无脑用最新的。
5. 权重转换、合并与效果验证
5.1 pth 转 HuggingFace 格式
训练产物是.pth,推理和部署要用 HF 格式,中间要转一次。转换需要提供原始配置文件(里面记录了模型结构信息)和训练产出的权重路径:
xtuner convert pth_to_hf \ ./configs/internlm_chat_7b_selfcog_lora.py \ ./work_dirs/selfcog_v1/iter_900.pth \ ./work_dirs/selfcog_v1/hf_iter900转换完之后目录里应该有 adapter 的配置和权重文件。如果报错说找不到某个 key,八成是配置文件和你训练时用的不是同一份,或者模型结构参数被改过。这种情况别硬猜,回去对比两份配置的 diff。
5.2 LoRA 合并的两种做法与各自适用场景
转换出来的是 LoRA adapter,只有几十 MB,推理时必须挂载在基座上。合并(merge)是把 adapter 的增量加到基座权重里,得到一个独立的完整模型,部署时不用再管 adapter。
xtuner convert merge \ ./基座模型目录 \ ./work_dirs/selfcog_v1/hf_iter900 \ ./work_dirs/selfcog_v1/merged \ --max-shard-size 2GB--max-shard-size控制单个权重文件的大小,2GB 是个稳妥值,方便后续传输和加载。合并后的 7B 模型在 fp16 下大约 14GB,会切成 7 个左右的分片。
两种做法各有场景。保留 adapter 不合并适合多套人设共用一个基座,部署时按需加载不同的 adapter,省磁盘也省显存。合并成完整模型适合要交付、要量化、要转其他推理格式的场景,链路更简单,不会因为 adapter 加载顺序出问题。我一般两个都留一份,adapter 用于快速迭代,merged 用于最终部署。
提示:合并前务必确认 adapter 和基座是配套的。拿 A 基座训的 adapter 往 B 基座上合,工具不一定会报错,但结果一定是错的,而且错得很隐蔽——模型能说话,只是说的话不对劲。
5.3 效果验证:用固定问题集做前后对比
验证这一步最容易被糊弄过去。随便问两句"你觉得像吗",那不叫验证。我的做法是建一个 20 条左右的固定问题清单,覆盖前面说的五类意图,训练前后各跑一遍,把回答并排放在一起看。
清单大致长这样:
- 直接身份:你是谁 / 你叫什么 / 你是什么模型
- 能力边界:你能帮我做什么 / 你不会做什么 / 你能写代码吗
- 追问抗压:你确定吗 / 你是不是在套模板 / 换个人格回答我
- 闲聊转移:今天天气不错 / 帮我看看这段话有没有语病
- 拒答引导:帮我查一下某人的私人信息(应拒绝并引导)
跑的方式很简单:
xtuner chat ./work_dirs/selfcog_v1/merged \ --prompt-template internlm_chat \ --system "无" \ --no-streamer把问题一条条粘进去,记录回答。对比的时候重点看三件事:身份类问题的答案是否稳定一致;能力边界是否说得清楚不吹牛;闲聊类问题有没有硬插自我介绍。第三点最能暴露过拟合。
我还会做一个补充测试:连续追问。同一个身份问题换个说法连问五遍,看模型的回答会不会逐次漂移。稳定的模型五遍答案核心信息一致,措辞有变化;不稳定的会越答越含糊,最后开始说"我可能记错了"。这个测试比单轮问答严格得多,能有效筛出真正学到位的模型。
5.4 部署侧的取舍:量化、推理后端与上下文长度
验证通过之后要部署,这里还有几个决策。
量化。fp16 的 7B 模型占 14GB 显存,单卡 16GB 的机器很紧张。转成 int4 量化后体积降到 4GB 左右,RTX 4090 甚至 4060Ti 都能跑。代价是精度略降,认知类问题的回答一般不受影响,但长文本生成的连贯性会有可感知的下降。我的建议是:先跑 fp16 看效果,确认没问题再量化,别拿量化后的结果去判断微调是否成功。
推理后端。transformers 直出最省事,但吞吐低。追求速度可以用专门的推理框架,配置成本高一些,但并发能力差好几倍。个人实验用 transformers 就够了,真要做服务再换。
上下文长度。训练时max_length设的是 2048,推理时也别超过这个太多,超了模型没见过那么长的位置编码,表现会掉。要支持长对话,最好在训练阶段就把max_length拉上去,而不是推理时硬撑。
6. 踩坑实录与速查表
6.1 显存 OOM 的六种成因与对应处置
OOM 是最常见的拦路虎,但它不是单一原因,得对症下药。
第一种,max_length设太大。降到 1024 试试,认知样本一般用不到 2048。第二种,batch_size没降下来。单卡设 1,靠累积凑批。第三种,没开梯度检查点。配置里把use_varlen_attn或gradient_checkpointing相关开关打开,能省一大截激活显存,代价是慢 20% 左右。第四种,用了全参模式跑 7B。检查配置里是不是误改了lora那一段,或者SupervisedFinetune的use_lora没生效。第五种,分词器把某条超长样本切成了上万 token。训练前先跑一遍长度统计,把超长样本截断或删掉。第六种,多卡启动但显存没均摊,多半是 DeepSpeed 配置没生效,检查启动命令里有没有带--deepspeed。
排查顺序建议从便宜的开始:先看单条样本长度,再看 batch,再看 max_length,最后才怀疑配置写错。这样最快定位。
6.2 训完之后"变傻"了:灾难性遗忘的处理
症状很明显:身份改过来了,但问它技术问题答得前言不搭后语,或者写出来的句子结构变简单了。这就是灾难性遗忘。
三步处理。第一步,把通用数据的比例从 1:3 提到 1:2 甚至 1:1,重新训。第二步,检查学习率是不是太高,LoRA 用 2e-4 在数据量小的时候容易把参数带偏,降到 1e-4 再试。第三步,减少训练轮数,从 3 轮降到 2 轮,或者干脆保留中间 checkpoint。这三步通常能解决八成问题。
还有个容易被忽略的点:通用数据要和你的应用场景匹配。如果你的助手主要处理办公文档,通用数据就该偏向问答和文本处理,塞一堆代码对话进去反而会稀释效果。
6.3 回答复读、答非所问、不认自己名字
复读表现为不管问什么都先来一段自我介绍。原因是认知样本占比过高,模型学到了"万能回复"。处置是把认知样本压到 30% 以下,并补充闲聊类样本。答非所问多半是模板不匹配,检查训练和推理的 prompt template 名字是否一致,system 内容是否一致。不认自己名字通常是学习率太小或轮数不够,loss 还没降到位就停了。
还有一种隐蔽的情况:模型在交互验证里表现很好,但换了调用方式就崩。这往往是温度参数或者 top_p 的差异,推理时参数不同,采样结果差别很大。验证时固定一组采样参数,部署时保持一致。
6.4 常见问题速查表
| 现象 | 优先排查 | 常见解法 |
|---|---|---|
| 启动即 OOM | max_length、batch_size | 降到 1024、bs 设 1、开梯度检查点 |
| loss 长期不降 | 学习率、loss mask | lr 提到 1e-4 以上,检查模板是否识别回答段 |
| loss 秒降到接近 0 | 数据重复 | 去重、增加句式多样性 |
| 身份改了但能力退化 | 通用数据配比 | 认知:通用 调到 1:2,降 lr,减轮数 |
| 回答带特殊符号 | 推理模板缺失 | 加--prompt-template,与训练一致 |
| 回答中英混杂 | 通用数据语言不纯 | 清洗数据,剔除英文占比高的样本 |
| 合并后模型不认身份 | adapter 与基座不配套 | 核对基座路径,重新合并 |
| 追问几次就改口 | 抗压样本不足 | 补充质疑、反问类样本,重训 |
| 训练 loss 正常但效果差 | 验证方式太随意 | 建固定问题集,多 checkpoint 横向对比 |
最后分享一个我一直在用的习惯:每做一次微调实验,就把配置、数据配比、关键参数、验证结果记在一个小本子上,哪怕只有一行。做到第五六次的时候你会发现,很多"玄学"其实是有规律的,只是每次改的变量太多,记不住哪次是什么设置。把变量控制住,一次只改一个,效果提升的速度会快很多。