从失败证据到训练分支:LLM 后训练 「目标诊断与阶段准入」学习笔记

学习来源

:DataWhale 社区・llm-algo-leetcode 仓库(202610 后训练优化营・Task 0)
教程仓库:

https://github.com/datawhalechina/llm-algo-leetcode
Task 0 Issue:

https://github.com/datawhalechina/llm-algo-leetcode/issues/190
配套课程:DeepLearning.AI — Post-Training LLMs:

https://www.deeplearning.ai/courses/fine-tuning-and-reinforcement-learning-for-llms-intro-to-post-training
本文按 Task 0 推荐学习顺序组织:

稳定失败证据 → 根因定位 → 最小干预 → 数据、反馈与评测准入 → 训练分支

,并附可直接运行的决策引擎代码


0. 写在前面:Task 0 到底在回答什么

模型上线后表现不好,第一反应不应该是 “赶紧微调”。Task 0 把 “要不要训练、训练到什么阶段” 变成一条可检查的决策链:

当前失败能否由 Prompt、RAG 或工具解决?

(不修改参数)
若必须更新参数:

应停在 SFT、进入离线偏好,还是需要在线奖励与探索?

核心原则一句话:失败根因决定候选,硬门槛决定候选能否执行,最小干预决定候选顺序。

贯穿全文的例子:你负责一个电商客服机器人,老板说 “用户投诉变多,你赶紧把模型微调一下”。本文所有概念都挂在这个例子(以及一个数学推理助手的例子)上讲。

全流程总览:

否

是

外部 / 时效知识

内部知识缺口

行为缺口

是

否

偏好权衡

探索缺口

无新反馈

偏好对

可验证奖励+探索

学习型奖励+探索

模型表现不达标

证据稳定吗?
可测目标 + 稳定复现
失败切片 + 独立评测
基线覆盖全部切片

回到工程:补评测 / 修复现 / 补基线

失败根因是什么?

RAG / 工具
知识在库不在参数

持续预训练
补领域表示

Prompt / RAG / 工具
已覆盖全部失败切片?

选非参数方案,不训练

SFT 建立监督基线

离线偏好优化 DPO

在线 RL
PPO / GRPO / RLVR

反馈信号类型?

stop_at_sft


1. 先搞懂这几个术语

下文会反复出现一些专业名词,先在这里进行补充讲解。

1.1 失败切片(Failure Slice)

定义:把 “模型在哪些地方失败” 按可枚举的维度切分成的独立单元。每个切片是一个 “场景 / 任务类型 / 样本组” 的最小失败单位。先知道失败长在哪,才能对症下药—— 而不是笼统地说 “模型不行”。

具体例子(电商客服):把 1000 条客服测试样本按意图切成 4 个切片:

失败切片样本数答对失败率
售后咨询401660%
物流查询3002826%
价格咨询3503373.7%
活动规则3102954.8%

整体答对率 93%,看起来不错;但 “售后咨询” 切片失败率高达 60%,说明问题高度集中在售后。结论不是 “模型整体不行”,而是 “售后切片是主战场”—— 切片决定了后面所有动作的靶心。

判定标准:

  • 切片要互斥、可枚举、有稳定规模(每个切片样本数不能太少,否则是噪声不是证据);

  • 粒度太粗定位不到问题,粒度太细统计不可靠 —— 一般按 “业务意图 / 任务类型 / 题型” 来切;

  • 在决策引擎代码里,Evidence.fail_slices(失败切片)和 Evidence.baseline_covers(基线覆盖的切片)就是它的实现形态。

1.2 基线(Baseline)

定义:不经过任何新干预的 “当前版本”(模型权重 + 提示词 + 检索 / 工具链路)在固定评测集上的表现,是衡量一切改进的对照组。没有基线,就无法判断失败到底是模型缺陷还是评测缺口。

具体例子:线上客服模型 v3 + 当前提示模板 = 你的基线。它在 “售后咨询” 切片上正确率只有 40%。现在你加了一段提示词,在同一个 1000 条测试集上复测:

  • 如果售后切片还是 40% → 提示词没有覆盖这个切片(残留失败),才允许继续往下走;

  • 如果变成 90% → 到此为止,不需要训练。

为什么要求 “基线覆盖全部失败切片”:如果基线评测集里根本没有 “售后咨询” 这个切片,那 “售后失败” 可能只是你没测到,而不是模型真不行 —— 这时正确的动作是先补评测、修基线,而不是训模型。

两种常见的基线:

  • 行为基线:Prompt / RAG / 工具方案复测后的表现,用于判断非参数方案是否覆盖全部失败切片;

  • SFT 基线:微调后的模型版本,是后续 RL 的起点、对照和参考策略来源。

1.3 稳定复现(Reproducible Failure)

定义:在固定输入、固定采样参数(如 temperature=0)、固定评测程序下,失败反复出现,而不是偶发。

具体例子:同一 100 条售后样本连跑 5 次,5 次都错同样 24 条 → 稳定,可以进入干预链;只有 1 次出错 → 可能是采样抖动或评测程序有 bug,先别动模型,去查评测。

代码里对应 reproduce_count >= min_repeat 且 fail_rate >= 0.5。

1.4 独立评测集(Independent Eval Set)

定义:训练数据之外、从未参与训练的 “未见数据” 样本集,用来检验泛化,而不是检验背诵。

具体例子:从 10000 条客服语料中切出 8000 条训练 + 2000 条评测;评测集任何人不得改动、不得混进训练池。如果训练集指标一直涨、但留出的评测集不涨 → 过拟合,先别升级训练,先查数据。

1.5 最小干预(Minimal Intervention)

定义:在能解决问题的方案里,选择侵入性最小、成本最低、最容易回滚的那一个。固定优先序:Prompt → RAG → 工具 → 持续预训练 → SFT → 离线偏好 → 在线 RL。

具体例子:售后答错是因为 “退款政策上个月刚改过” → 更新知识库(RAG)就能解决,完全不需要训练;如果是因为 “模型总爱在回答开头加无意义寒暄,改了 prompt 也压不住” → 这是行为问题,才轮到 SFT。

