Group-in-Group Policy Optimization for LLM Agent Training
Paper: arXiv:2505.10978 Code: langfengQ/verl-agent Code reference:
master@796ed310(2026-02-27)
1. Motivation(研究动机)
现有 group-based RL 如 GRPO、RLOO 在数学推理或代码生成这类 single-turn 任务上表现强,原因是 reward 几乎总是“整题做对/做错”式、credit assignment 简单;但多步 agent 任务完全不同。ALFWorld 最长可到 50 步,WebShop 需要跨页面检索、筛选、点击,search-augmented QA 也要求模型在 <think>、搜索、读取结果、再次搜索之间反复切换。此时单个动作的好坏往往要到很多步之后才显现,vanilla GRPO 对整条轨迹只给一个 episode-level advantage,会把“前面哪一步走错了”全部压平。
这篇论文要解决的具体问题是:能否在保留 group-based RL 的三个关键优点前提下,把 credit signal 从“整条轨迹”细化到“同一状态下的具体动作”。这三个优点是 critic-free、显存开销低、收敛稳定;作者不想回到 PPO 那种显式 value function,也不想为每个状态额外 rollout 一批 hypothetical actions。这个问题值得研究,因为多轮 LLM agent 的难点恰恰不在“会不会输出 fluent 文本”,而在于是否能持续做对局部决策。
若 credit signal 只能告诉模型“这条轨迹失败了”,模型无法知道失败是因为在 WebShop 点击了错误商品、在 ALFWorld 重复绕路,还是在 search QA 里发起了多余检索。只要把这种局部 credit 做得足够细,critic-free RL 才可能真正扩展到 tool use、web agent、embodied agent 等长时程场景。
2. Idea(核心思想)
GiGPO 的核心 insight 是:多条从同一任务、同一起始状态出发的 agent 轨迹,经常会在中途反复回到相同环境状态,例如同一张搜索结果页、同一个房间、同一个网页节点。既然这些“重复状态”天然存在,就不需要为每个状态重新 rollout 新动作;只要把历史轨迹里所有落在同一 anchor state 的动作重新分组,就能离线构造 step-level 比较组。 因此,GiGPO 不是替换 GRPO,而是在 GRPO 之外再嵌一层状态内比较。episode-level group 继续负责“整条轨迹整体好不好”,step-level group 负责“同一状态下哪个动作更好”。这种 group-in-group 结构把宏观任务完成度和微观动作质量同时编码到 advantage 里。 与只用 trajectory-level GRPO 的方法相比,GiGPO 的根本区别在于:GRPO 只能比较“哪条完整轨迹更好”,而 GiGPO 还能比较“在同一个状态里,点击商品 A、点击商品 B、翻页这几个动作哪个长期回报更高”。与为每个状态额外采样动作的昂贵方案相比,GiGPO 完全复用已有 rollout,因此几乎不增加 LLM forward 成本。
3. Method(方法)
3.1 Overall framework
Figure 1 解读:这张图直接定义了论文的问题与解法。左侧是 vanilla GRPO,只能对整条轨迹做一次 group computation,因此局部动作没有独立 credit;中间是理想但昂贵的 per-state extra rollouts,需要在同一状态 下再采样很多候选动作;右侧是 GiGPO,利用已有轨迹里在同一状态下发生过的不同动作 直接形成 step-level group,在不增加 rollout 的情况下得到细粒度优势估计。
Figure 2 解读:训练流程分成两层 group computation。第一层对同任务同初始状态下的 条轨迹计算 episode relative advantages;第二层把所有轨迹中重复出现的 anchor states 聚合起来,对这些状态上的动作再计算 step relative advantages。最后两者相加形成 joint advantage,用同一个 PPO-style clipped objective 更新策略。
直觉上,GiGPO 的关键不是“更多 reward”,而是“reward 对齐到了正确比较单位”。长时程 agent 失败常常源于局部错误反复累积;如果只看最终成功与否,策略梯度很难知道应修正哪一步。GiGPO 把比较单位从 trajectory 扩展到 state-conditioned action,使 RL signal 更像“在同一个局面里,哪个动作更优”的监督。
3.2 Episode relative advantages
固定任务 和初始状态后,策略 rollout 出 条完整轨迹:
每条轨迹的总回报为:
作者先构造 episode-level group:
然后像 GRPO 一样计算 episode relative advantage:
其中 可取标准差,也可固定为 。论文特别强调固定为 时,本质上对应一个 rescaled RLOO estimator,可以减少难度偏置。这个设计解释了文中的 w/ std 与 w/o std 两个变体:前者更像标准 GRPO,后者更接近 leave-one-out 风格的无标准差归一化。
3.3 Anchor state grouping
GiGPO 最重要的新组件是 anchor state grouping。作者先把所有轨迹中出现过的状态集合记作:
对于每个 anchor state ,把所有在该状态下采取过的动作收集成原始 step-level group:
Figure 3 解读:图里展示了 WebShop 的典型重复状态。两条轨迹都多次回到同一张搜索结果页,但后续动作不同:一条最终点击正确商品并成功,另一条继续翻页或偏离路径而失败。GiGPO 就是在这种“相同状态、不同动作、不同后果”的位置建立比较,从而把成功与失败的差异精确归因到具体动作。
这个模块的价值在于,它把“同状态比较”从理想化的 counterfactual rollout,降成了对已有轨迹的离线重排。对长时程 agent 来说,重复状态其实很常见,因为早期策略会频繁回环、重复搜索、来回切页。GiGPO 把这种冗余从训练噪声转化成可利用的监督信号。
3.4 Step relative advantages
由于即时奖励 可能很稀疏,论文不直接比较 immediate reward,而是先为每个状态-动作对计算 discounted return: 于是 step-level group 被改写为: 在每个 anchor state 内,再做一次 group normalization: 这一步给出的不是“该状态动作绝对值多大”,而是“它相对同状态其它候选动作是否更优”。对 multi-turn tool use 来说,这正是最需要的信号:重复查询、错误点击、无效回退并不是因为它们绝对 reward 很低,而是因为在同一状态下存在明显更优动作。
3.5 Joint advantage and policy optimization
两级 advantage 最终线性结合:
其中 控制局部 credit 的权重。策略更新目标是标准 clipped objective 加 reference KL:
其中
Figure 4 解读:ablation 证明 GiGPO 的收益不是简单来自某个归一化技巧,而是来自双层 advantage 结构本身。去掉 会破坏全局任务一致性,去掉 会失去局部 decision refinement;两者都在 Cool、Pick2、WebShop 这类更依赖精细决策的任务上明显掉点。
3.6 Pseudocode
论文给了整体算法框架,released code 则把它具体落在 gigpo/core_gigpo.py。下面的伪代码按“真实实现里的关键组件”拆成四块,而不是只给一个抽象流程。
def rollout_group(policy_old, env_group, task, group_size):
trajectories = []
for env_id in range(group_size):
state = env_group.reset_same_task(task, env_id)
traj = []
done = False
while not done:
action = policy_old.sample(state)
next_state, reward, done, info = env_group.step(env_id, action)
traj.append((state, action, reward, next_state, info))
state = next_state
trajectories.append(traj)
return trajectoriesdef build_anchor_state_groups(anchor_obs, episode_group_ids, enable_similarity=False, similarity_thresh=0.95):
step_group_uids = [None] * len(anchor_obs)
for episode_id in unique(episode_group_ids):
locs = [i for i, eid in enumerate(episode_group_ids) if eid == episode_id]
clusters = []
for loc in locs:
obs = anchor_obs[loc]
placed = False
for cluster in clusters:
same = obs == cluster["rep"]
similar = enable_similarity and lcs_ratio(obs, cluster["rep"]) >= similarity_thresh
if same or similar:
cluster["locs"].append(loc)
placed = True
break
if not placed:
clusters.append({"rep": obs, "locs": [loc]})
for cluster in clusters:
uid = uuid4()
for loc in cluster["locs"]:
step_group_uids[loc] = uid
return step_group_uidsdef compute_gigpo_advantages(token_level_rewards, step_rewards, response_mask,
anchor_obs, episode_group_ids, traj_index,
step_advantage_w=1.0, mode="mean_norm",
enable_similarity=False, similarity_thresh=0.95):
remove_std = (mode == "mean_norm")
episode_adv = episode_norm_reward(
token_level_rewards, response_mask, episode_group_ids, traj_index,
remove_std=remove_std
)
step_group_uids = build_anchor_state_groups(
anchor_obs, episode_group_ids, enable_similarity, similarity_thresh
)
step_adv = step_norm_reward(
step_rewards, response_mask, step_group_uids, remove_std=remove_std
)
scores = episode_adv + step_advantage_w * step_adv
return scoresdef gigpo_update(policy, policy_old, ref_policy, batch, clip_eps, beta):
advantages = compute_gigpo_advantages(
token_level_rewards=batch["token_level_rewards"],
step_rewards=batch["step_rewards"],
response_mask=batch["response_mask"],
anchor_obs=batch["anchor_obs"],
episode_group_ids=batch["index"],
traj_index=batch["traj_index"],
step_advantage_w=batch["step_advantage_w"],
mode=batch["gigpo_mode"],
enable_similarity=batch["enable_similarity"],
similarity_thresh=batch["similarity_thresh"],
)
ratio = torch.exp(policy.logprob(batch) - policy_old.logprob(batch))
clipped = torch.clamp(ratio, 1 - clip_eps, 1 + clip_eps) * advantages
pg_loss = -torch.min(ratio * advantages, clipped).mean()
kl_loss = beta * low_var_kl(policy, ref_policy, batch)
return pg_loss + kl_loss3.7 Code-to-paper mapping
论文公式与 released code 实现差异:论文把 KL 写在总体目标的最后一项里,而 released training scripts(如 examples/gigpo_trainer/run_alfworld.sh、run_webshop.sh、run_search.sh)设置的是 algorithm.use_kl_in_reward=False,同时启用 actor.use_kl_loss=True 与 kl_loss_coef。也就是说,代码把 KL 当作 actor loss 中的独立项,而不是 reward shaping 的一部分。另一个差异是 search 任务在论文中明确采用 similarity-based GiGPO,released code 通过 enable_similarity=True 与 similarity_thresh=0.9 仅在 run_search.sh 中开启;ALFWorld / WebShop 脚本保持 exact-match anchor grouping。
Code reference:
master@796ed310(2026-02-27) — pseudocode and mapping based on this commit
| Paper Concept | Source File | Key Class/Function |
|---|---|---|
| GiGPO 总体 advantage 组合 | gigpo/core_gigpo.py | compute_gigpo_outcome_advantage |
| Episode relative advantage(式 3) | gigpo/core_gigpo.py | episode_norm_reward |
| Anchor state grouping(式 6) | gigpo/core_gigpo.py | build_step_group |
| Step relative advantage(式 7) | gigpo/core_gigpo.py | step_norm_reward |
| 在 trainer 中接入 GiGPO | recipe/hgpo/hgpo_ray_trainer.py | compute_advantage 调用 core_gigpo.compute_gigpo_outcome_advantage |
| ALFWorld 训练脚本 | examples/gigpo_trainer/run_alfworld.sh | algorithm.adv_estimator=gigpo 等 launcher 参数 |
| WebShop 训练脚本 | examples/gigpo_trainer/run_webshop.sh | algorithm.gigpo.mode, env.max_steps=15 |
| Search QA 训练脚本 | examples/gigpo_trainer/run_search.sh | enable_similarity, similarity_thresh, env.max_steps=4 |
| Search 数据预处理 | examples/data_preprocess/preprocess_search_r1_dataset.py | process_single_row, tools_kwargs["search"] |
| GiGPO 配置默认项 | verl/trainer/config/ppo_trainer.yaml | algorithm.gigpo.* |
4. Experimental Setup(实验设置)
4.1 Datasets and scales
- ALFWorld:3,827 个 task instances,覆盖 6 类 household tasks:Pick, Look, Clean, Heat, Cool, Pick2。
- WebShop:超过 1.1M products、12k user instructions。
- Search-augmented QA:single-hop 包括 NQ、TriviaQA、PopQA;multi-hop 包括 HotpotQA、2Wiki、MuSiQue、Bamboogle。论文主表还报告了 Feverous。
4.2 Baselines
- Closed-source:GPT-4o、Gemini-2.5-Pro。
- Prompting agents:ReAct、Reflexion。
- RL baselines:PPO、RLOO、GRPO。
- Search QA baselines:R1-Instruct、Search-R1、ZeroSearch、StepSearch。
4.3 Training config
这里的数字优先引用 released launch scripts,而不是论文里的通用文字描述。
ALFWorld
- Config path:
examples/gigpo_trainer/run_alfworld.sh - Base model:
Qwen/Qwen2.5-1.5B-Instruct - Batch / grouping:
train_data_size=16,val_data_size=128,env.rollout.n=8 - Lengths / horizon:
data.max_prompt_length=2048,data.max_response_length=512,env.max_steps=50 - Optimizer:
actor.optim.lr=1e-6,actor.ppo_mini_batch_size=256,actor.ppo_micro_batch_size_per_gpu=32 - Regularization / RL:
use_kl_loss=true,kl_loss_coef=0.01,invalid_action_penalty_coef=0.1,gamma=0.95,algorithm.gigpo.step_advantage_w=1.0,algorithm.gigpo.mode=mean_std_norm - Hardware / runtime:
trainer.n_gpus_per_node=2,rollout.tensor_model_parallel_size=2,trainer.total_epochs=150
WebShop
- Config path:
examples/gigpo_trainer/run_webshop.sh - Base model:
Qwen/Qwen2.5-1.5B-Instruct - Batch / grouping:
train_data_size=16,val_data_size=128,env.rollout.n=8 - Lengths / horizon:
data.max_prompt_length=4096,data.max_response_length=512,env.max_steps=15 - Optimizer:
actor.optim.lr=1e-6,actor.ppo_mini_batch_size=64,actor.ppo_micro_batch_size_per_gpu=8 - Regularization / RL:
use_kl_loss=true,kl_loss_coef=0.01,invalid_action_penalty_coef=0.1,gamma=0.95,algorithm.gigpo.step_advantage_w=1.0,algorithm.gigpo.mode=mean_norm - Hardware / runtime:
trainer.n_gpus_per_node=2,rollout.tensor_model_parallel_size=2,trainer.total_epochs=150
Search-augmented QA
- Config path:
examples/gigpo_trainer/run_search.sh - Base model:
Qwen/Qwen2.5-7B-Instruct - Batch / grouping:
train_data_size=256,val_data_size=512,env.rollout.n=5 - Lengths / horizon:
data.max_prompt_length=4096,data.max_response_length=512,env.max_steps=4,env.history_length=4 - Optimizer:
actor.optim.lr=1e-6,lr_warmup_steps_ratio=0.1,actor.ppo_mini_batch_size=512,actor.ppo_micro_batch_size_per_gpu=16 - Regularization / RL:
kl_loss_coef=0.001,invalid_action_penalty_coef=0.01,gamma=0.95,algorithm.gigpo.mode=mean_std_norm,algorithm.gigpo.enable_similarity=True,algorithm.gigpo.similarity_thresh=0.9 - Hardware / runtime:
trainer.n_gpus_per_node=8,trainer.total_epochs=1
Script coverage note
论文主结果还报告了 Qwen2.5-3B / 7B 的部分 setting,但当前 released repo 中最直接可定位的 launcher 主要是 1.5B 的 ALFWorld / WebShop 和 7B 的 Search。对于未在仓库中显式暴露的 model-size-specific launcher,笔记只引用论文表格结果,不臆造训练命令。
4.4 Metrics
- ALFWorld:6 个子任务成功率与 overall success rate。
- WebShop:average score 与 success rate。
- Search QA:各数据集准确率,以及平均分
Avg.。 - Efficiency:search QA 额外报告平均工具调用次数。
5. Experimental Results(实验结果)
5.1 Main results on ALFWorld and WebShop
Figure 5 解读:这张图不是主精度图,而是成本图。作者用它支撑一个关键论点:GiGPO 的新增部分几乎不是 LLM 计算,而只是 hashmap 分组和简单算术,因此训练时间几乎和 GRPO 一样。对 agent RL 来说,这一点很关键,因为 credit 更细通常意味着 rollout 或 critic 成本更高,而 GiGPO 尽量避免了这一点。
在 Qwen2.5-1.5B 上,GiGPO 已经显著超过 PPO / RLOO / GRPO:
- ALFWorld All:GRPO
72.8 ± 3.6,GiGPO w/ std86.7 ± 1.7,GiGPO w/o std86.1 ± 4.7 - WebShop Success:GRPO
56.8 ± 3.8,GiGPO w/ std65.0 ± 3.2,GiGPO w/o std67.4 ± 4.5 - WebShop Score:GRPO
75.8 ± 3.5,GiGPO w/ std83.1 ± 1.6,GiGPO w/o std83.5 ± 1.8
在 Qwen2.5-7B 上,提升依然存在,而且 closed-source prompting 仍明显落后于 post-training:
- Gemini-2.5-Pro:ALFWorld
60.3,WebShop success35.9 - PPO:ALFWorld All
80.4 ± 2.7,WebShop success68.7 ± 5.1 - GRPO:ALFWorld All
77.6 ± 5.2,WebShop success66.1 ± 3.7 - GiGPO w/ std:ALFWorld All
90.2 ± 2.3,WebShop success73.9 ± 1.7 - GiGPO w/o std:ALFWorld All
86.2 ± 2.6,WebShop success75.2 ± 3.8
从总体趋势看,GiGPO 在 ALFWorld 上相对 GRPO 提升超过 12 个点,在 WebShop success 上提升超过 9 个点,这与摘要中的 claim 一致。值得注意的是,w/ std 与 w/o std 在不同任务上各有胜负,说明真正决定性能的不是某一种归一化,而是 step-level 与 episode-level credit 的结合。
5.2 Search-augmented QA

