这篇文章对应视频:【[Agentic RL] [VeRL] 08 MultiTurn Tool Use, Coding Agent SFT 训练,Cold Start for RL】(BV19PmwB5ERd)。

但我不会把它写成“逐句复盘”的笔记,而是把它抽象成一套你能复用的工程方法论:如果你要做一个 multi-turn tool-use 的 coding agent,并且希望后续能接上 PPO/GRPO/DAPO 这类 RL 训练,你在 数据、mask、rollout 结构、reward 形态 上要先把哪些坑填平。

系列导航:

关联阅读(建议顺序):

  1. SFT trainer 主篇:交叉熵 / loss mask / scheduler
  2. SFT 补充:teacher forcing / shift / mask 对齐
  3. RL 算法侧:GRPO/RLOO/REINFORCE++ 与 baseline
  4. 推理侧:vLLM 参数、显存/吞吐调优
  5. Tokenizer 非对称性与 Token-in-Token-out(RL 训练崩溃的根因)

配套仓库(你本地已下载)里,这篇最相关的材料(我会在文中引用这些路径):

  1. ReTool 的整体 pipeline(cold-start SFT + RL + interleaved rollout):
    • /Users/wangpeng/Downloads/modern_genai_bilibili-main/agentic_rl/verl/retool/ReTool-overall.ipynb
  2. Multi-turn SFT 数据与 loss_mask 可视化:
    • /Users/wangpeng/Downloads/modern_genai_bilibili-main/agentic_rl/verl/retool/ReTool-sft.ipynb
  3. Agent loop 的架构与异步 rollout:
    • /Users/wangpeng/Downloads/modern_genai_bilibili-main/agentic_rl/verl/agent/agent_loop_arch.ipynb
    • /Users/wangpeng/Downloads/modern_genai_bilibili-main/agentic_rl/verl/agent/agent_loop_details.ipynb
    • /Users/wangpeng/Downloads/modern_genai_bilibili-main/agentic_rl/verl/agent/agent_loop_code.ipynb
    • /Users/wangpeng/Downloads/modern_genai_bilibili-main/agentic_rl/verl/agent/agent_loop_config.ipynb
  4. reward shaping(在 tool-use 场景里,经常是 cold start 成败的关键):
    • /Users/wangpeng/Downloads/modern_genai_bilibili-main/agentic_rl/reward_model/reward-shaping.ipynb

0. 为什么 MultiTurn Tool Use + Coding Agent 比“普通 RLHF”难一个数量级

把“对话 SFT + RL”直接搬到 tool-use agent 上,最常见的失败模式不是“跑不起来”,而是“能跑但学不到”。

原因是 tool-use agent 的交互过程具备这些特征:

  1. 轨迹是 interleaved 的:LLM 生成 token → 调工具 → 工具返回(非策略生成)→ LLM 继续生成。
  2. 你需要两套 mask
    • SFT:loss_mask(哪些 token 参与交叉熵)。
    • RL:response_mask / action_mask(哪些 token 是策略动作,参与 logprob、ratio、KL、entropy 等计算)。
  3. reward 延迟且稀疏:coding agent 常见 reward 是“测试是否通过 / patch 是否正确”,中间步骤 reward 很弱。
  4. 环境噪声很大
    • 工具可能失败(网络/超时/权限/依赖缺失)。
    • 代码执行的 stderr/stdout 会引入大量 token,污染序列长度与训练稳定性。

一个实用结论是:

“tool-use RL”不是在 PPO/GRPO 上加一个工具就行了,你需要先把 trajectory 的可训练部分定义清楚。

后面这篇文章会围绕这件事展开。


1. ReTool Pipeline 的核心:Cold-Start SFT → RL(带工具)→ Interleaved Rollout

ReTool-overall.ipynb 把训练 pipeline 拆得很清楚,我建议你把它当成一个“工程模板”:

  1. Phase 1: Cold-Start SFT
  2. Phase 2: RL with Tool Integration
  3. Interleaved Rollout / Generation(LLM token 与 tool output 交错)

我用一张图把它画成闭环(不是论文图,是工程视角的“你会踩坑在哪里”图):

1
2
3
4
5
6
flowchart TD
A["Cold-Start SFT (tool-use traces)"] --> B["Policy Initialization (可解析/可执行协议)"]
B --> C["Interleaved Rollout (LLM tokens + Tool I/O)"]
C --> D["Reward + Metrics (pass/fail, parse rate, cost)"]
D --> E["RL Update (PPO/GRPO/DAPO + KL anchor)"]
E --> B

这里最容易被低估的是 Cold-Start SFT:它不是“可选项”,而是“定义动作空间”的一步。

