Skip to content

监督微调 (SFT)

SFT 将预训练模型变成能对话的助手,是 post-training 的第一步

在大模型体系中的位置

预训练 (Pre-training)          → 学习语言知识和世界知识

监督微调 (SFT)  ← 你在这里     → 学习指令跟随能力

偏好对齐 (RLHF/DPO/GRPO)      → 学习人类偏好,安全有用

预训练模型的目标是 next-token prediction,它学会了语言的统计规律,但不知道如何回答问题。SFT 的目标是将预训练模型从"续写文本"转变为"遵循指令"——通过在 (指令, 回答) 对上训练,模型学会了按照用户的意图生成有结构的回答,并在适当的时候停止输出。

什么是监督微调?

预训练模型的"复读机"问题

预训练模型直接用于对话时,往往会出现重复输出或不停止的情况:

python
# 预训练模型的典型输出(未经 SFT)
input_ids = tokenizer(['请解释什么是机器学习'], return_tensors='pt')['input_ids']
output_ids = model.generate(input_ids, max_new_tokens=128, do_sample=False)
result = tokenizer.decode(output_ids[0], skip_special_tokens=True)
# 输出: "请解释什么是机器学习 什么是深度学习 什么是强化学习 什么是神经网络
#        什么是卷积神经网络 什么是循环神经网络 什么是注意力机制 什么是 Transformer..."
# → 不断重复、无法停止,这就是"鹦鹉学舌"现象

原因在于预训练数据不是每条都有 <EOS> 标记,模型没有学会在适当的时候停止生成(常被称为"复读机"现象)。

SFT 的核心思路

SFT 仍然使用 next-token prediction 的训练目标,但有两个关键差异:

  1. 训练数据是结构化的指令-回答对,而非原始文本
  2. Loss Masking:只对回答部分计算损失,不学习如何生成指令
LSFT=tresponselogPθ(xt|x<t)

经过 SFT 后,模型能够正确回答问题并预测 <EOS> 停止:

python
# SFT 后的模型输出
# 输出: "机器学习是人工智能的一个分支,它通过算法让计算机从数据中自动学习规律。
#        核心思想是:给模型大量样本数据,让它找到输入和输出之间的映射关系,
#        从而对新数据做出预测或决策。常见方法包括监督学习、无监督学习和强化学习。"
# → 有逻辑、有结构、能正常停止

Full Fine-tuning

全参数微调(Full Fine-tuning)更新模型的所有参数,是最直接的微调方式。

python
from transformers import AutoTokenizer, AutoModelForCausalLM

# 加载预训练模型,所有参数均可训练
tokenizer = AutoTokenizer.from_pretrained('Qwen/Qwen3-0.6B')
model = AutoModelForCausalLM.from_pretrained('Qwen/Qwen3-0.6B')

# 全参微调:所有参数都参与梯度更新
optimizer = torch.optim.AdamW(model.parameters(), lr=1e-5)
优点缺点
效果最好,模型充分适配下游任务显存占用大:7B 模型全参微调需要 ~60GB 显存
实现简单,无需额外框架每个任务需要存储完整模型副本
适合有充足算力的场景容易过拟合,尤其在小数据集上

LoRA:低秩适配

核心原理

LoRA (Low-Rank Adaptation) 的核心假设是:微调过程中权重的更新矩阵是低秩的

对于预训练权重 W0Rd×d,LoRA 将更新分解为两个低秩矩阵的乘积:

W=W0+ΔW=W0+αWBWA

其中 WARd×rWBRr×drd 是秩。

数学直觉:一个 4096×4096 的矩阵有 1600 万参数,但如果 r=16,LoRA 只需 4096×16×2=131072 个参数,减少了 99.2%

PyTorch 实现 LoRA

python
import torch
import torch.nn as nn

