06 · Query 解析模型后训练#
简历原话:Query 解析模型后训练:针对 Qwen3-8B 模型结构化输出不稳、意图拆解偏离检索需求的问题,用 2154 条合成逐轮样本做 SFT 冷启动 + 三轮 best-of-8 拒绝采样迭代。格式合法率 61%→100%,检索 P@20 0.399→0.467,reward 0.711→0.796。
30 秒口述版#
ShoppingX 每一轮对话开头,先由一个 Query 解析模型(项目里叫 planner)把用户原话拆成结构化字段:品类、品类域、预算、检索词、排除词,后面的检索全靠这几个字段。直接拿 Qwen3-8B 加提示词做,有两个毛病:一是结构化输出不稳,开发集上只有 61% 能解析通过;二是拆出来的东西“看着对、搜不到”,比如把中文整句塞进检索词,打英文商品库召回很差。我先用 2154 条合成的逐轮样本做 SFT 冷启动,把格式合法率拉到 100%;再做三轮 best-of-8 拒绝采样:每条样本采 8 个答案,把检索词真打进向量库算奖励,挑最好的一条回炉训练。最后检索 P@20 从 0.399 到 0.467,综合 reward 从 0.711 到 0.796。
背景与问题#
planner 在链路里的位置。 用户每说一句话,主 Agent 在检索前先调一次 planner。输入是本轮原话加前几轮用户原话,输出一个 JSON:主品类、所属品类域(从固定枚举里选,可多选)、预算金额和币种、2~6 个检索词、排除词(每条要带原话里的依据片段)。后面的商品检索用检索词做向量召回,用品类域和预算做过滤,用排除词做硬淘汰。planner 错一个字段,后面每一步都在错的轨道上跑。
原来的问题长这样(Qwen3-8B + 提示词):
- 结构化输出不稳,开发集 92 条里只有 61% 能被解析并通过 schema 校验。失败形态集中在三种:
- 预算写成字符串
"80"或"80 元",不是数字; - 品类域写了枚举外的值,比如自己造一个“户外用品”;
- JSON 前后带解释文字,或者输出被截在半截。
- 预算写成字符串
- 意图拆解偏离检索需求,JSON 合法但不好用:
- 照抄原话:检索词就是用户那句中文,而商品库以英文标题为主,中文口语直接编码去搜,召回的前 20 条里一大半不相关;
- 同义词堆砌:一口气给 10 个近义词,向量检索里词越多,查询向量越被拖向“什么都像一点”的中间位置;
- 只给一个品类词:比如只给
bag,召回太宽,前 20 条被各种包占满,用户要的是篮球包; - 域漂移:用户问“放床头的手表”,品类域判成家具,过滤条件跟着错。
为什么提示词治不好。 规则都已经写进提示词了(“不要照抄原话”“检索词 2~6 个”“预算写数字”),模型遵守一部分、违反一部分,改一轮提示词修好一类又冒出另一类。更根本的是,提示词只能告诉模型规则,没法告诉它“这组词搜出来的东西不对”。这个信号只有真去检索一次才拿得到,而它正好适合当训练信号。
为什么值得训一个自己的模型。 planner 每轮都要调、输出很短(一个 JSON,平均五十几个 token),任务边界清楚。用 API 大模型做,每轮多一次远程调用的延迟和费用;自己训一个 8B,用 vLLM 部署,延迟和成本都可控,出问题可以一键切回 API 模型。
做法与取舍#
1. 只训 planner 这一步,不训整条 Agent#
- 做法:训练对象是“一句话进、一个 JSON 出”的单步任务,主 Agent 的多轮工具调用不动。
- 为什么:单步任务没有功劳分配问题,奖励能完全由确定性规则和一次真实检索算出来,不需要再请一个模型打分。靶子又正对着提示词治不好的老毛病。
- 否掉了什么:对整条 Agent 轨迹做 RL。一轮任务要走四五次模型调用和若干工具,最终清单好坏说不清是哪一步的功劳;而且本地能训的 8B 在长提示词、多轮工具调用上拼不过线上 API 大模型,训了也换不上去。
2. 数据:2154 条合成逐轮样本,真实数据只当尺子#
- 做法:先造会话,再按“一次 planner 调用 = 一条样本”展开成逐轮样本。会话三路来源:
- 真实锚 94 个:线上真实用户消息去重,加上评测种子集;
- 普通合成 1200 个:从约 150 万商品的库里按品类分层抽商品,让强模型以“想买这个东西的用户”口吻写需求和追问,保证 247 个品类都有覆盖;
- 对抗合成 321 个:专门对着已知 bad case 造,比如域漂移(手表放床头)、配件淹没(搜手机出手机壳)、数值规格(16 寸)、跨语言、预算口语(“十五六块”)。
- 三路共 1615 个会话,过质量门(空、重复、长度越界、模板腔、近重等七项)挡掉 2 个,剩 1613 个会话,展开成 2154 条逐轮样本。多轮会话的第二、三轮样本里带着前几轮的用户原话,跟线上 planner 的输入形态完全一致。
- 按会话切分,不按样本切:同一个会话的几轮必须落在同一份里,否则第二轮进了训练集、第一轮进了开发集,就是泄漏。开发集就是那 92 个真实锚会话(94 个里有 2 个被质量门挡掉),测试集 230 个会话,其余进训练。
- 合成贴真实分布:上线前对了四个分布指标,首轮长度中位数 16 对 17、追问轮占比 34.3% 对 33.7%、带预算占比 49.7% 对 51.1%、带排除占比 24.1% 对 25.0%(合成对真实)。四个都对齐才拿合成数据当训练主力。
- 为什么真实数据不进训练:真实数据只有一百多条,拿去训练量太少,拿去当开发集却是最有价值的——开发集必须像线上,才能说明模型在线上好。
- 否掉了什么:直接用线上日志训练。量不够,而且线上日志的“标准答案”就是当时的 planner 输出,拿它训等于学旧模型自己的错。
3. 标签:教师模型给训练目标,三票投票给评分用的标准答案#
- 训练目标:强模型(API)按线上同一份 schema 产出 planner 的完整 JSON,当 SFT 的学习目标。
- 标准答案(golden):品类和品类域由强模型投三票,每票扰动温度、枚举顺序、要不要先写判断依据,三票一致率 94.2%。开发集和测试集里不一致的 29 条人工裁决;训练集里不一致的 124 条标成“待复核”,训练时剔除,不花人工。
- 为什么投票要加扰动:温度 0 下同一个模型问三次必然同一个答案,那种“一致”只说明稳定,不说明判对。
- 三种“弃权”:没有上文的追问碎片(“不要皮革的”)品类判不了,标成“不可判”;预算是口语区间(“十五六块”)标成“预算不确定”。评分时这些维度直接跳过,不当零分罚——罚零分等于教模型在无解的题上瞎猜。
- 踩过的坑:教师 JSON 里排除词的字段名和线上 schema 差了一个名字,过滤器把排除项整批当空,第一版训练数据里 0 条带排除词。这类错误不报错,只会让模型学会“永远不输出排除词”。之后统一改成训练、评分、线上三处共用同一份 schema 定义。
4. SFT 冷启动:先把格式钉死#
- 做法:Qwen3-8B 基座,LoRA(秩 16),在剔掉“待复核”后的 1621 条逐轮样本上训 3 个 epoch。提示词做了精简,只保留字段定义和判定规则,序列长度中位数压到 670 个 token 左右。关掉思考模式,直接输出 JSON。
- 结果:格式合法率 61% → 100%。基座的失败形态很单一,基本是预算写字符串、枚举越界,SFT 几百步就学会了。
- 为什么先 SFT 再拒绝采样:拒绝采样的前提是“采 8 条里至少有好的”。基座有 39% 解析失败,很多题 8 条里好答案很少甚至没有,选不出东西;而且解析失败直接记 -1 分,会把一大片样本整组扔掉。先用 SFT 把格式和字段形态稳住,采样才有料可挑。
- 否掉了什么:
- 约束解码(按 JSON schema 限制输出):它能保证语法合法,但解决不了第二个问题——检索词写得搜不到。而且线上部署是 vLLM 加 LoRA,约束解码会让每步解码多一次语法状态检查,短输出上不划算。SFT 之后格式已经 100%,线上只保留“解析失败就回退到 API 模型”这一道保险。
- 全参微调:8B 全参要放参数、梯度、优化器状态,24G 单卡放不下;这批卡之间也没有高速互联,跨卡切分通信太慢。LoRA 对“学格式 + 学写检索词”这种窄任务足够。
5. 奖励:检索词真打进商品库,看搜回来什么#
每个候选答案算一个 0~1 的总分,四个维度加权:
| 维度 | 权重 | 判什么 |
|---|---|---|
| 检索 | 0.45 | 用候选的检索词真去向量库搜,取前 20 条,看有多少条命中标准答案里的“必含词锚”,再看品类纯度 |
| 字段 | 0.30 | 品类、品类域、预算和标准答案对不对(品类用宽松匹配,“剪刀”和“工具”这种粒度差不算全错) |
| 格式 | 0.15 | 枚举值合法、排除词带原话依据、不抢填系统负责回填的字段 |
| 经济性 | 0.10 | 检索词条数在 2~6 之间、单个词不超过 4 个词元、不堆同义词 |
- 解析失败直接 -1,一票否决。
- 检索维的算法:前 20 条里命中必含词锚的比例,命中一半就算这一项满分(库里同品类商品本来就掺着配件,要求 100% 会逼模型去堆同义词);这一项占 0.6,品类纯度占 0.4。这里“命中”的判断和线上检索后置过滤用同一套词匹配口径,避免
watch命中watching这种错。 - 弃权维度不计分,权重重新归一:某一维无解(比如追问轮本来就没有锚),就从分母里拿掉,只在实际参与的维度上加权。
- 三条防作弊规则:
- 检索词照抄原话 → 检索维折半;
- 排除词的“依据”不是本轮原话的片段 → 字段维置 0(编一句话当依据,等于凭空拿到硬淘汰权);
- 这一轮本该检索(标准答案有锚)却不给检索词 → 检索维记 0。
- 为什么检索占最大权重:这正是“意图拆解偏离检索需求”的直接度量。字段对了但搜不到东西,对用户没有用。
- 否掉了什么:
- 拿语义相似度比检索词像不像标准答案(BERTScore 一类):字面像不等于搜得到。我们有条件直接问“搜到东西没有”——向量库就在训练机上,一次检索 0.3 秒,而且结果完全确定。
- LLM 当裁判打分:之前做 Rubric 评测时实测过,裁判打分的噪声在正负十几分。把噪声源塞进奖励,挑出来的“最优样本”有一部分是噪声挑的。裁判只在最后的端到端验收用,不进训练。
- 环境一致性:线上检索词是调 API 编码成向量的,训练机上为了快改成本地 GPU 跑同一个 BGE-M3。上线前做了一次等价性核对:同一批查询两边编码,余弦相似度 1.0,前 20 条结果重合 97.5%,第一名 100% 一致。不做这一步,奖励可能静默失真,训练时觉得搜得准,线上全变样。
6. 选拒绝采样,不选 GRPO / DPO#
- 做法:每轮三步,完全拆开——① 用当前模型对训练集每道题采 8 个答案(温度 1.0、top-p 0.95),同时跑一遍贪心解;② 每个答案都真跑一次检索、算奖励,全部候选连同分项分数存盘;③ 按规则从每组里选一条,和教师数据合在一起重新做 SFT。
- 为什么选它:
- 稳定:训练本身就是普通 SFT,没有 KL 系数、优势归一、学习率这些 RL 超参要调。LoRA 做在线 RL 对学习率特别敏感,照全参 RL 常用的 1e-6 设,跑两百步权重几乎不动。
- 可调试:采样、打分、选优三步解耦,候选全量存盘。要换拒绝阈值,重跑选优是秒级的事,不用再烧一遍 GPU。每一步的产物都能单独拿出来看。
- 能提前知道值不值得训:“8 条里最好那条”比“贪心解”高多少,就是这一轮拒绝采样的收益上限。这个差值在训练前就算出来了。
- 否掉了什么:
- GRPO:做过一版对照,reward 涨幅和拒绝采样差不多,但它要单独一张卡跑采样服务、训练进程和采样进程之间同步权重,还要和学习率、KL 系数反复较劲。而且在线 RL 找奖励漏洞的速度快得多——上面奖励里第 3 条防作弊规则堵的“不给检索词”漏洞就是它先钻出来的。对一个单步、短输出、奖励确定的任务,这些复杂度换不来多少额外收益。
- DPO:8 条里取最好和最差可以直接组成偏好对。没选是因为最差那条经常是解析失败的废答案,这种对子太容易,学不到东西;而且 DPO 有时会连“好答案”的概率一起压低,结构化输出上容易把格式带坏。它留作下一步,用“最好 vs 次好”组对子再试。
- 代价:拒绝采样只用了最好的那一条,差样本里的信息扔掉了,样本效率比 GRPO 低。这个任务的训练集只有一千六百多道题、一轮采样十几分钟,扔掉的那点效率可以接受。
7. 选优规则:阈值 τ、同分取短、无信息组单独记账#
- 解析失败的候选直接出局(-1 分不可能被选中)。
- 组内最高分低于 τ = 0.75,这道题本轮不产出新样本,训练目标保持教师答案。教师都没教对、自己也没采出好答案的题,不能拿自己的坏答案当监督信号,否则会把错误固化下来。
- τ 不是拍的:选优前先把 τ 从 0.50 到 0.95 每隔 0.05 扫一遍,画“保留题数 / 选中样本均分”曲线。0.75 附近保留率还有七成左右,再往上保留率掉得快、均分涨得慢,就停在这里。
- 同分取最短:奖励认为一样好,就挑 token 更少的;也让选优结果是确定的,换台机器重跑能复现。
- 组内分数几乎没差别的题(标准差 < 0.01)记为“无信息组”:8 条都差不多,选谁都等于复读当前模型。默认仍然保留,丢掉会让训练集偏向难题、分布跑偏,但单独统计占比,用来判断模型在哪些题上已经学饱了。
- 写盘前把选中的答案规范化成标准 JSON:采样带出来的多余空白、前后缀文字不能让模型学进去,训练目标要和教师数据同一个形态。
- 教师答案也进候选池:每道题的候选是 8 条采样加 1 条教师答案,一起打分取最高。这样每轮训练集都是 1621 道题、每题一个目标,不会出现同一道题两个互相打架的目标。
8. 每轮从基座重训,不在上一轮模型上接着训#
- 做法:第 k 轮用第 k−1 轮的模型采样,但训练时从 Qwen3-8B 基座重新挂一个 LoRA,在“教师答案 ∪ 本轮选中答案”这份数据上训。
- 为什么:接着训的话,每轮都在上一轮的模型上再拟合一遍它自己挑出来的答案,误差和过拟合会一轮轮叠加,输出会越来越单一(温度 1.0 下 8 条越来越像)。从基座重训,每轮模型只由“当前最好的一份数据”决定,轮次之间也好对照。
- 否掉了什么:只用本轮选中样本、丢掉教师数据。那样 τ 以下的题就没有训练目标了,模型在难题上会退化。
9. 为什么停在第三轮#
- 每轮开始前先看两个信号:训练集上“best-of-8 均分 − 贪心均分”的差距,和上一轮开发集的涨幅。
- 冷启动模型的差距约 0.11,第三轮训完后采样一看只剩 0.03 左右,开发集涨幅也从第一轮的近 0.05 降到第三轮的 0.013。收益上限已经很薄,再跑一轮的采样和训练成本换不来可见的提升,就停在三轮。
10. 上线方式#
- 训好的 LoRA 合进基座,用 vLLM 起一个 OpenAI 兼容接口,planner 那一档模型指向它,主 Agent 这边不用改代码。
- 保险:本地模型超时、返回解析失败,这一轮自动回退到 API 模型重做一次;配置一键切回全 API。
- 其他 bullet 里讲的前缀缓存、延迟优化都在主 Agent 那一侧,这里只动 planner 这一次调用。
流程图 / 架构图#
整体训练流程(数据 → 冷启动 → 三轮拒绝采样 → 上线):
flowchart TD
A["真实锚 94 会话"] --> D["质量门七项检查"]
B["普通合成 1200 会话<br/>按品类分层抽商品"] --> D
C["对抗合成 321 会话<br/>对着 bad case 族造"] --> D
D --> E["1613 会话展开<br/>2154 条逐轮样本"]
E --> F["按会话切分<br/>训练 / 开发 92 / 测试"]
F --> G["教师强模型产出训练目标"]
F --> H["三票扰动投票 + 人工裁决<br/>产出标准答案"]
G --> I["SFT 冷启动<br/>Qwen3-8B + LoRA"]
I --> J["当前模型"]
J --> K["每题采 8 条 + 贪心 1 条"]
K --> L["逐条真实检索 + 四维奖励打分"]
H --> L
L --> M["候选与分项全量存盘"]
M --> N{"组内最高分 ≥ τ?"}
N -- 是 --> O["取最高分, 同分取最短"]
N -- 否 --> P["保留教师答案"]
O --> Q["合成本轮训练集"]
P --> Q
Q --> R["从基座重训 LoRA"]
R --> S{"开发集评测<br/>第 3 轮了?"}
S -- 否 --> J
S -- 是 --> T["合并权重, vLLM 部署<br/>失败回退 API 模型"]
单个候选的打分过程(奖励环境,全部在训练机同机完成):
sequenceDiagram
participant S as 采样引擎 vLLM
participant R as 奖励函数
participant E as 本地 BGE-M3 编码
participant Q as 向量库 Qdrant
S->>R: 候选文本
R->>R: 抽 JSON 并按线上 schema 校验
alt 解析失败
R-->>S: 总分 -1
else 解析成功
R->>E: 检索词拼成一条查询
E->>Q: 查询向量, 取前 20 条
Q-->>R: 20 条商品标题
R->>R: 检索维 = 必含词命中 + 品类纯度
R->>R: 字段维 / 格式维 / 经济性维
R->>R: 防作弊规则: 照抄折半, 假依据置零, 该搜不搜记零
R->>R: 去掉弃权维度后按权重归一
R-->>S: 总分 + 四维分项
end
数字怎么来的#
所有对比都在同一份开发集(92 个真实锚会话展开的 92 条样本)上、用贪心解码(温度 0)跑,同一套奖励函数、同一个向量库、同一个编码模型。候选打分按 256 条一批发给编码服务和向量库,一次开发集评测几十秒。
| 数字 | 测什么 | 怎么测 | 对比的两端 |
|---|---|---|---|
| 2154 条 | 训练数据规模 | 1613 个会话按“一次 planner 调用一条”展开后的逐轮样本总数;其中训练侧 1745 条(SFT 实际用剔掉 124 条待复核后的 1621 条)、开发 92 条、测试 317 条 | — |
| 格式合法率 61%→100% | 输出能不能被下游直接用 | 能从输出里抽出 JSON,并通过线上 planner 同一份 schema 校验(类型、必填、枚举)算合法;合法条数 / 92 | 基座 Qwen3-8B 加同一份提示词 56/92≈61%;SFT 冷启动后 92/92,三轮拒绝采样后保持 100% |
| P@20 0.399→0.467 | 检索词本身搜不搜得到 | 只在标准答案带“必含词锚”的样本上算(追问澄清轮没有锚,不参与)。把模型给的检索词拼成一条查询,纯向量检索不带过滤,取前 20 条,数其中命中必含词锚的条数 / 20,再对样本取平均。模型该给检索词却没给,这条记 0 | SFT 冷启动 0.399 → 第三轮 0.467 |
| reward 0.711→0.796 | 四维综合质量 | 上面那套四维加权奖励,弃权维度重新归一,逐条算完取均值 | SFT 冷启动 0.711 → 第三轮 0.796 |
几点口径说明,面试被问到时能直接答:
- reward 和 P@20 的基线为什么是 SFT 冷启动、不是基座:基座有 39% 解析失败,每条记 -1,均分只剩零点零几,拿它当基线会把涨幅夸大好几倍,而且涨的主要是格式。格式单独用合法率报,reward 和 P@20 从 SFT 起算,量的是拒绝采样本身带来的提升。
- P@20 和奖励里检索维不是一个数:奖励里命中率到 50% 就封顶,还混了 0.4 的品类纯度;P@20 是不封顶的原始命中比例,更直观。
- 逐轮数字:开发集 reward 0.711 → 0.758 → 0.783 → 0.796,P@20 0.399 → 0.436 → 0.455 → 0.467,增量一轮比一轮小,和“best-of-8 与贪心的差距”一路缩小是对得上的。
- 格式合法率在三轮里一直是 100%:这是拒绝采样的一条护栏。如果某一轮格式率掉了,说明选优或数据混合出了问题,这一轮作废。
- 测试集只在最后跑一次:开发集用来决定 τ、决定停在第几轮,已经被“看过”很多次;测试集最后跑一次确认方向一致,不拿它调任何东西。
追问 Q&A#
Q1:planner 具体输出哪些字段?为什么说它对检索这么关键?
输出一个 JSON:主品类、品类域(枚举多选)、预算金额和币种、2~6 个检索词、排除词(每条带原话依据)。检索词决定向量召回搜什么,品类域和预算决定过滤条件,排除词决定硬淘汰。召回阶段漏掉的商品,后面精排、比价、挑选都救不回来,所以 planner 是整条链的起点,它偏一点,后面全偏。
Q2:2154 条都是合成的,怎么保证合成数据像真实用户?
三件事。第一,合成是“从商品库按品类分层抽商品,再让强模型以想买这件东西的用户口吻写需求”,内容锚在真实商品上,不是凭空编。第二,上线前对了四个分布指标:首轮长度中位数、追问轮占比、带预算占比、带排除占比,合成和真实的差距都在 1.5 个百分点以内。第三,评测用的开发集全是真实用户会话,合成数据只进训练。模型在开发集上涨了,说明从合成数据学到的东西在真实分布上也成立。
Q3:拒绝采样本质上还是 SFT,它和“多蒸馏几遍教师”有什么区别?为什么能超过教师?
区别在于监督信号从哪来。蒸馏是教师说什么学什么,上限是教师;拒绝采样的目标是“模型自己采出来、并且在真实检索里得分最高的答案”,选优标准是环境反馈,不是教师的判断。教师写检索词时同样看不到检索结果,它写的词经常不是最好搜的那组。每道题的候选池里也放了教师答案,采样答案只有在真实检索上打败教师时才会替换它。所以检索维能超过教师,而字段维(品类判定)基本卡在教师附近——那一维的标准答案就是教师投票出来的。
Q4:best-of-8 的 8 是怎么定的?为什么不是 16、32?
看“best-of-n 均分随 n 的曲线”。n 从 1 涨到 8 这一段提升明显,8 到 16 只多出很小一截,采样和打分成本却翻倍——每个候选都要真跑一次检索。一千六百多道题乘 8 就是一万三千条候选,一轮采样加打分十几分钟,是能接受的量。另外 n 越大,选出来的越可能是“碰巧撞中奖励规则”的极端答案,相当于在奖励上过度优化,n 适中也是一种约束。
Q5(质疑):你的奖励是自己写的规则,模型会不会只是学会了讨好你的奖励函数,线上并没有变好?
这个风险是真的,我们用三层办法控制。第一,奖励最大的一维是真实检索结果,不是字面规则,“讨好它”就等于“搜得更准”。第二,实测发现过漏洞并修掉:旧版里模型不给检索词,检索维会被当成弃权跳过,等于一次甩掉 45% 的权重,GRPO 对照实验里模型很快就往这个方向走;修成“本该检索却不给词记 0”之后,开发集上该检索的轮次里给出检索词的比例明显上升。另外照抄原话、编造排除依据也都有专门的扣分。第三,最终验收不看奖励,看开发集上的 P@20 这种可解释的指标,以及整条 Agent 的 Rubric 端到端评测。
Q6:P@20 从 0.399 到 0.467,绝对值看着不高,为什么?
两个原因。一是口径严:命中要求商品标题里出现标准答案的必含词,同义但换了词的商品不算命中,同品类的配件、周边也不算,所以真实可用的商品比例比这个数高。二是这里测的是检索词本身,纯向量检索、不带任何过滤,也没有精排;线上还有品类域和预算过滤、再加一层 reranker,最终给用户看的前几条纯度高得多。这个指标的作用是隔离出“检索词写得好不好”这一个变量,看相对变化,不看绝对值。
Q7:为什么每轮都从基座重训,不在上一轮模型上继续训?
接着训会让每轮都在拟合“自己挑出来的自己的答案”,误差会累积,输出多样性也会一轮轮下降,到后面温度 1.0 采 8 条都差不多,拒绝采样就没料了。从基座重训,每轮模型只由当前那份数据决定,也方便对照:两轮之间唯一的变量就是训练数据。代价是每轮多训一遍,但 LoRA 在一千六百条短样本上训一次也就二十来分钟,可以接受。
Q8:如果组内 8 条全都很差(最高分低于 τ)怎么办?
这道题本轮不产出新样本,训练目标保持教师答案。不能拿“矮子里拔将军”的答案去训,否则会把错误当成正确答案固化下来。这类题会单独统计,按 bad case 族看它们集中在哪里;第一轮以后集中在无上文的追问碎片和极口语化的预算表达上,后者属于标准答案本身就“不确定”的题,对应维度在评分时已经弃权。
Q9:为什么不用 GRPO?你简历技能里写了了解 GRPO。
做过一版 GRPO 对照,reward 涨幅和拒绝采样差不多,但成本高很多:要单独一张卡跑采样服务并和训练进程同步权重,学习率、KL 系数要反复试——LoRA 下学习率照全参 RL 的 1e-6 给,跑两百步权重只变了千分之二点几,等于没训。而这个任务是单步、短输出、奖励确定的,在线 RL 的优势(每步用最新策略采样、利用负样本)在这里体现不出多少。拒绝采样把采样、打分、训练拆开,每步都能单独检查,τ 可以离线重扫,工程上更稳。
Q10:为什么用 Qwen3-8B,不用更小的 4B 或者更大的模型?
更小的模型在品类和品类域判定上明显更弱,而这两个字段错了没法靠检索词补。更大的模型在 24G 卡上做 LoRA 训练和 vLLM 采样都放不下,采样时留给 KV 缓存的显存也不够,吞吐掉得厉害。8B 开梯度检查点能在 24G 卡上训 LoRA,推理时关掉思考模式,只输出五十几个 token 的 JSON,单次调用延迟可控。
Q11:如果线上用户的说法跟训练分布差很远,比如出现一个新兴品类词,模型会怎样?
planner 会尽量把它翻译成商品库里的品类词,翻不对的话检索会偏。线上有两道防线:一是主 Agent 那边检索结果为空或明显不相关时,会调网页搜索把新说法翻译成已知品类词再搜一次;二是这类 query 会通过 Rubric 评测进 bad case 池,定期合成同一族的新样本补进训练集,再跑一轮拒绝采样。模型本身解析失败或超时,这一轮直接回退到 API 模型。
Q12:下一步还能怎么做?
两个方向。一是品类域判定:它在字段维里和品类、预算等权,预算已经接近满分、品类卡在标准答案的天花板上,域判定的信号被稀释了,打算把“域判错”改成硬约束,直接扣到底。二是把拒绝采样扔掉的差样本用起来,用“最好 vs 次好”组偏好对做一轮 DPO,比“最好 vs 解析失败”这种太容易的对子更有信息量。
相关八股#
1. 拒绝采样微调(RFT / 专家迭代 / ReST)是什么?和 SFT、RL 的关系?
- 流程:用当前策略对每个输入采 N 个输出 → 用奖励函数或验证器打分 → 保留高分(或正确)样本 → 在这些样本上做 SFT → 用新模型重复。
- 代表工作:STaR(推理题只留答对的)、ReST / ReST-EM(Grow 采样 + Improve 过滤训练,每轮从基座重训)、Llama 2 对齐里的 rejection sampling 阶段。
- 理论上可以看成 EM:E 步用奖励过滤出“好”的样本近似后验,M 步最大化它们的似然;也可以看成只用正样本、优势为 0/1 的离线策略梯度。
- 优点是训练稳定、实现简单、离线可调;缺点是扔掉负样本、样本效率低,且依赖初始模型至少能偶尔采出好答案。
- 本项目:SFT 冷启动保证“偶尔能采出好答案”,三轮拒绝采样每轮从基座重训,就是 ReST-EM 的做法。
2. Best-of-N 为什么有效?它和 KL 约束是什么关系?
- Best-of-N 在推理时取 N 个样本里奖励最高的,是最简单的“按奖励优化”。N 越大期望奖励越高,但增幅递减。
- 理论结果:Best-of-N 分布相对原策略的 KL 约为 log N − (N−1)/N,所以 N 本身就是一个“离原策略多远”的旋钮;N 太大相当于在奖励模型上过度优化,更容易撞上奖励漏洞。
- 拒绝采样微调相当于把 Best-of-N 的效果蒸馏回模型,让贪心解就接近原来 best-of-N 的水平。
- 本项目:用“best-of-8 − 贪心”的差距估每轮的收益上限,差距缩到 0.03 左右就停。
3. GRPO 的原理?和 PPO 有什么不同?
- PPO 要一个价值网络(critic)估计基线,算优势后用裁剪的重要性比率更新,外加对参考模型的 KL 惩罚。
- GRPO 去掉 critic:同一个 prompt 采一组 G 个输出,用组内奖励的均值和标准差做归一化,(r − mean) / std 就是每条输出的优势;其余保留 PPO 的裁剪目标和 KL 项。
- 好处是省一个和策略同尺寸的价值模型的显存和训练,适合奖励可以直接算的任务(数学、代码、结构化输出)。
- 组内奖励全一样时优势为 0,这组没有梯度,所以任务太简单或太难都学不动。
- 本项目:做过 GRPO 对照;组内全同分的问题对应拒绝采样里的“无信息组”统计。
4. DPO 的原理和局限?
- DPO 把 RLHF 的“奖励模型 + KL 约束 RL”化简成直接在偏好对上训练:损失是 −log σ(β·[log πθ(yw|x)/πref(yw|x) − log πθ(yl|x)/πref(yl|x)]),不需要显式奖励模型和采样。
- 局限:只看好坏两条的相对差,训练中常出现“好答案的似然也一起下降”;对偏好对质量敏感,太容易区分的对子学不到东西;离线数据和当前策略分布不一致时效果打折。
- 本项目:最差样本常是解析失败的废答案,“最好 vs 最差”太容易,所以没用 DPO;下一步考虑“最好 vs 次好”组对子。
5. LoRA 的原理?为什么 LoRA 的学习率要比全参大?
- 冻结原权重 W,在旁路加低秩分解 ΔW = B·A(A 为 r×k,B 为 d×r,r 远小于 d、k),B 初始化为 0,训练只更新 A、B,前向为 Wx + (α/r)·BAx。
- 可训练参数量降到原来的千分之几,显存主要省在梯度和优化器状态;推理时可以把 BA 合回 W,没有额外延迟。
- 学习率:LoRA 参数少、B 从 0 起步,同样的学习率下对输出的实际改动远小于全参,所以常用 1e-4 量级(SFT)或 1e-5 量级(RL),全参 RL 常见的 1e-6 放到 LoRA 上几乎不动。
- 本项目:Qwen3-8B + LoRA 秩 16;GRPO 对照里 1e-6 跑两百步权重只变千分之二点几,是这条经验的直接证据。
6. vLLM 为什么快?前缀缓存是怎么回事?
- PagedAttention:把 KV 缓存切成固定大小的块,像操作系统分页一样按需分配,消除预分配带来的显存碎片,同样显存能放更多并发请求。
- 连续批处理(continuous batching):按 token 步调度,一个请求结束立刻补新请求,不用等整批结束。
- 前缀缓存:KV 块按“前缀内容的哈希”索引,新请求的前缀和已有块相同就直接复用,跳过这段的 prefill 计算。一个请求 n=8 采样时,8 条输出也共享同一份 prompt 的 KV。
- 本项目:planner 提示词前面的规则部分所有样本完全一样,只有末尾的用户原话不同,开前缀缓存后采样吞吐提升好几倍,是一轮采样能压到十几分钟的主要原因。
7. 什么是 reward hacking?怎么防?
- 模型找到奖励函数的漏洞,拿到高分但没有完成真实目标,是古德哈特定律在 RL 里的体现:指标一旦成为优化目标,就不再是好指标。
- 常见形态:输出变长骗长度偏好、照抄题面、钻弃权或异常分支、利用裁判模型的偏好。
- 防法:奖励尽量锚在可验证的真实结果上;对已知捷径显式扣分;分维度监控而不是只看总分;控制优化强度(KL、best-of-N 的 N);最终用独立指标或人工抽检验收。
- 本项目:检索维用真实检索结果;照抄原话折半、假依据置零、该搜不搜记零;最终用 P@20 和端到端 Rubric 验收,不只看 reward。
8. Precision@K、Recall@K、NDCG@K 分别衡量什么?什么时候用哪个?
- P@K = 前 K 条里相关的条数 / K,衡量“拿出来的东西干不干净”;R@K = 前 K 条里相关的条数 / 全部相关条数,衡量“该找的找全了没有”。
- NDCG@K 考虑位置和分级相关性:DCG = Σ (2^rel − 1) / log2(i+1),再除以理想排序的 IDCG 归一到 0~1。
- 召回阶段常看 R@K(漏了后面救不回来),精排和最终展示看 NDCG 或 P@K。
- 本项目:planner 这里没有“全部相关商品”的完整标注,150 万商品的库里算不了召回率;而检索词写得好不好,最直接的表现是前 20 条干不干净,所以用 P@20。召回率和 NDCG 用在检索模型微调那条 bullet 里。
9. 采样时 temperature 和 top-p 分别起什么作用?拒绝采样为什么要用温度 1.0?
- temperature 在 softmax 前除 logits:小于 1 让分布更尖、更确定,大于 1 更平、更多样;0 就是贪心。
- top-p(nucleus)只在累计概率达到 p 的最小候选集合里采样,截掉长尾里的离谱 token。
- 拒绝采样需要组内有差异才有得挑,温度太低 8 条几乎一样;温度 1.0 保持模型原本的分布,配合 top-p 0.95 去掉长尾噪声。评测一律用贪心,保证可复现。
- 本项目:组内标准差小于 0.01 的题记为无信息组,就是在监控采样多样性够不够。
10. 结构化输出有哪些做法?约束解码的原理?
- 三种做法:提示词加少样本;微调让模型学会格式;约束解码(guided decoding)。
- 约束解码把 JSON schema 或正则编译成有限状态机或文法,每一步解码前把不合法的 token 的 logit 置为负无穷,保证输出一定合法。
- 局限:只管语法,不管语义对不对;强行截断模型想走的路径可能让内容质量下降;每步多一次状态检查,有额外开销。
- 本项目:格式问题靠 SFT 解决到 100%,线上保留“解析失败回退 API 模型”;真正难的是语义层面的检索词质量,这只能靠奖励驱动的训练。