本文属于《Ai大模型训练教程》系列,详细讲解多轮对话SFT如何避免灾难性遗忘,给出对话打包(packing)防跨对话污染的方法、system优先的截断策略,以及system指令归一化与权重处理的实操流程与排障清单。
背景:为什么多轮对话 SFT 更容易出现灾难性遗忘
在「Ai大模型训练教程」系列里,很多同学做完基础 SFT(单轮指令-回答)后,会很快转到多轮对话 SFT(Supervised Fine-Tuning)。这一步常见的坑是:模型在新数据上对话能力提升了,但旧能力(通用问答、格式遵循、工具调用习惯、系统安全边界等)明显退化,这就是训练中的灾难性遗忘。
多轮对话更容易触发遗忘,核心原因通常不是“多轮本身”,而是:
- 样本组织方式改变了:由“单轮问答”变为“长上下文序列”,损失函数的分布、有效 token 的比例、梯度方向都变了。
- 截断导致关键信息缺失:system 指令被截掉、对话开头被截掉,模型学到的是“没有约束的回答”。
- 对话打包(packing)不当:把多个对话硬拼成一个长序列,产生“跨对话污染”,模型学到了错误的上下文关联。
- system 指令处理混乱:不同数据源对 system 的写法、位置、权重不一致,导致模型对 system 的遵循能力波动。
本文聚焦三件最影响遗忘与稳定性的实操点:对话打包、截断策略、system 指令处理,并给出可落地的步骤与示例。
一、先定义你要“保住”的能力:用可观测指标约束遗忘
避免遗忘不是玄学,第一步是把“不能退化的能力”变成可测的指标,否则你只会在主观感受里反复摇摆。
建议至少建立三类小型评测集(几十到几百条即可):
1)通用能力回归集(General Regression)
- 覆盖:常识问答、总结改写、代码片段解释、数学推理(按你原模型常用场景)
- 目标:新模型相对 base 模型不下降或下降可控
2)指令遵循与格式集(Instruction/Format)
- 覆盖:严格 JSON 输出、表格、要点列表、拒答/安全边界
- 目标:system/开发者规范不被“聊天数据”冲掉
3)多轮一致性集(Multi-turn Consistency)
- 覆盖:上下文指代、约束记忆(用户改名/设定角色)、工具调用触发条件
- 目标:多轮收益真实存在
有了回归集,你才能验证下面每一种打包/截断/system 处理策略是否在“保住旧能力”的同时提升多轮能力。
二、多轮对话打包(Packing):既要提吞吐,也要防跨对话污染
对话数据通常长度差异很大,直接按样本 padding 会浪费大量 token。Packing 的目的,是把多个短样本拼到同一个序列里提高 GPU 利用率。但多轮对话 Packing 若处理不好,会产生严重的跨对话污染,表现为:
- 模型把上一个对话的设定带到下一个对话里
- 角色/口吻随机漂移
- 在不相关的问题里“延续上一段的结论”
2.1 Packing 的基本原则:边界清晰、损失隔离
你需要同时做到两件事:
- 在输入中显式分隔不同对话(让模型知道“新对话开始了”)
- 在损失上隔离不同对话(避免上一对话末尾对下一对话首部产生不合理梯度)
一个实用做法:在每个对话开头插入一个特殊分隔,例如:
- 纯文本:
<|conversation_start|> - 或沿用 chat 模板 token(不同模型不同)
更关键的是 label mask:
- 对 system 与 user token 通常设为
-100(不计入 loss) - 仅对 assistant 的回复 token 计算 loss
- 对分隔符 token 也设为
-100
这样即便你把两个对话打包在同一序列里,loss 也只在各自 assistant 段落上回传。
2.2 两种常用 packing 策略对比
策略 A:按“对话为单位”打包(推荐优先)
- 一个对话视为不可拆分的整体
- 尽量把多个“完整对话”拼到一个 sequence
优点:结构天然完整,system/上下文不会被拆开;对灾难性遗忘更友好。
缺点:对话过长时会造成较多浪费(因为长对话无法与其它对话共同填满)。
适用:你在做对话式助手,且非常在意 system 遵循与多轮一致性。
策略 B:按“轮次/消息”为单位打包(谨慎)
- 把一个长对话拆成多个片段(chunk),每个片段包含最近 N 轮
优点:更高吞吐,能让长对话覆盖更多训练步。
缺点:很容易把 system/关键信息截断掉;也容易让模型学会“无 system 时也要继续生成”。
适用:长对话占比很高、显存紧张且你有完善的截断与 system 重注入策略(见下文)。
2.3 实操建议:packing 的“安全阈值”
如果你是第一次把单轮 SFT 升级到多轮,建议:
- 先采用“对话为单位打包”
- 最大长度(max_seq_len)先不要追太激进(例如 2048/4096),先把流程跑稳定
- 确保每个对话开头都有 system,并且 system 不被截断(见第三部分)
当回归集指标稳定后,再逐步引入更激进的 packing(比如更高长度、更细粒度 chunk)。
三、截断策略:决定了模型到底学到“哪段上下文”
多轮对话截断(truncation)是灾难性遗忘的高发区,因为它会改变训练分布:
- 你以为你在训练“遵循 system 的对话”
- 实际上大部分样本的 system 被截掉了
- 模型学会了“system 约束可有可无”
3.1 先理解三种截断位置的后果
假设序列超长,需要截断:
1)截断对话开头(左截断)
- 常见于“只保留最近 K tokens”
- 风险最大:system 与早期约束最容易丢
- 结果:模型更像“无约束闲聊”,system 遵循能力下降
2)截断对话中间
- 结构破坏:user/assistant 可能断在半句
- 结果:训练噪声增大,模型学到不完整模式
3)截断对话末尾(右截断)
- 对多轮学习不利:最后的 assistant 回复可能被切碎
- 结果:loss 主要来自不完整回复,训练不稳定
因此,多轮对话 SFT 的截断目标通常是:
- 尽量保住 system
- 尽量保住最后几轮完整的 user→assistant 对
- 避免在 assistant 回复中间硬切
3.2 推荐的“system 优先 + 尾部保留”策略(实操版)
给一个可执行的策略,你可以直接照着实现:
步骤 1:将对话拆成结构化消息
每条消息包含:role(system/user/assistant)与 content。
步骤 2:固定保留 system(必要时重注入)
- 如果原始对话没有 system:补一个默认 system(见第四部分)
- 如果有 system:提取出 system 作为必须保留段
步骤 3:从尾部向前累加轮次,直到接近 max_len
- 以“轮次”为单位加入(user+assistant 作为一轮),不要只加单条消息
- 每次加入前先估计 token 长度(可用 tokenizer 预编码)
步骤 4:保证最后一个 assistant 回复尽量完整
- 如果最后一轮的 assistant 太长:宁可缩短更早的上下文,也优先保住最后一轮完整
- 若必须截断 assistant:建议在句子边界/段落边界截断(简单做法:按标点或换行做近似切分)
步骤 5:对 user/system 做 label mask
仅对 assistant 内容计算 loss,其他全部 -100。
这种策略的直觉是:
- system 作为“全局规则”必须稳定出现
- 最近的对话轮次对当前回答最相关
- 训练时让模型对“回答部分”负责,减少对 prompt 的过拟合与噪声
3.3 典型坏例子与修复
坏例子:直接 token 级左截断
- 结果:system 被切掉
- 模型逐渐不理会 system
修复:
- system 单独保留,不参与左截断
- 左截断只作用于“历史轮次块”
坏例子:把超长 assistant 回复切成两段分别作为两条样本
- 结果:第二段没有对应的 user,模型学到“无提问也要续写”
修复:
- 若要 chunk:每段都必须带上同一个 user 问题与必要上下文,并明确标识 continuation(不推荐新手这么做)
- 更简单:降低 max_new_tokens 的期望,清洗掉超长输出样本
四、system 指令处理:统一模板、稳定位置、控制权重
system 指令在多轮对话 SFT 里承担“行为规范”的角色。处理不好会直接导致:
- 安全/合规能力退化
- 输出格式不稳定
- 工具调用/函数调用约束失效
4.1 统一 system 模板:避免多来源数据互相打架
真实数据通常来自多个渠道:客服对话、标注对话、公开指令集、自建角色扮演集等。每个渠道的 system 写法可能不同。
建议做一次“system 归一化(normalization)”:
推荐结构
- 全局系统规则(Global System):对所有样本一致
- 任务系统规则(Task System,可选):对特定数据源/任务一致
- 样本特定补充(Sample System,可选):该对话独有
最终合并为一个 system message,放在对话最前面。
示例(可直接用)
全局 system 可包含:
- 语言与风格:默认中文、简洁、先结论后解释
- 安全边界:遇到违法/隐私请求拒答并给替代建议
- 格式规则:要求输出 JSON 时必须严格 JSON
注意:不要把过多业务细则塞进全局 system,否则会扩大“必须记住的约束集合”,训练更难稳定。
4.2 system 的位置必须稳定:永远在最前
多轮对话数据中,system 偶尔会出现在中间(例如角色切换)。如果你不做处理,模型会学到 system 是“可插入的提示词”,从而削弱它的权威性。
实操建议:
- 训练用数据:system 固定放最前;中途角色切换改成 user 明确描述(例如“从现在开始你扮演…”),或转为新的 conversation(新对话开始)。
- 推理用模板:严格遵循同样结构,否则训练-推理分布不一致。
4.3 system 是否参与 loss?一般不参与,但要用“出现频率”保证学习
在 SFT 中,通常只对 assistant 计算 loss,这是主流做法。
问题是:system 不参与 loss,会不会学不会?
- 模型学习 system 的方式,是通过“在 system 条件下生成的 assistant 回复”间接学习
- 因此关键是:system 必须高频、稳定出现且不被截断
如果你发现模型仍然不听 system,优先排查:
- system 是否经常被截断
- 不同数据源的 system 是否冲突(一个要求简洁,一个要求啰嗦)
- 是否大量存在“无 system 的训练样本”(比如把 system 丢了)
在少数情况下,你可以尝试给 system 后面加一个“确认性 assistant 轮次”来强化约束,例如:
- system: 规则…
- user: 请确认你理解上述规则
- assistant: 我已理解,将遵循…
但这会引入额外对话模式,可能影响真实推理体验;建议只在你明确需要增强 system 遵循时使用,并控制比例。
4.4 多 system 合并与冲突解决:给出可执行规则
当一条样本里出现多个 system(例如数据源 A 的全局 system + 标注员额外 system),建议:
- 以“更严格/更安全”的规则优先
- 同类规则只保留一条(避免重复)
- 相互矛盾时,保留更上层(全局)并删除下层冲突项
可以用简单的 YAML/JSON 配置维护规则集合,训练前做合并。
五、把“对话打包 + 截断 + system”串起来的推荐数据流水线
下面给一个从原始对话到训练样本的最小可行流水线(你可以按自己的框架实现):
5.1 数据清洗与结构化
- 过滤明显低质样本:空回复、答非所问、重复刷屏、极端超长输出
- 统一字段:
messages = [{role, content}, ...] - 归一化 role:system/user/assistant(工具调用另算)
5.2 system 归一化与注入
- 若无 system:注入默认 global system
- 若有多个 system:合并为一个放最前
- 统一措辞与格式(例如都用中文、同一套风格约束)
5.3 截断:system 优先 + 尾部轮次保留
- system 单独保留
- 从尾部向前加入轮次直到接近 max_len
- 避免切碎 assistant 回复;必要时按句边界截断
5.4 打包(Packing)
- 优先以“完整对话”为单位打包
- 对话之间插入
<|conversation_start|>或等价分隔 - labels 仅覆盖 assistant token,其余 -100
5.5 训练时的两个小技巧(降低遗忘)
- 混合一小部分旧任务数据:例如 5%~20% 的单轮指令/格式数据作为回放(replay),是最直接有效的抗遗忘手段之一。
- 控制学习率与训练步:多轮对话数据分布更集中,过大 LR 或过多 epoch 更容易把原能力“推走”。建议从更小 LR、更少 epoch 起步,用回归集决定是否加大。
六、一个可复用的多轮样本示例(展示截断与 mask 思路)
假设我们有如下对话(简化示例):
- system:你是企业知识库助手,回答需引用来源;不确定就说不确定。
- user:我们产品 A 支持离线模式吗?
- assistant:支持……(引用文档 1)
- user:那离线模式有哪些限制?
- assistant:限制包括……(引用文档 2)
训练时建议构造成“模型输入 = system + 历史 user/assistant + 当前 user”,并对“当前 assistant 回复”计算 loss。
如果对话很长导致超长:
- 保留 system
- 优先保留最后一轮(“离线模式有哪些限制?”及其回答)
- 更早的轮次(是否支持)可选择保留或丢弃
labels 的直觉:
- system 与 user 都是条件,不让模型去“背诵提示词”
- assistant 才是监督目标
这会显著降低在多源数据下的过拟合噪声,并提升 system 约束的一致性。
七、排障清单:出现遗忘时优先检查什么
当你发现“多轮更强,但旧能力掉了”,按优先级检查:
- system 是否被截断:统计训练样本中 system 完整保留比例(建议接近 100%)。
- system 是否冲突:抽样 200 条 system,看是否互相打架(风格、拒答、格式)。
- packing 是否跨对话污染:检查对话边界是否有分隔,labels 是否正确 mask。
- 无 system 样本占比是否过高:尤其是从日志提取的数据。
- 训练超参是否过猛:LR、epoch、warmup、weight decay;优先减小 LR/步数。
- 是否缺少回放数据:加入少量通用/格式数据往往立竿见影。
结语:用“结构化样本 + 稳定 system + 合理截断”把多轮收益变成可控工程
多轮对话 SFT 的收益很大,但前提是你把数据工程做到位:
- 打包要有边界与 loss 隔离
- 截断要保 system、保完整轮次、少切碎回答
- system 要统一、稳定、可复用
把这三件事做好,你会发现模型的多轮一致性提升同时,通用能力与指令遵循不再大幅退化,灾难性遗忘从“必然发生”变成“可测、可控、可迭代优化”的工程问题。
Prev:LoRA/QLoRA微调详解:Rank怎么选、目标模块怎么配与效果权衡