class LoRALinear(nn.Module):
    """LoRA 线性层:冻结原参数,只训练低秩旁路"""
    def __init__(self, original_linear, rank=4, alpha=0.1):
        super().__init__()
        self.alpha = alpha                              # 缩放因子
        self.dim_in = original_linear.in_features
        self.dim_out = original_linear.out_features
        self.r = rank                                   # 低秩维度

        # 冻结原始权重
        self.weight = nn.Parameter(original_linear.weight.data)
        self.weight.requires_grad = False
        if original_linear.bias is not None:
            self.bias = nn.Parameter(original_linear.bias.data)
            self.bias.requires_grad = False
        else:
            self.register_parameter('bias', None)

        # LoRA 旁路参数(可训练)
        self.WA = nn.Linear(self.dim_in, self.r, bias=False)   # 降维: d → r
        self.WB = nn.Linear(self.r, self.dim_out, bias=False)   # 升维: r → d

        # 初始化:WA 置零保证初始时旁路输出为 0
        nn.init.constant_(self.WA.weight.data, 0.0)
        nn.init.normal_(self.WB.weight.data, mean=0.0, std=0.01)

    def forward(self, X):
        # 原始前向(无梯度)
        bsz, seq_len, dim_in = X.shape
        h = X.view(-1, dim_in) @ self.weight.T
        h = h.view(bsz, seq_len, -1)
        if self.bias is not None:
            h += self.bias

        # LoRA 旁路(有梯度): h_lora = alpha * X @ WA @ WB
        h_lora = h + self.alpha * self.WB(self.WA(X))
        return h_lora

替换模型中的 Linear 层

python
def apply_lora_adapter(model, rank=4):
    """递归替换模型中所有 Linear 层为 LoRALinear"""
    for name, module in model.named_children():
        if isinstance(module, nn.Linear):
            new_layer = LoRALinear(module, rank)
            setattr(model, name, new_layer)
        else:
            apply_lora_adapter(module, rank)  # 递归处理子模块
    return model

# 替换后,只有 LoRA 参数有梯度
apply_lora_adapter(model, rank=4)
for name, param in model.named_parameters():
    if param.requires_grad:
        print(f"{name} {param.shape}")  # 只有 WA, WB 的参数
    else:
        print(f"{name} no_grad")        # 原始参数冻结

关键参数详解

参数含义经验值
r (rank)低秩维度,控制旁路容量8~64,推荐 16
alpha缩放因子,控制旁路影响强度通常设为 r2*r
target_modules要加 LoRA 的层["q_proj", "k_proj", "v_proj", "o_proj"]
lora_dropoutLoRA 旁路的 Dropout0.05~0.1

使用 PEFT 库(推荐生产用法)

python
from peft import LoraConfig, get_peft_model

config = LoraConfig(
    r=16,
    lora_alpha=8,
    target_modules=["q_proj", "k_proj", "v_proj", "o_proj"],
    lora_dropout=0.05,
    bias="none",
    task_type="CAUSAL_LM"
)
model = get_peft_model(model, config)
model.print_trainable_parameters()
# 输出: trainable params: 1,310,720 || all params: 630,000,000 || trainable%: 0.21%

QLoRA:量化 + LoRA

QLoRA 在 LoRA 基础上对原始参数进行量化,进一步降低显存占用。核心思想是:既然原始参数是冻结的,那就把它压缩存储,计算时再解压。

三大技术

  1. 4-bit NormalFloat (NF4) 量化:利用预训练权重近似正态分布的特点,用 4-bit 量化替代 FP16 存储
  2. 双重量化 (Double Quantization):对量化的缩放因子再做一次量化,进一步节省显存
  3. 分页优化器 (Paged Optimizers):利用 CPU 内存处理显存不足时的梯度状态

实现 QLoRA 核心:量化线性层

python
import bitsandbytes as bnb

class QuantizedLinear(nn.Module):
    """INT8 量化的线性层,前向时动态去量化"""
    def __init__(self, module, dim_in, dim_out, block_size=64):
        super().__init__()
        self.in_features = dim_in
        self.out_features = dim_out
        self.block_size = block_size

        # 量化原始权重
        w = module.weight.data
        quant_weight, quant_state = bnb.functional.quantize_blockwise(
            w, blocksize=self.block_size
        )
        self.quant_weight = quant_weight    # int8 类型,显存减半
        self.weight_scale = quant_state     # 量化缩放因子
        self.weight = None                  # 删除原始参数节省显存
        self.quantized = True

    def forward(self, x):
        # 计算时动态反量化回 fp16
        dequant_weight = bnb.functional.dequantize_blockwise(
            self.quant_weight, self.weight_scale, blocksize=self.block_size
        )
        return nn.functional.linear(x, dequant_weight, None)

QLoRA = 量化 + LoRA

python
# 先对模型进行量化,再加上 LoRA 旁路
apply_quant(model, QuantizedLinear, dim=512, block_size=64)
apply_lora_adapter(model, LoRALinear, r=4)

