☰
agno 拒绝采样(Rejection Sampling)数据标注工作流:验证器门控、Best-of-N 评判与逐步过程奖励的实战实现
2026/10/1 1:44:49 网站建设 项目流程

agno 拒绝采样(Rejection Sampling)数据标注工作流:验证器门控、Best-of-N 评判与逐步过程奖励的实战实现

【免费下载链接】agnoBuild, run, and manage agent platforms.项目地址: https://gitcode.com/GitHub_Trending/ag/agno

拒绝采样(Rejection Sampling)是构建冷启动推理训练数据的核心数据形态:对每个 prompt 采样 K 个候选输出,用程序化验证器或 LLM 评判器逐个把关,只保留幸存样本。本文以 agno 仓库cookbook/data_labeling/_21_rejection_sampling/下的四个可运行脚本为骨架,系统讲解在 agno Agent 框架下如何生成已验证推理轨迹、对无程序化答案的开放任务做 Best-of-N 门控、用通过率筛选 RL 训练提示,以及用蒙特卡洛回滚把结果监督蒸馏为逐步过程奖励;并结合测试日志给出的真实运行数据与校准结论,帮助读者理解这些门控在实战中的行为边界。

目录

  • 拒绝采样在数据标注流水线中的定位
  • 运行环境与依赖
  • basic.py:纯代码验证的推理轨迹(verified reasoning traces)
  • judge_gate.py:无程序化验证器时的 Best-of-N 门控
  • rl_prompt_selection.py:用通过率挑选值得训练的 RL 提示
  • step_rewards.py:蒙特卡洛逐步过程奖励
  • 测试日志中的校准结论
  • 何时使用与生态衔接

拒绝采样在数据标注流水线中的定位

在 agno 的cookbook/data_labeling/目录下,_20至_25是一组"合成数据生成"型工作流,它们不像前面的标注/分类/抽取示例那样给已有输入打标签,而是直接产出训练数据(JSONL,带逐行 provenance,过滤时会打印 kept/dropped 计数)。_21_rejection_sampling/正是其中"采样 → 校验 → 保留"这一数据类型的代表。

它的核心思想一句话概括:过滤本身就是产品。通过门控的样本成为训练数据(已验证推理轨迹、Best-of-N 精选),而逐 prompt 的通过率本身又告诉你哪些 prompt 值得投入训练算力。这与_17_llm_as_judge/有本质区别:在那里,评判器产出一份评分报告由人来阅读;在这里,验证器或评判器的判定逐行决定哪些行进入数据集——它直接门控生成循环。

四种门控形态的差异(摘自 README.md):

脚本门控输出行内携带的 provenance
basic.py纯代码验证(最终答案与金标整数相等)无分数,provenance 就是被验证过的最终答案本身
judge_gate.py评判器绝对分数线(argmax 且 score >= 4)所有 N 个分数 + 评判理由
rl_prompt_selection.py通过率区间(0 < pass@K < 1)该 prompt 的 pass_rate
step_rewards.py不丢行,把结果验证内化进轨迹逐步分数 step_scores + k

运行环境与依赖

四个脚本都基于 agno 的Agent与 Pydanticoutput_schema,模型统一为google:gemini-3.5-flash(一个推理模型),因此需要设置环境变量GOOGLE_API_KEY。按 data_labeling/README.md 的说明,可以从仓库根目录创建并激活 demo 虚拟环境:

./scripts/demo_setup.sh source .venvs/demo/bin/activate

然后逐个运行(路径均为仓库根目录相对路径):

python cookbook/data_labeling/_21_rejection_sampling/basic.py python cookbook/data_labeling/_21_rejection_sampling/judge_gate.py python cookbook/data_labeling/_21_rejection_sampling/rl_prompt_selection.py python cookbook/data_labeling/_21_rejection_sampling/step_rewards.py

其中rl_prompt_selection.py最慢——测试日志记录约需 15 分钟,耗时主要被困难题目的推理 token 占据。所有脚本把结果写入各自的data/generated/目录,输出文件分别为verified_traces.jsonl、judge_gated.jsonl、rl_prompts.jsonl、prm_rows.jsonl。运行日志遵循 cookbook 约定记录在 TEST_LOG.md。

