GRPO 训练基金助手 SubAgent 实战笔记
原文:GRPO 训练基金助手 SubAgent 全流程 | 续篇:多步场景优化
这篇文章是基于知乎原文的深度扩写。我以第一视角重新梳理了整个实验过程,补充了大量原文没展开的细节、数学解释和工程直觉。续篇记录了模型上线后发现多步场景从 97% 跌到 63% 的真实情况,以及我们怎么把它打回来。
这篇文章说了什么
我用 GRPO 把一个 7B 小模型训成了基金助手 SubAgent,替代了原来的 Mixtral 8X22B。7B 模型参数量只有 8X22B 的 1/30——目标是在这个受限模型上,把工具选择准确率做到大模型的水准。
这篇文章分两个回合。第一回合:20+ 个版本迭代,把单步场景训到了 96% 平均分、97% 工具准确率。踩坑记录包括过采样反而退化、各种"高级"论文方法全翻车、最后发现训练 label 本身就是错的。第二回合:模型上线后发现多步场景准确率只有 63%,于是升级奖励函数到 Ground Truth 比对、强化多步格式、展开训练数据,最终多步场景也打到 97%。
核心结论:问题定义 > 数据质量 > 算法选择。这句话我会在文章里反复说,因为你读完之后会发现,每一步踩的坑,根因都可以追溯回这句话。
两个回合下来,有效的实验就这几招
踩了 30+ 个版本的坑,真正有效的方法一只手数得过来。我把它们列在这里,做个索引——后面所有章节都是在展开这些具体做法和背后的数据。
第一回合(单步场景 63%→97%)
| 方法 | 做了什么 | 效果 |
|---|---|---|
| NGRPO | 每组真实样本后追加虚拟满分(reward=1.0),强制制造 advantage 方差 | 平均分 63%→76%,frac_reward_zero_std 50%→0% |
| ReAct 格式 | 输出改为 thought + action + plan 三层结构,修正了错的训练 label | Combo 12%→91%,是单步骤提升最大的改动 |
| 奖励上限拉到 1.0 | 奖励函数各维度加权后总分上限从 0.8 拉到 1.0 | Tool 准确率 +6% |
| 针对性惩罚 | 模型在非市场场景滥用 check_market_status → 扣 0.3 | Tool 准确率 +3%(掐断了 35% 的错误来源) |
| DART 梯度分离 | reasoning token 和 tool token 分开算 loss,0.3:0.7 加权 | Combo 稳定在 78%,但对数据量要求高 |
第二回合(多步场景 63%→97%)
| 方法 | 做了什么 | 效果 |
|---|---|---|
| Ground Truth 奖励比对 | 奖励函数从规则匹配升级为直接和正确答案比对,Tool correctness 维度占 0.5 | 多步 63%→97% |
| 多步格式强化 | max_completion_length 200→350 token,num_generations 4→8 | 给模型足够的推理空间和探索多样性 |
| 逻辑关键词奖励 | thought 含 first/then/because 等关键词 → +0.05 | 鼓励显式推理链,减少 thought-action 脱节 |
| Plan 强制非空 | 多步场景 plan 为空不给 reasoning 分 | 强制模型做规划 |
| 多步数据展开 | 每个多步场景拆成多条样本(454 单步 + 823 多步) | 模型学会基于上一步执行结果做决策 |
| NGRPO 持续用 | zero-std 从 67% 降到 63.5%,仍是绝对多数(63.5% 步数无梯度) | 基础设施——没有它 63.5% 的训练步数全白费 |
试了但没用(甚至倒退)的方法:过采样、领域数据扩充、PRS、GRPO-LEAD、SFT 注入(300~1500 条)。
可以看到一个规律:有用的方法全是对症下药——NGRPO 治零梯度,ReAct 治 label 错误,GT 奖励治多步区分度,针对性惩罚治滥用。 而失败的方法全是"我觉得这个方法好"后堆上去的通用算法。
一、先搞懂这些概念(不然后面会懵)
这篇文章涉及的概念比较多,我先帮你铺个底。
1.1 GRPO 是什么
GRPO(Group Relative Policy Optimization)是一种训练大模型的强化学习方法。你不用记全称,你只需要记住它的核心逻辑:
GRPO 的核心逻辑是"跟别人比"。 每次训练,对同一个 prompt(比如"查我的余额"),模型会生成 8 个不同的输出。我的奖励函数给每个输出打分,然后计算组内相对排名。
举个例子,假设某次训练 8 个输出的得分是:
[0.5, 0.3, 0.8, 0.6, 0.4, 0.7, 0.2, 0.9]
- 均值 = 0.55,标准差 = 0.24
- 0.9 分的那个输出,
advantage = (0.9 - 0.55) / 0.24 = +1.46,梯度往这个方向推 - 0.2 分的那个输出,
advantage = (0.2 - 0.55) / 0.24 = -1.46,梯度远离这个方向
GRPO 不关心 0.9 够不够好,它只关心 0.9 比组内其他人好多少。
那具体怎么打分?我的奖励函数长这样(最终版本):
| 检查项 | 得分 | 例子 |
|---|---|---|
输出包含 thought 字段 | +0.2 | "thought": "用户要求转账..." |
输出包含 action 且 tool 合法 | +0.3 | "action": {"tool": "transfer_funds"} |
tool 是 6 个合法 API 之一 | +0.2 | transfer_funds 合法,transfer_money 不合法 |
params 参数正确 | +0.1 | {"amount": 1000000} |
输出包含 plan 字段 | +0.2 | "plan": ["transfer", "withdraw"] |
滥用 check_market_status | -0.3 | 用户问题没提"市场",模型却调了这个 |
这就是 GRPO 的工作原理:不依赖人定的绝对分数有多精确,只依赖组内的相对差异。
1.2 Advantage 和梯度是什么
Advantage(优势值):某个输出的 advantage 越大,模型越应该往这个方向学。
梯度(Gradient):模型参数应该往哪个方向改、改多少。advantage 算出方向,梯度算出力度。
举个实际的危险情况:训练中后期,模型已经很稳了,8 个输出的得分全是 0.8。
[0.8, 0.8, 0.8, 0.8, 0.8, 0.8, 0.8, 0.8]
均值 = 0.8,标准差 = 0
advantage = (0.8 - 0.8) / 0 = NaN(除零)
梯度 = 0
这就是 GRPO 训练里最常见也最隐蔽的问题——所有样本得分趋同,advantage 全部为零,模型不更新。我的实验里 frac_reward_zero_std 一度飙到近 50%,意味着每两步就有一步在浪费算力。
1.3 SFT 和 RL 的区别
SFT(Supervised Fine-Tuning,监督微调):给模型"标准答案",让它照着学。比如给 300 条"输入「转账 10 万再提现 5 万」→ 输出「transfer_funds」",模型就学着在遇到类似输入时输出这个答案。
RL(Reinforcement Learning,强化学习):不给标准答案,只给规则(奖励函数),让模型自己试探出什么输出得分高。
SFT 的命门在 label 质量——如果 label 就是错的,学到的东西也错。RL 的命门在奖励函数——如果设计得不好,模型会钻空子。
我在实验里亲眼见证了这两种方法的碰撞:
| 方法 | 实际情况 | 结果 |
|---|---|---|
| SFT 注入 300 条领域知识 | 考 reasoning 变好 | Tool 准确率反而下降(SFT 的梯度跟工具选择的梯度方向冲突) |
| RL + GRPO | 反复迭代 20+ 个版本 | 最终 97% 准确率 |
我的结论:先用 RL 把模型的基本能力训出来,再考虑要不要加 SFT。
1.4 ReAct Agent 是什么
ReAct = Reasoning + Acting,就是先思考,再行动。
普通 Agent 直接输出一个工具调用——没有规划,没有推理。我的实验里,模型在 combo 场景(如"转账 10 万再提现 5 万")只得了 12% 的准确率。
ReAct Agent 的输出分三部分:
{
"thought": "用户要求先转账再提现,这是两步操作。当前执行第一步:转账。",
"action": {"tool": "transfer_funds", "params": {"amount": 1000000}},
"plan": ["transfer 10 lakh", "withdraw 5 lakh"]
}
thought:在行动前先分析用户意图action:当前这一轮要执行的工具plan:完整的多步路线图,后续步骤下一轮执行
换了这个格式之后,combo 从 12% 直接跳到了 91%。为什么?两个原因:① 原来的训练 label 是错的——"转账 10 万再提现 5 万"被标成了 check_balance,ReAct 格式让 label 自然修正了;② thought 字段给了模型一个"思考缓存",推理不干扰工具选择。
二、任务定义:要训一个什么东西
2.1 为什么是基金助手
我的目标是用 7B 小模型(Qwen2.5-7B)替代已有的 Mixtral 8X22B 来做 SubAgent。
为什么要替代?
- Mixtral 8X22B 太大了,推理延迟高、成本高
- 7B 模型推理快、便宜、能在端侧跑
- 但是 7B 能不能在大模型的任务上达到同样的准确率,这是未知数
2.2 6 个工具和 4 种场景
模型需要能选对的 6 个 API:
| API | 作用 |
|---|---|
check_balance | 查余额 |
transfer_funds | 转账 |
withdraw_funds | 提现 |
place_order | 下单 |
get_transaction_history | 查交易记录 |
check_market_status | 查市场状态 |
我把用户问题分成 4 个难度级别。基线训练后的结果完美验证了这个分层——简单的很高,难的极低:
- 简单场景(关键词匹配):"查一下我的余额" →
check_balance。基线准确率 89% - 领域知识场景:"我今天早上卖了 AMZN 股票,钱什么时候到账?" → 模型需要知道美股 T+1 结算(今天交易,明天到账),才知道该查
get_transaction_history而不是check_balance。基线准确率 18%——几乎等于不会 - 盘中规则场景:"现在是 2:30,还能下单吗?" → 需要理解交易时间规则。基线准确率 23%
- 组合场景(Combo):"转账 10 万再提现 5 万" → 不止要选对工具,还要规划"先干啥、再干啥"。基线准确率 12%——跟瞎猜差不多
场景从关键词匹配到领域推理再到多步规划,难度逐级递增,准确率从 89% 递减到 12%。这个坡度告诉你:模型缺的不是语言理解,是领域知识和多步规划能力。后面所有实验都在试图补这两个缺口。
三、训练全流程:20+ 个版本的踩坑记录
我按实验时间线来写——"每次踩坑 → 每次尝试 → 再踩坑"的完整记录,不是最终方案。
第一阶段:基线实验 — 第一次撞墙
3.1.1 标准 GRPO 基线
标准 GRPO 跑了一版基线:平均分 63%,Tool 准确率 67%。6 个分类的场景结果前面已经列过了,直接看数据就明白问题在哪——简单场景 82-89%,弱场景 12-31%。差距最大的一对:balance 89% vs combo 12%,差了 77 个百分点。
另外注意一个关键监控指标:frac_reward_zero_std 已经在 50% 左右了。说明模型刚起步就开始"享乐"——相当一部分训练步数已经没在学习。
3.1.2 过采样翻车 — 第一个重要教训
看到这个结果,我的第一反应跟大多数人一样:"弱场景数据不够,多喂点就行了。"
于是对弱类别做了 3 倍过采样——同一批数据重复训练 3 遍。
结果:全面退化,比基线更差了。
为什么?我查日志发现:近 50% 的训练步数梯度为零。
过采样怕的是"数据没有差异"。你把同一个样本放进 batch 里 3 次,GRPO 生成 8 个输出,因为输入是一样的、模型在同一阶段,生成的 8 个答案得分很可能一模一样。8 个一样的得分 → advantage 全是 0 → 梯度全是 0 → 白训。
更糟糕的是:本来那些简单场景(如 balance)能学到东西,现在 batch 里塞了大量无梯度的重复样本,把有效学习信号稀释了。
教训 1:GRPO 里数据量不重要,数据能产生的 reward 方差才重要。重复数据 = 零方差 = 零学习。
3.1.3 领域数据扩充也无效
我又准备了 100+ 条新样本,专门覆盖弱场景。比如这条是补 settlement 场景的:
输入:"我上周五卖出的基金,钱什么时候能到?"
期望:get_transaction_history(T+1 结算,周五卖 → 周一才到账)
但训练结果显示完全没用。frac_reward_zero_std 飙升到几乎 100%——每一步的 reward 方差都是零,模型没有从任何一条新数据中学到东西。
原因:新样本的格式、结构跟老数据完全一样,模型直接套用已有模式就答对了。没有一个"让它犹豫"的样本。GRPO 的瓶颈在于奖励方差,不在于知识覆盖广度。模型对所有样本给出了一致性高的答案时,就停止成长了。
第二阶段:NGRPO 突破 — 终于找到路了
3.2.1 核心问题到底是什么
把所有失败串起来看,核心问题就一个:
GRPO 在模型收敛后,所有样本得分趋同 → advantage 为零 → 学习停止。
这个问题在强化学习里叫"reward collapse"(奖励坍缩)——梯度消失让模型停在原地。
传统解法是什么?让任务更难(课程学习)、挑出难样本加权重(hard example mining)、换更难的 reward 函数。但我试了,全失败了。
3.2.2 NGRPO 的做法:不改任务,改参照系
这时候我读到了 NGRPO 论文,思路非常反直觉,但又极其优雅:
你不需要让任务变难,你只需要让参照系变了就行。
具体做法:在每一组 8 个真实样本后面,额外追加一个 reward = 1.0 的虚拟满分样本。
拿我的实验数据算一遍:
# 原本:8 个真实样本得分完全一致
rewards = [0.8, 0.8, 0.8, 0.8, 0.8, 0.8, 0.8, 0.8]
mean = 0.8, std = 0.0
→ advantage 全是 NaN,梯度全是 0
# NGRPO:追加一个虚拟满分
augmented = [0.8, 0.8, 0.8, 0.8, 0.8, 0.8, 0.8, 0.8, 1.0]
mean = 0.822, std = 0.063
advantage(0.8) = (0.8 - 0.822) / 0.063 = -0.35 ← 非零!
真实样本的得分 0.8 完全没变,但加了满分样本之后均值和标准差都变了。0.8 从"在组里跟所有人一样好"变成了"比满分差了一截"——model 收到负 advantage,知道自己还有进步空间。不需要让任务更难,只需要让参照系变了就行。
3.2.3 NGRPO 的效果
| 指标 | 基线 | NGRPO |
|---|---|---|
| 平均分 | 63% | 76% (+13) |
| Tool 准确率 | 67% | 81% (+14) |
| intraday | 23% | 58% (+35) |
frac_reward_zero_std | ~50% | 0% |
frac_reward_zero_std 从近 50% 降到 0%——梯度回来了,模型可以继续学了。
3.2.4 为什么 NGRPO 有效:数学层面的解释
我后来反复琢磨,发现 NGRPO 本质是在批归一化(Batch Normalization)的思路上做了一个变种。
GRPO 的 advantage 计算依赖组内标准化:
advantage = (r - μ) / σ
当这组样本已经"够好"时,所有 r 都接近 1.0,σ ≈ 0,标准化就变成了除以一个接近零的数——数学上这个操作是不稳定的,算出来的 advantage 可能是 NaN 或无穷大。NGRPO 加满分样本相当于注入了人工方差,保证了 σ > 0,让标准化操作在数学上始终是良定义的。
和数据增强(Data Augmentation)的思路完全一致——加的是"扰动量",让模型不要因为数据太干净而停止学习。
第三阶段:各种"高级"方法为什么会翻车
拿到 NGRPO 这个不错的 baseline 后(76% 平均分),我跟很多研究员一样,开始往上面叠各种论文里的"高级"方法。结果每一种都翻车。
3.3.1 PRS(渐进式奖励塑形)— 跌 5 点
PRS 的思路:不同训练阶段给不同维度的权重——前 50% 步数重点奖励 thought(+0.5),后 50% 步数重点奖励 action(+0.5)。理论上"先扶上马再快跑"。
结果:平均分从 76% 跌到 71%,跌了 5 点。
原因:NGRPO 阶段模型已经能稳定输出正确的格式了,再去分阶段调制奖励权重,等于给已经会跑的人重新教走路——只干扰了他已有的策略。PRS 适合模型输出格式混乱的场景(JSON 语法错误、缺少字段),我的数据格式已经很规范,就成了过度优化。
3.3.2 GRPO-LEAD(难度感知重加权)— combo 跌近 20 点
GRPO-LEAD 的思路:每个样本按其历史错误率加权,错误率高的样本权重放大(比如 combo 本来权重 1.0,如果经常错就放大到 3.0),简单的样本权重缩小(balance 降到 0.3)。
结果:combo 从 78% 跌到 60% 左右,跌了近 20 点。
为什么?因为我数据集里 95% 的样本正确率都很高(>80%),根本区分不出"难样本"和"简单样本"。LEAD 算法把小噪声当成了信号来放大——combo 里一道本来做对的题,因为被误判为"难"而给了高权重,模型额外收到错误的惩罚信号,把做对的也学歪了。
3.3.3 SFT 领域知识注入 — 推理变好但工具选择变差
这是最诡异的一个失败。既然领域知识(T+1 结算、交易时间规则)模型学不会,我用 SFT 直接"灌"进去。
先试了 300 条 SFT 数据,格式是标准的"输入 → 期望输出"对:
输入:"今天早上卖了 AMZN,钱什么时候到?"
期望输出:{"tool": "get_transaction_history", "reasoning": "美股 T+1 结算,今天卖明天到账"}
结果:reasoning 评分提升了约 10%,但 tool 准确率从 81% 掉到了 75% 左右。
教了模型更多知识,工具选择反而变差了。又加到 1500 条 SFT,结果一样——还是没有超越纯 NGRPO 的 81%。这个现象太反常识了,为第四阶段的 DART 论文埋下了伏笔。
第四阶段:DART — Reasoning 和 Tool-use 是两种能力
3.4.1 DART 论文说了什么
SFT 失败后,我读到了 DART 论文(arXiv:2602.00994)。这篇论文的核心发现一句话:
Reasoning(推理)和 Tool-use(工具使用)的梯度方向几乎是正交的。
"正交"可以理解为两个方向互相垂直——往东的力和往北的力互不影响。在 ML 里,"梯度正交"意味着优化 reasoning 方向的参数更新,和优化 tool-use 方向的参数更新,走的是几乎不同的路径。
现在能解释 3.3.3 了:300 条 SFT 数据里每一条都有详细的 reasoning("美股 T+1 结算,今天卖明天到账"),SFT 强化了这条路径,但却把 tool-use 的优化方向推偏了。所以 reasoning +10%,tool -6%。
3.4.2 我怎么用 DART
我的实现是简化版 Token-Level 梯度分离:
loss = 0.3 * reasoning_loss + 0.7 * tool_loss
具体实现:把模型输出 token 按字段分两类——"thought": "..." 里的 token 算 reasoning_loss,"action": {"tool": ...} 里的 token 算 tool_loss。加权求和时 tool 给 0.7,因为"工具选择准确率"才是主任务。
3.4.3 DART 的效果和局限
结果:整体平均分 73%(比 NGRPO 的 76% 低 3 点),但 Combo 从 78% 提升到了 78%(持平,但在复杂场景更稳定)。
DART 的局限很明显——需要大量 token 级标注来区分 reasoning token 和 tool token,我的数据量不够,训练不充分。DART 是好方法,但它对数据量和标注精度的要求比 NGRPO 高得多。
第五阶段:ReAct Agent — 最大的突破来自重新定义问题
3.5.1 停下来重新想 Combo
在用了一堆论文方法后,Combo 虽然从 12% 提到了 78%(DART 的功劳),但离可用还差得远。这时候我做了一件最简单但也最重要的事:停下来,重新看 Combo 场景的训练数据 label 到底是什么。
一看吓一跳。原训练数据长这样:
{"input": "转账10万再提现5万", "output": "{\"tool\": \"check_balance\"}"}
label 本身就是错的。 用户说"转账 10 万再提现 5 万"——两步操作。测试期望输出 transfer_funds(第一步),训练数据教的却是 check_balance。这意味着我花了 20 个版本折腾算法,从第一步就是错的——模型被要求判定为正确答案的东西,本身就是错的。
换成 ReAct 格式后,不仅 label 自然正确了,"思考+规划+执行"的三层结构也让模型在复杂场景有了明确的方向感。Combo 从 12% 飙到 91%,是整篇文章里单步骤提升最大的改动。
3.5.2 ReAct 格式怎么改
我重新设计了输出格式:
{
"thought": "用户要求先转账再提现,这是两步操作。当前执行第一步:转账。",
"action": {"tool": "transfer_funds", "params": {"amount": 1000000}},
"plan": ["transfer 10 lakh", "withdraw 5 lakh"]
}
三个字段的分工:
thought:模型在行动前先思考。这强迫模型在输出具体工具之前,先理解用户意图。text generation 和 tool selection 其实是两种能力,给 reasoning 单独一个字段,等于是把这部分计算"卸出来",不干扰 tool selectionaction:输出当前步骤要执行的具体工具。这是"这一轮"要做的事,不是整个任务plan:列出完整的后续步骤规划。这给了模型一个"路线图",让它知道下一步是谁
你可能会问:那后续步骤(withdraw_funds)什么时候执行?答案是:下一轮。这就是 ReAct 的核心——每一轮"思考 → 行动 → 观察结果 → 再思考 → 再行动",循环往复。
3.5.3 效果
| 指标 | 之前 | ReAct |
|---|---|---|
| Combo | 12%→78% | 91% |
| 平均分 | 76% | 89% |
| Tool 准确率 | 81% | 92% |
Combo 从 12% 直接干到了 91%。这是唯一一个单步骤带来 79% 提升的改动。
为什么 ReAct 这么有效?三个层面:
- 给了模型"思考空间":原来模型是一口气输出 tool,没有中间推理。ReAct 让模型在
thought里先理清思路,不会出现"绳子打结"的情况 - 解耦了规划和执行:
plan管规划,action管执行。模型不需要在执行的同时做规划,两个任务分开了 - 修正了 label:这是最重要的一点。原来的 label 就是错的,换什么算法都没用。换了输出格式之后,label 自然就对了
教训 3:问题定义 > 数据质量 > 算法选择。 这话我在开头就说了,到这里你应该感受到分量了。
第六阶段:奖励函数精调 — 最后一公里
ReAct 后效果已经很好了(92%),但还差一点。我深挖发现两个问题。
3.6.1 奖励上限太低
我重新审视了我的奖励函数:
reasoning: +0.2
action.tool: +0.3
valid_tool: +0.2
params: +0.1
# 最大分 = 0.8
所有正确的输出最高分都是 0.8。一个答对"转账+提现"的 combo 和一个答对"查余额"的简单题,得分完全一样。模型收到的信号是"这两个一样好",但实际上 combo 显然更难——考试里选择题和附加题同分,学生只会去刷选择题。
我把上限拉满到 1.0。效果:立即提升 6%。
这里有一个很重要的工程直觉:奖励函数的上限必须能达到 1.0,否则最优输出和次优输出之间没有区分度,模型会停在"够用就行"的水平。
3.6.2 针对性惩罚
分析错误模式,发现最频繁的误用:过度调用 check_market_status。每 100 条错误输出里,大约 35 条是模型在不需要市场信息的情况下(比如 settlement、combo 场景),硬要查市场状态。
这个工具只查开市/闭市,对结算时间判断没用。我加了一条精准惩罚:用户问题不含市场关键词("开市""闭市""交易时间"),模型却调用了 check_market_status → 扣 0.3 分。
改动非常小(一行 if 判断),但效果精确——directly 掐断了占 35% 的错误来源。Tool 准确率再提升 3%。
3.6.3 最终效果
| 指标 | NGRPO | ReAct | 最终 |
|---|---|---|---|
| 平均分 | 76% | 89% | 96% |
| Tool 准确率 | 81% | 92% | 97% |
| Combo | 78% | 91% | 95% |
| Settlement(最低) | - | - | 79% |
最低的 settlement 也到 79% 了,对于一个 7B 小模型来说,这个结果相当可以了。
四、Insight 提炼:读完该记住什么
4.1 Insight 1:问题定义是第一性原理
这个故事最有冲击力的对比:修正 label 一个动作加了 79 个点(combo 12%→91%),而 NGRPO 只加了 13 点、DART 的梯度正交也只提供了概念解释。
Combo 从 12% → 91%,换个算法最多几十个点,修正 label 一个动作加了 79 个点。
如果我当时没有停下来重新审视问题定义,而是在算法层面死磕——换 loss 函数、调超参数、加更多数据——最大概率的结果是 combo 始终提不上去,最终结论可能是"7B 模型搞不定 combo"。但真实原因是我在教它做一件错的事——训练数据要模型把"转账+提现"判成 check_balance,模型照做了,我反而怪它没学会。
这是一个 ML 领域的经典教训在 Agent 训练场景的复现。很多公司数据标注团队只管"标",模型团队只管"训",问题定义没人 check——label 错了,所有人都在错误目标上优化,越努力越离谱。
怎么防? 训练开始前,随机抽 50 条数据人工检查。花半天,省 20 个版本。
4.2 Insight 2:GRPO 的奖励方差是命门
传统训练关注 loss 曲线,GRPO 训练应该关注 frac_reward_zero_std。
frac_reward_zero_std= reward 标准差为零的训练步数 ÷ 总训练步数- 这个值越高,说明越多步数模型在"无梯度训练"——参数不更新,浪费算力
- 健康范围 < 30%,危险信号 > 50%
为什么 loss 不能告诉你真相?因为 GRPO 里 loss 可能很低但梯度为零——模型输出得分很高且完全一致,loss 看着漂亮但完全在"躺平"。
这个指标解释了我实验里的三件怪事:
- 过采样退化:重复 3 倍数据把
frac从 ~50% 推到更高——重复样本直接消灭方差 - 领域数据扩充无效:100+ 条新数据毫无效果,
frac≈ 100%——所有数据都太"容易"了 - NGRPO 成功:打开后
frac从 50% → 0%——梯度恢复
我的建议:如果你用 GRPO 训练,在 wandb/tensorboard 里加一张 frac_reward_zero_std 的监控图。这个图一涨,先不要调参数,先去检查数据是否有重复模式、奖励函数是否缺乏区分度。
4.3 Insight 3:NGRPO 是 GRPO 训练的安全网
NGRPO 加虚拟满分样本这件事,听起来像是一个 trick,但本质是一个 safety net:
它保证了在任何情况下,advantage 的分母都不会是零。
你不需要在所有阶段都用它:
- 训练初期,模型还在探索,自然会有方差,NGRPO 不是必需的
- 训练中后期,模型开始收敛,方差逐渐消失,NGRPO 的价值就体现出来了
- 你可以设置一个开关:当
frac_reward_zero_std> 30% 时自动启用 NGRPO
这种"动态 NGRPO"比从头到尾加虚拟样本更高效——前期不需要,加它浪费计算;后期需要,不加它模型卡住。
4.4 Insight 4:Reasoning 和 Tool-use 是两种能力
DART 论文揭示了一个被很多人忽略的事实:LLM 的内部表征在处理"思考"和"行动"时,走的是几乎正交的特征空间。
这背后可能有更深层的原因。在预训练阶段,模型学的是纯文本生成,所有 token 一视同仁。但在 Agent 场景中,"thought": "用户想转账" 和 "tool": "transfer_funds" 虽然都是 token,但语义功能完全不同——前者是中间推理,后者是可验证的执行动作。
SFT 同时优化这两个目标时,梯度互相干扰的原因可能就在这里:模型在预训练时没有"推理"和"行动"的区分,同一个 token 流里混着两种不同性质的输出,微调时就会出现"按下葫芦浮起瓢"。我的实验里,300 条 SFT → reasoning +10%、tool -6%,就是活生生的例子。
启示:
- 训练 Agent 时,reasoning 和 tool 准确率分开监控,不能只看总分
- 如果 reasoning 提升但 tool 下降(如我的实验),直接考虑 DART 式的梯度分离
- 复杂场景(combo)比简单场景(balance)更需要梯度分离——combo 同时调用两种能力的强度更高
- 发现"SFT 注入后 tool 反而变差",这不是 bug——DART 论文从数学上证明了它的必然性
4.5 Insight 5:针对性惩罚 > 通用算法改进
回看整个实验过程,各方法的效果排名:
| 排名 | 方法 | 提升 |
|---|---|---|
| 1 | 修正 label(ReAct) | +79(combo) |
| 2 | 奖励上限 0.8→1.0 | +6 |
| 3 | NGRPO | +13 |
| 4 | 针对性惩罚 | +3 |
| 5 | DART | combo +78%但整体持平 |
这个排名不单是比数字,而是要看模式:
前两名全是"针对具体问题的精准修复"。 修正 label 解决的是"问题定义错误",奖励上限解决的是"区分度不足",针对性惩罚解决的是"过度使用某个工具"。而 3-5 名是通用算法改进。
通用算法也有用——NGRPO 解决了零梯度的"基础设施问题",它是底座。但一旦底座稳了,再往上堆通用算法就边际收益递减了。此时需要的是花时间看错误模式,针对性地做调整,而不是换一种更高级的算法。
Karpathy 说过"不要用大炮打蚊子"。当你有一个 92% 的 baseline 时,接下来要打开错误分析表,找到最大的 3 个错误模式,逐个击破——而不是换一个更酷的 loss 函数。
4.6 Insight 6:几个工程经验值
这是我从这次实验里提炼出来的,直接抄走就行:
| 指标 | 健康范围 | 危险 | 你的行动 |
|---|---|---|---|
frac_reward_zero_std | < 30% | > 50% | 开 NGRPO(如我实验:打开后从 50%→0%)或用更难样本 |
| 奖励上限 | 1.0(必须) | < 0.8 | 调整奖励权重拉满上限(如我实验:0.8→1.0 立即 +6%) |
grad_norm | 非零,有波动 | 持续为零 | 检查数据和 reward 函数——很可能是 frac_reward_zero_std 过高 |
reward_std | 有波动,> 0.05 | 稳定在零 | 同 frac_reward_zero_std |
| 输出 token 多样性 | 不同样本有差异 | 词频完全一致 | 调高 temperature(如我实验里用 0.9)或加惩罚 |
| SFT 注入量 | 先 50 条验证 | 超过 300 条 | 分别监控 reasoning 和 tool 准确率(如我实验:300 条后 tool 从 81%→75%) |
最后一个特别强调:SFT 注入不要一次来 300 条。先用 50 条跑一轮,看 reasoning 和 tool 的方向。如果 tool 降了就停——在我的实验里,300 条 SFT 让 tool 从 81% 掉到 75%,方向不对加量是自杀。
五、最终方案速查
如果你想复现这个实验,这是我最终的配置:
# 模型
base_model: Qwen/Qwen2.5-7B-Instruct
lora_r: 16
lora_alpha: 32
# GRPO 训练
num_generations: 8 # 每 prompt 生成 8 个样本
temperature: 0.9 # 较高的温度保证输出多样性
learning_rate: 5e-6
epochs: 4
# 奖励函数要点
# 1. thought 有分(鼓励思考)
# 2. action.tool 正确有分(核心目标)
# 3. params 正确有分
# 4. plan 有分(鼓励规划)
# 5. 过度使用 check_market_status 扣分
# 6. 总分上限 1.0
# 7. 加虚拟满分样本(NGRPO)
六、一个我还在琢磨的问题
最后说一个我自己的猜测,不一定对:
NGRPO 加虚拟满分样本,本质是不是在给 GRPO 做一个"无穷远处的锚点"?
GRPO 需要一个"最好"和"最差"的参照来产生相对排名。当所有真实样本都接近满分时,上下限重合,排名消失。NGRPO 等于在无穷远处放了一个"绝对满分"——不改变真实样本,但拉开参照系。
这个 trick 很聪明。但它的一个潜在代价是:虚拟满分是"不存在但有可能"的样本,会把模型往"更好但不一定存在"的方向拉。如果 reward 函数设计有偏差,这个拉力可能导致模型被拽偏。
我的直觉是:NGRPO 在收敛阶段用(frac_reward_zero_std > 30% 时开启),比从头到尾用更安全。这也呼应了 Insight 3。
七、上线后的第二回合:多步场景从 63% 到 97%
7.1 97% 是假的
模型上线跑了两周,整体指标很漂亮——tool 准确率稳定在 95%+。直到我拆开分场景统计一看,多步组合场景下准确率只有 63%。
什么叫多步组合场景?就是用户一句话里同时涉及多个操作:
"先查一下余额,如果够的话帮我转 10 万到理财账户"
"查一下我的持仓,盈利超过 10% 的都卖了"
这种场景需要模型理解"先做什么、后做什么",选对当前步骤该执行的工具。单步场景是查余额 → check_balance,一对一映射不会出错。多步场景里 check_balance 和 transfer_funds 都是正确答案——不同步骤各对应一个。
7.2 为什么 thought 写对了,tool 却选错了
拉了几十条错误 case,发现一个模式:模型的 reasoning 写的头头是道,最终选的 tool 经常是错的。
看这个真实错误:
// 用户输入
"先查一下余额,如果够的话帮我转 10 万到理财账户"
// 模型输出
{
"thought": "用户想先确认余额是否充足,然后转账。需要先查余额。",
"action": {"tool": "transfer_funds", "params": {"amount": 100000}},
"plan": ["check balance", "transfer if sufficient"]
}
thought 完全正确——"需要先查余额"。plan 也列对了——先查再转。但 action 直接跳到了 transfer_funds。
这就是多步场景的核心问题:thought 和 action 脱节。 单步场景里意图和工具是一对一映射,不存在脱节的可能。多步场景里,一句话同时涉及 check_balance 和 transfer_funds,模型需要理解执行顺序,选出当前步骤的工具——靠规则奖励函数是做不出这种区分的。
问题出在两个层面:一是奖励函数精度不够,二是输出格式没给模型足够的推理空间。
7.3 奖励函数升级:从规则匹配到 Ground Truth 比对
上一版的奖励函数是基于规则的——检查格式是否完整、工具是否在合法列表里、有没有 reasoning。这在单步场景下够用:格式正确的输出,tool 大概率也是对的。
但多步场景下,check_balance 和 transfer_funds 都是合法工具,格式都完整,规则匹配给出的分数几乎一样。奖励函数区分不了"对的第一步"和"对的第二步"。
升级方向很明确:不猜了,直接和 Ground Truth 做精确比对。
class MultistepGTReward:
def __call__(self, prompts, completions, **kwargs):
solutions = kwargs.get("solution", [])
rewards = []
for completion, solution in zip(completions, solutions):
parsed = parse_json(completion)
expected = parse_json(solution)
expected_tool = expected.get("action", {}).get("tool")
actual_tool = parsed.get("action", {}).get("tool")
# Tool correctness — 占最大权重
if expected_tool and actual_tool == expected_tool:
t_score = 0.5 # 选对了
elif actual_tool in VALID_TOOLS:
t_score = 0.1 # 选错了但合法,保留格式激励
else:
t_score = 0.0
# Params bonus
p_score = 0.2 if has_valid_params(parsed) else 0.0
# Reasoning + Plan
r_score = reasoning_score(parsed) # 最高 0.3
rewards.append(t_score + p_score + r_score)
return rewards
分数设计有三个考量:
1. Tool correctness 占 0.5,是最大的单项分。 选对(+0.5)和选错但合法(+0.1)之间差了 0.4,足够产生明确的学习信号。不要搞一堆细碎的维度平摊分数——你最关心什么,就把最大的分给什么。
2. 选错但合法给 0.1,不是 0。 这个细节很重要。如果你把错误输出全给 0 分,模型会学到"什么都不输出比输出错误工具的分数更高"——因为不出声至少不会扣分。给 0.1 保留了格式激励,模型至少还愿意尝试输出。
3. Reasoning 和 Plan 保留为辅助信号,最高 0.3。 鼓励思考,但权重明确低于正确性。不能让模型学会用"花里胡哨的推理"刷分,而 tool 随便选一个。
7.4 多步场景的格式强化
奖励函数解决了"怎么评判",但还需要解决"怎么表达"。多步推理比单步需要更多的思考空间。
加长思考空间:max_completion_length 从 200 token 提到 350。原来 200 token 对"查余额"这种简单场景绰绰有余,但多步推理需要模型把"先做 A,因为 B,然后 C"这个链条完整写出来。200 token 写到一半就被截断了,模型根本没机会把推理做完。
增加探索多样性:num_generations 从 4 提到 8。多步场景的正确答案不像单步那样显然——"查余额"只有 check_balance 一个正确答案,但"先查余额,够的话转账"的正确 action 需要模型先推理再选择,采样不够的时候可能根本碰不到正确路径。
奖励推理深度:thought 里包含逻辑关键词额外加分。
LOGIC_KEYWORDS = ["first", "then", "before", "because",
"therefore", "need to", "in order to", "since"]
# thought 中包含逻辑词 → +0.05
if any(kw in thought.lower() for kw in LOGIC_KEYWORDS):
r_score += 0.05
这不是凑字数。是鼓励模型把推理链显式地写出来——写出来的推理比"心算"更可靠,也更容易验证 thought 和 action 是否一致。
Plan 从装饰变成核心:之前 plan 只是可选的加分项,多步场景下我改成 plan 非空才给分。多步场景的 plan 不是锦上添花,是模型理解任务执行顺序的证据。如果 plan 是空的,说明模型根本没做规划,action 踩对的概率就低。
7.5 数据怎么准备
多步场景的训练数据需要特殊处理。每个多步场景展开成多条训练样本,每条对应执行链中的一步:
// 场景: "查余额,够的话转账 10 万"
// 样本 1 (第一步):
{
"input": "查余额,够的话转账 10 万",
"output": {
"thought": "先查余额确认资金是否充足",
"action": {"tool": "check_balance"},
"plan": ["check", "transfer"]
}
}
// 样本 2 (第二步):
{
"input": "查余额,够的话转账 10 万\n\nPrevious steps:\nStep 1: check_balance → 余额 50 万",
"output": {
"thought": "余额 50 万,足够转账",
"action": {"tool": "transfer_funds", "params": {"amount": 100000}},
"plan": ["transfer"]
}
}
第二步的 input 包含了前一步的执行结果("余额 50 万")。模型需要基于上下文决定下一步做什么——这要求模型不仅理解用户原始意图,还要理解当前执行到了哪一步、上一步的结果意味着什么。 比单步场景难了不止一个量级。
最终数据集:454 条单步数据 + 823 条多步展开数据。
7.6 NGRPO 依然不可或缺
跑完多步训练后看 NGRPO 统计:Zero std: 3243/5108 (63.5%)。 超过六成的 group 内所有生成结果的 reward 完全相同。
有意思的是,上一轮单步训练时这个比例是 67%,多步训练反而降到了 63.5%。多步场景更难,模型生成结果更分散,reward 方差自然更大。但 63.5% 仍然是绝对多数——如果没有 NGRPO 的虚拟满分样本注入,这些 group 的梯度就是零,模型在 63.5% 的训练步数里没有学到任何东西。
NGRPO 不是一次性的 trick。只要你的任务存在"模型一致性输出"的样本,就需要它作为基础安全网。
7.7 结果
多步场景准确率:63% → 97%。 整体准确率维持 97%,单步场景没有退化。
剩余 3 个错误全是同一类 case:
"I need to take out 75 rupees from ACC888" → 模型选 withdraw,标注是 check_balance
"Withdraw 50 rupees from ACC002" → 模型选 withdraw,标注是 check_balance
"Made profit in day trading. Can I withdraw it tonight?" → 模型选 withdraw,标注是 check_balance
用户明确说了"取钱""withdraw",模型选 withdraw 完全合理。标注认为应该先 check_balance 确认余额——也有道理。这是标注层面的歧义,不是模型的问题。
97% 大概是这个数据集的天花板了。到这一步,继续优化模型没有意义——剩余的错误是"定义层面的分歧",改标注比改模型更高效。
7.8 这一回合的教训
奖励函数精度要跟着任务难度升级。 简单任务用规则匹配够了,复杂任务需要 Ground Truth 直接比对。不是原来的方案有 bug,而是任务复杂度变了。
分数分配反映优先级。 Tool correctness 0.5 > Params 0.2 > Reasoning 0.3——一笔一划都是你的意图。如果你自己都不知道哪项最重要,模型更不会知道。
给模型足够的思考空间。 completion length、generation 数量、plan 的权重——这些"格式约束"决定了模型能表达多深的推理。多步场景里,格式约束本身就是训练能力的一部分。
知道什么时候该停下来。 剩下的错误是标注歧义,继续调模型是过拟合。天花板到了就是到了,接受它比强行突破更专业。