# 前向时:量化权重无梯度,LoRA 参数有梯度
H = model(X)
loss.backward()
print(model.Wq.weight.quant_weight)  # 无梯度,仍为 int8
print(model.Wq.WA.weight.grad)       # 有梯度,正常更新

Transformers 快捷调用

python
from transformers import BitsAndBytesConfig

bnb_config = BitsAndBytesConfig(
    load_in_4bit=True,                    # 4-bit 量化
    bnb_4bit_quant_type="nf4",            # NormalFloat4 量化类型
    bnb_4bit_use_double_quant=True,       # 双重量化
    bnb_4bit_compute_dtype=torch.bfloat16 # 计算精度
)
model = AutoModelForCausalLM.from_pretrained(
    'Qwen/Qwen3-0.6B', quantization_config=bnb_config
)
# 1B 模型 fp16 占 ~2GB 显存,4-bit 只占 ~0.5GB

SFT 数据集准备

数据格式对比

SFT 数据通常有三种格式:

格式结构示例数据集
Alpacainstruction + input + outputtatsu-lab/alpaca
ShareGPT多轮 conversations 列表ShareGPT 系列
OpenAI/ChatMLmessages 列表 with roleOpenAI API 格式

Chat Template:对话模板

Chat Template 将对话结构化为模型能理解的格式。以 ChatML 格式为例:

python
DEFINED_SOS_TOKEN = '<|im_start|>'
DEFINED_EOS_TOKEN = '<|im_end|>'
DEFINIED_SYSTEM_PROMPT = '你是一个有帮助的AI助手,请准确详细地回答用户的问题'

# 使用 tokenizer 内置模板
messages = [
    {'role': 'system', 'content': DEFINIED_SYSTEM_PROMPT},
    {'role': 'user', 'content': '请解释什么是机器学习'},
    {'role': 'assistant', 'content': '机器学习是人工智能的一个子领域,通过数据驱动的方式让模型自动学习规律...'},
]
prompt = tokenizer.apply_chat_template(messages, tokenize=False)
# 输出:
# <|im_start|>system
# 你是一个有帮助的AI助手,请准确详细地回答用户的问题<|im_end|>
# <|im_start|>user
# 请解释什么是机器学习<|im_end|>
# <|im_start|>assistant
# 机器学习是人工智能的一个子领域,通过数据驱动的方式让模型自动学习规律...<|im_end|>

Token 级别的数据处理(含 Label Masking)

SFT 的关键在于只对 assistant 回答部分计算 loss。通过 is_label 标记区分:

python
def ChatTemplateToken(example, tokenizer):
    """将对话转为 token ids,并标记哪些 token 参与 loss 计算"""
    sos_token_id = tokenizer(DEFINED_SOS_TOKEN).input_ids[0]
    eos_token_id = tokenizer(DEFINED_EOS_TOKEN).input_ids[0]

    input_ids = [sos_token_id]
    is_labels = [0]    # 0 表示不计算 loss,1 表示计算 loss

    for item in example:
        if item['role'] == 'ASSISTANT':
            prompt = '\n#' + item['role'] + ':'
            content = item['content']
            prompt_ids = tokenizer(prompt).input_ids
            content_ids = tokenizer(content).input_ids

            # ASSISTANT 的角色标记不算 loss,内容和 EOS 算 loss
            is_labels += [0]*len(prompt_ids) + [1]*len(content_ids) + [1]
            input_ids += prompt_ids + content_ids + [eos_token_id]
        else:
            prompt = '\n#' + item['role'] + ':' + item['content']
            prompt_ids = tokenizer(prompt).input_ids
            input_ids += prompt_ids
            is_labels += [0]*len(prompt_ids)    # 系统/用户部分不算 loss

    return input_ids, is_labels

Padding 与 Collate 函数