basic.py:纯代码验证的推理轨迹(verified reasoning traces)

basic.py实现最基础的拒绝采样配方:sample(采样)→ check(校验)→ keep(保留)。一个 teacher agent 对每个数学/代码问题采样 K=4 条推理轨迹,纯代码验证器(整数与金标相等)只保留最终答案验证通过的轨迹,全程没有任何 judge 参与 keep 路径。

关键实现

Teacher agent 的关键配置(见 basic.py):

teacher = Agent( model="google:gemini-3.5-flash", instructions=( "Solve the problem step by step. Show your full reasoning, then " "give the final integer answer." ), output_schema=Solution, )

两个设计要点:

  • 使用默认温度:这样 K 次采样才有差异;若温度过低,K 条轨迹会趋同,拒绝采样退化为单次采样。
  • 不配置会话记忆:注释明确说明"no session memory is configured, so repeated .run() calls are independent samples of the same prompt"——这正是 K 次独立采样的前提。从 agno 源码看,Agent.run() 返回的 RunOutput 携带content字段,配合output_schema后run.content即为反序列化后的 Pydantic 对象(这里是Solution)。

验证器是纯代码,即最终答案与金标的精确相等:

if solution.final_answer == problem["gold"]: correct += 1 rows.append({...}) # prompt / reasoning / final_answer / sample_index else: dropped += 1

输出 schema 保证了final_answer是整数、reasoning是字符串:

class Solution(BaseModel): reasoning: str = Field(..., description="step by step reasoning") final_answer: int = Field(..., description="the final integer answer alone")

需要警惕的假阳性模式

脚本 docstring 直言不讳地指出了 answer-only 拒绝采样的已知假阳性:推理文本本身不被检查,一条推导有缺陷但最终落在正确整数上的轨迹会被保留。这正是为什么每个金标在提交前都经过手工与脚本双重校验——一个错误的金标会静默污染所有保留的轨迹。

示例数据行

basic.py产出的每一行都经过验证(最终答案与金标相等后才被保留):

{"prompt": "How many ways can you choose 3 books from 7 distinct books?", "reasoning": "To find the number of ways to choose 3 books from 7 distinct books, we use the combination formula C(n, k) = n! / (k!(n-k)!). Applying this with n = 7 and k = 3 yields C(7, 3) = (7 * 6 * 5) / (3 * 2 * 1) = 35.", "final_answer": 35, "sample_index": 1}

测试日志观测

TEST_LOG.md 记录了一次实测:6 道题(涵盖烘焙问题、组合数、Python 程序追踪、火车行程、三位数排列、字符串长度累加)中,每题的通过数为 p1 4/4、p2 4/4、p3 3/4(有一次采样把循环走错)、p4 4/4、p5 4/4、p6 4/4;最终打印pass@4: 6/6 problems with at least one correct sample (1.00)与wrote 23 rows, kept 23, dropped 1 of 24 samples。对 JSONL 重读确认了 23 行、字段为 prompt/reasoning/final_answer/sample_index,且所有 final_answer 均为整数。日志同时强调:计数每次运行都会变化,这只是当次运行的观测值。

judge_gate.py:无程序化验证器时的 Best-of-N 门控

当任务没有程序化可验证的答案时,basic.py的纯代码验证器就失效了。judge_gate.py针对这类开放任务实现 Best-of-N 门控:生成器以默认温度采样 N=3 个候选,一个 temperature=0 的评判器按评分规则对每个候选打 1–5 分,只有当 argmax 候选的分数达到绝对分数线(score >= 4)时才保留。

为什么 argmax 不够

脚本 docstring 给出了关键洞察:"argmax alone is not enough, because the best of N bad samples is still bad"——N 个坏样本里的最优者依然是坏的,所以必须叠加绝对分数线。评分规则(judge_instructions)也值得一提,它明确要求:

  • 1 分:不可用(错误、跑题、无视 prompt);
  • 2 分:差(部分回答或破坏明确约束);
  • 3 分:可接受(正确但平淡、笼统、略不精确);
  • 4 分:好(正确、清晰、遵守所有约束);
  • 5 分:优秀(正确、精确、措辞良好、遵守所有约束)。

