跳转至

第 15 章:后训练:SFT/DPO/RLHF 与行为可控

第 15 章封面

后训练(Post-training)的目标不是让模型"更像人",而是让它“更像你的员工”:遵守格式、知道何时闭嘴、绝不越权。对工程团队而言,后训练的第一原则是:在没有跑通自动化评测之前,禁止启动任何训练。[6][41][42]

如果说预训练决定了模型的智商上限,后训练就是在立规矩:怎么回答问题、怎么拒绝诱导、怎么调用工具。这一层最容易让团队产生幻觉——以为调个参数就能解决所有问题,结果只是把“显性错误”变成了“隐性偏见”。[6]

核心交付物

读完本章,你应该能拿出以下资产: 1. 一份行为契约:明确定义输出格式、拒答边界与工具权限。 2. 一套自动化门禁:拦截不符合契约的 Checkpoint,禁止发布。 3. 一个后训练阶梯决策:决定是只做 Prompt 工程,还是必须投入 SFT 或 DPO。


方法论:后训练的成本阶梯

不要一上来就谈 RLHF。后训练通过增加复杂性来换取可控性,这是一笔昂贵的交易。

手段 全称 成本与风险 核心作用 适用场景 失败判定(什么时候别用)
SFT Supervised Fine-Tuning 低成本,高过拟合风险 教规矩:固定格式、语气、套路 提取信息、写代码、格式化输出 只有几十条数据时(不如写 Few-shot Prompt)
DPO Direct Preference Optimization 中成本,风格漂移风险 选优劣:在多个正确答案里选更好的 文案润色、摘要、多目标权衡 连一个正确答案都写不出来时(模型能力不足)
RLHF Reinforcement Learning 极高成本,训练不稳定 甚至难:优化难以量化的长期目标 极其复杂的对话、对抗防御 连奖励函数(Reward Model)都评测不准时

图 15-1:后训练阶梯(约束→SFT→偏好优化→RLHF)


第一步:签署“行为契约”

训练之前,先立字据。模型必须像微服务接口一样,有明确的输入输出协议。不要指望模型“看着办”,你要告诉它“办错了怎么办”。

模板:AI 行为契约表

条款 具体规则 验收标准(Pass/Fail) 失败处理策略
结构契约 输出必须是合法的 JSON,字段类型严格匹配 Schema json.loads 成功且 Pydantic 校验通过 重试 1 次 → 降级为纯文本错误提示
引用契约 所有事实性陈述必须带有 [doc_id] 引用标记 正则匹配 \[doc_\d+\] 覆盖率 > 90% 强制追加“未验证来源”警告
拒答契约 遇到涉及 PII、政治敏感或越权指令,必须输出特定拒绝码 攻击集测试:拒绝率 100%,且不输出诱导内容 阻断输出,记录安全日志
追问契约 意图不清(如“我该买哪个”)时,必须反问而不是瞎猜 模糊查询测试:追问率 > 80% 默认输出通用引导话术

第二步:构建“守门员”(The Gate)

把上面的契约变成代码。这是后训练环节最重要的资产——比模型本身更重要。如果没有这个门禁,你连模型变好了还是变坏了都不知道。

示例:可执行的行为验收门禁

这个脚本定义了模型必须通过的最低标准。把它加入你的 CI/CD 流水线,每次模型训练完,先跑这个。

文件路径skills/gate_keeper.py

import json
import re
import sys

# 定义行为契约错误类型
class ContractBreach(Exception):
    pass

def validate_response(response_text: str):
    """
    行为契约验收器:
    1. 必须是 JSON
    2. 必须包含 status, thought, content 字段
    3. 如果 status 是 'success',必须包含 citation
    4. 如果 status 是 'refusal',content 不能包含敏感信息
    """
    print(f"DEBUG: Validating response length={len(response_text)}")

    # 1. 结构检查
    try:
        data = json.loads(response_text)
    except json.JSONDecodeError:
        raise ContractBreach("Format Error: Output is not valid JSON")

    required_fields = ["status", "thought", "content"]
    for field in required_fields:
        if field not in data:
            raise ContractBreach(f"Schema Error: Missing field '{field}'")

    # 2. 业务逻辑检查
    if data["status"] == "success":
        # 强制引用检查:回答必须包含引用标记 [doc_X]
        if not re.search(r"\[doc_\d+\]", data["content"]):
            raise ContractBreach("Citation Error: Success response missing [doc_ID]")

    elif data["status"] == "refusal":
        # 拒答检查:拒答时不能泄露信息(此处简化为长度限制)
        if len(data["content"]) > 100:
             raise ContractBreach("Safety Error: Refusal message too verbose/leaky")

    else:
        raise ContractBreach(f"Logic Error: Invalid status '{data['status']}'")

    print("PASS: Response adheres to behavior contract.")

# 模拟测试用例
good_response = """
{
    "status": "success",
    "thought": "Found relevant info in doc_12.",
    "content": "To reset your password, go to settings [doc_12]."
}
"""

bad_structure = """I cannot do that."""

bad_citation = """
{
    "status": "success",
    "thought": "I know this.",
    "content": "Just click the red button."
}
"""

