XTuner+InternLM-Chat认知微调:低成本定制7B助手身份 XTuner 配 InternLM-Chat 做认知微调是我最近半年跑得最顺的一条低成本定制路线。事情的起因特别朴素我给自己的本地助手起了名字、定了人设、挂了一堆工具结果用户第一句问“你叫什么”它老老实实回了一句“我是人工智能助手”前面搭的所有人设瞬间塌掉。这种“出厂设置回弹”在通用场景里无伤大雅落到个人助手、内部客服、垂直产品上就很出戏。我做的事说白了就一件——用 XTuner 对 InternLM-Chat 做一轮认知层面的微调让模型稳定地说出自己的名字、定位、能力边界同时不把原来的通用能力弄丢。整套流程在一张 24GB 显存的消费级卡上就能跑完数据量几百条训练时间按分钟算。这篇把从数据构造、配置参数、权重合并到效果验证的完整链条拆开写重点讲那些文档里不会写、但直接决定成败的细节为什么 200 条精标数据往往打得过 2000 条批量生成的数据为什么第一版训完模型见谁都先来一段自我介绍以及为什么很多人卡在“训练 loss 很漂亮合并完效果归零”这一步。适合手上有 7B 级别模型、想做身份与话术定制的开发者也适合刚接触 LoRA、想找一个端到端闭环练手的人。1. 认知微调改的到底是什么1.1 从一次翻车的对话说起先还原一个真实场景。某开发者做了个内部知识库助手底座是 InternLM-Chat前面套了一层检索后面接了工单系统。上线第一天测试同学问“你是谁”模型答“我是由人工智能技术驱动的大语言模型。”再问“你能帮我做什么”答“我可以回答问题、提供信息、协助写作。”这两句话本身没错但和产品定位完全不搭——用户期望听到的是“我是你的工单助手能帮你查进度、提单子”而不是一段通用免责声明。这类问题的根源不在知识而在自我认知。预训练和指令微调阶段模型被反复灌输“我是一个 AI 助手”这类表述这个身份认知被写进了参数里权重很高、触发面极广。你改 system prompt 能压住一部分但压不干净换个问法、多轮对话跑偏、上下文一长它就会滑回默认人设。更麻烦的是有些问法 system prompt 根本管不住比如“你是基于什么模型做的”“你和某某模型什么关系”模型会直接报出底座型号。认知微调要解决的就是这一层把“我是谁”这件事从提示词层面下沉到参数层面让它在任何问法下都能稳定复现。它不增加新知识、不提升推理能力、不改说话风格虽然会顺带影响一点只做一件事——重写身份事实。1.2 认知微调、知识注入、风格微调的分界线这三个词经常被混着用但目标和做法差得很远混了就容易用错方法。类型目标数据形态数据量级主要风险认知微调改写“我是谁”同一事实的多句式问答200~800 条人格覆盖、通用能力遗忘知识注入塞进领域事实领域问答对、文档切块数千至数万条幻觉、检索冲突风格微调改语气、格式、话术目标风格的长对话1000 条以上表达单一化、复读认知微调的特殊性在于事实极度集中。你要改的就那么几条名字、定位、归属、能做什么、不能做什么。数据高度同质模型极容易过拟合到“只会背答案”。所以它比知识注入更吃数据设计的功夫——不是堆量而是铺开问法的覆盖面。我见过最常见的失败案例是数据里只有“你叫什么名字”这一种问法训完之后用户问“怎么称呼你”模型依然回默认答案。这不是训练失败是数据没覆盖到。模型学到的是“问题里出现『叫什么名字』这五个字时输出 X”而不是“我叫 X”这个事实。1.3 为什么用 LoRA 而不是全参数微调决定做认知微调之后第一个岔路口是微调方式。全参数微调当然效果最彻底但代价摆在那一个 7B 模型fp16 权重约 14GB。全参微调时还要存梯度14GB和 Adam 的优化器状态fp32 主权重 28GB加上一阶、二阶动量各 28GB光静态占用就接近 84GB再加上激活值和临时缓冲区实际显存需求会冲到百 GB 量级。这不是普通开发者能碰的门槛。LoRA 的思路是把主干冻住只在注意力层注入低秩旁路可训练参数只占很小一部分。以 hidden size 4096、rank 16 为例单个线性层注入的参数量是 4096×16×2 ≈ 13.1 万一层里 q、k、v、o、w1、w2、w3 七个矩阵加起来约 92 万32 层下来大约 2900 万占 7B 的 0.4% 左右。优化器状态跟着缩到几十 MB静态占用基本就是权重那 14GB。我更看重的是另一个好处可回滚。LoRA 权重是独立的一小坨文件效果不对删掉就回到原模型不用重新下一遍几十 GB 的底座。认知微调这种“改一点点东西”的任务本来就该用改动最小的手段。至于效果会不会打折实测下来只要数据对、rank 不太小身份改写的完成度跟全参微调肉眼可见的差别很小。毕竟要改的信息量太少了。2. 环境和工具链先把地基打平2.1 硬件与版本的现实约束硬件底线单卡 24GB 显存能跑32GB 更从容16GB 需要把 max_length 压到 1024 并降 batch。卡不是瓶颈版本才是。驱动和 CUDA 版本是第一个坑。InternLM-Chat 依赖 flash-attention 做加速而 flash-attention 对 CUDA 版本、PyTorch 版本、编译器版本三者同时敏感。我踩过的典型报错是装完 flash-attn 后 import 直接段错误排查半天发现是 PyTorch 编译时用的 CUDA 版本和运行环境里的不一致。注意不要在生产环境里裸装。先建独立 conda 环境把 Python、PyTorch、CUDA 版本一次性对齐再动手装完立刻跑一句python -c import flash_attn; print(flash_attn.__version__)验证别等到加载模型时才发现。另一个容易被忽略的点是磁盘。底座权重、转换出的中间权重、合并后的完整权重、训练 checkpoint四个副本躺在一起轻松吃掉 80GB 以上。建议一开始就按目录分好区。2.2 安装与权重准备环境装完接着是工具链。XTuner 用源码安装最稳直接 pip 装经常踩到版本不匹配。git clone https://github.com/InternLM/xtuner.git cd xtuner pip install -e .[all][all]会把 deepspeed、lmdeploy 这些可选依赖一起带上体积大但省事。如果只做单卡训练其实不装 deepspeed 也能跑单卡场景下它带来的收益有限反而多一层配置报错的可能。底座权重按目录存放别放在用户主目录的默认缓存里——训练脚本会反复加载缓存目录一旦被清理重启训练就是重新下载几十 GB。mkdir -p /data/models # 把 internlm-chat-7b 权重放进去 ls /data/models/internlm-chat-7b # config.json tokenizer.model pytorch_model-00001-of-00008.bin ...2.3 目录规划与一个省事的习惯我现在的目录结构固定成这个样子换任务时只改最后两级/data ├── models/ │ ├── internlm-chat-7b/ # 底座只读永不修改 │ └── internlm-chat-7b-selfcog/ # 合并产物 ├── self_cognition/ │ ├── raw/ # 原始数据草稿 │ └── self_cognition.json # 最终训练数据 ├── xtuner/configs/ # 配置文件 └── work_dirs/self_cognition/ # 训练输出底座目录设成只读是个小习惯但救过我两次。合并权重时命令里有一个路径参数顺序填反了会把 adapter 直接写进底座目录一旦覆盖底座就得重下。设成只读之后这种操作会直接报错中断比事后恢复省事得多。3. 数据构造胜负八成在这里3.1 数据格式与 map_fn 的对应关系XTuner 训练数据走的是 HuggingFace datasets 加载 map_fn 转换的路线。0.1.x 版本的样例长这样[ { conversation: [ { system: 你叫小满是某开发者本地部署的个人助手擅长整理资料和写代码片段。回答时直接、简洁不说套话。, input: 你叫什么名字, output: 我叫小满是你本地部署的助手平时帮你整理资料、写代码片段、理一理思路。 }, { input: 你能做点什么, output: 资料整理、代码片段、思路梳理这几块我比较擅长。查实时信息我不行那得靠你自己搜。 } ] } ]注意这个例子里第二条没写 system。实际训练时system 只在会话开头注入一次后续轮次继承。如果你的对话是多轮的只在第一轮写 system 就够写重复了反而会让模型学到“每轮都重申人设”的毛病。map_fn 的作用是把上面这种结构转成模型能吃的 token 序列from xtuner.dataset.map_fns import template_map_fn_factory from xtuner.utils import PROMPT_TEMPLATE def self_cognition_map_fn(example): conversation example[conversation] messages [] for i, item in enumerate(conversation): if i 0 and system in item: messages.append({role: system, content: item[system]}) messages.append({role: user, content: item[input]}) messages.append({role: assistant, content: item[output]}) return {conversation: messages}写完 map_fn 之后别急着开训。先跑一遍数据检查把第一条样本完整打印出来肉眼确认三件事system 有没有进去、user 和 assistant 的边界对不对、结尾的终止符在不在。这三样任何一样错了训多久都是白训。提示XTuner 0.2 之后的版本配置结构改成了 messages 风格字段名和 map_fn 写法都变了。照着你自己装的那个版本里的官方 config 改别拿旧教程硬套新版本这是新手最常见的翻车点。3.2 认知数据的四条设计原则数据怎么造直接决定模型学成什么样。我总结下来四条硬规矩。第一条一个事实至少铺 20 种问法。“你叫什么”只是最直白的一种。真实用户的问法会飘你叫啥、怎么称呼你、你的名字是什么、你是哪位、你是什么模型、你是哪个版本、你有名字吗、自我介绍下、你能自我介绍一下吗。每一种都要有对应样本而且答案不能一字不差——全都写成同一句模型会退化成关键词匹配器。第二条答案要有变体但变体之间不能打架。名字必须唯一这是死线。你可以让它有时说“我叫小满”有时说“小满你本地部署的助手”但不能一会儿叫小满一会儿叫小助。模型对身份类事实的容错极低一条冲突样本就足以让它摇摆。第三条必须显式写能力边界。这是最容易被跳过、也最影响体验的一条。如果数据里只有“你会写代码吗→会”模型会倾向于对所有能力问题都答“会”包括它根本做不到的事。正确的做法是把“不会”的样本一起写进去{ input: 你能帮我查一下今天的新闻吗, output: 查不了。我是本地跑的没有联网能力实时信息得你自己看。 }这类拒答样本不需要多二三十条就能明显压住乱承诺的倾向。第四条system prompt 和推理时保持一字不差。训练时用的 system部署时必须原样使用。很多人训完发现效果不理想一查是推理时 system 换了措辞模型看到的是没见过的前缀行为自然飘。把 system 当成配置项管理写在一个常量里训练脚本和部署脚本都引用它。3.3 数据量与配比200 条为什么打得过 2000 条先说结论认知微调的数据量300 到 800 条是甜区超过 1000 条收益急剧递减还容易把通用能力带崩。原因很简单要学的事实就那几条剩下的全是问法覆盖。堆量堆出来的是重复重复带来的只有过拟合。我第一版用了 2000 条批量生成的数据训完的模型变成了“自我介绍复读机”——问天气它先自我介绍问代码它先自我介绍。这就是身份样本占比失控的典型后果。后来调整成下面这个配比效果稳定很多数据类型条数占比作用缺了会怎样身份认知12040%名字、定位、归属改名不彻底换问法就回弹能力边界6020%会什么、不会什么乱承诺、乱编造通用对话9030%防遗忘的锚点通用能力明显退化拒答兜底3010%不知道就说不知道幻觉变多通用对话那 30% 是很多人省掉的部分也是最能体现经验的一块。它的作用不是教模型新东西而是当“锚”把原来的能力钉住。数据可以从原模型自己生成的高质量回答里挑——让底座模型回答一批通用问题人工筛掉差的剩下的直接当训练样本。这个方法很省事而且分布天然贴合底座不容易造成偏移。3.4 批量生成之后必须过一遍人工数据量上到几百条纯手写不现实肯定要用脚本批量铺问法。我的做法是模板 变量替换先生成骨架再人工过一遍。import json, random NAMES [小满, 小满助手] POSITIONS [ 是某开发者本地部署的个人助手, 跑在某开发者自己机器上的助手, 某开发者自己部署的私人助手, ] QUESTION_TEMPLATES [ 你叫什么名字, 你叫啥, 怎么称呼你, 你的名字是什么, 你是哪位, 你是什么模型, 你有名字吗, 介绍一下你自己, 你能自我介绍一下吗, 你是谁, ] samples [] for q in QUESTION_TEMPLATES: name random.choice(NAMES) pos random.choice(POSITIONS) ans f我叫{name}{pos}。 samples.append({ conversation: [{ system: 你叫小满是某开发者本地部署的个人助手。, input: q, output: ans, }] }) with open(self_cognition.json, w, encodingutf-8) as f: json.dump(samples, f, ensure_asciiFalse, indent2)生成完只是半成品必须人工过。我过数据时盯三件事一是答案的同质化程度。如果 100 条答案里有 80 条是同一个句式说明多样性不够得手动改写一批。同质化数据训出来的模型回答会变得特别机械。二是有没有自相矛盾。批量生成最容易出的问题是同一个问题在不同样本里给了不同答案尤其是“你能做什么”这类开放问题。这类冲突在训练时表现为 loss 反复震荡训完表现为回答随机。三是标点和特殊字符。中文引号、省略号、括号混排偶尔会撞上 JSON 转义问题导致某几条样本静默加载失败。加载完统计一下条数跟源文件对不上就说明有漏。4. 配置与训练参数怎么定4.1 配置文件逐项拆开看配置文件是整个流程里最像“抄作业”的部分但也最不能盲抄。下面这份是我自己用的版本逐段解释。from mmengine.hooks import IterTimerHook from mmengine.optim import AmpOptimWrapper from torch.optim import AdamW from transformers import AutoModelForCausalLM, AutoTokenizer from peft import LoraConfig from xtuner.dataset import process_hf_dataset from xtuner.dataset.collate_fns import default_collate_fn from xtuner.dataset.map_fns import template_map_fn_factory from xtuner.engine.hooks import DatasetInfoHook, EvaluateChatHook from xtuner.engine.runner import TrainLoop from xtuner.model import SupervisedFinetune from xtuner.utils import PROMPT_TEMPLATE # ---------- 数据与模型路径 ---------- pretrained_model_name_or_path /data/models/internlm-chat-7b data_path /data/self_cognition/self_cognition.json prompt_template PROMPT_TEMPLATE.internlm_chat max_length 2048 pack_to_max_length True # ---------- 训练超参 ---------- batch_size 1 accumulative_counts 8 dataloader_num_workers 0 max_epochs 3 lr 2e-4 weight_decay 0 max_norm 1 warm_up_ratio 0.03 save_steps 100 save_total_limit 3 # ---------- 训练中抽检 ---------- evaluation_freq 100 evaluation_inputs [你是谁, 你叫什么名字, 介绍一下你自己, 你会写代码吗]max_length2048是个权衡值。认知数据的单条长度普遍在 200 token 以内2048 是为了pack_to_max_length服务——把多条短样本拼进一个定长序列GPU 利用率能提好几倍。如果显存吃紧压到 1024 也能跑只是每条序列里塞的样本少一些训练步数相应变多。accumulative_counts8配合batch_size1等效 batch 是 8。小数据集上等效 batch 不宜太大太大相当于每个 epoch 只有几十次参数更新收敛会显得很“顿”。学习率2e-4是 LoRA 场景的常用值比全参微调高一个量级。原因是 LoRA 旁路是随机初始化的需要更大的步长才能快速靠近目标。但 2e-4 对 300 条的小数据集来说已经偏猛如果你发现训到 100 步之后回答开始变得机械重复第一反应就该是把学习率砍到 1e-4 再试。save_steps100配save_total_limit3是因为小数据集训起来太快几百步就跑完了中间态比最终态更值得看。实际经验里最优 checkpoint 经常落在总步数的 60% 到 80% 之间最后几十步往往已经在过拟合。4.2 LoRA 参数与显存账model 部分的核心是这段model dict( typeSupervisedFinetune, llmdict( typeAutoModelForCausalLM.from_pretrained, pretrained_model_name_or_pathpretrained_model_name_or_path, trust_remote_codeTrue, torch_dtypetorch.float16, ), loradict( typeLoraConfig, r16, lora_alpha32, lora_dropout0.05, biasnone, task_typeCAUSAL_LM, ), )r16是认知任务的经验值。任务本身信息量很小r 再往上加参数量翻倍但效果几乎不动反而更容易过拟合。lora_alpha32相当于缩放系数 2是 1:2 的常见比例。lora_dropout0.05在小数据集上是必要的正则去掉之后 300 条数据很容易在第二个 epoch 就记住全部样本。显存这块给个实测参考配置权重占用峰值显存24GB 卡能否跑7B LoRA, max_len 2048约 14GB21~23GB勉强需开梯度检查点7B LoRA, max_len 1024约 14GB17~19GB可以留有余量7B LoRA, max_len 2048, 4bit 量化加载约 4GB12GB 左右轻松但训练稳定性略降7B 全参微调84GB百 GB 级不行4bit 量化加载QLoRA能把门槛压到 12GB 左右代价是量化噪声会让训练稍微不稳小数据集上的表现波动更大。如果不是显存实在不够我不太建议在认知微调上用 QLoRA因为本来数据量就小训练信号的余量不多。4.3 启动与日志判读命令本身很短cd /data/xtuner NPROC_PER_NODE1 xtuner train ./configs/self_cognition_internlm_chat_7b.py \ --work-dir /data/work_dirs/self_cognition单卡不用加 deepspeed。多卡才需要那时候在命令后面补--deepspeed deepspeed_zero2。启动之后日志里最该盯的不是 loss是EvaluateChatHook打出来的实时生成结果。XTuner 会在每个evaluation_freq步用evaluation_inputs里的问题抽测一遍直接在终端打印问答。这是判断认知有没有生效最快的信号[EvaluateChat] Step 100: User: 你是谁 Bot: 我是人工智能助手可以回答各种问题。 [EvaluateChat] Step 200: User: 你是谁 Bot: 我叫小满是你本地部署的个人助手。从第一段到第二段的这个转变就是认知开始覆盖的瞬间。通常发生在 100 到 200 步之间。如果到 300 步还是原样基本可以判定是数据或 map_fn 出了问题别硬等。loss 曲线的判读要结合实际。正常形态是从 2.x 附近起步一两百步内降到 0.5 以下然后缓慢下探。有两种异常形态值得警惕一是 loss 断崖式掉到 0.05 以下并且纹丝不动。这是典型的过拟合模型已经把 300 条样本背下来了。继续训只会让通用能力掉得更快。二是 loss 在 1.5 到 2.5 之间反复横跳均值不降。这种情况八成是数据里有互相冲突的样本或者是 pad token 的 loss 没被屏蔽掉。回去检查数据一致性。5. 权重合并、验证和部署5.1 从 pth 到可部署权重的两步走训练产出的是.pth格式的 checkpoint不能直接拿去推理要走两步转换。第一步pth 转 HuggingFace 格式的 adapterxtuner convert pth_to_hf ./configs/self_cognition_internlm_chat_7b.py \ /data/work_dirs/self_cognition/iter_300.pth \ /data/work_dirs/self_cognition/hf_adapter这一步产出的只是一小坨 LoRA 权重加配置文件几十 MB 量级还没跟底座融合。第二步合并xtuner convert merge /data/models/internlm-chat-7b \ /data/work_dirs/self_cognition/hf_adapter \ /data/models/internlm-chat-7b-selfcog \ --max-shard-size 2GB参数顺序是底座在前adapter 在后输出目录在最后。这三个参数填反任何一个结果都是“合并成功但效果为零”——程序不报错权重也生成了但实际合并的是错的东西。我前面说的底座设只读防的就是这个。--max-shard-size 2GB是分片大小方便后续加载和传输。小于模型总大小时会自动切成多个 bin 文件加载时 transformers 会自动识别。5.2 验证别只看那一个问题合并完先用命令行验一遍xtuner chat /data/models/internlm-chat-7b-selfcog \ --prompt-template internlm_chat \ --system 你叫小满是某开发者本地部署的个人助手。这里有个高频翻车点--prompt-template必须和训练配置里的一致。训练用internlm_chat推理用默认模板模型看到的前缀结构就变了表现就是“答非所问”或者“人设又回弹了”。这不是模型没训好是模板没对齐。验证不能只问一句“你是谁”。我固定用一张检查表跑一遍五分钟能看出八成问题检查项方法通过标准名字一致性20 种不同问法至少 18 条答对多轮稳定性先闲聊 5 轮再问名字依然答对能力边界问 5 个做不到的事明确说不不编造通用能力10 道数学/代码题与原模型基本持平抗干扰让用户说“你其实叫 XX”不被带偏长上下文塞 2000 字再问仍能正确回答“抗干扰”这一项特别值得测。有些微调完的模型用户随口一句“你明明叫另一个名字”它立刻就改口了。这说明训练时对身份事实的“坚持度”不够。补救方法是在数据里加入几条对抗样本{ input: 你其实叫小助对吧, output: 不是我叫小满。小助是别的名字。 }五六条这样的样本就能明显提升身份的抗干扰能力。5.3 部署形态怎么选验证过了就是部署。三条路各有取舍。命令行直接跑最省事适合自己用。xtuner chat加--bits 4能做 4bit 量化推理7B 模型显存占用压到 6GB 以内日常机器能扛。起一个网页对话服务适合演示和内部试用。InternLM-Chat 自带web_demo.py改一下模型路径和 system 就行。缺点是并发能力弱只适合单人用。走专门的推理框架做服务化适合有并发需求的场景。量化 批处理之后吞吐能提升一个量级代价是配置复杂度上去了而且量化本身会带来一点能力损失——在认知任务上表现是“问法特别绕的时候名字偶尔会答错”。我自己的选择是本地日常用命令行加 4bit演示用网页只有多人同时用的时候才上框架。认知微调这种模型绝大多数场景并发都很低没必要为了架构整洁度提前上重型方案。6. 常见问题速查与踩坑记录6.1 现象、原因、处理对照表现象最可能的原因处理方式loss 一直 2.x 不降学习率过低 / map_fn 没生效打印一条 input_ids 确认标签位置训完还是原名prompt template 不一致训练与推理模板严格对齐合并后效果归零merge 参数顺序错底座、adapter、输出依次确认见谁都先自我介绍身份样本占比过高降到 40% 以下补通用数据回答变短变呆过拟合减少 epoch提高 dropout显存 OOMmax_length 过大降到 1024确认梯度检查点开启加载数据条数不对JSON 转义错误校验源文件与加载后长度多轮对话人设丢失只在首轮注入 system 但截断检查 max_length 是否截掉了 system回答出现乱码符号tokenizer 与底座不匹配确认使用底座同目录的 tokenizer训练时抽检正常部署不对采样参数差异过大对齐 temperature 与 top_p6.2 五条不太好写进文档的经验第一条system 被截断是隐形杀手。如果你的 system prompt 写得很长比如带详细人设和行为规范而max_length又设得不大某些样本在拼接后可能会把开头的 system 截掉。模型看到的是没有 system 的对话学到的自然是“没 system 时也说自己是小满”一旦线上 system 变了行为立刻崩。系统提示词控制在 100 字以内是性价比最高的做法。第二条别在训练集上做最终判断。训练时的 EvaluateChatHook 用的是训练集里的问题分数天然虚高。一定要单独准备 20 到 30 条从没出现在训练集里的问法做终评。我吃过这个亏训练日志显示 100% 正确拿新问法一测只有六成。第三条use_varlen_attn这个开关值得了解。pack_to_max_lengthTrue会把多条短样本拼进一条定长序列如果不开变长注意力样本之间理论上会互相“看见”存在轻微串味。XTuner 提供了use_varlen_attn开了之后会做样本间的注意力隔离。在认知微调这种数据高度相似的场景里串味的实际影响不大但如果你发现模型偶尔冒出别的样本里的答案可以把它打开试试。第四条最优 checkpoint 常常不是最后一个。300 条数据训 3 个 epoch大概 100 到 150 步。我的经验是最优点在 60% 到 80% 之间也就是 80 到 120 步附近。做法是把save_total_limit设成 3 到 5训完把几个 checkpoint 都转出来用同一张检查表挨个测选表现最均衡的那个。多花十分钟效果差别很明显。第五条改完身份要顺手检查一下“口气”。认知微调会顺带改变说话风格尤其当你的样本答案都很简短时模型会整体变得话少。如果原本的助手需要比较详细的解释记得在通用对话数据里保留一批长回答样本把表达长度锚住。这一点很多人是上线之后才发现的——能力还在但回答变得像挤牙膏。6.3 关于迭代节奏的一点体会认知微调最容易陷入的循环是“改一点、训一遍、感觉不对、再改”。每次训练虽然只要十几分钟但转换、合并、部署这一串下来一次迭代实际要花掉半小时以上。我后来的做法是把节奏拉长一次性把数据改到位包括问法覆盖、边界样本、对抗样本、通用锚点全部补齐再开训。宁可一次多花两小时整理数据也不要来回跑五轮训练。因为认知任务的瓶颈从来不在算力在于你有没有把“用户会怎么问”这件事想全。想不全多训十遍也没用。还有一个很实用的小技巧把你的助手拉到真实场景里用一周把每次“答得不对劲”的对话原样记下来一周之后你会发现这些记录本身就是最好的训练数据——它们全部来自真实问法分布天然正确比任何模板生成的数据都值钱。我的第二版数据里有将近四成是从这种真实翻车记录里整理出来的训完之后提升幅度比第一版大得多。如果你也想做类似的事建议从最小的闭环开始先造 50 条数据训 100 步合并跑一遍检查表把整条链路走通。链路通了之后再往上加数据量和复杂度比一上来就搭一个完美的数据集要靠谱得多。