两个判据(缺一不可):

  • 机制覆盖:这个方案能否覆盖当前的失败信号?

  • 执行条件:数据、评测、预算、链路是否齐备?

1.6 硬门槛(Hard Gate)

定义:某个候选方案 “能执行” 的最低资产要求;不满足就视为 “未准入 /blocked”,而不是勉强执行。

具体例子:SFT 的硬门槛是 “版本化监督数据 + 冻结评测清单 + GPU 小时预算” 三件套。哪怕你判断 “行为缺口该走 SFT”,只要监督数据还没整理好,决策结果就是 blocked(先补齐硬门槛),而不是硬着头皮开训。

1.7 SFT 给后续阶段留下的三样东西

  • 策略初始化:RL 从 SFT 模型的权重开始训练(π_θ 的初始值),而不是从预训练模型开始;

  • 稳定基线:SFT 版本作为对照,衡量 RL 是否真的变好、是否跑偏;

  • 参考策略 π_ref:DPO 损失里做比值分母、PPO 的 KL 正则参照,防止新策略偏离太远。

1.8 偏好对(Preference Pair)

定义:(prompt, chosen, rejected) 三元组:同一个问题下,人类 / 评审认为 “更好” 的回答(chosen)和 “更差” 的回答(rejected)。

具体例子:售后问题,回答 A 直接给出退款路径和时限(chosen),回答 B 绕圈子不解决问题(rejected)。DPO 学习的目标就是 “提高生成 A 的概率、降低生成 B 的概率”。

注意:偏好对 ≠ 示范数据。示范数据教模型 “怎么做”,偏好对教模型 “哪个更好”—— 这就是 SFT 与 DPO 的信号差异。

1.9 可验证奖励与奖励退化

  • 可验证奖励(Verifiable Reward):不用人类打分、能由程序 / 规则自动判定对错的奖励:数学最终答案是否等于参考答案、代码是否通过单元测试、Agent 是否成功完成任务、JSON 是否符合 schema。

  • 奖励退化(Reward Degeneration):奖励信号 “失去区分度”—— 所有回答都得同样的分(全 0 或全 1)、或奖励分数与独立评测成功率背离。此时先修奖励 / 验证器 / 采样分布,再谈训练,否则 RL 会把模型带偏。

1.10 质量平台期(Plateau)

定义:继续增加监督数据或训练轮数,独立评测指标不再明显提升。

具体例子:SFT 数据从 5k → 10k → 20k,独立评测 acc 从 0.85 → 0.88 → 0.881,后面基本不动 —— 说明 “示范学习” 的收益吃完了。此时若仍有稳定残余错误,且错误属于 “需要判断哪个更好” 或 “需要探索新行为”,才考虑升级 RL。

1.11 版本化监督数据 / 去重 / 去污染 / 冻结评测清单

  • 版本化:数据按版本管理(data_v1、data_v2…),记录来源、清洗规则、筛选标准,保证 “这次训练用的数据” 随时可复查;

  • 去重:删除重复样本,避免同一题反复出现导致过拟合;

  • 去污染:检查训练数据是否混入了评测集样本 —— 否则 “评测提升” 是背答案,不是泛化;

  • 冻结评测清单:训练期间,评测集、指标、判分程序一律不许改动,否则前后结果无法对比。

1.12 GPU 小时预算与回滚检查点

  • GPU 小时预算:训练前锁定 “这次允许花多少算力 / 多少钱”(如 40 卡时 A100),既是准入证据,也防止训练失控;

  • 回滚检查点(Rollback Checkpoint):训练前的模型权重备份。RL 在线探索可能让模型变差,随时能切回上一个可用版本。


2. 背景:预训练 vs 后训练

两者复用同一套底层机制(前向 / 反向传播、优化器、训练状态),差别集中在数据对象、学习信号、采用证据三件事上:

对比维度预训练(Pre-training)后训练(Post-training)
核心目标大规模通用语料上学 “下一词元预测”,获得语言规律与世界知识把基础模型接到任务接口、人类偏好和环境反馈上
数据万亿级 token,无标注几千~几十万条:指令 - 回答、偏好对、带奖励的轨迹
学习信号自监督 next-token 交叉熵SFT 监督交叉熵 / DPO 偏好边际 / PPO 优势 / RLVR 可验证奖励
标注无需要人工筛选与质量把关
算力几千卡跑数月单机几小时~几天
典型产物Base Model(只会续写)Instruct / Chat / Reasoning Model
验收证据通用能力、训练稳定性、规模效率任务质量、安全、鲁棒性、奖励一致性

一句话:

预训练打地基,后训练做装修、改 bug、对齐行为。

后训练同样可以注入领域知识(持续预训练)、强化推理与工具使用,所以它不是 “知识 vs 行为” 的绝对二分,而是 “目标与信号” 的切换。


3. 第一步:稳定失败证据(Evidence Gate)

没有稳定证据之前,一切干预都是拍脑袋。进入干预链的前提是全部满足:

  1. 可测目标 + 目标阈值:指标明确(acc /pass@1 /format_ok),验收门槛明确。例:售后切片目标正确率 ≥ 90%,当前只有 40%;

  2. 稳定复现:同一固定工作负载下,失败重复次数达到下限、失败率达到门槛(见 1.3);

  3. 失败切片:明确哪些场景 / 任务 / 样本组持续失败(见 1.1);

  4. 独立评测:能检验未见数据上的泛化(见 1.4);

  5. 基线覆盖全部失败切片:当前基线必须覆盖所有切片,否则修基线而不是训模型(见 1.2)。

代码实现(Python 标准库,可直接运行):

from dataclasses import dataclass
from typing import Dict, List


@dataclass
class Evidence:
    task: str                       # 任务名
    metric: str                     # 可测指标,如 acc / pass@1 / format_ok
    target_threshold: float         # 目标阈值,如 0.90
    current: float                  # 当前指标
    fail_slices: List[str]          # 失败切片(场景 / 任务 / 样本组)
    baseline_covers: List[str]      # 基线覆盖的失败切片
    min_repeat: int = 3             # 稳定复现的最小重复次数
    fail_rate: float = 0.0          # 失败率(同一固定工作负载下)
    reproduce_count: int = 0        # 已复现次数
    independent_eval: bool = False  # 是否有独立评测集(未见数据泛化)