评判器被要求显式检查约束:给定字数/句数限制时逐词数数,扫描违禁词或字母;对禁字母约束要逐词点名违规词;违反任何显式约束的候选最高只给 2 分。

关键实现

generator = Agent( model="google:gemini-3.5-flash", instructions=("Write a short, high-quality response. Follow every constraint in the prompt exactly."), output_schema=Draft, ) judge = Agent( model=Gemini(id="gemini-3.5-flash", temperature=0), instructions=judge_instructions, output_schema=Verdict, )

注意judge显式以temperature=0构造,保证门控尽可能稳定;而生成器用默认温度保证 N 个候选有差异。候选与评判输入拼接由build_judge_input完成,判定用确定性 argmax——平分时取最早候选:

all_scores = [v.score for v in verdicts] best = max(range(N), key=lambda j: all_scores[j]) if all_scores[best] >= SCORE_BAR: # 保留:chosen / chosen_score / all_scores / judge_reason else: dropped += 1

示例数据行

评判器门控产出的行携带全部 N 个分数作为 provenance:

{"prompt": "Write a coherent paragraph of 30 to 40 words about winter mornings that does not contain the letter 'e' anywhere.", "chosen": "Cold air grips a frosty world. A soft light slips through our window. Frost clings to glass. Our sun glows with gold, warming a cold, still city. Fog drifts, but this bright dawn brings joy.", "chosen_score": 5, "all_scores": [5, 5, 5], "judge_reason": "The response is a coherent paragraph of exactly 35 words about winter mornings that completely avoids the letter 'e'."}

测试日志观测

测试针对 4 个开放式 prompt(其中两个是刻意对抗性的:一段 30–40 词且不含字母 'e' 的段落;一个语法正确的 10 词全 'x' 开头句子),N=3。结果四个 prompt 全部打出[5, 5, 5]并被保留,打印wrote 4 rows, kept 4 of 4 prompts, dropped 0——drop 路径当次未触发。日志同时做了代码侧核验,证明评判器并非宽松:lipogram 候选确实是 35 词且零个 'e',全 'x' 候选是 10 个真实词典单词("Xylophagous, xenophobic, xanthic xenophobes xeroxed xeric, xylographic, xenolithic xylographs xenophobically.")。这一"同强度生成器与评判器在短约束 prompt 上饱和 1–5 刻度"的现象被记录为校准说明(详见后文"测试日志中的校准结论")。

rl_prompt_selection.py:用通过率挑选值得训练的 RL 提示

rl_prompt_selection.py换了一个视角:不直接产出样本,而是用通过率当 prompt 过滤器。对每个问题采样 K=4 条轨迹并对照验证过的金标计算通过率,只有0 < correct < K(学习区间)的 prompt 被保留为 RL 训练提示。

背后的直觉是:模型 4/4 全对的问题教不了新东西(无梯度),0/4 全错的问题给不出奖励信号(也学不动),只有处于中间地带的 prompt 才值得投入 RL 或课程学习算力。

关键实现

问题集刻意横跨"设计为平凡"到"设计为不可能"的难度(见 rl_prompt_selection.py):

iddesignedpromptgold
r1trivial7 + 512
r2trivial12 * 11132
r3easy24 支铅笔 × 17 盒减 35058
r4medium1..500 中数位和能被 7 整除的整数个数68
r5mediumx0=7, x_{n+1}=(x_n²+1) mod 1013 的 x_60718
r6hard第 613 个质数4517
r7hard17 位 × 17 位精确乘法6747742486863689476416508396372901
r8impossible第 12345 个质数132241

teacher 的 instructions 相比basic.py多了"不要用任何工具,手工计算"("Do not use any tools; compute by hand"),并把designed难度记录在问题元数据里,打印时与观测通过率并排展示——这正是"测量而非猜测"的意义所在。

筛选逻辑:

if correct == K: dropped_easy += 1 elif correct == 0: dropped_hard += 1 else: rows.append({"prompt": ..., "gold": ..., "pass_rate": pass_rate})