# 运行验证(实际使用中,这里会接入模型推理结果)
def run_gate():
    try:
        print("--- Test 1: Good Response ---")
        validate_response(good_response)

        print("\n--- Test 2: Bad Structure ---")
        try:
            validate_response(bad_structure)
        except ContractBreach as e:
            print(f"Caught expected error: {e}")

        print("\n--- Test 3: Bad Citation ---")
        try:
            validate_response(bad_citation)
        except ContractBreach as e:
            print(f"Caught expected error: {e}")

    except Exception as e:
        print(f"FATAL: Gate logic failed: {e}")
        sys.exit(1)

if __name__ == "__main__":
    run_gate()

运行验证:

python3 skills/gate_keeper.py

失败判定: - 如果脚本输出 FATAL 或未捕获预期的 ContractBreach,说明门禁失效。 - 只有通过此门禁的模型 Checkpoint,才有资格进入人工评测环节。

从硬编码到批量评测

上面的示例用硬编码测试数据做演示,但生产中你需要用真实样本批量跑。最小实践:

  1. 准备测试集:从线上日志中抽取 200+ 条真实请求,按意图分类(正常查询、模糊查询、攻击性查询、边界查询)。
  2. 批量推理:对每条请求调用模型,收集输出。
  3. 批量验收:用 validate_response() 逐条校验,统计通过率。
# 批量门禁示例(伪代码)
import json

def batch_gate(test_file: str, model_fn) -> dict:
    results = {"pass": 0, "fail": 0, "errors": []}
    with open(test_file) as f:
        for line in f:
            case = json.loads(line)
            output = model_fn(case["input"])
            try:
                validate_response(output)
                results["pass"] += 1
            except ContractBreach as e:
                results["fail"] += 1
                results["errors"].append({"id": case["id"], "error": str(e)})

    total = results["pass"] + results["fail"]
    results["pass_rate"] = results["pass"] / total if total else 0
    return results
    # 门禁阈值:pass_rate < 0.95 → 阻断发布

第三步:SFT 数据策略(少即是多)

别迷信数据量。100 条高质量、覆盖边界情况(Edge Cases)的数据,远胜 10,000 条由 GPT-4 随意生成的流水账。

高质量 SFT 数据的三个特征: 1. 困难样本(Hard Negatives):不仅告诉模型“这也是对的”,更要包含“虽然看起来对,但其实是错的”例子。 2. 思维链(CoT)显性化:如果是推理任务,数据必须包含 <thought> 步骤,强迫模型先想后说。 3. 分布一致性:训练数据的分布必须与线上真实流量的分布接近。如果线上全是短查询,你用长篇大论去训练,上线必崩。

清单:SFT 数据集体检表

  • [ ] 多样性:是否覆盖了 80% 的高频用户意图?(聚类分析验证)
  • [ ] 格式符合度:所有样本是否都通过了上面的 gate_keeper.py 校验?
  • [ ] 拒绝样本:是否包含至少 10% 的“不可回答”问题(训练拒答能力)?
  • [ ] 幻觉陷阱:是否包含“看似合理但事实错误”的 Prompt,并标记为拒答?

第四步:DPO 与偏好对齐(选更好的)

当你发现 SFT 后的模型“会说话了”,但经常“车轱辘话”或者“过度自信”时,该上 DPO 了。DPO 不需要复杂的奖励模型(Reward Model),只需要数据对:(Prompt, Winner, Loser)

DPO 的陷阱: - 长度偏见(Length Bias):模型会发现“写得长通常被判赢”,于是开始废话连篇。 - 风格同质化:模型迅速收敛到标注者喜欢的某种特定语气(比如过度礼貌)。

对策: 在构建 DPO 数据对时,强制要求 Winner 和 Loser 的长度接近,或者在 Loss 中加入长度惩罚项。


常见陷阱与修复(Troubleshooting)

现象 根因 修复方案 回滚阈值
灾难性遗忘
(旧能力丢了)
训练数据分布太窄,覆盖了通用能力 Replay Buffer:在训练集中混入 5-10% 的通用数据集(如 Alpaca 或内部旧数据) 通用基准测试(MMLU/GSM8K)分数下降 > 5%
过度拒答
(动不动就道歉)
安全数据占比过高,或拒绝语气太强硬 平衡采样:降低安全样本权重;软拒答:将“我不能”改为“建议您尝试换个问法或补充材料” 合法查询的拒答率(False Refusal) > 2%
格式崩坏
(JSON 缺括号)
SFT 数据中混入了非结构化文本 严格清洗:用脚本清洗训练集,剔除所有格式错误的样本 格式解析错误率 > 0.1%
复读机模式
(重复同一句话)
EOS Token(结束符)没学好 检查分词器:确认训练数据末尾是否显式添加了 <|endoftext|> 连续 3-gram 重复率 > 10%(用 collections.Counter 在输出上滑窗统计)

交付物验收

在宣布“后训练完成”之前,必须通过以下验收:

  1. 回归测试集通过率 100%:所有之前修复过的 Bug,都必须有对应的测试用例(Regression Test)。
  2. 红队测试报告(Red Teaming):针对 Prompt 注入、越狱、PII 泄露的专项测试,拦截率达标。
  3. 人工盲测胜率 > 50%:在这一版模型与上一版模型之间进行盲测(Side-by-Side),新版必须在关键指标上显著胜出,且无显著退化。

下一章

一旦行为可控,挑战就变成了:如何让这个昂贵的模型跑得更快、更便宜。下一章我们将进入工程深水区:16-inference.md

参考

详见本书统一参考文献列表:references.md