def evidence_gate(ev: Evidence) -> Dict[str, object]:
    """4 项全过,失败证据才算"稳定",才允许进入干预链。"""
    checks = {
        "可测目标+阈值": ev.metric != "" and ev.target_threshold > 0,
        "当前未达标": ev.current < ev.target_threshold,
        "稳定复现": ev.reproduce_count >= ev.min_repeat and ev.fail_rate >= 0.5,
        "失败切片非空": len(ev.fail_slices) > 0,
        "独立评测可用": ev.independent_eval,
        "基线覆盖全部切片": all(s in ev.baseline_covers for s in ev.fail_slices),
    }
    return {"ready": all(checks.values()), "checks": checks,
            "gap": round(ev.target_threshold - ev.current, 4)}

典型反例

:只在 2 次运行里见过一次错误、没有独立评测集、失败率 20% ——

evidence_gate

返回

ready=False

,此时正确的动作是

先稳定证据

(补评测、修复现、加切片),而不是启动任何训练。


4. 第二步:根因定位:知识 / 行为 / 偏好 / 探索

证据稳定后,要回答 “失败到底缺什么”。Task 0 把根因分成四类,每类根因对应不同的候选方案族:

根因主要失败信号首选候选为什么具体例子
外部 / 时效知识缺失事实错误、最新信息、私有知识、需要溯源RAG / 工具知识在库不在参数,可更新、可溯源售后答错 “最新退款政策”(政策上个月刚改)
内部知识缺口领域概念从未见过、表示层缺失持续预训练需要把领域表示写进参数医学助手从不认识 “司美格鲁肽” 这类领域词
行为缺口格式乱、不遵循指令、风格不符、任务接口不稳SFT用高质量示范固化行为,是最小的参数干预客服总在回答开头加无意义寒暄,改了 prompt 也压不住
偏好权衡缺口回答 “能用” 但不够好,需要 “哪个更好” 的判断离线偏好优化(DPO)偏好对直接优化,无需在线探索数学助手答案对但过程冗长,用户更喜欢简洁推导
探索缺口需要新行为、多步推理、奖励最大化、可验证结果在线 RL(PPO / GRPO / RLVR)需要策略在环境中试错与探索竞赛数学要尝试多种解法路径,用最终答案正确率当奖励

判定代码(互斥优先级:行为 > 偏好 > 探索 > 知识;实际项目以残留失败切片中最集中的信号为准):

ROOT_CAUSES = {
    "knowledge_external": {"name": "外部/时效知识缺失", "signal": "事实错误、最新信息、私有知识",
                           "candidate": "rag"},
    "knowledge_internal": {"name": "内部知识缺口(领域表示)", "signal": "领域概念从未见过",
                           "candidate": "continued_pretraining"},
    "behavior":           {"name": "行为缺口", "signal": "格式乱、不遵循指令、风格不符",
                           "candidate": "sft"},
    "preference":         {"name": "偏好权衡缺口", "signal": "回答能用但不够好",
                           "candidate": "offline_preference"},
    "exploration":        {"name": "探索缺口", "signal": "需要新行为/多步推理/奖励最大化",
                           "candidate": "online_rl"},
}


def classify_root_cause(signals: Dict[str, bool]) -> Dict[str, str]:
    priority = ["behavior", "preference", "exploration",
                "knowledge_internal", "knowledge_external"]
    for kind in priority:
        if signals.get(kind, False):
            rc = ROOT_CAUSES[kind]
            return {"kind": kind, "name": rc["name"], "signal": rc["signal"],
                    "candidate": rc["candidate"]}
    return {"kind": "unknown", "name": "根因不明", "signal": "证据不足,先回到证据稳定",
            "candidate": "no_entry"}

5. 第三步:最小干预:先试不改参数的方案

固定优先序:Prompt → RAG → 工具 → 持续预训练 → SFT → 离线偏好 → 在线 RL。

  • 缺口是表达、格式、指令歧义 → 首选 Prompt:零训练成本、迭代最快,永远先试;

  • 缺口是外部 / 时效 / 私有知识 → 首选 RAG:知识在库不在参数,重点检查检索命中与引用正确性;

  • 缺口是实时查询、精确计算、外部动作 → 首选工具(API / 计算器 / 验证器):参数学不会实时事实,也做不可靠算术;

  • 缺口是模型内部行为(风格、策略、格式习惯) → 才轮到 SFT:只有改参数才能固化行为。

关键纪律:走 SFT 必须意味着 Prompt 和 RAG 都没覆盖全部失败信号。非参数方案要在 “同一工作负载、同一验收门槛” 下复测,而不是 “好像试过”。

维度PromptRAGSFT
本质推理时给模型的指令和上下文检索外部知识拼进 Prompt用标注数据继续训练模型权重
改什么不改变模型,只改变输入不改变模型,改变输入中的知识改变模型本身的行为模式
成本最低,改文字即可中等,需建知识库 + 检索链路较高,需标注数据 + 训练资源
擅长解决输出格式、角色设定、简单约束私有知识问答、需溯源的场景领域话术、固定格式、行为习惯
知识更新修改上下文即可更新知识库即可需要再训练或参数编辑

6. 第四步:必须更新参数时,如何选训练分支

如果非参数方案已确认无法覆盖全部失败切片,才进入 “改参数” 决策。失败根因决定候选;硬门槛决定候选能否执行。

候选针对根因进入执行前的硬门槛方法边界
持续预训练内部知识缺口领域语料 + GPU 预算 + 独立评测补表示层知识;成本高,需防遗忘
SFT行为缺口版本化监督数据 + 冻结评测清单 + 非零预算最小参数干预;是后续 RL 的基线
离线偏好(DPO)偏好权衡SFT 基线 + 偏好对数据 + 评测离线、轻量;不探索新行为
在线 RL(PPO / GRPO / RLVR)探索缺口SFT 基线 + 奖励信号 + 预算 + 回滚检查点在线探索与奖励最大化;成本最高