测试日志观测

一次实测的通过率:r1–r7 全部 4/4,r8(12345 个质数)2/4。最终打印kept 1 of 8 prompts (learning zone 0 < pass@4 < 1)与wrote 1 rows, dropped 7 always-solved, dropped 0 never-solved,保留行是{"prompt": "What is the 12345th prime number?", "gold": 132241, "pass_rate": 0.5}。

日志记录了两个重要现象:

  1. 设计难度与观测难度严重背离:每个"设计为困难"的问题都被 4/4 解出(模型能在推理中完成精确的 17×17 位乘法);反而是"设计为不可能"的第 12345 个质数落在了学习区间——模型无法心算筛法,但它会估计,约一半概率(2/4)恰好命中。
  2. K=4 时带内成员判定噪声大:在更早一轮的不同问题集上,第 613 个质数只得了 2/4;校准探针测得 60 步迭代映射 3/4、第 12345 个质数 1/4。这说明单次 K=4 观测不可靠,应视为演示上限而非测量手段。

step_rewards.py:蒙特卡洛逐步过程奖励

step_rewards.py把basic.py的结果验证下推到轨迹内部,实现 Math-Shepherd 风格的蒙特卡洛逐步评分。solver 为每个问题写一份逐步解(instructions 限制最多 5 步、每步一句话、执行一个操作);对每个步骤前缀,用默认温度的 completer 跑 K=3 次延续回滚,该步的分数 = 回滚中最终答案通过basic.py纯代码验证器(与金标整数相等)的比例。

与basic.py的对比一目了然:basic.py付的是结果奖励(整条轨迹按最终答案保留或丢弃);step_rewards.py付的是过程奖励——没有 judge 给步骤打分(对比_17_llm_as_judge/),也没有逐步骤的人类标注。DeepSeek-R1 跳过神经 PRM 的原因(步骤正确性难定义、步骤标注不可扩展、训练出的奖励模型会招致奖励黑客)在此被绕过:MC 逐步评分仍能给出逐步信用分配,代价是每步 K 次回滚,且在验证器可信的范围内天然免疫奖励黑客。

关键实现

问题与金标直接from basic import PROBLEMS导入(复用而非复制,是避免金标漂移的工程细节)。延续回滚 agent 的指令是承载正确性的关键:

rollout = Agent( model="google:gemini-3.5-flash", instructions=( "You are given a problem and the first steps of a solution. " "Continue from those steps and finish the solution, then give the " "final integer answer. Treat the given steps as fixed: build on " "them exactly as written, even if you believe one contains an " "error. Do not audit, correct, or restart them." ), output_schema=Continuation, )

MC 估计的目标是P(gold | prefix continued as written)——按前缀原样继续到达金标的概率。一个会审计并修复前缀的 completer 测到的是"可恢复性"而不是"步骤正确性",错误步骤就不再得到低分(这正是测试日志记录的校准发现,详见下节)。

脚本还内置了一个"设计为失败"元素:p1 的第 2 步被手工替换为损坏拼接(72 - 15被错算成 67),使分数悬崖可见。损坏前缀的正确延续会落在67 * 5 = 335而非金标 285。

逐步评分与悬崖检测:

for prefix_len in range(1, len(steps) + 1): prefix = steps[:prefix_len] passed = 0 for _ in range(K): continuation = rollout.run(build_rollout_input(problem["prompt"], prefix)).content if continuation.final_answer == problem["gold"]: passed += 1 step_scores.append(passed / K)

first_sharp_drop以 0.5 为阈值,把 step 1 前的基线视为 1.0(对模型能解的问题,如果第 1 步就封顶了求解率,那第 1 步本身就是断点):

prev = 1.0 for i, score in enumerate(scores): if prev - score >= SHARP_DROP: return i prev = score return None

示例数据行

注意第 2 步是故意损坏的拼接,其分数悬崖到 0.0,第 3 步重新推导出正确的日销量后恢复:

