给模型写一段"请正确调用工具"的提示词,可以让演示跑起来,却不一定形成稳定能力。换一个参数名称、增加一次工具失败、把目标数字从整数换成负数,模型就可能输出错误 JSON、重复调用写接口,或者在没有完成任务时自信宣布成功。
监督微调可以教会模型模仿高质量轨迹,但它通常要求先准备"每一步应该怎么做"的示范。对于存在多条合法路径的 Agent 任务,手工写唯一标准轨迹既昂贵又容易限制策略。强化学习提供了另一种方式:让模型在隔离环境中多次尝试,环境只根据可验证结果给出奖励,训练算法提高高奖励轨迹的概率。
GRPO(Group Relative Policy Optimization)因 DeepSeekMath 和后续推理模型受到广泛关注。它针对同一个提示采样一组输出,用组内相对奖励构造优势,不需要像 PPO 那样训练单独的价值模型。到了工具 Agent 场景,输出不再只是一段数学答案,而是"模型消息---工具调用---环境观察---下一次决策"组成的 Rollout。真正困难的部分往往不是写一行 GRPOTrainer,而是构造不会泄漏、不会被钻空子、能够稳定复现的环境和奖励。
本文用一个"库存调拨"玩具任务讲清完整链路:Agent 只能通过查询与调拨工具,让目标仓库达到指定库存。我们先实现可重置环境和确定性奖励,再解释 GRPO 的组内优势,最后给出 Hugging Face TRL 当前环境接口的训练骨架、监控指标和失败复盘方法。示例环境使用 Python 标准库,可直接运行;真正训练需要 GPU、兼容工具调用的模型以及与安装版本匹配的 TRL。
1. 先明确训练对象:不是一段答案,而是一条环境轨迹
普通结果型 GRPO 可以把提示交给模型,采样若干完整回答,再调用奖励函数打分。工具 Agent 多了一层闭环:模型可能先调用 get_stock,环境返回库存;模型根据观察调用 transfer;环境改变状态;模型继续查询或提交最终答案。一次 Rollout 直到成功、失败、达到最大步数或超时才结束。
这意味着训练系统至少包含五个部分:
- 任务分布:每回合生成目标、初始状态和可见信息;
- 策略模型:根据对话历史选择文本或工具调用;
- 工具环境:校验参数、执行动作并返回观察;
- 奖励函数:根据最终状态和必要约束给出标量;
- 优化器:比较同一任务的一组 Rollout,更新策略概率。
#mermaid-svg-BkM8EurvXI2DyOt5{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-BkM8EurvXI2DyOt5 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-BkM8EurvXI2DyOt5 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-BkM8EurvXI2DyOt5 .error-icon{fill:#552222;}#mermaid-svg-BkM8EurvXI2DyOt5 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-BkM8EurvXI2DyOt5 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-BkM8EurvXI2DyOt5 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-BkM8EurvXI2DyOt5 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-BkM8EurvXI2DyOt5 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-BkM8EurvXI2DyOt5 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-BkM8EurvXI2DyOt5 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-BkM8EurvXI2DyOt5 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-BkM8EurvXI2DyOt5 .marker.cross{stroke:#333333;}#mermaid-svg-BkM8EurvXI2DyOt5 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-BkM8EurvXI2DyOt5 p{margin:0;}#mermaid-svg-BkM8EurvXI2DyOt5 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-BkM8EurvXI2DyOt5 .cluster-label text{fill:#333;}#mermaid-svg-BkM8EurvXI2DyOt5 .cluster-label span{color:#333;}#mermaid-svg-BkM8EurvXI2DyOt5 .cluster-label span p{background-color:transparent;}#mermaid-svg-BkM8EurvXI2DyOt5 .label text,#mermaid-svg-BkM8EurvXI2DyOt5 span{fill:#333;color:#333;}#mermaid-svg-BkM8EurvXI2DyOt5 .node rect,#mermaid-svg-BkM8EurvXI2DyOt5 .node circle,#mermaid-svg-BkM8EurvXI2DyOt5 .node ellipse,#mermaid-svg-BkM8EurvXI2DyOt5 .node polygon,#mermaid-svg-BkM8EurvXI2DyOt5 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-BkM8EurvXI2DyOt5 .rough-node .label text,#mermaid-svg-BkM8EurvXI2DyOt5 .node .label text,#mermaid-svg-BkM8EurvXI2DyOt5 .image-shape .label,#mermaid-svg-BkM8EurvXI2DyOt5 .icon-shape .label{text-anchor:middle;}#mermaid-svg-BkM8EurvXI2DyOt5 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-BkM8EurvXI2DyOt5 .rough-node .label,#mermaid-svg-BkM8EurvXI2DyOt5 .node .label,#mermaid-svg-BkM8EurvXI2DyOt5 .image-shape .label,#mermaid-svg-BkM8EurvXI2DyOt5 .icon-shape .label{text-align:center;}#mermaid-svg-BkM8EurvXI2DyOt5 .node.clickable{cursor:pointer;}#mermaid-svg-BkM8EurvXI2DyOt5 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-BkM8EurvXI2DyOt5 .arrowheadPath{fill:#333333;}#mermaid-svg-BkM8EurvXI2DyOt5 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-BkM8EurvXI2DyOt5 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-BkM8EurvXI2DyOt5 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-BkM8EurvXI2DyOt5 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-BkM8EurvXI2DyOt5 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-BkM8EurvXI2DyOt5 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-BkM8EurvXI2DyOt5 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-BkM8EurvXI2DyOt5 .cluster text{fill:#333;}#mermaid-svg-BkM8EurvXI2DyOt5 .cluster span{color:#333;}#mermaid-svg-BkM8EurvXI2DyOt5 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-BkM8EurvXI2DyOt5 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-BkM8EurvXI2DyOt5 rect.text{fill:none;stroke-width:0;}#mermaid-svg-BkM8EurvXI2DyOt5 .icon-shape,#mermaid-svg-BkM8EurvXI2DyOt5 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-BkM8EurvXI2DyOt5 .icon-shape p,#mermaid-svg-BkM8EurvXI2DyOt5 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-BkM8EurvXI2DyOt5 .icon-shape .label rect,#mermaid-svg-BkM8EurvXI2DyOt5 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-BkM8EurvXI2DyOt5 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-BkM8EurvXI2DyOt5 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-BkM8EurvXI2DyOt5 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 工具调用
最终回答
步数/超时上限
任务生成器
重置独立环境
策略模型生成动作
文本还是工具调用?
参数校验与权限检查
环境执行并返回观察
结束Rollout
读取最终环境状态
可验证奖励
同一提示的组内标准化
GRPO策略更新
独立验证集与回归评测
训练时优化的是策略在完整上下文下生成动作 Token 的概率。环境状态不应只存在于提示词描述中,而应由外部程序维护;否则模型可以通过声称"已经调拨"骗过文本奖励。奖励也不应直接相信最终自然语言,而要检查仓库状态、权限日志和调用次数。
2. GRPO与PPO的核心区别
PPO 类 RLHF 通常需要策略模型、参考模型、奖励模型和价值模型。价值模型估计当前状态的期望回报,用来计算优势。GRPO 的关键简化是:对同一个问题采样 G 条输出,获得奖励 r1 ... rG,再用组内均值和标准差将每条奖励转成相对优势:
text
A_i = (r_i - mean(r_1...r_G)) / (std(r_1...r_G) + epsilon)
高于组平均的轨迹得到正优势,低于平均的轨迹得到负优势。随后使用类似 PPO 的裁剪目标限制一次更新幅度,并可加入相对参考策略的 KL 约束,避免模型为追逐奖励过快偏离原有语言与工具能力。
组内比较有两个直观好处。第一,同一道任务内部比较可以减少不同题目天然难度对奖励尺度的影响;第二,不需要单独训练价值网络,降低了部分显存开销和系统复杂度。但"不需要价值模型"不等于训练便宜:每个提示仍需生成多条 Rollout,Agent Rollout 还包含多轮模型推理和环境调用,生成侧往往成为主要成本。
GRPO也有明显边界。如果一个组里所有轨迹奖励相同,标准差接近零,组内没有可学习的排序信号。最常见的两种情况是任务太难导致全为零,或任务太容易导致全为一。盲目增加训练步数不会解决它,应调整任务课程、基础模型、采样多样性或奖励粒度。
下面用标准库计算组内优势。代码显式处理零方差,避免为了数值形式制造虚假信号。
python
from __future__ import annotations
from math import sqrt
def group_advantages(rewards: list[float], eps: float = 1e-8) -> list[float]:
if len(rewards) < 2:
raise ValueError("GRPO 每组至少需要两个 Rollout")
mean = sum(rewards) / len(rewards)
variance = sum((reward - mean) ** 2 for reward in rewards) / len(rewards)
std = sqrt(variance)
if std < eps:
return [0.0 for _ in rewards]
return [(reward - mean) / (std + eps) for reward in rewards]
def demo_advantages() -> None:
values = group_advantages([0.0, 0.2, 1.0, 0.2])
assert len(values) == 4
assert values[2] > 0
assert values[0] < 0
assert abs(sum(values)) < 1e-6
assert group_advantages([1.0, 1.0]) == [0.0, 0.0]
if __name__ == "__main__":
demo_advantages()
论文与不同实现对优势归一化、Token 级损失、KL 估计和长度归一化有不同细节。使用 TRL 时应以安装版本的文档和配置为准,不要从一篇博客复制公式后假设与代码完全一致。文章中的函数用于理解组内信号,不是重写训练器。
3. 先把环境做对:重置、隔离、可终止、可审计
工具环境是 Agentic RL 的事实来源。它必须具备四个基本性质。
可重置:每条 Rollout 从明确的初始状态开始。同组候选若共享上一个候选修改后的库存,奖励就失去可比性。
隔离:一个 Rollout 的写操作不能影响另一个 Rollout,更不能触碰生产系统。数据库可以使用独立事务、临时 schema、容器快照或内存状态;外部邮件、支付和删除工具应替换为可验证模拟器。
可终止:设置最大步数、最大模型调用、工具超时和非法动作上限。模型可能进入"查询---查询---查询"循环,环境必须能够结束并给出失败奖励。
可审计:记录每次动作、规范化参数、返回摘要、状态变化和终止原因。只保留最终奖励,无法解释模型到底学会了什么。
下面的环境维护两个仓库的库存。工具方法使用明确类型、业务校验和幂等记录;get_reward 只根据真实状态与约束评分。为了防止"从空气中增加库存",调拨会同时扣减源仓与增加目标仓。
python
from __future__ import annotations
from dataclasses import dataclass, field
@dataclass
class InventoryEnv:
"""隔离的库存调拨环境;每条 Rollout 使用独立实例。"""
initial_a: int = 8
initial_b: int = 2
target_b: int = 6
max_steps: int = 5
episode_initial_a: int = field(init=False)
episode_initial_b: int = field(init=False)
stock: dict[str, int] = field(init=False)
steps: int = field(init=False, default=0)
invalid_actions: int = field(init=False, default=0)
transfers: list[tuple[str, str, int]] = field(init=False, default_factory=list)
def __post_init__(self) -> None:
self.reset()
def reset(
self,
initial_a: int | None = None,
initial_b: int | None = None,
target_b: int | None = None,
**_: object,
) -> str:
a = self.initial_a if initial_a is None else initial_a
b = self.initial_b if initial_b is None else initial_b
target = self.target_b if target_b is None else target_b
if min(a, b, target) < 0:
raise ValueError("库存和目标不能为负数")
self.episode_initial_a = a
self.episode_initial_b = b
self.stock = {"A": a, "B": b}
self.target_b = target
self.steps = 0
self.invalid_actions = 0
self.transfers = []
return f"请通过工具让仓库 B 的库存恰好达到 {target},不得制造库存。"
def get_stock(self, warehouse: str) -> int:
"""查询仓库库存。
Args:
warehouse: 仓库名称,只能是 A 或 B。
Returns:
当前库存数量。
"""
self._count_step()
if warehouse not in self.stock:
self.invalid_actions += 1
raise ValueError("未知仓库")
return self.stock[warehouse]
def transfer(self, source: str, target: str, amount: int) -> dict[str, int]:
"""在两个仓库之间调拨库存。
Args:
source: 源仓库,只能是 A 或 B。
target: 目标仓库,只能是 A 或 B,且不能等于源仓库。
amount: 正整数调拨数量,不能超过源仓库存。
Returns:
调拨后的两个仓库库存。
"""
self._count_step()
valid = (
source in self.stock
and target in self.stock
and source != target
and isinstance(amount, int)
and not isinstance(amount, bool)
and amount > 0
and self.stock.get(source, -1) >= amount
)
if not valid:
self.invalid_actions += 1
raise ValueError("非法调拨参数或库存不足")
self.stock[source] -= amount
self.stock[target] += amount
self.transfers.append((source, target, amount))
return dict(self.stock)
def _count_step(self) -> None:
if self.steps >= self.max_steps:
raise RuntimeError("已达到工具调用上限")
self.steps += 1
def get_reward(self) -> float:
total_preserved = (
sum(self.stock.values()) == self.episode_initial_a + self.episode_initial_b
)
reached = self.stock["B"] == self.target_b
if not total_preserved or not reached:
return 0.0
# 正确完成是主要信号;非法动作会降低但不反转成功奖励。
efficiency = max(0.0, 1.0 - 0.05 * max(0, self.steps - 1))
safety = 0.0 if self.invalid_actions else 1.0
return 0.8 * efficiency + 0.2 * safety
def demo_environment() -> None:
env = InventoryEnv()
prompt = env.reset(initial_a=8, initial_b=2, target_b=6)
assert "6" in prompt
assert env.get_stock("B") == 2
assert env.transfer("A", "B", 4) == {"A": 4, "B": 6}
assert env.get_reward() > 0
assert sum(env.stock.values()) == 10
env.reset(initial_a=11, initial_b=1, target_b=4)
env.transfer("A", "B", 3)
assert sum(env.stock.values()) == 12
assert env.get_reward() > 0
if __name__ == "__main__":
demo_environment()
这个示例仍是训练用玩具环境。真实库存系统还要处理版本并发、单位、批次、锁定量、权限与审计,不应把它直接连接生产数据库。训练环境的目的,是提供与核心行为一致但风险可控的模拟,而不是绕过业务服务重新实现一套真实库存逻辑。
4. 奖励设计:先奖励结果,再谨慎塑形过程
奖励就是模型真正优化的规格。提示词写"不要重复调用",但奖励只看最终库存,模型若发现重复调用不影响得分,就没有动力减少步骤。反过来,如果每少一步都给大量奖励,模型可能不查询库存就盲目调拨,偶然成功时获得高分。
可靠的奖励通常分层:
- 硬结果:目标状态是否真实达成;
- 硬约束:是否越权、破坏守恒、调用禁用工具或污染环境;
- 格式与协议:工具 JSON 是否可解析、参数是否符合 Schema;
- 效率:在正确完成前提下,减少无效步骤与 Token;
- 表达质量:最终回复是否准确说明结果和限制。
硬结果应占主导。效率奖励最好只在成功轨迹内部区分,避免"什么都不做"因为零步骤而胜过真正完成任务的轨迹。安全违规可以直接令回合失败;对特别危险行为,不只是给负分,还应在环境层阻止执行。
可验证奖励不等于只有 0 和 1。稀疏二元奖励最不容易被误解,却可能让早期训练全部为零。可以增加不改变目标的中间信号,例如工具调用可解析、读取了必要状态、最终距离目标缩小。但每一项都要问:"模型能否在不完成真实任务的情况下刷到它?"
下面的独立奖励函数从环境和最终回复中组合信号,并返回各分量,便于监控。它不会因为回复里声称"成功"就给结果分。
python
from dataclasses import dataclass
@dataclass(frozen=True)
class RewardBreakdown:
outcome: float
protocol: float
efficiency: float
honesty: float
@property
def total(self) -> float:
# 结果是门槛:未达成时,其他漂亮行为不能换来正向总奖励。
if self.outcome == 0:
return 0.0
return (
0.75 * self.outcome
+ 0.10 * self.protocol
+ 0.10 * self.efficiency
+ 0.05 * self.honesty
)
def score_rollout(env: InventoryEnv, final_answer: str) -> RewardBreakdown:
reached = env.stock["B"] == env.target_b
total_preserved = (
sum(env.stock.values()) == env.episode_initial_a + env.episode_initial_b
)
outcome = float(reached and total_preserved)
protocol = float(env.invalid_actions == 0)
efficiency = max(0.0, 1.0 - 0.1 * max(0, env.steps - 2))
claims_success = "成功" in final_answer or "已达到" in final_answer
honesty = float(claims_success == bool(outcome))
return RewardBreakdown(outcome, protocol, efficiency, honesty)
def demo_reward() -> None:
failed = InventoryEnv()
failed_score = score_rollout(failed, "调拨成功")
assert failed_score.outcome == 0
assert failed_score.total == 0
passed = InventoryEnv()
passed.transfer("A", "B", 4)
passed_score = score_rollout(passed, "已达到目标库存")
assert passed_score.total > 0.9
if __name__ == "__main__":
demo_reward()
训练指标必须分别记录 outcome、protocol、efficiency 和 honesty。只看总奖励上升,会漏掉一种危险情况:结果分下降,但模型学会用格式和措辞补偿。如果任何硬失败仍能通过其他分量获得高总分,奖励组合就需要重新设计。
5. Rollout生成:多样性、组大小与上下文边界
GRPO要求同一提示下的候选具有奖励差异。采样温度太低,轨迹近乎相同;温度过高,模型大量生成无法解析的工具调用。应该观察每组唯一动作序列数量、工具参数多样性、奖励标准差和解析失败率,而不是盲目复制论文超参数。
组大小越大,组内相对估计通常更丰富,但生成成本和内存压力也更高。工具 Agent 的一条 Rollout 可能包含多次模型前向和环境步骤,组大小翻倍的代价远高于单轮数学答案。第一轮实验先选择能产生奖励差异的最小组,确认环境和奖励有效,再扩大吞吐。
上下文必须有明确边界。环境返回内容是不可信数据,可能包含提示注入文本;系统规则应说明工具观察不能修改权限与训练目标。每一步只提供当前任务所需工具,不应把其他环境的工具一起暴露。不同任务混合训练时,路由字段选择环境,每条 Rollout 只获得该环境的方法。
还要定义结束协议。模型输出最终文本、调用 submit 工具、达到目标状态还是环境主动终止?若没有一致约定,模型可能完成后继续操作,把成功状态破坏掉。高副作用任务通常让环境在满足目标并提交后锁定,后续动作返回终止状态。
6. 用TRL连接环境:接口是实验性的,先锁定版本
Hugging Face TRL 的 GRPOTrainer 支持自定义奖励函数,并在当前文档中提供 environment_factory 来训练交互式工具环境。环境公开方法可作为工具,reset 为每条任务重置状态并返回提示,get_reward 在 Rollout 结束后提供环境奖励。该模式要求 Transformers 5.2.0及以上,而且环境拥有任务数据,不能再同时传入外部 train_dataset。文档明确标注环境工厂相关能力仍可能变化,因此项目必须固定 TRL、Transformers、Accelerate 与推理后端版本,并把实际配置随实验保存。
下面是最小连接骨架。模型名和参数只是演示配置,不代表任何硬件都能训练,也不宣称能在库存任务上直接收敛。真正运行前,要确认模型聊天模板支持工具调用,环境的 reset 返回当前任务提示,并先用几十条 Rollout 做冒烟验证。
python
# train_grpo.py:需要按已安装的 TRL 版本核对接口
from trl import GRPOConfig, GRPOTrainer
def make_environment() -> InventoryEnv:
return InventoryEnv()
config = GRPOConfig(
output_dir="outputs/inventory-grpo",
num_generations=4,
max_completion_length=1024,
per_device_train_batch_size=4,
gradient_accumulation_steps=4,
learning_rate=1e-6,
logging_steps=1,
save_steps=50,
report_to="none",
)
trainer = GRPOTrainer(
model="Qwen/Qwen3-0.6B",
args=config,
environment_factory=make_environment,
)
if __name__ == "__main__":
trainer.train()
这里有几个必须核对的点。第一,使用 environment_factory 时不要再传 train_dataset;任务场景应由环境持有或采样,并由 reset 返回提示。第二,per_device_train_batch_size 与 num_generations、进程数之间有整除和显存约束,应让启动时校验失败,而不是为了凑数复制样本。第三,工具调用需要模型聊天模板支持;仅有普通指令模型时,Trainer 能暴露工具也不代表模型会正确生成协议。
如果当前稳定版本尚不包含你需要的环境能力,不要直接追踪主分支用于长期训练。可以先离线生成轨迹,把每条完整 Rollout 视为 completion 交给普通 GRPO 奖励函数;代价是无法在 Trainer 内实时交互,但接口更容易冻结。等环境 API 稳定、测试覆盖足够后再迁移。
7. 训练前的三道检查,比直接烧GPU更重要
7.1 随机策略检查
用随机动作或手写坏轨迹跑环境,确认非法参数被拒绝、总库存不能凭空增加、超过步数会终止、重置后状态完全恢复。若随机策略也能频繁拿高分,奖励存在漏洞;若人工正确轨迹拿不到高分,环境实现有错。
7.2 基础模型Rollout检查
不训练,先采样一批基础模型轨迹。读取原始工具调用,不只看奖励均值。需要知道它主要失败在协议、参数、计划还是能力。如果模型几乎从不生成合法工具调用,直接使用稀疏结果奖励可能全组为零;应先做少量工具格式 SFT,或选择已经具备工具调用能力的基础模型。
7.3 奖励反例检查
主动构造作弊轨迹:最终文字声称成功但状态没变;直接篡改状态而不走工具;重复调拨后回到目标;制造负库存;读取环境内部答案;异常退出以逃避惩罚。奖励应拒绝这些反例。每发现一种奖励漏洞,就把它变成自动测试和验证集任务。
训练系统还需固定随机种子、模型与数据修订、容器镜像、环境代码提交号和奖励版本。RL 方差比常规监督训练更明显,只有保存完整实验条件,才能区分真实改进与一次幸运运行。
8. 训练监控不能只看reward曲线
总奖励上升可能有多种解释:模型更会完成任务;模型找到奖励漏洞;任务采样变简单;输出长度变化影响了归一化;验证服务故障把错误当成功。至少同时监控以下指标:
- 真实结果通过率与各奖励分量;
- 每组奖励均值、标准差和零方差组比例;
- 合法工具调用率、参数校验失败率和未知工具率;
- 每条 Rollout 的模型轮数、工具步数、输入与输出 Token;
- 正常结束、超时、步数耗尽和环境异常的比例;
- 策略与参考策略的 KL、裁剪比例与梯度统计;
- 训练集和固定验证集的独立结果。
零方差组比例特别关键。大量全零意味着任务过难、协议失败或奖励太稀疏;大量全一意味着任务过易或数据泄漏。两者都会减少有效学习信号。解决方案是调整课程和任务分布,而不是简单提高学习率。
长度也需要单独看。某些目标实现对 Token 级损失的归一化会形成长度偏好;奖励又可能鼓励短轨迹,二者组合后模型可能过早结束。相反,若格式奖励覆盖每个正确标签,模型可能生成冗长重复标签。必须以最终成功率和真实成本判断,而不是认为"更短"或"更长"天然更好。
异步 Rollout 可以提高硬件利用率,但生成策略与训练中的最新参数存在滞后。滞后过大时,优化器用旧策略轨迹更新新策略,训练稳定性可能下降。监控每条轨迹的策略版本,限制可接受陈旧度,并在吞吐与 on-policy 程度之间做明确权衡。
9. 奖励投机:模型会优化你写下的规则,而不是你的愿望
奖励投机不是模型"恶意",而是优化目标与真实目标有缝隙。库存任务中常见漏洞包括:
- 最终回答写"库存为6"就得分,实际状态未变化;
- 环境暴露
target_b内部字段,模型直接读取答案而不完成任务; transfer接受负数,模型反向增加库存;- 只检查目标仓,不检查总量守恒,模型凭空造货;
- 步数奖励过强,模型跳过必要查询靠猜测;
- 工具异常被当成回合成功结束;
- 训练数据包含固定初始状态,模型记住永远调拨4件。
防护原则是让奖励落在不可伪造的外部状态上,环境在动作入口做严格校验,并让任务参数程序化变化。隐藏测试不应通过模型可调用工具暴露。状态对象与评分器放在权限边界外,模型只看到业务允许的观察。
高风险安全规则不要仅靠负奖励。比如训练支付 Agent 时,"向未授权账户转账"应被工具层直接拒绝;负奖励只用于教模型减少这种尝试。RL 训练会探索,探索意味着它一定会尝试错误动作,环境必须保证这些尝试不会造成真实损害。
定期读取高奖励轨迹,而不是只读低奖励失败。低分告诉你模型不会什么,高分轨迹能发现它如何拿到分。如果高奖励轨迹在人类看来不符合目标,应立即暂停训练、修奖励并从修复前检查点重新评估。
10. 失败复盘:从一条Rollout定位根因
复盘一条失败轨迹时,按顺序检查四层。
第一层是环境真值:初始状态是什么,每个工具动作后状态如何变化,最终是否真的达成目标。如果状态日志不完整,先修环境可观测性。
第二层是协议:模型是否输出可解析工具调用,工具名与参数类型是否匹配聊天模板。大量解析错误通常是模板、SFT 数据或序列化问题,不是 GRPO 超参数问题。
第三层是决策:模型看到正确库存后是否计算错调拨量,工具报错后是否理解观察,是否在目标完成后继续操作。这里才涉及策略能力和奖励信号。
第四层是优化:同组其他轨迹奖励如何,失败轨迹获得了正还是负优势,KL 与裁剪是否让更新过弱或过强。只盯着这层调学习率,会把环境与协议错误掩盖成"训练不稳定"。
把失败归类为可统计标签,例如 tool_parse_error、invalid_argument、wrong_plan、ignored_observation、loop、false_success_claim、environment_error。每次实验比较类别分布。总通过率相同但错误从"参数格式"迁移到"复杂规划",说明模型能力结构已经变化,下一阶段数据和奖励也应调整。
11. 从失败样本建立课程,而不是反复训练同一批题
Agentic RL 的课程可以按环境难度逐步增加:先固定两个仓库和一次调拨,再增加缺失信息、工具暂时失败、多个仓库、并发版本冲突和需要澄清的任务。每个阶段都要求基础协议与安全不变量保持通过。
课程不能只按题目表面复杂度排列。真正决定可学习性的,是基础模型在一组采样中是否能偶尔成功。若一个任务组始终全零,它暂时不给 GRPO 排序信号;可以先通过 SFT、提示改造、工具文档或更简单子任务让成功进入可观测范围。若一组始终全一,则降低采样比例,把算力留给能力边界附近的任务。
失败样本进入课程前要去重并检查环境错误。同一个解析 bug 产生的一万条失败并不等于一万个独立教学信号。保留代表性最小轨迹,修好公共根因,再重新采样。
训练集、调参集和最终测试集必须分离。程序生成任务也可能泄漏:如果所有训练目标都是偶数,模型会学到捷径;测试集应改变数值范围、组合方式、工具返回顺序和语言表达,但仍属于相同业务规则。
12. 安全执行:沙箱不是可选项
训练代码 Agent、浏览器 Agent 或系统运维 Agent 时,模型生成的工具参数就是不可信输入。代码执行需要容器或虚拟机隔离,禁用不必要网络,限制 CPU、内存、磁盘、进程数和运行时间;文件系统只挂载临时目录;凭证不进入环境;训练结束销毁实例。
网络工具应使用域名与方法白名单,防止访问云元数据、内网控制面或任意下载地址。浏览器环境使用测试账户和模拟支付。数据库通过最小权限账号连接临时实例,不允许模型获得 DDL、跨租户或生产写权限。
工具返回也可能携带恶意指令。网页、文档和错误消息只能作为观察数据,不能覆盖系统规则。环境应限制单次返回大小、清理控制字符,并在对话中清晰标注来源。模型即使被注入诱导,外层授权器仍要拦截危险调用。
日志会包含提示、工具参数、代码和环境状态,可能带有敏感数据。训练前脱敏,训练日志设置访问权限与保留期,模型产物进入发布前还要做记忆泄漏和隐私评测。不要因为环境是"训练用"就放宽数据治理。
13. 训练完成不等于可以发布
最终验收应在独立任务上比较基础模型、SFT 检查点和 GRPO 检查点。至少报告单次任务成功率、多次试验稳定性、非法工具调用率、平均与长尾步数、Token、超时、拒绝与安全违规。只报告训练奖励无法说明泛化。
评测环境要比训练环境更苛刻但规则一致:不同数字与表述、工具返回乱序、可恢复错误、缺失信息和诱导性文本。还要包含不应执行的请求,确认模型能澄清、拒绝或转人工,而不是把"积极调用工具"学成无条件行动。
模型升级可能牺牲原有能力,因此保留通用对话、格式遵循和非工具任务回归。KL 约束能减缓漂移,但不能替代外部评测。若工具能力提升却语言质量、安全拒绝或长上下文能力下降,需要调整训练混合、奖励或选择更早检查点。
上线采用小流量灰度,真实写操作继续受策略引擎和人工确认保护。生产反馈可以进入下一轮任务集,但先脱敏、人工复核并最小化,不能把用户会话直接回灌训练。任何检查点都要能回滚,环境和奖励代码也要与模型版本一同归档。
14. 哪些场景不适合直接用GRPO
如果任务没有可靠结果信号,只能让另一个模型凭感觉打分,GRPO 很容易放大评分器偏见。先完善评测与人工数据,可能比直接做 RL 更有效。
如果基础模型连工具协议都不会,几乎所有 Rollout 都解析失败,先做小规模 SFT 通常更省计算。如果简单提示词、工具 Schema 改进或确定性编排已经解决问题,也没有必要为了"使用强化学习"增加训练系统。
如果任务动作不可逆、无法安全模拟,不能让训练策略在真实系统探索。应建立数字孪生、离线日志重放或受控仿真;仿真与现实差距过大时,训练收益需要谨慎验证。
如果数据量很小且任务规则稳定,直接写业务状态机可能更可靠。Agentic RL 适合路径多样、策略难以手写、结果可验证且能大量安全采样的场景,不是所有工具调用的默认答案。
15. 保存什么,才能复现实验和回滚
只保存模型权重不够。一次可复现的 Agentic RL 实验还需要基础模型修订、聊天模板、Tokenizer、工具 Schema、环境代码、奖励代码、任务生成种子、训练配置、依赖锁文件和验证集版本。任何一项变化,都可能让同一检查点表现不同。
检查点旁边应保存一份模型卡式记录:它在哪些环境训练,允许调用哪些工具,已知失败类型是什么,安全边界如何,离线指标使用了多少次 Trial。部署系统引用不可变版本,而不是会移动的 latest 标签。灰度失败时,回滚的不只是权重,还包括与其配套的模板、工具定义和策略阈值。
为了控制存储,不必永久保留每个中间检查点的全部 Rollout;但应保留关键里程碑、聚合指标、代表性成功与失败轨迹,以及能重新生成任务的种子。含敏感数据的轨迹必须脱敏并按期限删除,可复现性不能成为无限保存用户数据的理由。
总结
用 GRPO 训练工具 Agent,算法只占整个工程的一部分。可靠流程是:把任务做成可重置、隔离、可终止、可审计的环境;让奖励检查真实结果和安全不变量;为同一提示采样有差异的一组 Rollout;用组内相对奖励形成优势;监控零方差、协议错误、长度、KL 与环境异常;持续阅读高奖励轨迹寻找投机;最后在独立任务与灰度流量中验证泛化。
不要从 trainer.train() 开始。先证明人工正确轨迹能稳定得高分,作弊轨迹无法得分,基础模型偶尔能产生成功样本,环境在并发下互不污染。四件事成立后,GRPO 才有可学习信号。训练的目标也不是让模型写出更长的"思考",而是让它在真实约束下更频繁地选择正确工具、使用正确参数,并用更少风险完成可验证的任务。
参考资料
- DeepSeekMath:Pushing the Limits of Mathematical Reasoning in Open Language Models
- DeepSeek-R1:Incentivizing Reasoning Capability in LLMs via Reinforcement Learning
- Hugging Face TRL:GRPO Trainer
- Hugging Face OpenEnv:End-to-end GRPO walkthrough
- Hugging Face TRL:OpenEnv Integration
- Hugging Face TRL:OpenReward Integration
- Hugging Face:Liger GRPO meets TRL
- Proximal Policy Optimization Algorithms
- Toolformer:Language Models Can Teach Themselves to Use Tools
- Hugging Face OpenEnv:Agentic Execution Environments