三条铁律:

  1. SFT 是 “改参数” 里最轻的一步:没有 SFT 基线时,行为缺口直接进 SFT;格式 / 指令遵循还不稳定、或独立质量仍随高质量示范提升时,不要跨到 RL;

  2. 升级 RL 的条件是 “反馈信号换类型”,不是 loss 数值:

  • 有可复查的偏好对(chosen /rejected)且无需探索 → 离线偏好优化(DPO);

  • 数学答案 / 代码测试 / 环境终态可验证,候选间奖励可区分 → RLVR(GRPO / PPO);

  • 有学习型奖励模型且需要在线探索多步轨迹 → PPO;

  • 没有任何新反馈信号 → 停在 SFT(stop_at_sft)。

  1. 奖励退化要前置检查:奖励大量同分、全正 / 全负,或与独立成功率背离 → 先修奖励 / 验证器 / 采样分布,再谈训练。

SFT 给后续阶段稳定留下三样东西:策略初始化、稳定基线和冻结参考策略来源(DPO 的 π_ref、PPO 的 KL 正则都依赖它,见 1.7)。


7. 第五步:数据、反馈与评测准入

进入任何训练分支前,资产必须齐备(硬门槛),否则视为 “未准入”:

训练分支数据准入反馈准入评测准入
SFT版本化监督数据(去重 / 去污染 / 切分隔离,见 1.11)无需反馈独立评测集 + 冻结指标清单 + GPU 小时预算
离线偏好 DPO偏好对 (prompt, chosen, rejected),可复查偏好数据质量稳定SFT 基线可复查 + 评测
在线 RL采样 / 轨迹预算奖励模型 / 验证器 / 规则可靠评测 + 回滚检查点 + 安全约束

一个常见的误区

:

幻觉不是升级 RL 的充分理由

。模型不知道某个事实,优先 RAG / 工具 / 知识更新;只有当 “示范学习出现瓶颈(平台期)+ 优化目标改变 + 有可靠奖励信号 + 收益覆盖成本” 四个条件同时成立时,才值得从 SFT 升级到 RL。


8. 第六步:训练配套代码(可直接运行)

8.1 完整决策引擎 posttraining_stage_selector.py

把第 3~7 章的逻辑实现为一个可运行脚本,覆盖 Task 0 的完整问题链(纯 Python 标准库,无第三方依赖):

# -*- coding: utf-8 -*-
"""
后训练问题链 · 阶段选择 —— 微调准入与最小干预(Task 0 决策引擎)

对应 DataWhale llm-algo-leetcode 202610 Task 0:
    稳定失败证据 -> 知识/行为/偏好/探索根因 -> 最小干预 -> 数据、反馈与评测准入 -> 训练分支

纯 Python 标准库实现,无第三方依赖,Python 3.8+ 可直接运行。
运行:python posttraining_stage_selector.py
"""

from __future__ import annotations

from dataclasses import dataclass, field
from typing import Dict, List


# ===========================================================================
# 第一步:稳定失败证据(Evidence Gate)
#    可测目标 + 目标阈值 + 稳定复现 + 失败切片 + 独立评测 + 基线覆盖全部切片
# ===========================================================================

@dataclass
class Evidence:
    task: str                       # 任务名
    metric: str                     # 可测指标,如 acc / pass@1 / format_ok
    target_threshold: float         # 目标阈值,如 0.90
    current: float                  # 当前指标
    fail_slices: List[str]          # 失败切片(场景 / 任务 / 样本组)
    baseline_covers: List[str]      # 基线覆盖的失败切片
    min_repeat: int = 3             # 稳定复现的最小重复次数
    fail_rate: float = 0.0          # 失败率(同一固定工作负载下)
    reproduce_count: int = 0        # 已复现次数
    independent_eval: bool = False  # 是否有独立评测集(未见数据泛化)


def evidence_gate(ev: Evidence) -> Dict[str, object]:
    """4 项全过,失败证据才算"稳定",才允许进入干预链。"""
    checks = {
        "可测目标+阈值": ev.metric != "" and ev.target_threshold > 0,
        "当前未达标": ev.current < ev.target_threshold,
        "稳定复现": ev.reproduce_count >= ev.min_repeat and ev.fail_rate >= 0.5,
        "失败切片非空": len(ev.fail_slices) > 0,
        "独立评测可用": ev.independent_eval,
        "基线覆盖全部切片": all(s in ev.baseline_covers for s in ev.fail_slices),
    }
    ready = all(checks.values())
    return {
        "ready": ready,
        "checks": checks,
        "gap": round(ev.target_threshold - ev.current, 4),
    }


# ===========================================================================
# 第二步:根因定位(知识 / 行为 / 偏好 / 探索)
#    根因决定候选:同一类失败信号,只能由对应候选族覆盖
# ===========================================================================

ROOT_CAUSES = {
    "knowledge_external": {
        "name": "外部/时效知识缺失",
        "signal": "事实错误、最新信息、私有知识、需要溯源",
        "candidate": "rag",
        "cheaper_first": ["prompt", "rag", "tool"],
    },
    "knowledge_internal": {
        "name": "内部知识缺口(领域表示)",
        "signal": "领域概念从未见过、表示层缺失",
        "candidate": "continued_pretraining",
        "cheaper_first": ["prompt", "rag"],
    },
    "behavior": {
        "name": "行为缺口",
        "signal": "格式乱、不遵循指令、风格不符、任务接口不稳",
        "candidate": "sft",
        "cheaper_first": ["prompt", "rag", "tool"],
    },
    "preference": {
        "name": "偏好权衡缺口",
        "signal": "回答「能用」但不够好:需要选择更优答案而非示范答案",
        "candidate": "offline_preference",
        "cheaper_first": ["prompt", "rag", "tool"],
    },
    "exploration": {
        "name": "探索缺口",
        "signal": "需要新行为 / 多步推理 / 奖励最大化 / 可验证结果",
        "candidate": "online_rl",
        "cheaper_first": ["prompt", "rag", "tool"],
    },
}