python
def paddding_collate_fn(batch_data, pad_token_id, ignore_index=-100):
    """将不等长的样本 padding 到同一长度,并构造 labels"""
    bs = len(batch_data)
    max_len = max(data['input_ids'].shape[0] for data in batch_data)

    input_ids = torch.ones(bs, max_len, dtype=torch.long) * pad_token_id
    attention_masks = torch.zeros(bs, max_len, dtype=torch.long)
    labels = torch.ones(bs, max_len, dtype=torch.long) * ignore_index  # -100

    for i in range(bs):
        seq_len = batch_data[i]['input_ids'].shape[0]
        input_ids[i, :seq_len] = batch_data[i]['input_ids']
        attention_masks[i, :seq_len] = 1

        # 只有 is_label=1 的位置参与 loss 计算
        idx = torch.where(batch_data[i]['is_label'] != 0)[0]
        labels[i, idx - 1] = batch_data[i]['input_ids'][idx]

    return {'input_ids': input_ids, 'attention_masks': attention_masks, 'labels': labels}

注意labels-100 的位置会被 CrossEntropyLoss(ignore_index=-100) 自动忽略。

完整 SFT 训练流程

数据加载

python
from datasets import load_dataset
from torch.utils.data import DataLoader

# 加载 Alpaca 指令数据集
dataset = load_dataset('tatsu-lab/alpaca')
# DatasetDict({ train: Dataset({ features: ['instruction','input','output','text'], num_rows: 52002 }) })

# 转换为对话格式
def dataset_to_messageslist(dataset):
    messages = []
    for item in dataset['train']:
        messages.append([
            {'role': 'SYSTEM', 'content': DEFINIED_SYSTEM_PROMPT},
            {'role': 'USER', 'content': item['instruction'] + item['input']},
            {'role': 'ASSISTANT', 'content': item['output']},
        ])
    return messages

# 构建 DataLoader
dataset_train = TokenSFTDataset(dataset_instruction, tokenizer)
collate_fn = PaddingCollateFunction(pad_token_id=pad_token_id, ignore_index=-100)
dataloader = DataLoader(dataset_train, batch_size=8, shuffle=True, collate_fn=collate_fn)

训练循环

python
import torch.optim as optim
from tqdm import tqdm

learning_rate = 1e-5
epochs = 1
loss_fn = nn.CrossEntropyLoss(ignore_index=-100)
optimizer = optim.AdamW(model.parameters(), lr=learning_rate)

for epoch in range(epochs):
    train_bar = tqdm(dataloader, desc=f'Epoch {epoch+1}/{epochs}')

    for step, batch in enumerate(train_bar):
        optimizer.zero_grad()
        bsz, seq_len = batch['input_ids'].shape

        # 前向传播
        output = model(input_ids=batch['input_ids'],
                       attention_mask=batch['attention_masks'])
        logits = output.logits

        # 计算 loss(只在 label != -100 的位置)
        loss = loss_fn(
            logits.view(bsz * seq_len, -1),   # (B*T, V)
            batch['labels'].view(bsz * seq_len) # (B*T,)
        )

        # 反向传播 + 更新
        loss.backward()
        optimizer.step()

        if step % 10 == 0:
            train_bar.set_postfix(loss=f'{loss.item():.4f}')

推理验证

python
def generate(model, tokenizer, prompt, max_new_tokens=128):
    """使用 SFT 模型生成回答"""
    messages = [
        {'role': 'SYSTEM', 'content': DEFINIED_SYSTEM_PROMPT},
        {'role': 'USER', 'content': prompt},
        {'role': 'ASSISTANT', 'content': ''},
    ]
    input_ids, _ = ChatTemplateToken(messages, tokenizer)
    input_ids = torch.tensor([input_ids[:-1]], dtype=torch.long)  # 去掉末尾 EOS

    output_ids = model.generate(input_ids, max_new_tokens=max_new_tokens, do_sample=False)
    return tokenizer.decode(output_ids[0], skip_special_tokens=True)

result = generate(model, tokenizer, '请解释什么是机器学习')
# 输出: "机器学习是人工智能的一个分支,它通过算法让计算机从数据中自动学习规律。
#        核心思想是:给模型大量样本数据,让它找到输入和输出之间的映射关系,
#        从而对新数据做出预测或决策。常见方法包括监督学习、无监督学习和强化学习。"

训练参数调优指南

参数推荐值说明
学习率1e-5 ~ 5e-5比预训练低 1-2 个数量级
Batch Size8~32配合梯度累积使用
Epochs1~3过多会过拟合
Warmup总步数的 3%~10%稳定训练初期
序列长度512~2048根据数据分布决定
梯度裁剪1.0防止梯度爆炸

数据质量 > 数据数量:LIMA 论文证明,1000 条精心标注的高质量数据优于 10 万条低质量数据。

