【Bug已解决】Can gold_trainer support privileged context 解决方案

📅 2026/7/22 7:37:30
【Bug已解决】Can gold_trainer support privileged context 解决方案
【Bug已解决】Can gold_trainer support privileged context 解决方案一、现象长什么样在用一个在线 RL 训练器社区里常称GOLD/gold_trainer即 Generalized Online LLM Training 的基类做数学推理训练时我们希望引入特权上下文privileged context在训练阶段的 rollout 里给模型看一点参考答案提示比如题干的隐含条件、或一条简短的提示路径但在最终部署/评测时这些提示并不存在。直觉上特权信息能帮模型更快学会好答案长什么样再靠课程式衰减让它逐步脱离提示。但当前的gold_trainer把 prompt 当成一条不可拆分的对话直接喂给生成与优势计算没有提供任何机制去区分普通 prompt和仅训练期可见的特权片段。于是现象是你想在prompt里塞特权信息结果部署时模型也见到了同样的内容泄漏你想只在生成时给、在算 reward 时收回但 trainer 没有这样的钩子issue 里的问题本质就是gold_trainer是否、以及如何支持 privileged context这不是一个会抛异常的硬 bug而是一个能力缺失 容易用错导致信息泄漏的设计缺口。二、背景特权上下文privileged context源自模仿学习/RL 里的经典技巧训练时给策略额外的、部署时拿不到的状态信息privileged information加速收敛再逐步把特权撤掉。在 LLM RL 里常见形态提示型特权训练 rollout 时把参考答案的骨架或关键中间结论拼到 prompt 后面帮助模型探索到高 reward 区域条件型特权某些环境如 agentic 任务在训练时暴露正确答案/状态真值推理时不暴露课程型特权特权强度随训练步数衰减λ → 0让模型从扶着走过渡到自己走。难点在于privileged context 必须只参与生成、不参与最终评判的公平比较且绝不能泄漏进评测/部署。如果 trainer 把整条 prompt含特权既用于生成又原样用于 reward 比较就会出现训练指标虚高、但离线评测崩盘——因为评测时没有了特权。gold_trainer当前把prompt当作单一字段贯穿生成 → logprobs → 优势 → 更新全链路没有生成用完整 prompt、更新用剥离特权的 prompt的分离点所以想加特权就得 hack极易把特权带进更新导致泄漏。三、根因根因是数据契约里没有特权这一独立维度单一 prompt 字段dataset 里只有prompt生成和更新都用它无法表达生成时多一段、更新时少一段。缺少分离钩子trainer 没有build_generation_prompt(privileged)与build_training_prompt()这样两个清晰的分叉点用户只能在外部手动拼字符串难保证一致性。无泄漏护栏没有任何断言/检查确保用于更新的 prompt 不包含特权标记于是 hack 时一不留神就把特权喂进了更新链路。本质是 trainer 把prompt 的呈现方式这件本应可配置的事固化成了单一不可拆分的输入。四、最小可运行复现下面用纯文本模拟复现特权泄漏把特权直接拼进 prompt更新时也用了它于是带特权的得分被当成了真实能力from typing import List, Dict def rollout_with_privilege(model_reply_fn, base_prompt: str, privileged: str) - str: # 生成时把特权拼上 gen_prompt base_prompt \n[特权提示] privileged return model_reply_fn(gen_prompt) def score(reply: str, golden: str) - float: return 1.0 if golden in reply else 0.0 def train_loop(base_prompt, privileged, golden): # 错误做法用含特权的生成直接当训练信号且评测也给了特权 reply rollout_with_privilege(lambda p: 答案是 golden, base_prompt, privileged) train_reward score(reply, golden) # 评测时部署 환경没有特权模型自己答 eval_reply 答案是 (42 if golden ! 42 else x) # 模型其实不会 eval_reward score(eval_reply, golden) return train_reward, eval_reward def demo(): tr, ev train_loop(求 11, 答案是 2, 2) print(f训练 reward{tr} 评测 reward{ev} - 严重虚高特权泄漏) if __name__ __main__: demo()输出训练 reward1.0 评测 reward0.0 - 严重虚高特权泄漏这正是问题所在训练时因为特权给了答案reward 满分的假象一旦评测撤掉特权原形毕露。正确做法是生成可用特权探路但更新/优势计算与评测都必须基于无特权的公平设定。五、解决方案第一层在数据层显式拆分prompt与privileged第一层给 dataset 增加一个独立字段让特权成为一等公民而不是藏在 prompt 字符串里from typing import Dict, List, Optional def make_example(prompt: str, privileged: Optional[str], answer: str) - Dict: return { prompt: prompt, # 部署/评测时模型看到的无特权 privileged: privileged, # 仅训练生成期可见 answer: answer, } def build_generation_messages(ex: Dict) - List[Dict[str, str]]: 生成阶段base prompt 特权提示帮助探索。 content ex[prompt] if ex.get(privileged): content \n[privileged_hint] ex[privileged] return [{role: user, content: content}] def build_scoring_messages(ex: Dict) - List[Dict[str, str]]: 打分/优势计算阶段只用 base prompt保证公平、不泄漏。 return [{role: user, content: ex[prompt]}] def demo_split(): ex make_example(求 11, 答案是 2, 2) gen build_generation_messages(ex) score_msgs build_scoring_messages(ex) print(生成用, gen[0][content]) print(打分用, score_msgs[0][content]) assert [privileged_hint] not in score_msgs[0][content] print(OK: 打分链路不含特权无泄漏) if __name__ __main__: demo_split()核心改动privileged 是独立字段生成时拼、打分时绝不拼。这就从数据契约上杜绝了把特权带进更新。六、解决方案第二层在 trainer 里加两个分离钩子 课程衰减第二层把拆分固化进 trainer并提供特权强度随步数衰减的课程机制from typing import Dict, List, Optional class PrivilegedGoldTrainerMixin: def __init__(self, privileged_decay: float 0.999, start_step: int 0): self.privileged_decay privileged_decay self.step 0 def _use_privilege(self) - bool: # 课程式衰减前 N 步用之后按概率逐步撤掉 if self.step start_step : 0: return True keep_prob self.privileged_decay ** self.step import random return random.random() keep_prob def generation_messages(self, ex: Dict) - List[Dict[str, str]]: content ex[prompt] if ex.get(privileged) and self._use_privilege(): content \n[privileged_hint] ex[privileged] return [{role: user, content: content}] def training_messages(self, ex: Dict) - List[Dict[str, str]]: # 更新/优势永远基于无特权 prompt return [{role: user, content: ex[prompt]}] def on_step_end(self): self.step 1这样generation_messages控制生成时给不给特权带课程衰减training_messages永远返回无特权的 base prompt喂给 logprobs 与优势计算衰减让模型早期扶着走、后期自己走避免对特权产生依赖。注意一个关键点logprobs 必须在training_messages无特权上重算而不能复用生成时的带特权 logprobs——否则优势计算本身就被特权污染。这正是gold_trainer需要补的第二个分离点。七、解决方案第三层泄漏护栏 评测隔离第三层加一道硬性护栏确保任何特权字符串都不会进入更新/评测链路并在训练循环里强制生成 prompt ≠ 更新 prompt的断言PRIV_TAG [privileged_hint] def assert_no_leak(scoring_text: str): if PRIV_TAG in scoring_text: raise RuntimeError(特权上下文泄漏进打分/更新链路) def safe_advantage_step(trainer, ex: Dict): gen_msgs trainer.generation_messages(ex) # 可能含特权 score_msgs trainer.training_messages(ex) # 永远无特权 # 护栏任何进入优势计算的文本都不能含特权标记 for m in score_msgs: assert_no_leak(m[content]) # 用 score_msgs 重算 logprobs 与优势绝不混用 gen_msgs 的 logps trainer.compute_logprobs(score_msgs) adv trainer.compute_advantage(score_msgs, logps) trainer.step(logps, adv) trainer.on_step_end()同时评测脚本必须显式只用ex[prompt]并在 CI 里断言评测集的privileged字段永远不被读取def evaluate(trainer, eval_examples): total 0.0 for ex in eval_examples: # 评测严禁使用 ex[privileged] msgs [{role: user, content: ex[prompt]}] total trainer.score(trainer.generate(msgs), ex[answer]) return total / len(eval_examples)护栏 评测隔离双保险特权就再也不可能悄悄泄漏到代表真实能力的指标里。八、落地建议从 dataset 改造到 trainer 接入如果你想在现有gold_trainer上支持特权建议改动顺序改 dataset schema增加privileged字段可空存量数据privilegedNone即为无特权任务向后兼容。加 mixin把PrivilegedGoldTrainerMixin混入 trainer复用generation_messages/training_messages。改生成调用rollout 用generation_messageslogprobs/优势用training_messages重算。加衰减默认privileged_decay适中如 0.999可按需调。加护栏断言assert_no_leak进训练主循环CI 跑一次即知是否泄漏。评测隔离评测脚本只读prompt并在测试里断言privileged未参与。这样gold_trainer就原生支持 privileged context且默认安全不泄漏。九、排查清单如果你怀疑特权上下文用错了/泄漏了按这个顺序查看训练 reward 与评测 reward 是否严重背离训练虚高、评测崩盘是特权泄漏的典型指纹。搜 prompt 拼接处是否有把privileged直接拼进单一prompt字段。确认更新链路用的是无特权 promptlogprobs 与优势计算必须在training_messagesbase prompt上不能用生成时的带特权版。加assert_no_leak任何进打分的文本都不能含特权标记。评测隔离评测脚本只读prompt断言不读privileged。课程衰减如果模型后期仍依赖特权调小privileged_decay或提前撤权。dataset 显式拆分privileged作为独立字段存量数据置None保持兼容。十、小结gold_trainer原本不支持 privileged context并不是因为它有 bug而是数据契约里没有特权这个独立维度——prompt 被当成单一不可拆分的字段贯穿生成、logprobs、优势、更新全链路。想加特权就只能 hack 拼字符串稍不留神就把特权带进更新与评测造成训练满分、部署崩盘的泄漏假象。修复分三层第一层在 dataset 里把privileged提为独立字段生成时拼、打分时绝不拼从契约上杜绝泄漏第二层给 trainer 加generation_messages/training_messages两个分离钩子并用课程式衰减让模型逐步脱离特权注意 logprobs 必须在无特权 prompt 上重算第三层加assert_no_leak护栏与评测隔离把特权不可进更新变成硬性不变量。核心心法是特权只该存在于生成期的探索里更新与评判必须回到无特权的公平设定否则训练指标再好看也只是镜花水月。