def classify_root_cause(signals: Dict[str, bool]) -> Dict[str, str]:
    """
    输入四个根因信号,输出主导根因(互斥优先级:行为 > 偏好 > 探索 > 知识)。
    实际项目中优先看"残留失败切片"里最集中的信号类型。
    """
    priority = ["behavior", "preference", "exploration", "knowledge_internal", "knowledge_external"]
    for kind in priority:
        if signals.get(kind, False):
            rc = ROOT_CAUSES[kind]
            return {"kind": kind, "name": rc["name"], "signal": rc["signal"], "candidate": rc["candidate"]}
    return {"kind": "unknown", "name": "根因不明", "signal": "证据不足,先回到证据稳定", "candidate": "no_entry"}


# ===========================================================================
# 第三步:候选方案表 —— 失败根因决定候选;硬门槛决定候选能否执行
# ===========================================================================

@dataclass
class Assets:
    """执行候选所需的资产与资源(硬门槛)。"""
    prompt_template_ok: bool = False      # 可编辑提示模板 + 复测用例
    rag_stack_ok: bool = False            # 知识库 + 检索器 + 引用链路
    tool_stack_ok: bool = False           # API / 计算器 / 验证器可调用
    corpus_ok: bool = False               # 领域语料(持续预训练)
    sft_data_ready: bool = False          # 版本化监督数据(去重/去污染/切分)
    eval_suite_ready: bool = False        # 独立评测集 + 指标 + 冻结清单
    gpu_budget_ready: bool = False        # 非零训练预算
    sft_baseline_ready: bool = False      # 已有可复查的 SFT 基线
    preference_data_ready: bool = False   # 偏好对 (prompt, chosen, rejected)
    reward_signal_ok: bool = False        # 奖励模型 / 验证器 / 规则可计算
    rollback_ok: bool = False             # 回滚检查点 + 安全约束


CANDIDATES: List[Dict[str, object]] = [
    {
        "name": "prompt", "cost": 1, "family": "non_parameter", "root_cause": ["behavior"],
        "gates": ["prompt_template_ok"],
        "boundary": "只改上下文与示例;解决表达/格式/指令表达不充分,不补知识、不固化行为",
    },
    {
        "name": "rag", "cost": 2, "family": "non_parameter", "root_cause": ["knowledge_external"],
        "gates": ["rag_stack_ok"],
        "boundary": "知识在库不在参数;动态更新、可溯源,但依赖检索质量,增加延迟",
    },
    {
        "name": "tool", "cost": 3, "family": "non_parameter", "root_cause": ["knowledge_external"],
        "gates": ["tool_stack_ok"],
        "boundary": "实时查询 / 精确计算 / 外部动作交给 API 与验证器;参数学不会实时事实",
    },
    {
        "name": "continued_pretraining", "cost": 4, "family": "parameter", "root_cause": ["knowledge_internal"],
        "gates": ["corpus_ok", "gpu_budget_ready", "eval_suite_ready"],
        "boundary": "用领域语料补表示层知识;成本高,需防遗忘与灾难性退化",
    },
    {
        "name": "sft", "cost": 5, "family": "parameter", "root_cause": ["behavior"],
        "gates": ["sft_data_ready", "eval_suite_ready", "gpu_budget_ready"],
        "boundary": "用高质量示范固化行为/格式/风格;最小参数干预,是后续 RL 的基线与参考策略来源",
    },
    {
        "name": "offline_preference", "cost": 6, "family": "parameter", "root_cause": ["preference"],
        "gates": ["sft_baseline_ready", "preference_data_ready", "eval_suite_ready"],
        "boundary": "DPO 用偏好对直接优化,离线、轻量;不探索新行为,偏好数据质量敏感",
    },
    {
        "name": "online_rl", "cost": 7, "family": "parameter", "root_cause": ["exploration"],
        "gates": ["sft_baseline_ready", "reward_signal_ok", "gpu_budget_ready", "rollback_ok"],
        "boundary": "PPO/GRPO/RLVR 在线探索与奖励最大化;需奖励信号可靠、预算充足、可回滚",
    },
]


def viable_candidates(root_kind: str, assets: Assets) -> List[Dict[str, object]]:
    """根因决定候选 + 硬门槛决定能否执行:返回按成本升序的可用候选。"""
    out = []
    for c in CANDIDATES:
        if root_kind not in c["root_cause"]:
            continue
        if all(getattr(assets, g) for g in c["gates"]):
            out.append(c)
    return out


# ===========================================================================
# 第四步:训练分支选择(最小干预 + 数据/反馈/评测准入)
# ===========================================================================

@dataclass
class SFTStatus:
    """SFT 完成度:格式/指令遵循稳定 + 独立质量进入平台期。"""
    format_stable: bool = False       # 格式与指令遵循已稳定
    quality_plateau: bool = False     # 继续加示范数据不再明显提升
    residual_is_preference: bool = False  # 残余错误属于"哪个更好"的偏好
    residual_is_exploration: bool = False # 残余错误需要探索新行为/多步推理
    verifiable_reward: bool = False   # 数学答案/代码测试/环境终态可验证
    learned_reward: bool = False      # 有学习型奖励模型
    reward_degenerate: bool = False   # 奖励大量同分/全正全负/与独立成功率背离


