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/ stdw/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 trajectories
def 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_uids
def 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 scores
def 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_loss

3.7 Code-to-paper mapping

论文公式与 released code 实现差异:论文把 KL 写在总体目标的最后一项里,而 released training scripts(如 examples/gigpo_trainer/run_alfworld.shrun_webshop.shrun_search.sh)设置的是 algorithm.use_kl_in_reward=False,同时启用 actor.use_kl_loss=Truekl_loss_coef。也就是说,代码把 KL 当作 actor loss 中的独立项,而不是 reward shaping 的一部分。另一个差异是 search 任务在论文中明确采用 similarity-based GiGPO,released code 通过 enable_similarity=Truesimilarity_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 ConceptSource FileKey Class/Function
GiGPO 总体 advantage 组合gigpo/core_gigpo.pycompute_gigpo_outcome_advantage
Episode relative advantage(式 3)gigpo/core_gigpo.pyepisode_norm_reward
Anchor state grouping(式 6)gigpo/core_gigpo.pybuild_step_group
Step relative advantage(式 7)gigpo/core_gigpo.pystep_norm_reward
在 trainer 中接入 GiGPOrecipe/hgpo/hgpo_ray_trainer.pycompute_advantage 调用 core_gigpo.compute_gigpo_outcome_advantage
ALFWorld 训练脚本examples/gigpo_trainer/run_alfworld.shalgorithm.adv_estimator=gigpo 等 launcher 参数
WebShop 训练脚本examples/gigpo_trainer/run_webshop.shalgorithm.gigpo.mode, env.max_steps=15
Search QA 训练脚本examples/gigpo_trainer/run_search.shenable_similarity, similarity_thresh, env.max_steps=4
Search 数据预处理examples/data_preprocess/preprocess_search_r1_dataset.pyprocess_single_row, tools_kwargs["search"]
GiGPO 配置默认项verl/trainer/config/ppo_trainer.yamlalgorithm.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/ std 86.7 ± 1.7,GiGPO w/o std 86.1 ± 4.7
  • WebShop Success:GRPO 56.8 ± 3.8,GiGPO w/ std 65.0 ± 3.2,GiGPO w/o std 67.4 ± 4.5
  • WebShop Score:GRPO 75.8 ± 3.5,GiGPO w/ std 83.1 ± 1.6,GiGPO w/o std 83.5 ± 1.8

Qwen2.5-7B 上,提升依然存在,而且 closed-source prompting 仍明显落后于 post-training:

  • Gemini-2.5-Pro:ALFWorld 60.3,WebShop success 35.9
  • PPO:ALFWorld All 80.4 ± 2.7,WebShop success 68.7 ± 5.1
  • GRPO:ALFWorld All 77.6 ± 5.2,WebShop success 66.1 ± 3.7
  • GiGPO w/ std:ALFWorld All 90.2 ± 2.3,WebShop success 73.9 ± 1.7
  • GiGPO w/o std:ALFWorld All 86.2 ± 2.6,WebShop success 75.2 ± 3.8

从总体趋势看,GiGPO 在 ALFWorld 上相对 GRPO 提升超过 12 个点,在 WebShop success 上提升超过 9 个点,这与摘要中的 claim 一致。值得注意的是,w/ stdw/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 advantagesw/o A^E)会在所有任务上显著下降,因为模型失去全局轨迹一致性信号。
  • 去掉 step relative advantagesw/o A^S)在 Cool、Pick2、WebShop 等更复杂任务上下降尤其明显,说明细粒度 credit 对难任务至关重要。
  • w/ stdw/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。