Figure 6 解读:这张图实际上来自开源框架附录,但它能帮助理解 search QA setting 为何适合 GiGPO。左边的 full-history concatenation 会让多轮 search agent 的上下文快速膨胀;右边 verl-agent 用 step-wise multi-turn rollout 与可控 memory 组织交互,使 step-level anchor grouping 有了稳定的工程载体。换句话说,GiGPO 的算法收益和 verl-agent 的多轮执行框架是配套的。
Search QA 主表中,GiGPO 在 Qwen2.5-3B-Instruct 上取得:
- NQ
42.0 - TriviaQA
59.5 - PopQA
42.4 - HotpotQA
36.9 - 2Wiki
37.0 - MuSiQue
12.6 - Bamboogle
64.1 - Avg.
42.1
对应 baseline:
- R1-Instruct Avg.
19.8 - Search-R1 Avg.
35.9 - ZeroSearch Avg.
34.4 - StepSearch 未给出统一 Avg.,但在 HotpotQA / 2Wiki / MuSiQue / Bamboogle 上分别为
34.5 / 32.0 / 17.4 / 34.4
在 Qwen2.5-7B-Instruct 上,GiGPO 结果进一步提升到:
- NQ
46.4 - TriviaQA
64.7 - PopQA
46.1 - HotpotQA
41.6 - 2Wiki
43.6 - MuSiQue
18.9 - Bamboogle
68.9 - Avg.
47.2
同尺度 baseline 为:
- R1-Instruct Avg.
22.4 - Search-R1 Avg.
38.5 - ZeroSearch Avg.
39.1 - StepSearch 在 multi-hop / Bamboogle 上为
38.6 / 36.6 / 22.6 / 40.0
作者还强调工具效率:在最多 3 次 tool call 限制下,7B 模型 single-hop 平均只需约 0.9 次调用、multi-hop 约 1.6 次,已接近 OTC 的 ~1.0 / ~1.7。这说明 GiGPO 学到的不只是“更敢搜”,而是“更少做无效重复搜索”。
5.3 Ablation and dynamics
Figure 7 解读:左图是 ALFWorld 成功率曲线,右图是 step-level group size 分布随训练变化。早期大量大 group 表示策略常陷入重复状态和循环;后期分布向 6 到 8 收敛,说明同一 episode group 内多条轨迹越来越一致,策略开始学会稳定完成任务。这个现象从机制层面支持了 GiGPO:它确实在利用“重复状态”这个结构,而不是偶然拟合 benchmark。
- 去掉 episode relative advantages(
w/o A^E)会在所有任务上显著下降,因为模型失去全局轨迹一致性信号。 - 去掉 step relative advantages(
w/o A^S)在 Cool、Pick2、WebShop 等更复杂任务上下降尤其明显,说明细粒度 credit 对难任务至关重要。 w/ std与w/o std的差异相对小于结构性 ablation,表明真正有效的是“双层 advantage”,不是某个特定归一化 trick。
5.4 Computational cost
附图给出的时间分解显示,GiGPO 的额外成本几乎可以忽略:
- 主体 rollout + old/ref logprob + policy update 总计约
362.83s / iter - Anchor state grouping 只需约
0.01s / iter - Step-relative advantage computation 只需约
0.53s / iter
因此 GiGPO 的结论不是“更强但更贵”,而是“几乎同样的显存和 rollout 成本下,获得更细 credit 与更高性能”。
5.5 Limitations
- 论文没有公开所有主表 setting 的逐一 launch scripts,尤其是部分 3B / 7B 结果在 released repo 中缺少一一对应的命令文件。
- similarity-based GiGPO 当前只在 text-only search task 中明确开放;
gigpo/core_gigpo.py也直接限制“Only text-based observations are supported for similarity-based GiGPO in this version.” - 方法依赖跨轨迹出现重复状态;如果环境几乎不重访状态,step-level grouping 的密度会下降,GiGPO 收益可能变小。
5.6 Overall conclusion
GiGPO 的贡献可以压缩成一句话:它把 group-based RL 从“只能比较整条轨迹”推进到“还能比较同一状态下的动作”,而且几乎不增加 rollout 或显存成本。对 LLM agent 训练来说,这是一种非常实用的中间点:不引入 critic,不做额外 per-state rollout,却能显著改善 long-horizon multi-turn decision making。