TRL SFTTrainer 实战

TRL (Transformer Reinforcement Learning) 提供了开箱即用的 SFTTrainer,是目前最流行的 SFT 训练工具之一。它封装了 Hugging Face Trainer,并内置了 chat template 处理、packing、loss masking 等 SFT 专属功能。

基本用法

python
from trl import SFTConfig, SFTTrainer

training_args = SFTConfig(
    output_dir="sft-model",
    max_seq_length=2048,
    packing=True,          # 多条短样本打包为一条长序列
    num_train_epochs=3,
)
trainer = SFTTrainer(
    model="Qwen/Qwen2-0.5B",
    args=training_args,
    train_dataset=dataset,
)
trainer.train()

为什么推荐 SFTTrainer?

相比手写训练循环,SFTTrainer 自动处理了 chat template 转换、loss masking、序列打包等繁琐细节,让你专注于数据质量和超参调优。

Chat Template 处理

TRL 自动调用模型 tokenizer 的 chat template 将对话格式转换为模型输入:

  • 数据集格式:{"messages": [{"role": "user", "content": "..."}, {"role": "assistant", "content": "..."}]}
  • 自动添加 role tokens(如 <|im_start|>user
  • 自动对 assistant 部分做 loss masking,user 和 system 部分不参与损失计算

数据格式详解

SFTTrainer 支持三种数据格式:

格式数据列适用场景
Conversationalmessages 列(标准 chat 格式)多轮对话、ChatBot
Instructionprompt + completion单轮指令跟随
Language modelingtext续写式微调
Conversational 格式示例
python
dataset = [
    {"messages": [
        {"role": "user", "content": "解释什么是梯度下降"},
        {"role": "assistant", "content": "梯度下降是一种优化算法..."},
    ]}
]

Packing(序列打包)

Packing 是提升 SFT 训练效率的关键技巧:

  • 将多条短训练样本打包成一条长序列,提高 GPU 利用率
  • 使用 attention mask 防止跨样本注意力泄露,保证训练正确性
  • 设置 packing=True 即可启用
  • 特别适合训练数据长度差异大的场景
不使用 Packing:
[样本1--padding--padding] [样本2--------padding] [样本3-padding-padding]
  ↑ GPU 大量时间在处理 padding token

使用 Packing:
[样本1--样本2--------样本3] [样本4------样本5------]
  ↑ GPU 利用率大幅提升

WARNING

Packing 会改变每个 batch 的有效样本数,可能需要相应调整学习率。

LoRA 集成

SFTTrainer 原生支持 PEFT,只需传入 peft_config 即可启用 LoRA 训练:

python
from peft import LoraConfig

peft_config = LoraConfig(
    r=16,
    lora_alpha=32,
    target_modules=["q_proj", "v_proj"],
    lora_dropout=0.05,
)
trainer = SFTTrainer(
    model="meta-llama/Llama-3-8B",
    args=training_args,
    train_dataset=dataset,
    peft_config=peft_config,  # 直接传入LoRA配置
)

这种方式将 SFT 训练和 LoRA 微调无缝结合,无需手动修改模型结构。

多轮对话训练

实际场景中,用户与模型的交互往往是多轮对话。多轮对话训练需要特别注意 loss 计算和数据格式。

数据格式

python
# 多轮对话数据格式
dataset = [
    {"messages": [
        {"role": "system", "content": "你是一个有帮助的助手"},
        {"role": "user", "content": "什么是 Transformer?"},
        {"role": "assistant", "content": "Transformer 是一种基于自注意力的..."},
        {"role": "user", "content": "它的核心创新是什么?"},
        {"role": "assistant", "content": "核心创新是自注意力机制..."},
    ]}
]

训练要点

  • Loss Masking:每轮只对 assistant 回复计算 loss,system 和 user 部分被 mask 掉
  • Chat Template:自动添加 role tokens,正确分隔每轮对话
  • Truncation 策略:长对话可能超过 max_seq_length,需要合理截断

长对话截断建议

优先保留最后几轮对话(最相关的上下文),截断最早的轮次。也可以将长对话拆分为多条训练样本,每条保留完整的上下文窗口。

多轮对话 Loss Masking 示意:

[system] 你是一个助手  [user] 什么是 Transformer? [assistant] Transformer 是...
 ↑ 不计算 loss          ↑ 不计算 loss              ↑ 计算 loss ✓

[user] 核心创新? [assistant] 核心创新是自注意力...
 ↑ 不计算 loss     ↑ 计算 loss ✓

训练加速与显存优化

当模型规模增大或数据量增长时,训练效率和显存管理成为关键瓶颈。以下是常用的优化手段:

Gradient Checkpointing

以计算换显存——前向传播时不保存中间激活值,反向传播时重新计算:

python
training_args = SFTConfig(
    gradient_checkpointing=True,
    # ... 其他参数
)

通常可节省 50%-70% 显存,代价是训练速度下降约 20%-30%。

Mixed Precision 混合精度训练

使用 bf16 或 fp16 替代 fp32,显存减半、计算加速:

python
training_args = SFTConfig(
    bf16=True,   # 推荐在 A100/H100 上使用 bf16
    # fp16=True, # 旧款 GPU 可用 fp16
)

bf16 vs fp16

bf16 的动态范围与 fp32 相同,不容易出现溢出,是目前主流选择。fp16 需要配合 loss scaling 使用。

DeepSpeed ZeRO

ZeRO 将优化器状态、梯度、模型参数分阶段切分到多张 GPU 上:

阶段切分内容显存节省
ZeRO-1Optimizer States~4x
ZeRO-2+ Gradients~8x
ZeRO-3+ Parameters~Nx(N 为 GPU 数)
bash
# 使用 DeepSpeed ZeRO-2 启动训练
deepspeed --num_gpus=4 train.py --deepspeed ds_config_zero2.json

Gradient Accumulation

小批次累积模拟大 batch,在不增加显存的前提下提升有效 batch size:

python
training_args = SFTConfig(
    per_device_train_batch_size=2,
    gradient_accumulation_steps=4,  # 有效 batch size = 2 × 4 = 8
)

Flash Attention

Flash Attention 通过 IO 感知的分块计算,将 attention 的显存复杂度从 O(n2) 降至 O(n)

python
from transformers import AutoModelForCausalLM

model = AutoModelForCausalLM.from_pretrained(
    "meta-llama/Llama-3-8B",
    attn_implementation="flash_attention_2",
)

vLLM 集成

TRL 支持使用 vLLM 做生成加速,主要用于需要在线生成的训练算法(如 GRPO、PPO)。vLLM 的 PagedAttention 和 continuous batching 可以大幅加速生成过程。

各优化技术对比总结
技术节省显存速度影响适用场景
Gradient Checkpointing✅ 大幅⬇ 略慢单卡大模型
Mixed Precision (bf16)✅ 减半⬆ 加速所有场景
DeepSpeed ZeRO✅ 线性扩展➡ 通信开销多卡训练
Gradient Accumulation❌ 不变➡ 不变模拟大 batch
Flash Attention✅ 显著⬆ 加速长序列训练
Packing❌ 不变⬆ 加速短样本数据集

苏格拉底时刻 💡

  1. LoRA 的低秩假设为什么成立? 微调只是让模型适配新任务,权重变化集中在少数方向上,不需要改变全部参数空间。
  2. SFT 为什么只需 1-3 epochs? 预训练模型已经学会了语言能力,SFT 只是"教它怎么用",少量训练即可。过多训练反而会导致过拟合——模型记住了训练数据的模式而非泛化能力。
  3. Loss Masking 为什么重要? 如果不做 masking,模型会学习生成用户提问的模式,导致输出中混入提问语气的内容。
  4. LoRA 的 rank 选多少? rank=8 适合简单任务(格式、风格),rank=64 适合复杂任务(新知识、新领域)。过高的 rank 接近全参微调,失去了效率优势。

常见问题 & 面试考点

  • Q: LoRA 和 Full Fine-tuning 效果差多少? 在大多数任务上差距很小(<2%),但在需要大量新知识注入的场景下 Full FT 更优。
  • Q: QLoRA 精度如何? 量化会引入精度损失,一般比 LoRA 略差。在显存受限时是性价比最高的选择。
  • Q: SFT 数据中的 system prompt 有什么用? 定义模型的角色和行为边界,是控制模型行为的重要手段。
  • Q: 为什么 SFT 后模型的通用能力可能下降? 这被称为"对齐税"(Alignment Tax),过度 SFT 会让模型过度拟合特定格式,损失预训练阶段学到的通用能力。

推荐资源