def decide_next_step(ev: Evidence, signals: Dict[str, bool], assets: Assets,
                     sft: SFTStatus | None = None,
                     non_param_covered_slices: List[str] | None = None) -> Dict[str, object]:
    """
    主决策函数,按 Task 0 问题链顺序执行:
      稳定证据 -> 根因 -> 先试不改参数的方案 -> 必须改参数时选训练分支。
    返回 (stage, reason, gates, candidates)。

    non_param_covered_slices:提示词/RAG/工具在"同一工作负载、同一验收门槛"
    下复测后,能覆盖的失败切片列表。只有覆盖全部失败切片,才选中非参数方案。
    """
    # ① 证据门
    eg = evidence_gate(ev)
    if not eg["ready"]:
        return {"stage": "no_entry", "reason": "失败证据未稳定,先补齐:%s" % str(eg["checks"]),
                "candidates": [], "evidence": eg}

    # ② 根因
    rc = classify_root_cause(signals)
    if rc["kind"] == "unknown":
        return {"stage": "no_entry", "reason": rc["signal"], "candidates": [], "evidence": eg}

    # ③ 最小干预:先试不修改参数的候选(同一工作负载、同一验收门槛复测)
    non_param = viable_candidates(rc["kind"], assets)
    covered = set(non_param_covered_slices or [])
    if non_param and all(s in covered for s in ev.fail_slices):
        chosen = non_param[0]
        return {"stage": chosen["name"], "reason": "最小干预优先(不改参数)且已覆盖全部失败切片:%s" % chosen["boundary"],
                "candidates": [c["name"] for c in non_param], "evidence": eg}

    # ④ 必须更新参数:停在 SFT / 离线偏好 / 在线奖励与探索
    if rc["kind"] == "knowledge_internal":
        cands = viable_candidates("knowledge_internal", assets)
        if cands:
            return {"stage": cands[0]["name"], "reason": "知识缺口 -> 领域表示需参数更新:%s" % cands[0]["boundary"],
                    "candidates": [c["name"] for c in cands], "evidence": eg}
        return {"stage": "blocked", "reason": "知识缺口但语料/预算/评测未齐,先补齐硬门槛",
                "candidates": [], "evidence": eg}

    if rc["kind"] == "behavior":
        # SFT 是"改参数"里最轻的一步;没有 SFT 基线就先进 SFT
        if not assets.sft_baseline_ready:
            cands = viable_candidates("behavior", assets)
            if not cands:
                return {"stage": "blocked", "reason": "行为缺口但 SFT 硬门槛未齐:需要 监督数据+评测+预算",
                        "candidates": [], "evidence": eg}
            return {"stage": "sft", "reason": "行为缺口 + 无 SFT 基线 -> 先上 SFT(最小参数干预)",
                    "candidates": ["sft"], "evidence": eg}
        # 已有 SFT 基线:按 SFT 完成度 + 反馈信号类型升级
        s = sft or SFTStatus()
        if not s.format_stable or not s.quality_plateau:
            return {"stage": "continue_sft", "reason": "SFT 未完成:格式/指令遵循不稳或质量仍在提升 -> 补数据继续 SFT",
                    "candidates": ["sft"], "evidence": eg}
        if s.reward_degenerate:
            return {"stage": "fix_reward", "reason": "奖励退化(同分/全正全负/与独立成功率背离)-> 先修奖励与验证器",
                    "candidates": [], "evidence": eg}
        if s.verifiable_reward and s.residual_is_exploration:
            cands = viable_candidates("exploration", assets)
            if cands:
                return {"stage": "rlvr", "reason": "可验证奖励 + 需探索 -> RLVR(GRPO/PPO) 在线优化",
                        "candidates": ["online_rl"], "evidence": eg}
        if s.learned_reward and s.residual_is_exploration:
            cands = viable_candidates("exploration", assets)
            if cands:
                return {"stage": "ppo", "reason": "学习型奖励模型 + 需在线探索 -> PPO",
                        "candidates": ["online_rl"], "evidence": eg}
        if s.residual_is_preference:
            cands = viable_candidates("preference", assets)
            if cands:
                return {"stage": "dpo", "reason": "偏好对可复查 + 无需探索 -> 离线偏好优化 DPO",
                        "candidates": ["offline_preference"], "evidence": eg}
        return {"stage": "stop_at_sft", "reason": "无新反馈信号(偏好对/奖励模型/验证器)-> 停在 SFT",
                "candidates": [], "evidence": eg}

    if rc["kind"] == "preference":
        cands = viable_candidates("preference", assets)
        if not cands:
            return {"stage": "blocked", "reason": "偏好缺口但硬门槛未齐:需要 SFT基线+偏好对+评测",
                    "candidates": [], "evidence": eg}
        return {"stage": "dpo", "reason": "偏好权衡 -> 离线偏好优化 DPO(无需在线探索)",
                "candidates": ["offline_preference"], "evidence": eg}

    if rc["kind"] == "exploration":
        cands = viable_candidates("exploration", assets)
        if not cands:
            return {"stage": "blocked", "reason": "探索缺口但硬门槛未齐:需要 SFT基线+奖励信号+预算+回滚",
                    "candidates": [], "evidence": eg}
        if sft and sft.verifiable_reward:
            return {"stage": "rlvr", "reason": "可验证奖励 -> RLVR(GRPO) 组内相对优势在线优化",
                    "candidates": ["online_rl"], "evidence": eg}
        return {"stage": "ppo", "reason": "学习型奖励 -> PPO 在线策略更新",
                "candidates": ["online_rl"], "evidence": eg}

    return {"stage": "unknown", "reason": "无法判定", "candidates": [], "evidence": eg}


# ===========================================================================
# 配套训练代码示例:SFT 数据契约(标签掩码)与 GRPO 组内优势
# ===========================================================================

def build_sft_sample(instruction: str, response: str) -> Dict[str, List[int]]:
    """
    把 (指令, 输入, 参考回答) 变成 input_ids + labels + loss_mask。
    仅用整数 token 下标示意;真实训练中由 tokenizer + 聊天模板完成。
    原则:只对"有效监督 token(回答部分)"算交叉熵,指令部分掩码掉。
    """
    # 示意 tokenizer:按字符编号,实际使用 HuggingFace tokenizer
    vocab = {ch: i for i, ch in enumerate(set(instruction + response))}
    input_ids = [vocab[ch] for ch in instruction] + [vocab[ch] for ch in response]
    labels = [-100] * len(instruction) + [vocab[ch] for ch in response]  # -100 不参与损失
    loss_mask = [0] * len(instruction) + [1] * len(response)
    return {"input_ids": input_ids, "labels": labels, "loss_mask": loss_mask,
            "n_supervised_tokens": len(response)}


def grpo_group_advantage(rewards: List[float]) -> List[float]:
    """
    GRPO 组内相对优势:A_i = (r_i - mean) / std(同一 prompt 的一组采样)。
    高于组内均值 -> 提高概率;低于 -> 降低概率。无需 Critic。
    """
    mean = sum(rewards) / len(rewards)
    std = (sum((r - mean) ** 2 for r in rewards) / len(rewards)) ** 0.5 or 1.0
    return [round((r - mean) / std, 2) for r in rewards]


# ===========================================================================
# 场景测试
# ===========================================================================