{"problem": "A bakery makes 12 trays of muffins per day, with 6 muffins per tray. Each day 15 muffins are set aside for staff and the rest are sold. How many muffins are sold across 5 days?", "steps": ["Multiply 12 trays of muffins by 6 muffins per tray to find the daily total of 72 muffins.", "Each day the bakery bakes 12 * 6 = 72 muffins; setting aside 15 for staff leaves 72 - 15 = 67 muffins sold per day.", "Multiply 57 muffins sold per day by 5 days to find the total of 285 muffins sold."], "step_scores": [1.0, 0.0, 1.0], "k": 3}

测试日志观测

对前 3 道题(复用basic.py的手工验证金标集)的实测:p1 的分数为[1.00, 0.00, 1.00],打印精确标记了first sharp drop at step 2 (1.00 -> 0.00) - reasoning breaks here,恰好落在损坏步骤上,第 3 步完全恢复(重新推出57 * 5 = 285);p2、p3 均为全程 1.00 并打印no sharp drop。最终输出wrote 3 rows, scored 12 steps, ran 36 rollouts;JSONL 重读确认 3 行、每行恰有 problem/steps/step_scores/k 四个键、每行 len(steps) == len(step_scores)、k == 3。

测试日志中的校准结论

TEST_LOG.md 与 README.md 一致地记录了三条针对gemini-3.5-flash(推理模型)的实测校准结论,在信任这些门控之前值得先了解:

  1. 生成器与评判器同强度时,judge 门控会饱和。实测中每个 prompt 都得[5, 5, 5]、没有任何样本被丢——而代码侧核验确认候选确实满足约束(包括 35 词无 'e' 段落和语法正确的 10 词全 'x' 句子)。分数线开始起作用的前提是:生成器弱于评判器、输出变长、或评分规则变严。如果需要更尖锐的区分度,应改用成对比较(见 cookbook/data_labeling/_05_text_pairwise_preference/)而非绝对分数。
  2. 难度直觉在推理模型面前不成立。设计为困难的问题(60 步迭代映射、第 613 个质数、17 位精确乘法)全部 4/4 通过;唯一落进学习区间的竟是设计为不可能的第 12345 个质数——模型不能心算筛法但会估计,4 次中恰好对 2 次。且 K=4 时带内判定噪声大:第 613 个质数一轮 2/4、下一轮 4/4。结论是:测量通过率而不是猜测它,并把 K=4 当作演示上限而非测量手段。
  3. MC 逐步分数只和 completer 的忠实度一样诚实。用宽松的延续指令("build on the given steps; do not restart from scratch")时,损坏步骤得了 0.67——回滚中途发现算术错误并当场修复,分数测的是可恢复性而非步骤正确性。换用随附的固定指令("treat the given steps as fixed ... even if you believe one contains an error")后,同一损坏步骤得 0.0 且下一步干净恢复为 1.0。另外 K=3 时分数粒度粗,可观测值只有 0、1/3、2/3、1。

何时使用与生态衔接

四个脚本分别对应四类典型需求(README.md "When to use"):

  • 从 teacher 模型蒸馏已验证推理轨迹为 SFT 数据:答案程序化可查时用basic.py,不可查时用judge_gate.py;
  • 挑选值得投入 RL 算力的 prompt:用rl_prompt_selection.py;
  • 定位推理轨迹断点、或为过程监督产出逐步奖励标签:用step_rewards.py。若想让 judge 直接给步骤打分而非回滚,参考 cookbook/data_labeling/_17_llm_as_judge/;
  • 只给已有模型输出打分、不门控数据集:同样参考 cookbook/data_labeling/_17_llm_as_judge/;
  • 对幸存的样本去重、过滤、打包:衔接 cookbook/data_labeling/_22_dataset_curation/。

在更大的合成数据流水线里,_21承接 cookbook/data_labeling/_20_instruction_generation/ 生成的问题与金标思路,再交给_22做质量门控与去重。对于需要把这类采样放大到十万级并断点续跑的场景,cookbook/data_labeling/_26_scale_out/ 提供了异步扇出与按行恢复的机制。理解_21的四个门控,就掌握了整个"采样—校验—保留"流水线的核心判据。

【免费下载链接】agnoBuild, run, and manage agent platforms.项目地址: https://gitcode.com/GitHub_Trending/ag/agno

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询