如果 cold start 不稳,RL 会出现两种典型崩法:

  1. 策略学会逃避工具:用胡编的自然语言绕开工具调用,骗过弱 reward 或弱 verifier。
  2. 策略学会滥用工具:疯狂调用工具制造长序列,KL/entropy/ratio 统计彻底失真,训练发散。

2. 数据结构:messages/tools 的“协议”决定你能不能做 RL

ReTool-sft.ipynb 的思路,multi-turn tool-use SFT 的样本基本长这样:

  • messages: 一个列表,元素有 role(system/user/assistant/tool)与 content(或 tool_call 字段)
  • tools: 可调用函数(或工具)的 schema 列表

你做 coding agent 时,常见工具可以抽象成三类:

  1. 执行器code_interpreter / sandbox runner(输入 code,输出 stdout/stderr/returncode)
  2. 检索器:search / doc lookup(输入 query,输出结果片段)
  3. 文件系统:read/write/list(输入 path,输出内容)

工程建议(和模型/框架无关):

  1. 工具 schema 先冻结再训练:schema 每改一次,你的“协议分布”就变一次,SFT 与 RL 的收敛都会变差。
  2. tool output 永远不要当作监督目标:它属于环境观测(observation),不是 action。

3. 两套 mask:loss_mask(SFT)与 response_mask(RL)

3.1 SFT 的 loss_mask:只监督 assistant 的输出

这一点在你之前的文章里已经讲过,但它在 coding agent 上更关键:

  • system/user/tool 的 token:mask=0
  • assistant 的 token:mask=1(包含 tool_call 协议片段 + final answer)

如果你把 tool output 也训了,模型会倾向于“把工具返回当 continuation 生成”,这是 agent 最危险的坏习惯之一。

3.2 RL 的 response_mask:只对“策略生成的 token”算 logprob/ratio/KL

agent_loop_details.ipynb 有一个非常关键的标注(我把它翻成一句话):

  • LLM 生成的 token:mask=1
  • Tool 返回的 token:mask=0

这件事如果错了,你的 RL 训练会出现非常诡异的现象:

  1. KL 看起来异常大(因为你把 tool output 当成策略生成,而策略从未“生成过它”)
  2. ratio 统计爆炸(logp_old/logp_new 对不上)
  3. clip fraction 长期高或长期为 0(完全失真)

所以我建议你在实现里把 mask 语义写死成两句话(写进注释/日志里):

  1. loss_mask: supervised labels(SFT)
  2. response_mask: policy actions(RL)

两者都可能是 [B, T],但语义完全不同。


4. Cold-Start SFT:coding agent “能用”比“聪明”更重要

ReTool-overall.ipynb 把 cold start 放在第一阶段,我非常认同。

对 coding agent 来说,cold-start SFT 的目标不是把它训成“最强写码者”,而是:

  1. 稳定地产生可执行的工具调用
  2. 稳定地把工具返回转成下一步行动(多轮闭环)
  3. 输出结构可评测(比如最终 patch/答案有固定格式)

4.1 你应该盯的 3 个指标(比 loss 更重要)

  1. tool-call parse rate(可解析率)
  2. tool-call success rate(工具执行成功率)
  3. end-to-end success 的 proxy(例如:单测通过率、静态检查通过率、编译成功率)

它们决定了你后续 RL 的“可学习信号”是否存在。

4.2 冷启动数据从哪来:不要迷信人工标注

对于 tool-use / coding agent,人工标注高质量多轮轨迹成本很高。更常见的冷启动方式是混合:

  1. 少量人工高质量轨迹(定义协议上限)
  2. 大量合成轨迹(覆盖分布)
  3. rejection sampling(用 verifier/规则过滤)

注意:合成轨迹不是“越多越好”,而是“越能覆盖错误模式越好”。你需要刻意包含失败样例,让模型学会修复流程,而不是只学会一条成功路径。


5. RL with Tool Integration:reward 形态决定你用不用 shaping

coding agent 的 reward 典型是:

  • 稀疏:是否通过 tests(0/1)
  • 高噪声:依赖环境、随机种子、外部网络

这时 reward-shaping.ipynb 的思路就很实用:你可以给工具调用增加一些 过程性奖励(shaping),让 credit assignment 不至于完全靠最终成败。

常见 shaping(只要能被稳定评测,就值得):

  1. 工具调用格式正确(JSON/schema pass)
  2. 工具执行成功(returncode=0)
  3. 关键中间产物出现(比如生成了 patch、生成了测试命令并执行)