def _mk_evidence(**kw) -> Evidence:
    base = dict(task="数学推理", metric="pass@1", target_threshold=0.90, current=0.72,
                fail_slices=["代数", "几何"], baseline_covers=["代数", "几何"],
                min_repeat=3, fail_rate=0.6, reproduce_count=5, independent_eval=True)
    base.update(kw)
    return Evidence(**base)


def main() -> None:
    # 场景 A:行为缺口(格式乱),Prompt/RAG/工具试过仍残留 -> 进 SFT
    ev_a = _mk_evidence(task="格式化输出", metric="format_ok", target_threshold=0.95, current=0.60)
    assets_a = Assets(prompt_template_ok=True, rag_stack_ok=True, tool_stack_ok=True,
                      sft_data_ready=True, eval_suite_ready=True, gpu_budget_ready=True)
    r_a = decide_next_step(ev_a, {"behavior": True}, assets_a, non_param_covered_slices=[])
    assert r_a["stage"] == "sft", r_a
    print("[A] 行为缺口、外部方案试过仍残留 ->", r_a["stage"], "|", r_a["reason"])

    # 场景 B:外部时效知识缺失,RAG 复测覆盖全部切片 -> 最小干预选中 RAG(不改参数)
    ev_b = _mk_evidence(task="实时新闻问答", metric="fact_accuracy", target_threshold=0.95, current=0.55)
    assets_b = Assets(prompt_template_ok=True, rag_stack_ok=True)
    r_b = decide_next_step(ev_b, {"knowledge_external": True}, assets_b,
                           non_param_covered_slices=ev_b.fail_slices)
    assert r_b["stage"] == "rag", r_b
    print("[B] 外部知识缺口 + RAG 就绪且覆盖全部切片 ->", r_b["stage"], "|", r_b["reason"])

    # 场景 B2:提示模板已覆盖全部失败切片 -> 直接选 Prompt,不进入训练
    ev_b2 = _mk_evidence(task="角色扮演格式", metric="format_ok", target_threshold=0.95, current=0.60)
    assets_b2 = Assets(prompt_template_ok=True)
    r_b2 = decide_next_step(ev_b2, {"behavior": True}, assets_b2,
                            non_param_covered_slices=ev_b2.fail_slices)
    assert r_b2["stage"] == "prompt", r_b2
    print("[B2] 提示模板覆盖全部切片 ->", r_b2["stage"], "|", r_b2["reason"])

    # 场景 C:SFT 完成、格式稳定、质量平台期,出现偏好权衡 -> DPO
    ev_c = _mk_evidence(task="客服话术", metric="prefer_win_rate", target_threshold=0.80, current=0.62)
    assets_c = Assets(sft_baseline_ready=True, preference_data_ready=True, eval_suite_ready=True)
    sft_c = SFTStatus(format_stable=True, quality_plateau=True, residual_is_preference=True)
    r_c = decide_next_step(ev_c, {"preference": True}, assets_c, sft=sft_c, non_param_covered_slices=[])
    assert r_c["stage"] == "dpo", r_c
    print("[C] 偏好权衡 + 偏好对就绪 ->", r_c["stage"], "|", r_c["reason"])

    # 场景 D:数学推理,答案可验证 + 需要探索 -> RLVR(GRPO 组内优势)
    ev_d = _mk_evidence(task="竞赛数学", metric="pass@1", target_threshold=0.90, current=0.72)
    assets_d = Assets(sft_baseline_ready=True, reward_signal_ok=True,
                      gpu_budget_ready=True, rollback_ok=True, eval_suite_ready=True)
    sft_d = SFTStatus(format_stable=True, quality_plateau=True,
                      residual_is_exploration=True, verifiable_reward=True)
    r_d = decide_next_step(ev_d, {"exploration": True}, assets_d, sft=sft_d, non_param_covered_slices=[])
    assert r_d["stage"] == "rlvr", r_d
    print("[D] 可验证奖励 + 需探索 ->", r_d["stage"], "|", r_d["reason"])

    # 场景 E:证据不稳定(复现次数不足)-> 禁止进入干预链
    ev_e = _mk_evidence(reproduce_count=1, fail_rate=0.2)
    r_e = decide_next_step(ev_e, {"behavior": True}, Assets())
    assert r_e["stage"] == "no_entry", r_e
    print("[E] 失败证据未稳定 ->", r_e["stage"], "|", r_e["reason"])

    # 场景 F:SFT 已完成但无新反馈信号 -> stop_at_sft
    ev_f = _mk_evidence(task="摘要生成", metric="rouge", target_threshold=0.60, current=0.52)
    assets_f = Assets(sft_baseline_ready=True, eval_suite_ready=True)
    sft_f = SFTStatus(format_stable=True, quality_plateau=True)
    r_f = decide_next_step(ev_f, {"behavior": True}, assets_f, sft=sft_f, non_param_covered_slices=[])
    assert r_f["stage"] == "stop_at_sft", r_f
    print("[F] 无新反馈信号 ->", r_f["stage"], "|", r_f["reason"])

    # 配套示例:SFT 标签掩码 + GRPO 组内优势
    sample = build_sft_sample("把结果输出为 JSON:", '{"ok": true}')
    print("[G] SFT 样本:监督 token 数 =", sample["n_supervised_tokens"], "| labels 头部掩码 =", sample["labels"][:5])
    rewards = [8.5, 6.8, 4.2, 2.5]
    print("[H] GRPO 组内优势:", grpo_group_advantage(rewards))

    print("\n全部断言通过:决策引擎行为符合 Task 0 问题链。")


if __name__ == "__main__":
    main()

8.2 运行输出(本地 Python 3.13・Windows・全部断言通过)

