# PR #2536 完整报告

- 仓库：`radixark/miles`
- 标题：Carry the rollout id on both agentic paths
- 合并时间：2026-08-14 11:47
- 原文链接：http://prhub.com.cn/radixark/miles/pull/2536

---

# 执行摘要

- 一句话：修复 agentic v1/v2 的 rollout_id 不一致
- 推荐动作：值得精读，因为这是 session v1/v2 parity 设计与后处理语义交汇处的一次小而关键的修正。建议重点理解两点：一是为什么只对 v2 多叶写 rollout_id，二是它与 #2347 如何在同一个守卫上收敛；这两点对后续 agentic rollout 逻辑有直接影响。

# 功能与动机

PR body 指出 `tests/e2e/sglang/test_session_v1_v2_parity.py::test_qwen3_8b_h200_fa3_agentic_v2_drop_retries_matches_v1_training_payload_bitwise` 在 main 上稳定复现：AssertionError: sample.rollout_id is not bitwise equal left = None, right = 0。根因是 agentic_tool_call.py 只在 v2 分支把 input.sample.index 写进 sample.rollout_id，v1 分支从不写，而 parity 测试逐字段位级比较训练 payload，两条路径必然冲突。PR body 明确说 rollout id 是样本身份，不是 v2 概念，两条路径应当对齐。

# 实现拆解

1. 变更入口：`miles/rollout/generate_hub/agentic_tool_call.py` 的 `generate()`，在 `collect_samples` 返回后对 `result.samples` 做后处理。原始提交尝试把 rollout_id 的推导提升到分支外，让 v1 也能按需戳印，从而直接满足 parity 测试。
2. 重定基修正：因 #2347 已把同一条件改为 `if use_v2 and len(samples) > 1`，guapisolo 将本 PR 调整为相同条件。v2 多叶结果需要共享 rollout id 以标识样本归属；v1 与 v2 单例结果保持 `None`，从而保留普通后处理（样本裁剪、动态全局 batch 元数据），避免触发 compact 判定。
3. 配套验证：依赖现有测试，`tests/fast/rollout/generate_hub/test_agentic_v2.py` 的 7 个用例与 `ruff check agentic_tool_call.py` 通过；PR body 记录 CI lane 从 4/5 提升到 5/5 通过，证明问题被真实修复而非被跳过。本次没有新增或修改测试文件。

关键文件：
- `miles/rollout/generate_hub/agentic_tool_call.py`（模块 样本生成；类别 source；类型 core-logic；符号 generate）: 唯一改动文件。generate() 中 v2 分支原无条件对每个样本写 rollout_id，导致 v1/v2 parity 位级断言失败；将条件收窄为 v2 多叶场景后，单例语义与 v1 一致。

关键符号：generate

## 关键源码片段

### `miles/rollout/generate_hub/agentic_tool_call.py`

唯一改动文件。generate() 中 v2 分支原无条件对每个样本写 rollout_id，导致 v1/v2 parity 位级断言失败；将条件收窄为 v2 多叶场景后，单例语义与 v1 一致。

```python
# miles/rollout/generate_hub/agentic_tool_call.py —— generate() 中样本收集完成后的核心处理

samples = result.samples

# rollout id 只对 v2 多叶结果做共享标注：多叶展开需要用 id 标识样本归属。
# v1 与 v2 单例结果一律保持 None，避免后处理误判为 compact rollout，
# 从而保住样本裁剪、动态全局 batch 元数据等普通路径。
if use_v2 and len(samples) > 1:
    # FIXME: handle sample index issues.
    rollout_id = input.sample.rollout_id if input.sample.rollout_id is not None else input.sample.index
    assert rollout_id is not None, "v2 agentic samples require input Sample.rollout_id or Sample.index"
    for sample in samples:
        sample.rollout_id = rollout_id

# v1 路径：agent 元数据由驱动侧合并；v2 的元数据已随 wire 返回，无需在此叠加。
if not use_v2:
    for s in samples:
        s.metadata.update(agent_metadata or {})

```

# 评论区精华

guapisolo 在 issue 评论中澄清最终行为：本分支对 v1 与 v2 单例结果保持 `rollout_id=None`，仅通过 `if use_v2 and len(samples) > 1` 为 v2 多叶结果分配共享 rollout id，并说明这样可以保留样本裁剪与动态全局 batch 元数据等普通后处理。此外 guapisolo 的批准意见为 `Do changes to align with session server v2 design`，表示该条件与 session server v2 的设计意图一致。PR body 也预判了与 #2347 的 rebase 冲突，最终两个意图兼容并收敛到同一条件。

- rollout_id 戳印语义：两条路径都打还是仅 v2 多叶打 (design): 采用仅 v2 多叶打 rollout_id，保留 v1 与 v2 单例的 None；PR 描述已过时，以评论记录为准。
- 与 #2347 的同一个守卫改动冲突 (other): 本 PR 最终头版本与该条件收敛一致，两个意图兼容；审阅人批准意见表示与 session server v2 设计对齐。
- parity 测试与 CI 验证 (testing): 问题被真实修复；快速验证未重跑 e2e parity，依赖 CI 覆盖。

# 风险与影响

- 风险：行为风险：v2 单例不再无条件获得 rollout_id，依赖该字段的下游（如 dashboard、训练数据转换）需要确认对单例路径没有隐含假设；现有测试通过但不代表所有消费方都被覆盖。测试覆盖：本次没有新增断言，e2e parity 验证依赖 CI；若未来 parity 测试扩展到多叶场景，v1 多叶仍无 rollout_id，可能再次暴露不一致。跨 PR 协调：#2347 与本 PR 改同一守卫，时序上已通过 rebase 解决，后续改动需注意保持单一职责。整体是单行核心逻辑变更，风险面较小但处于 rollout 主路径。
- 影响：影响范围覆盖 agentic rollout 的 session v1/v2 样本生成路径，以及依赖 `sample.rollout_id` 的训练数据转换与 parity 校验。对用户而言，修复了训练 payload 中 rollout_id 不一致导致的确定性失败，且不会让 v1 承接到新的失败模式；对团队而言，CI 恢复绿色，同时明确了 v2 多叶与单例在 rollout_id 上的语义边界。该改动为单行条件收窄，无接口或配置变更。
- 风险标记：核心路径变更 , 缺少新增测试 , 跨 PR 交互

# 关联脉络

- PR #2129 test(session): validate H200 v1/v2 agentic parity: 该 PR 添加了位级 parity 测试，把 v1/v2 的 sample.rollout_id 差异变成确定性失败，是本次修复的直接触发点。
- PR #2368 fix(rollout): group session v2 leaf samples: 该 PR 引入 v2-only 的 sample.rollout_id 戳印逻辑，是根因来源；本 PR 收窄同一逻辑。
- PR #2347 （标题未提供，见 PR body 提及的同一守卫改动）: PR body 与最终提交都对同一守卫 `if use_v2 and len(samples) > 1` 有改动，两个意图最终收敛为相同条件，存在 rebase 交互。