反例(我不建议):

  1. 用模型自评打分当 shaping(会被 reward hacking)
  2. 用“输出更长/更像思考”当 shaping(会被 length hacking)

6. Agent Loop:为什么 async rollout 是必需品(不是优化项)

tool-use agent 的最大吞吐瓶颈往往不是 GPU,而是工具 IO(网络、文件、执行器)。

agent_loop_details.ipynb 用“同步点”解释了一个关键问题:如果你按 batch 同步推进 multi-turn,你会被最慢的那个样本拖死。

工程上更合理的做法是:

  1. 每个 sample 一个 async task
  2. 每个 task 跑自己的状态机(LLM → tool → LLM → …)
  3. 全部完成后再 merge 成 batch(在“回传梯度/做更新”处同步)

你可以把它理解成:

rollout 阶段尽可能异步,update 阶段必须同步。

这也是 agent_loop_arch.ipynb 里反复强调的 server-based async rollout 的意义。

6.1 从代码抽象看架构:Manager / Worker / AgentLoop / ServerManager 各管一件事

如果你读 agent_loop_arch.ipynb,会看到一个非常“工程化”的拆分方式(我把它总结成四个角色):

  1. AgentLoopManager:接收 Trainer 的“生成一批 rollout”请求,把 batch 切块并分发给多个 Worker。
  2. AgentLoopWorker:在 CPU 节点上跑并发(asyncio),每个 sample 一个 task,避免 straggler 拖垮整批。
  3. AgentLoop(例如 ToolAgentLoop):只负责“怎么聊/怎么调工具”的状态机逻辑(PENDING→GENERATING→TOOLS→…)。
  4. AsyncLLMServerManager:只负责“怎么生成”(推理服务网关 + 负载均衡 + sticky session)。

你可以把它画成一条直线:

1
2
3
4
5
flowchart LR
T["Trainer / PPOTrainer"] --> M["AgentLoopManager"]
M --> W["AgentLoopWorker (CPU, asyncio)"]
W --> L["AgentLoop (ToolAgentLoop / SingleTurnAgentLoop)"]
L --> S["AsyncLLMServerManager (vLLM/SGLang servers)"]

这个拆分的好处是:你后续想改“工具协议/状态机”,只需要改 AgentLoop;想改并发策略/worker 数量,只需要改 Worker/Manager;想换推理引擎,只需要改 ServerManager。

6.2 两个关键点:async task 粒度 + 状态机

在 async rollout 里,“并发粒度”基本决定你能不能把 GPU 吃满。

如果你按 “一个 batch 一个 coroutine” 并发,那你仍然会被最慢样本拖住;真正有效的是:

  • 每个 sample 一个 coroutine(task):快样本先结束,慢样本继续跑,GPU 侧的 continuous batching 可以不断接新请求。

状态机这块,你至少要建立这 5 个状态的直觉(工具调用 agent 基本都能套进来):

1
2
3
4
5
6
7
8
9
10
11
stateDiagram-v2
[*] --> PENDING
PENDING --> GENERATING
GENERATING --> PROCESSING_TOOLS
GENERATING --> INTERACTING
GENERATING --> TERMINATED
PROCESSING_TOOLS --> GENERATING
PROCESSING_TOOLS --> TERMINATED
INTERACTING --> GENERATING
INTERACTING --> TERMINATED
TERMINATED --> [*]

最重要的一条工程原则是:“环境/工具返回”永远被当成 observation,绝不当成 action。

6.3 response_mask:interleaved trajectory 里怎么只对“策略 token”求梯度

multi-turn tool-use 轨迹是交错的:

  • LLM 生成(action)
  • tool 返回(observation)
  • LLM 继续生成(action)

RL 训练(PPO/GRPO)需要计算 logprob_new/logprob_old、ratio、KL、entropy 等,这些量只能对 LLM 生成出来的 token 有意义。

所以实现里会维护一个 response_mask(或 action_mask):

  • LLM 生成 token:mask=1
  • tool 输出 token:mask=0

这样做的直接效果是:计算 policy gradient 时只对 mask=1 的 token 回传梯度,tool 输出不会污染 logprob/ratio/KL。

把它写成伪代码(对齐 agent_loop_details.ipynb 的核心片段):

1
2
3
4
5
6
7
# after LLM generate:
response_ids += llm_generated_ids
response_mask += [1] * len(llm_generated_ids)

# after tool execution:
response_ids += tool_output_ids
response_mask += [0] * len(tool_output_ids)

你要把这条规则当作 debug 的第一优先级:一旦 mask 语义错,后面 KL/ratio/clipfrac 全会“看起来像随机数”。