[A] 行为缺口、外部方案试过仍残留 -> sft | 行为缺口 + 无 SFT 基线 -> 先上 SFT(最小参数干预)
[B] 外部知识缺口 + RAG 就绪且覆盖全部切片 -> rag | 最小干预优先(不改参数)且已覆盖全部失败切片:知识在库不在参数;动态更新、可溯源,但依赖检索质量,增加延迟
[B2] 提示模板覆盖全部切片 -> prompt | 最小干预优先(不改参数)且已覆盖全部失败切片:只改上下文与示例;解决表达/格式/指令表达不充分,不补知识、不固化行为
[C] 偏好权衡 + 偏好对就绪 -> dpo | 偏好权衡 -> 离线偏好优化 DPO(无需在线探索)
[D] 可验证奖励 + 需探索 -> rlvr | 可验证奖励 -> RLVR(GRPO) 组内相对优势在线优化
[E] 失败证据未稳定 -> no_entry | 失败证据未稳定,先补齐:{'可测目标+阈值': True, '当前未达标': True, '稳定复现': False, '失败切片非空': True, '独立评测可用': True, '基线覆盖全部切片': True}
[F] 无新反馈信号 -> stop_at_sft | 无新反馈信号(偏好对/奖励模型/验证器)-> 停在 SFT
[G] SFT 样本:监督 token 数 = 12 | labels 头部掩码 = [-100, -100, -100, -100, -100]
[H] GRPO 组内优势: [1.3, 0.56, -0.56, -1.3]

全部断言通过:决策引擎行为符合 Task 0 问题链。

复现方式

:把 8.1 的完整脚本保存为

posttraining_stage_selector.py

,执行

python posttraining_stage_selector.py

即可得到以上输出。场景 A~F 覆盖六种典型决策路径:进 SFT / 选 RAG / 选 Prompt / 进 DPO / 进 RLVR / 证据不足被拦截 / 停在 SFT。

8.3 配套训练代码示例

以下两个函数已包含在 8.1 的完整脚本中,这里给出使用方式与运行结果。

SFT 数据契约:只对 “有效监督 token” 算交叉熵(标签掩码)

sample = build_sft_sample("把结果输出为 JSON:", '{"ok": true}')
print(sample["n_supervised_tokens"], sample["labels"][:5])
# 输出:12 [-100, -100, -100, -100, -100]   (指令部分全部掩码,只有回答参与损失)

GRPO 组内相对优势(不需要 Critic)

print(grpo_group_advantage([8.5, 6.8, 4.2, 2.5]))
# 输出:[1.3, 0.56, -0.56, -1.3]   (高于组内均值的回答提高概率,低于的降低)

DPO 目标函数(跳过奖励模型,直接用偏好对优化):

L D P O ( π θ ; π r e f ) = − E ( x , y w , y l ) ∼ D [ log ⁡ σ ( β log ⁡ π θ ( y w ∣ x ) π r e f ( y w ∣ x ) − β log ⁡ π θ ( y l ∣ x ) π r e f ( y l ∣ x ) ) ] L^{DPO}(\pi_\theta; \pi_{ref}) = - \mathbb{E}_{(x, y_w, y_l) \sim D} \left[ \log \sigma \left( \beta \log \frac{\pi_\theta(y_w|x)}{\pi_{ref}(y_w|x)} - \beta \log \frac{\pi_\theta(y_l|x)}{\pi_{ref}(y_l|x)} \right) \right] LDPO(πθ​;πref​)=−E(x,yw​,yl​)∼D​[logσ(βlogπref​(yw​∣x)πθ​(yw​∣x)​−βlogπref​(yl​∣x)πθ​(yl​∣x)​)]

PPO 的 clip 目标(限制每次策略更新幅度 + KL 约束):

L P P O ( θ ) = E [ min ⁡ ( ρ ( θ ) A ,   clip ⁡ ( ρ ( θ ) , 1 − ϵ , 1 + ϵ ) A ) ] − β   K L ( π θ ∥ π r e f ) L^{PPO}(\theta) = \mathbb{E} \left[ \min\left( \rho(\theta) A,\ \operatorname{clip}(\rho(\theta), 1-\epsilon, 1+\epsilon) A \right) \right] - \beta\, KL(\pi_\theta \| \pi_{ref}) LPPO(θ)=E[min(ρ(θ)A, clip(ρ(θ),1−ϵ,1+ϵ)A)]−βKL(πθ​∥πref​)


9. 概念说明:RLHF / PPO / DPO / GRPO / RLVR 与 RL 四模型

9.1 强化学习中的四个模型

在经典 RLHF-PPO 架构中,通常有四个模型角色:

模型角色主要功能是否更新
Actor / Policy(策略模型)生成回答,实际被训练更新
Reference(参考模型)冻结的 SFT 基线,用于 KL 约束 / 偏好参照冻结
Reward(奖励模型)对回答质量评分,把人类偏好转为奖励通常冻结
Critic / Value(价值模型)估计状态价值,为 PPO 计算优势提供基线通常更新

注意:GRPO

不需要 Critic

,用组内奖励的均值和标准差估计相对优势,省显存、省训练链路。还要区分

π_old

(生成当前训练数据的策略,算概率比用)与

π_ref

(参考策略,KL 正则用)不是同一个概念。

9.2 RLHF / PPO / DPO / GRPO / RLVR 概念

概念全称一句话理解
RLHFRL from Human Feedback一种对齐范式:用人类偏好反馈对齐模型,经典实现是 PPO;不是单一算法
PPOProximal Policy Optimization在线策略优化算法:clip 限制更新幅度 + KL 约束,稳定但链路重(Actor+Ref+Reward+Critic)
DPODirect Preference Optimization离线偏好优化:跳过奖励模型,直接用 (prompt, chosen, rejected) 偏好对优化,轻量
GRPOGroup Relative Policy Optimization去掉 Critic 的策略优化:同一 prompt 采一组答案,用组内相对奖励估计优势
RLVRRL with Verifiable Rewards奖励信号来源范式:用可自动验证的奖励(数学答案、单测、终态)替代主观打分,常与 GRPO/PPO 组合

一句话关系图:

RLHF(范式,人类反馈)
 ├── 在线:PPO(Actor+Critic,clip+KL)
 └── 离线:DPO(偏好对直接优化,无需奖励模型)
策略优化去 Critic:GRPO(组内相对优势)
奖励更客观:RLVR(可验证奖励,配 PPO 或 GRPO 用)

RLVR 的关键不是 “必须有 ground truth”,而是奖励能通过可靠的验证机制自动计算—— 数学答案是否等于参考答案、代码是否通过单测、Agent 任务是否成功。

参考资料

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