AI 工程基础体系 · 第 26/100 篇。内容覆盖机器学习、深度学习与生成式 AI;模型、数据、评测、权限和成本会作为同一生产系统处理。
模型微调与适配:SFT、LoRA、数据治理、评测和何时不该微调
1. 先区分“适配”与“训练”
模型适配是让一个已有模型在特定任务、风格、领域或运行约束下表现更符合要求。它不一定意味着更新模型参数,常见适配方式包括:
- 提示词适配:通过系统提示词、少样本示例和输出格式约束改变输入。
- 检索增强生成(RAG):在推理时从外部知识库取回相关内容,不把知识写入模型参数。
- 工具或 Agent 适配:让模型调用搜索、数据库、代码执行器等外部能力。
- 全参数微调:更新模型的绝大多数或全部参数。
- 参数高效微调(PEFT):冻结基础模型,只训练少量附加参数,LoRA 是其中最常用的方法之一。
- 继续预训练:用领域文本继续训练语言建模目标,主要改变模型对领域语言和知识分布的建模能力。
- 监督微调(SFT):使用输入—目标输出样本,直接训练模型遵循任务指令。
因此,“微调”不是所有模型行为变化的总称。一个生产系统应先判断问题属于:
如果只是需要给模型提供最新文档,通常是检索问题;如果需要稳定地输出固定格式,可能是提示词或 SFT 问题;如果模型根本不会使用某个工具协议,则可能需要示例训练、工具封装,甚至更换模型。
一个实用的生产视图如下:
flowchart LR
A[业务需求] --> B{问题类型}
B -->|知识新鲜度/私有知识| C[RAG或工具]
B -->|格式/语气/流程稳定性| D[SFT或LoRA]
B -->|缺少基础能力| E[更换模型或继续预训练]
B -->|偶发性行为调整| F[提示词与解码参数]
C --> G[统一评测]
D --> G
E --> G
F --> G
G --> H{达到上线门槛?}
H -->|否| I[诊断数据、目标、推理链路]
I --> B
H -->|是| J[权限、成本、版本发布]
这里的“达到上线门槛”不能只看训练损失。微调会同时改变模型版本、数据责任、推理成本、权限边界和回滚方式,因此必须作为一个生产系统处理。
2. SFT:用监督目标训练模型遵循指令
2.1 SFT 的定义
SFT 是 Supervised Fine-Tuning,即监督微调。它使用带有目标输出的样本训练模型,使模型在给定输入条件下提高生成目标输出的概率。
对自回归语言模型而言,一条样本可以表示为:
其中:
- 是提示、指令、上下文或对话历史;
- 是期望的回答 token 序列;
- 模型根据前面已经出现的 token,预测下一个 token。
标准的监督损失通常是目标 token 的平均交叉熵:
其中 是模型参数, 表示目标序列中第 个 token 之前的 token。
这个公式的含义不是“让模型记住整条字符串”,而是让模型在每个目标位置提高正确 token 的条件概率。训练时通常采用 teacher forcing:预测第 个 token 时,输入使用真实的前置目标 token,而不是模型上一步自己生成的结果。
2.2 为什么要遮蔽提示部分的损失
实际输入往往拼成:
用户:请解释幂等性。
助手:幂等性是指……
模型会接收整段序列,但通常只对“助手”部分计算损失。可定义一个掩码 :
则损失为:
其中 是拼接后的完整 token 序列。
如果不遮蔽提示部分,模型也会被训练去预测用户问题、角色标记和格式前缀。这不一定完全错误,但会稀释目标信号,甚至让模型学习到错误的对话边界。
一个简单算例:
提示 token:请 解释
目标 token:幂等性 是
假设模型对两个目标 token 给出的概率分别是 和 ,则只计算目标部分时:
如果把提示部分也加入损失,就必须再提供“请”和“解释”的目标概率。此时损失反映的是“复现整段模板”的能力,而不再只反映回答质量。
2.3 SFT 数据格式的关键不是 JSON,而是监督边界
一种常见的数据结构是:
{
"messages": [
{"role": "system", "content": "你是一个严谨的技术助手。"},
{"role": "user", "content": "什么是幂等性?"},
{"role": "assistant", "content": "幂等性是指同一操作执行一次或多次,结果保持一致。"}
],
"metadata": {
"source": "internal_manual_v3",
"owner": "platform-team",
"license": "internal"
}
}
这里至少有三个独立问题:
- 序列化格式:如何将
messages转为模型的对话模板。 - 监督边界:哪些 token 是训练目标,哪些 token 只是上下文。
- 质量与权利:答案是否正确,数据是否允许用于训练。
不同模型的 chat template 可能不同。常见实现会使用 tokenizer 提供的模板函数,但模板名称、参数和行为属于具体模型与库版本能力,不能假定所有模型都相同。上线前应保存最终序列化结果,例如:
<|system|>你是一个严谨的技术助手。<|end|>
<|user|>什么是幂等性?<|end|>
<|assistant|>幂等性是指同一操作执行一次或多次,结果保持一致。<|end|>
并检查:
- 助手起始标记是否存在;
- 训练目标是否只覆盖助手内容;
- EOS token 是否正确;
- 长度截断是否截掉问题或答案的关键部分;
- 多轮对话中是否错误地把历史助手回答当成当前目标。
2.4 SFT 能学到什么,不能学到什么
SFT 主要改变的是训练样本分布上的条件生成行为。它适合学习:
- 输出结构,如 JSON、SQL、固定字段;
- 任务流程,如先分类再抽取;
- 领域术语的使用方式;
- 特定语气、风格和拒答模式;
- 工具调用的参数格式;
- 从输入到输出的稳定映射。
SFT 不等于可靠地写入事实数据库。若训练集包含“截至某日的产品价格”,模型可能学会生成这些价格,但不能保证未来价格仍然正确,也不能保证面对训练集之外的实体时不会编造。
可以把这个边界写成:
而不是一个具有事务一致性、可更新和可审计查询能力的知识库。
2.5 SFT 的典型失败模式
训练损失下降但业务指标下降,通常有以下原因:
- 训练集与线上输入格式不同;
- 训练目标包含大量模板 token,真正回答 token 很少;
- 数据中存在重复答案,模型学会套话;
- 训练数据的拒答规则与生产安全策略冲突;
- 长样本截断,目标只剩下开头;
- 训练集和评测集有近重复,离线结果虚高;
- 目标答案本身不一致,模型被迫平均多个风格;
- 数据量不足却训练过多轮,模型开始记忆具体样本。
SFT 还存在灾难性遗忘:若新数据集中只有少量窄领域样本,更新参数可能损害通用能力。缓解方法包括混入经过筛选的通用数据、降低学习率、减少训练轮数、使用 LoRA、扩大回归集,而不是只追求训练损失继续下降。
3. LoRA:冻结基础模型,只学习低秩更新
3.1 从全参数更新到低秩更新
设一个线性层的权重为:
全参数微调直接学习一个更新矩阵 ,推理时使用:
LoRA(Low-Rank Adaptation)假设有用的更新可以近似为低秩矩阵:
其中:
- ;
- ;
- 是低秩维度,通常远小于输入和输出维度;
- 是缩放系数;
- 在训练时冻结,只更新 和 。
前向计算变为:
直觉是:不允许模型在整个高维参数空间任意移动,而是限制更新落在由 和 表示的低维子空间中。
LoRA 常把 随机初始化,把 初始化为零,使得训练刚开始时:
因此模型初始行为与基础模型一致,训练变化由优化过程逐渐引入。
3.2 参数量算例
假设某个线性层:
全参数更新量为:
LoRA 参数量为:
这还没有考虑偏置和其他模块,但数量级差异已经很明显。LoRA 的收益包括:
- 优化器状态更小;
- 梯度显存更低;
- 一个基础模型可以挂载多个任务 adapter;
- adapter 文件更容易分发和回滚;
- 多租户可以共享同一个基础权重。
但 LoRA 不是“免费训练”。前向计算仍需要加载基础模型,序列长度、激活值、批大小和量化方式仍会决定显存与吞吐。训练参数少也不意味着训练数据可以不治理。
3.3 LoRA 应该插入哪些层
Transformer 中常见插入位置包括注意力模块的:
- query projection;
- key projection;
- value projection;
- output projection;
以及 MLP 模块的投影层。
具体模块名称依赖模型实现,不能写死为某一组字符串。错误地只匹配到零个模块,或者把 LoRA 挂在不影响目标任务的层上,可能出现“训练正常、模型不变”的假象。
训练前应验证:
- 可训练参数数量大于零;
- 基础权重
requires_grad=False; - LoRA 参数确实位于预期层;
- 一次反向传播后 LoRA 参数梯度非零;
- 保存的 adapter 能独立加载并复现结果。
一个常见的 Python 配置形式如下,具体字段和版本行为应以所使用的 PEFT 版本为准:
from peft import LoraConfig, TaskType, get_peft_model
config = LoraConfig(
task_type=TaskType.CAUSAL_LM,
r=16,
lora_alpha=32,
lora_dropout=0.05,
target_modules=["q_proj", "v_proj"],
bias="none",
)
model = get_peft_model(base_model, config)
model.print_trainable_parameters()
这段代码的前置条件是已经正确加载了一个因果语言模型,并且该模型确实存在名为 q_proj、v_proj 的模块。并非所有架构都使用这些名字。若输出的可训练参数为零,不能继续训练,应先检查模型结构:
for name, module in base_model.named_modules():
if "proj" in name:
print(name, type(module).__name__)
3.4 rank、alpha 和 dropout 的取舍
- rank 增大,adapter 表达能力和可训练参数量增加,但显存、训练时间和过拟合风险也增加。
- alpha 控制 LoRA 更新的缩放,不等价于学习率。相同的 下改变 alpha 会改变更新幅度。
- dropout 可作为正则化手段,但样本很少时不能靠 dropout 解决标签错误、模板错误或数据泄漏。
如果任务是简单格式转换,较小 rank 可能已经足够;如果任务需要复杂的领域推理,过小 rank 可能形成明显的能力瓶颈。但 rank 不是越大越好,正确选择应由验证集和回归集决定。
3.5 LoRA、合并和 QLoRA 的关系
LoRA adapter 可以保持独立:
也可以在发布前合并为:
独立 adapter 便于多任务切换和回滚;合并后的模型部署路径更简单,但失去了直接切换 adapter 的便利。合并前后必须做等价性验证,因为量化、权重类型转换和实现差异可能产生数值误差。
QLoRA 通常指“量化基础模型 + LoRA 训练”,它不是另一种低秩公式。其核心仍是冻结基础权重并训练 LoRA 参数,只是基础模型以低比特形式存储或计算。量化会引入额外的数值和硬件约束,不能把“使用 LoRA”自动等同于“使用 QLoRA”。
4. 数据治理:训练数据是可追责的生产资产
数据治理不是训练前做一次清洗,而是对数据的来源、权利、内容、转换、使用范围和删除能力进行持续管理。
4.1 数据对象和血缘
每条样本至少应能回答:
- 来源是什么;
- 谁拥有或批准使用它;
- 采集时间和有效期是什么;
- 是否包含个人信息、商业秘密或受限内容;
- 经历了哪些清洗和改写;
- 被用于哪个数据集版本和模型版本;
- 哪些线上请求可能受到它影响。
可以为样本保存如下元数据:
{
"sample_id": "doc-20250101-00042",
"source_uri": "s3://training/raw/manual.json",
"source_version": "manual-v3",
"owner": "platform-team",
"license": "internal-use",
"purpose": "support-assistant",
"contains_pii": false,
"retention_until": "2026-01-01",
"transform_version": "redact-v2",
"split": "train"
}
sample_id 不只是审计字段。若某个来源后来被撤销授权,系统需要通过它定位受影响的样本、数据集、adapter 和发布版本。
4.2 训练数据的质量维度
训练数据质量至少包括以下不同维度:
- 正确性:答案是否事实正确,代码是否可运行。
- 一致性:相同问题的答案是否遵循同一规则。
- 覆盖性:是否覆盖线上真实输入的主要分布。
- 代表性:是否包含难例、边界例和拒答例,而不是只有简单样本。
- 去重性:是否有同一文档、模板或答案的多份副本。
- 安全性:是否混入提示注入、恶意指令、越权操作样本。
- 权利可用性:是否允许用于该用途和该模型。
- 可评测性:是否能够定义清晰的目标和验收规则。
数据量增加不一定提升效果。如果新增数据主要是低质量重复样本,经验风险下降可能伴随泛化能力下降。对于监督数据,宁可保留可解释的少量高质量样本,也不能把自动生成数据未经审核地全部加入训练集。
4.3 划分数据集时防止泄漏
随机按行切分经常不够。以下样本不能简单视为独立样本:
- 同一用户的多条对话;
- 同一文档的不同切片;
- 同一问题的改写;
- 同一代码仓库的不同文件;
- 同一工单的多轮记录。
如果这些样本被分别放入训练集和测试集,模型可能只是记住了实体、模板或文档内容。
更可靠的划分方式是按实体或时间分组:
并满足同一 user_id、document_id 或 case_id 不跨集合。对于知识随时间变化的任务,可用较早时间训练、较晚时间验证和测试,从而检验模型面对未来分布的表现。
4.4 一个可运行的数据检查示例
下面的脚本只依赖 Python 标准库,用于检查 JSONL 监督数据的基本结构、角色顺序、空内容、重复样本和明显的个人信息模式。它不能替代人工审核、专业 DLP 或法律审查,但可以作为数据集构建流水线中的硬门槛。
#!/usr/bin/env python3
import hashlib
import json
import re
import sys
from collections import Counter
EMAIL = re.compile(r"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b")
PHONE = re.compile(r"(?<!\d)(?:\+?86[- ]?)?1[3-9]\d{9}(?!\d)")
def normalize(text: str) -> str:
return re.sub(r"\s+", " ", text).strip()
def main(path: str) -> int:
errors = []
hashes = Counter()
total = 0
with open(path, "r", encoding="utf-8") as f:
for line_no, line in enumerate(f, 1):
if not line.strip():
continue
total += 1
try:
obj = json.loads(line)
except json.JSONDecodeError as e:
errors.append((line_no, f"JSON 无法解析: {e.msg}"))
continue
messages = obj.get("messages")
if not isinstance(messages, list) or not messages:
errors.append((line_no, "messages 必须是非空数组"))
continue
roles = [m.get("role") for m in messages]
if roles[-1] != "assistant":
errors.append((line_no, "最后一条消息必须是 assistant"))
for i, msg in enumerate(messages):
if msg.get("role") not in {"system", "user", "assistant"}:
errors.append((line_no, f"非法 role: {msg.get('role')}"))
if not isinstance(msg.get("content"), str) or not msg["content"].strip():
errors.append((line_no, f"第 {i} 条消息内容为空"))
text = "\n".join(
normalize(m.get("content", "")) for m in messages
)
digest = hashlib.sha256(text.encode("utf-8")).hexdigest()
hashes[digest] += 1
if EMAIL.search(text) or PHONE.search(text):
errors.append((line_no, "疑似包含邮箱或手机号,需脱敏或豁免审核"))
duplicate_rows = sum(n - 1 for n in hashes.values() if n > 1)
print(f"有效读取行数: {total}")
print(f"重复内容多出的行数: {duplicate_rows}")
print(f"错误数: {len(errors)}")
for line_no, message in errors[:20]:
print(f"[第 {line_no} 行] {message}")
return 1 if errors else 0
if __name__ == "__main__":
if len(sys.argv) != 2:
print(f"用法: {sys.argv[0]} data.jsonl", file=sys.stderr)
sys.exit(2)
sys.exit(main(sys.argv[1]))
运行:
python validate_sft.py train.jsonl
可能输出:
有效读取行数: 12000
重复内容多出的行数: 183
错误数: 4
[第 431 行] 最后一条消息必须是 assistant
[第 982 行] 疑似包含邮箱或手机号,需脱敏或豁免审核
脚本返回码为 0 表示未发现这些基础错误,返回码为 1 表示应阻止流水线继续。它不会判断答案是否正确,也不会证明数据拥有训练授权;这些需要额外的标注审核和权限流程。
4.5 隐私、删除和访问权限
训练数据中的个人信息、密钥、内部源代码和客户对话,不能因为“模型是内部部署”就自动合法或安全。至少需要:
- 在采集前定义用途和保存期限;
- 对手机号、邮箱、身份证号、访问令牌等进行脱敏;
- 通过角色权限限制原始数据、脱敏数据和训练产物的访问;
- 对导出、下载、训练任务和模型发布做审计;
- 建立从数据来源到模型版本的反向追踪;
- 在删除请求发生时定位受影响的数据集和发布版本。
删除原始数据不一定等于模型中相关信息立即消失。若数据已经影响模型参数,通常需要重新训练、继续训练以抵消影响,或废弃相关模型版本。这个事实必须在数据使用协议中明确,而不能把“数据库删除”误认为“模型遗忘”。
5. 训练系统:参数、状态、产物和故障路径
一个可复现的训练任务至少包含:
- 基础模型及其精确版本;
- tokenizer 和 chat template;
- 数据集版本和切分规则;
- LoRA 配置或全参数配置;
- 优化器、学习率、批大小、梯度累积;
- 随机种子和硬件环境;
- 训练日志、验证指标和检查点;
- 代码提交版本和依赖锁定文件。
训练状态可抽象为:
stateDiagram-v2
[*] --> Draft
Draft --> Validated: 数据/权限/配置检查通过
Validated --> Running: 启动作业
Running --> Checkpointed: 定期保存
Checkpointed --> Running: 从检查点恢复
Running --> Failed: OOM/节点故障/数据错误
Failed --> Running: 修复后恢复
Running --> Evaluated: 训练结束
Evaluated --> Rejected: 未达门槛或存在安全问题
Evaluated --> Candidate: 评测通过
Candidate --> Released: 灰度发布
Released --> RolledBack: 回归或故障
RolledBack --> Candidate: 修复后重新验证
Released --> [*]
Rejected --> [*]
5.1 梯度累积与有效批大小
显存不足时常用梯度累积。若单次设备批大小为 ,累积步数为 ,设备数为 ,则近似有效批大小为:
每个微批次执行前向和反向,但不立即更新参数;累计 次后再执行一次优化器更新。
如果错误地在每个微批次都调用 optimizer.step(),实际批大小就不是期望值。若损失没有除以累积步数,累计梯度还会放大约 倍。因此训练循环通常要:
loss = loss / accumulation_steps
loss.backward()
if (step + 1) % accumulation_steps == 0:
optimizer.step()
optimizer.zero_grad(set_to_none=True)
混合精度、梯度裁剪和分布式训练会进一步影响实现。PyTorch 的自动微分会根据参与计算图的张量记录梯度;冻结基础模型意味着其参数不应被加入可更新参数集合,但前向计算仍然需要它们。
5.2 检查点不是只有模型权重
可靠检查点至少应包含:
- 模型或 adapter 权重;
- optimizer 状态;
- scheduler 状态;
- 当前 epoch、step;
- 随机数状态;
- tokenizer 和配置;
- 数据迭代位置或可复现的采样状态。
只保存 LoRA 权重后从头开始优化器,虽然可以恢复“模型参数”,但不能严格恢复训练过程。故障恢复时应明确选择:
- 精确恢复:恢复全部状态,继续原训练轨迹;
- 参数恢复:只加载模型权重,重新设定优化器,视为新实验。
两者的结果不可混为同一个实验版本。
5.3 常见训练故障及诊断顺序
损失变成 NaN 时,先检查:
- 输入是否含有非法 token id;
- 学习率是否过大;
- 混合精度和 loss scaling 是否异常;
- 是否存在极端长样本;
- 梯度范数是否爆炸;
- 数据中是否有空目标或错误标签。
显存溢出 不应只盲目降低批大小。显存通常由参数、优化器状态、激活值和临时张量共同决定。序列长度对激活显存影响很大;优先检查 padding 是否把一个批次全部补到异常长样本,并考虑按长度分桶、梯度检查点、减少序列长度或使用量化。
训练后输出重复或退化,应同时检查数据重复率、EOS 处理、学习率、训练轮数和解码参数。仅调整 temperature 往往不能修复模型已经学到的重复模式。
6. 评测:测量模型变化,而不是只看训练损失
6.1 评测对象应分层
一个完整的评测系统至少区分:
- 基础能力集:确认模型没有明显退化。
- 任务效果集:测量目标任务的正确率、召回率或结构有效性。
- 边界和安全集:测量越权、隐私、危险请求和拒答行为。
- 回归集:固定保存历史线上难例。
- 分布外集:测试新用户、不同长度、不同语言或不同数据来源。
- 成本与性能集:测量延迟、吞吐、显存和 token 消耗。
如果系统包含 RAG 或 Agent,不能只评估最终回答。还要分别评估:
- 检索是否取回正确文档;
- 引用是否能支持结论;
- 工具选择是否正确;
- 工具参数是否符合 schema;
- 失败后是否重试或降级;
- 是否出现无效循环;
- 轨迹中的中间动作是否越权。
6.2 任务指标必须匹配目标
分类任务可使用准确率、精确率、召回率和 F1,但类别不平衡时,准确率可能误导。
对于结构化生成,至少分别测:
- JSON 是否可解析;
- 必填字段是否存在;
- 字段类型是否正确;
- 业务约束是否满足;
- 字段内容是否正确。
“字符串完全相同”常常过于严格,“Judge 觉得不错”又可能过于宽松。应优先使用可执行规则,例如:
import json
def valid_invoice_output(text: str) -> bool:
try:
obj = json.loads(text)
except json.JSONDecodeError:
return False
required = {"invoice_id", "total", "currency"}
if not required.issubset(obj):
return False
if not isinstance(obj["total"], (int, float)):
return False
if obj["currency"] not in {"CNY", "USD", "EUR"}:
return False
return True
这个规则只能判断结构和部分业务约束,不能判断金额是否真的与发票内容一致;后者需要数据库校验或人工标注。
6.3 Judge 的作用与局限
Judge 是使用另一个模型或规则对生成结果评分。它适合辅助评价开放式回答,但不是客观真理。Judge 可能存在:
- 偏好更长的回答;
- 对特定措辞有位置偏差;
- 被格式或引用风格影响;
- 与被测模型具有相同错误;
- 对安全、法律和事实问题判断不稳定。
因此应使用明确评分标准、隐藏模型身份、随机化候选顺序,并定期用人工标注校准 Judge。对“能否解析”“是否越权”“金额是否一致”等问题,优先使用确定性程序,而不是模型评分。
6.4 回归与统计显著性
假设基线模型在 100 个样本中正确 68 个,微调模型正确 72 个。表面提升为 4 个百分点,但这并不自动说明微调有效。
对于单个模型的二项比例,粗略标准误为:
微调模型 ,则:
近似 95% 区间约为 ,即约 到 。样本量很小时,4 个百分点的差异可能只是抽样波动。
如果基线和微调模型在同一批样本上评测,更适合记录配对结果:
- 两者都正确;
- 两者都错误;
- 基线正确、微调错误;
- 基线错误、微调正确。
重点是比较后两类的数量,而不是把两次独立准确率简单相减。对生成任务还可以使用配对 bootstrap,按样本重采样并计算指标差异分布。
6.5 评测集本身也需要治理
评测集不应被训练过程反复查看到失去独立性。至少应区分:
- 公开开发集:允许调参;
- 内部验证集:用于模型选择;
- 私有测试集:只在候选版本验收时使用;
- 线上回归集:从真实失败案例中抽取并脱敏。
如果开发集被多轮实验反复优化,最终指标会过拟合到开发集。模型版本应记录评测集版本,否则“提升了多少”无法复现。
7. 一个完整的适配实验应如何组织
以“客服工单分类并输出结构化结果”为例,可以按以下因果链设计实验。
第一步:定义任务契约
输入:
客户描述:订单已经扣款,但页面仍然显示未支付。
目标:
{
"category": "payment_status",
"priority": "normal",
"needs_human": false
}
先规定字段枚举、缺省行为和无法判断时的处理方式。例如:
category必须来自固定集合;priority只能是low、normal、high;- 信息不足时
needs_human必须为true; - 不允许输出额外自然语言。
这一步先定义业务判定规则,再决定是否微调。否则训练目标本身是不稳定的。
第二步:建立基线
分别测试:
- 原始模型 + 清晰系统提示;
- 原始模型 + 少样本示例;
- 原始模型 + 结构化输出约束;
- 原始模型 + RAG 或工具(若任务需要外部信息)。
如果提示词和结构化约束已经达到门槛,微调就没有必要。微调只能在基线不足的维度上提供增量价值。
第三步:构造 SFT 数据
每条数据包括真实输入、正确结构和必要的拒答或转人工样本。应避免把同一工单的不同改写分散到训练和测试集合。
训练集还应包含边界例:
{
"messages": [
{"role": "system", "content": "只输出符合 schema 的 JSON。"},
{"role": "user", "content": "客户说最近扣款有问题,但没有提供订单号。"},
{"role": "assistant", "content": "{\"category\":\"unknown\",\"priority\":\"normal\",\"needs_human\":true}"}
]
}
这个样本训练的不是某个具体订单事实,而是“证据不足时转人工”的行为边界。
第四步:先用 LoRA 做可回滚实验
训练时冻结基础模型,只生成 adapter。不同实验可以使用不同 adapter:
base-model-v1
├── adapter-format-v1
├── adapter-format-v2
└── adapter-safety-v1
这样可以分别比较数据版本、rank、目标层和训练轮数,而不必复制整套基础模型。每个 adapter 必须与明确的基础模型版本绑定,不能把针对 base-model-v1 的 adapter 随意加载到结构或 tokenizer 不兼容的模型上。
第五步:验证结构、业务和回归
对每个候选版本同时记录:
结构化解析率
字段级准确率
拒答/转人工准确率
基础能力回归
安全规则通过率
平均输入 token
平均输出 token
P50/P95 延迟
GPU 显存
例如,一个版本可能把 JSON 解析率从 85% 提升到 98%,但把 needs_human 的召回率从 95% 降到 80%。对客服系统而言,后一个变化可能更危险,不能用前一个指标掩盖。
8. 何时不该微调
8.1 需要最新知识时,不应优先把知识写进参数
以下内容经常变化:
- 价格、库存、政策;
- 员工或客户状态;
- 法规和内部制度;
- 数据库记录;
- 实时监控指标。
把这些内容用于 SFT 后,模型可能学会过期答案,而且更新成本高、可追溯性差。更合适的方案是:
如果问题是“模型不会调用数据库”,可以训练工具调用格式;但数据库中的具体记录仍应由工具在推理时提供。
8.2 只是输出格式偶尔错误时,不应直接微调
先尝试:
- 明确 schema;
- 使用结构化输出接口或语法约束;
- 给出一个正确示例;
- 在服务端做解析和重试;
- 对失败结果走降级路径。
如果模型本身已经能完成任务,只是格式约束没有落实,微调会增加版本和数据治理成本,却不一定比约束解码可靠。
8.3 样本很少且规则不稳定时,不应微调
几十条未经统一标注的样本可能只能表达标注人的偏好,而不能表达任务规则。训练后常见表现是:
- 记住具体名称;
- 复制答案措辞;
- 遇到新输入就退化;
- 对相互冲突的样本随机选择。
此时应先稳定标签定义,补充困难样本,并建立独立评测集。若无法说明“什么答案算正确”,微调只会把不确定性固化到参数中。
8.4 需要新增基础能力时,不应期待 LoRA 凭空创造能力
LoRA 能改变已有表示的使用方式,但不能保证为模型增加可靠的:
- 新语言理解能力;
- 复杂数学能力;
- 长上下文能力;
- 工具执行权限;
- 外部系统访问能力;
- 事实验证能力。
如果基础模型在目标语言或任务上几乎没有能力,继续调 rank 往往不是正确方向。应考虑更换基础模型、继续预训练、接入工具,或拆分任务。
8.5 训练成本高于推理成本时,先做系统级比较
微调成本不只包括 GPU:
其中维护成本包括重新训练、数据删除、版本回归、权限审计和故障回滚。
如果每天只有少量请求,为一个窄任务训练大模型可能不如提示词、检索或调用小模型划算。反过来,如果任务高频、输出格式极其稳定、提示词很长且推理成本占主导,微调可能通过缩短提示和减少重试降低长期成本。必须以端到端成本比较,而不是只比较训练 GPU 费用。
9. 生产发布、权限与回滚
模型发布对象应明确区分:
- 基础模型权重;
- LoRA adapter;
- tokenizer 和模板;
- 推理参数;
- 系统提示词;
- 检索索引版本;
- 工具 schema;
- 评测集版本。
一个模型文件本身不足以复现线上行为。线上响应实际由多部分共同决定:
权限也应按组件隔离。能够读取训练原始数据的用户,不应自动拥有生产模型发布权限;能够发布 adapter 的用户,也不应自动能够修改工具调用白名单。
发布时应采用候选版本、灰度流量和回滚指针。回滚必须提前验证:
- 旧模型权重和 adapter 仍可加载;
- 旧 tokenizer 和模板仍可用;
- 依赖镜像和硬件环境仍可调度;
- 旧版本的安全规则和工具权限未被新配置覆盖;
- 回滚后指标和日志能被区分。
如果 adapter 与基础模型通过文件名松散关联,部署时很容易加载错组合。应在产物中写入基础模型摘要、tokenizer 摘要和配置校验值,加载时进行硬校验。
10. 用失败表现判断问题属于哪里
| 现象 | 更可能的原因 | 优先检查 |
|---|---|---|
| 训练损失下降,线上无提升 | 数据与线上分布不同,或任务本来是知识问题 | 线上样本、基线、数据切分 |
| 输出格式更稳定,但事实错误增加 | SFT 学到了格式,未获得事实来源 | RAG、工具、事实校验 |
| 只在训练样本附近表现好 | 重复、泄漏或过拟合 | 按实体去重、私有测试集 |
| LoRA 训练无效果 | target module 不匹配或目标过窄 | 可训练参数和梯度 |
| 基础能力明显退化 | 灾难性遗忘或数据分布过窄 | 通用回归集、混合数据、训练轮数 |
| 生成频繁重复 | EOS、重复数据、学习率或解码设置异常 | token 化、数据统计、梯度 |
| Judge 分数提升,用户投诉增加 | Judge 偏好与业务标准不一致 | 人工样本、规则指标、分桶分析 |
| 灰度后延迟升高 | adapter、量化或序列长度导致计算增加 | P50/P95、token、显存、批处理 |
诊断时不要先改变多个变量。一次实验应尽量只改变数据版本、rank、学习率或目标模块中的一项,否则即使结果变化,也无法解释因果。
11. 最终判断框架
微调值得做,通常需要同时满足以下条件:
- 目标行为可以用稳定、可标注的输入—输出样本描述;
- 基础模型已有相关能力,只需要改变行为分布或输出协议;
- 任务不是主要依赖实时事实;
- 有足够的数据权利、质量和可追溯性;
- 有独立评测集证明基线存在稳定差距;
- 微调收益能够覆盖训练、评测、发布和维护成本;
- 失败时可以通过版本化产物和权限控制安全回滚。
其中任一项不成立,都应重新考虑提示词、RAG、工具、结构化解码、更换模型或重新定义任务。
SFT 解决“用监督样本改变模型行为”的问题;LoRA 解决“以较少可训练参数实现这种改变”的问题;数据治理决定“这些改变是否有权利、有质量、可追责”;评测决定“改变是否真的有效且没有破坏其他能力”;生产发布则决定“出了问题能否发现、隔离和恢复”。把这几个部分拆开,微调容易变成一次不可解释的训练实验;把它们作为同一系统,才可能成为可维护的模型适配流程。
系列导航与关联阅读
- 系列入口:AI 工程完整学习路线:从机器学习与 Transformer 到 RAG、Agent 和生产治理
- 上一篇:AI 编程 Agent 与 Skills:上下文、工具、补丁、验证和仓库边界
- 下一篇:AI 模型服务:批处理、连续批次、KV Cache、量化和 GPU 容量
- 延伸:大语言模型生命周期:预训练、指令微调、对齐、推理和版本评测
- 延伸:LLM 与 Agent 评测:数据集、规则、Judge、轨迹、回归和统计
官方资料
本文依据研究论文、标准组织与主流框架官方文档重新梳理;正文、示例与工程清单由 WR BLOG 编写。

评论
0 条讨论