6.4 Async rollout 里的 reward 计算:为什么会被“前置”到 rollout 阶段

agent_loop_details.ipynb 里还强调了一个容易被忽略的点:

  • 在 async rollout 模式下,reward 计算经常被放到 Worker 内部完成(rollout 阶段),并把 rm_scores 附着到轨迹上返回 Trainer;
  • Trainer 看到 batch 已经包含 rm_scores,就会跳过“再算一遍 reward”。

这在 tool-use/coding agent 场景很常见,因为 reward 往往需要:

  • 真实执行(跑测试/编译/静态检查)
  • 或者调用 verifier(额外模型/规则系统)

它们都是 IO/CPU 重,不适合卡住 GPU 推理进程。

6.5 Sticky session:request_id 为什么重要

一条 tool-use 轨迹会多次调用推理引擎:Prompt→Tokens→ToolResult→Prompt→…

当你有多个推理 server replica 时,如果每次都“随机路由”,你可能会在工程上踩到两类坑:

  1. 同一条轨迹跨 replica 迁移,KV cache / session 管理变复杂(甚至实现不支持)。
  2. 负载均衡短期抖动导致 tail latency 变大(多轮交互把延迟放大)。

因此 AsyncLLMServerManager 通常会用 request_idsticky session:同一条轨迹固定路由到同一 replica(配合 LRU 缓存/heap-based 最少请求优先等策略)。

6.6 规模化时怎么数“replica”:num_replicas vs 引擎内部 DP

agent_loop_config.ipynb 里有一段很实用的“数服务实例”公式(把它当作配置 sanity check):

  • 一个 rollout server 实例内部可能用了 TP/DP/PP(引擎内部并行),其 world size 为:

$$\text{rollout_world_size}=\text{TP}\times \text{DP}\times \text{PP}$$

  • 你总 GPU world size 为:

$$\text{world_size}=\text{n_gpus_per_node}\times \text{nnodes}$$

  • 那么可启动的独立推理服务 replica 数为:

$$\text{num_replicas}=\frac{\text{world_size}}{\text{rollout_world_size}}$$

这背后最关键的直觉是:

  • num_replicas(服务级 DP):多个完全独立的推理服务,靠 ServerManager 做负载均衡,容错更好,KV cache 各自独立。
  • data_parallel_size(引擎内部 DP):同一推理引擎内部做 DP,耦合更紧,可能共享状态,但通信/同步也更复杂。

7. Coding Agent 的“训练闭环”:SFT 里就要内置 debug 接口

coding agent 的训练最怕的是“训练看似稳定,但实际不可用”。我建议你在 SFT 阶段就内置 2 个 debug 钩子:

  1. 抽样打印(decode + mask 上色)
  2. 真实执行(把 tool-call 真的跑一遍,记录失败原因)

你可以把它当成 agent 的单元测试:SFT 训练前先跑 50 条样本,确保协议与工具都没问题。

(这部分你可以直接复用 ReTool-sft.ipynb 的 mask 可视化代码。)


8. Cold Start for RL:一个可落地的路线(不要求你一上来就烧很多卡)

最后我给一条“能跑起来且不容易跑歪”的路线。它不追求最少步骤,而追求可控:

  1. 先做 cold-start SFT,把 tool-call 协议训稳(parse rate、success rate)
  2. 做一个弱 verifier(规则 + 小模型都行),把“明显不合法的轨迹”筛掉
  3. RL 阶段先用强 KL anchor(离 SFT 很近),先学会“少犯错”
  4. reward shaping 从强到弱:先用过程性 shaping 提供密集信号,再逐步退火到 end-to-end reward
  5. 最终再做“更难任务”的 curriculum(不要一开始就 SWE-bench 级别)

这条路线对应的工程直觉是:

  • 先让 agent 变得“可执行”,再让 agent 变得“更优”
  • 先让 reward 信号稳定,再让 reward 目标更苛刻

9. 小结

MultiTurn Tool Use / Coding Agent 的核心不是“换个 RL 算法”,而是:

  1. 你定义的 trajectory 里,哪些 token 属于 action,哪些属于 observation(response_mask)
  2. 你在 SFT 与 RL 阶段分别监督什么(loss_mask vs response_mask)
  3. 你是否有一条稳定的 cold-start 数据路线(否则 RL 只会放大坏习惯)

如果你愿意,下一篇我可以继续沿着这条线把“RL 端的实现细节”补全成可直接对照框架的 checklist:

  1. interleaved rollout 下 logprob 的对齐与缓存
  2. tool output 的长度爆炸如何处理(truncate/pack/summary)
  3. reward manager 的工程分层(rule/verifier/RM/ensemble)