ReTool: Reinforcement Learning for Strategic Tool Use in LLMs
速读卡片 (TL;DR)
一句话:ReTool 把 Python code interpreter 当作 deterministic verifier-tool, 用 cold-start SFT 教会基础格式后, 用 outcome-only reward 的 PPO 让 LLM 自己学会什么时候、用什么代码来辅助推理——结果在 AIME 2024 上 Qwen2.5-32B-Instruct 训 400 步就达 67.0%, 比同 backbone 的纯 text RL (40.0%, 1080 步) 又快又强, 并 emergent 出 "Oops, let me fix that" 的 code self-correction 行为。
立场:当 reasoning 任务包含大量算术 / 枚举 / 符号操作时, 长 CoT RL 是 token efficiency 上的死胡同; 让 RL outcome reward 直接信号化"调用 Python interpreter 是不是更好的决策", 就能在不引入 process reward 的前提下涌现出 strategic tool use。这是 Search-R1 把搜索当 tool 的 code-domain 对偶。
1 · 动机: 为什么长 CoT RL 在数学题上撞墙
1.1 历史脉络: 从 PoT 到 long-CoT RL, 再到"长 CoT 不够"
从时间线看, "让 LLM 算数学" 这件事在 2023–2025 之间走了三段:
三段曲线的真正张力在第二段到第三段: long-CoT RL (R1-Zero 风格) 让模型把每一步算术都"用自然语言写"出来——但 LLM 对 9430 mod 7 这种多位数模运算的逐位推理本质上靠的是 token-level 的 implicit table lookup,错误率不为零, 且错一步导致后面整条 trajectory 报废。论文 Figure 6 的对比就是这个症状: 同一道四位数 mod 7 题, text-based reasoning 得出 N=5624 → Q+R=629 (错), CI-powered 得出 N=5694 → Q+R=699 (对)。
1.2 别的方案为什么不够 (对照表)
| 方案 | 怎么做 | 本质局限 |
|---|---|---|
| 纯 CoT prompting | "让我们 step by step" | 算术误差累积, 没 verifier |
| PoT / PAL (prompt-only) | 固定让模型生成完整 Python 程序 | 把 reasoning 全外包给代码, 失去 natural-language 推理的灵活性 |
| ToolFormer / MathCoder (SFT) | 用 self-curated 含工具调用的轨迹做 SFT | 只学到训练分布里的固定调用 pattern, 不会自适应判断"该不该调" |
| DeepSeek-R1-Zero / QwQ (long-CoT RL) | outcome reward + GRPO/PPO, 但纯文本 | 超长 CoT (10k+ tokens), 算术 token 多但效率低 |
| ToRL (concurrent, Qwen2.5-Math 1.5B/7B) | RL+tool 在小模型上 | scale 上不去, AIME 表现仍 suboptimal |
| ReTool (本文) | SFT cold-start → outcome-only RL, interpreter-mask, 32B scale | 填上了 "RL + tool + 大模型 + 大数据" 这格 |
关键观察是中间两行: SFT-tool 派和 long-CoT RL 派都看到了对方的缺陷, 但都没融合。SFT-tool 没有 outcome 信号, 只能模仿; long-CoT RL 有 outcome 信号, 但没工具。ReTool 的位置就是把这两件事在同一 trajectory内拼起来——模型一边写自然语言推理, 一边在合适的时机 <code>...</code> 出去, sandbox 跑完把 <interpreter>...</interpreter> 塞回上下文。
1.3 为什么这事不平凡
三件事让"RL + code interpreter"在工程上比想象的难:
- Mixed-source token 的 loss masking 必须做对。Trajectory 里既有 policy 自己生成的 token (会走梯度), 又有 sandbox 塞回来的
<interpreter>段 (外部环境产物)。如果 interpreter 输出参与 loss, 等于让模型去"学"它本不该产出的字符——会立刻发散。论文用 Interpreter Feedback Mask 把这段从 loss 里 mask 掉。 - Rollout 必须 pause-resume。每次生成到
</code>trigger 都要冻结 KV cache → 把 code 拿去执行 → 把 stdout 塞回 context → 继续 generate。论文做了 KV-cache reuse: code 之前的 KV 不重算, 只增量计算 interpreter feedback 段的 KV, 这才让 32B + multi-turn rollout 可负担。 - Sandbox 必须 async + load-balanced。同步等 Python 跑完会让 GPU 闲置; 论文用 pod-pool 的异步 sandbox, 每个 worker 按容量主动 pull 任务。这跟标准 RL infra 不同——environment 不是单 process 而是分布式服务。
- Cold-start 不能省。从 Qwen2.5-32B-Instruct 直接做 outcome RL, 模型连
<code>tag 都不会输出, reward 全是 -1, RL 完全无法启动。所以必须先做 SFT on DCI 把"调工具的语法"灌进去。 - Reward 只能粗 (outcome-only)。论文显式拒绝加 "code executability" reward——加了反而会 reward hacking (模型只为生成可执行代码而不解题)。单一 sparse 信号反而促成多样行为。
把这五点凑齐, 才能让"模型自己学会该不该调 Python"这件事真的在 RL loop 里收敛。Figure 3 给出的训练动力学就是这五件事都做对了之后的结果: 平均 response length 先降后稳, code ratio 上升到 98%, code lines 升至 5×, code invocation timing 从 ~0.62 (响应中后段) 提前到 ~0.52 (前半段)——所有指标都指向"模型把 Python 当 first-class reasoning primitive"这件事真的发生了。
2 · 背景速查
| 术语 | 含义 |
|---|---|
| Code Interpreter (CI) | 本文专指 Python sandbox, 接受 print()-based stdout 作为唯一返回通道 |
| Code-integrated reasoning | 一条 trajectory 里 natural language thought / <code> / <interpreter> 交错的格式 |
| Sandbox | 受限执行环境, 这里是 ByteDance 的 async pod pool, 每个 worker 拉任务跑 Python |
| Cold-start SFT | RL 前的 SFT 阶段, 让模型先学会输出格式 / 基本工具使用模式 |
| PPO | Schulman 2017。clipped importance ratio + value baseline 的 on-policy 算法 |
| Outcome reward | 只在 trajectory 终点判答案对错, 不给中间步骤打分 (与 process reward 相对) |
| Interpreter mask | RL 训练时, <interpreter>...</interpreter> 内的 token 不参与 loss / advantage 计算 |
| VeRL | Volcengine 开源的 RL training framework, 本文实现底座 |
| AIME | American Invitational Mathematics Examination, 30 题, 数学奥赛预选级难度 |
| TIR | Tool-Integrated Reasoning, Qwen2.5-Math-Instruct-TIR 的命名约定 |
| Pass@1 (avg@32) | 同一题用 temperature=1.0 重复采样 32 次取平均, 估计单次正确率 |
PPO 速复习 (本文唯一公式)
注意 ratio 的条件里多了 "; CI"——意思是 rollout 时 context 里穿插了 sandbox 返回的 interpreter feedback。这不是说 CI 进了 loss, 只是说生成 wt 时模型看到的上下文包含 interpreter 输出。Loss 仍只在 model-generated token 上算 (见 §5)。Reward 极简:
其中 a 是 ground-truth, â 是 model 在 \boxed{...} 里给出的最终答案。没有 step reward, 没有 KL 惩罚 (论文设 KL coef = 0.0), 没有 code-executability bonus。
3 · 方法详解
3.1 Trajectory format: thought / code / interpreter 交错
ReTool 的核心 abstraction 是把一条 rollout 形式化为:
其中 ti 是自然语言 thought (model 生成), ci 是 <code>```python ... ```</code> 段 (model 生成), fi 是 <interpreter>...</interpreter> 段 (sandbox 返回, 外部), o 是 <answer>\boxed{...}</answer>。i 的次数由 model 决定——可以一次都不调 (退化到纯 CoT), 也可以调五六次。
\boxed{} 由 rule-based verifier 给 ±1 reward, 没有 step reward。反向论证 (如果不 mask interpreter 段会怎样?) 假设把 fi 一起算 loss——这相当于在告诉模型 "你应该能输出 1000 mod 7 = 6 这个 stdout"。但这个 stdout 的概率分布完全由 Python 决定, 跟 model 的 reasoning 无关。Loss 会把 model 的输出分布拉向 Python 输出风格, 破坏自然语言段的 coherence。论文一句话称之为 "blocks external tokens from interfering with loss calculations, ensuring training stability"。
3.2 Cold-start SFT pipeline: 从 Dinit 到 DCI
这一步是最容易被忽略但工程上最关键的一步——直接做 RL 模型连 <code> 都不会写。Pipeline 三步:
Worked example: DCI 里一条样本长什么样
假设 Dinit 里原始 long-CoT 包含 "1000 mod 7 = 6, 100 mod 7 = 2, 10 mod 7 = 3, 1 mod 7 = 1, 所以方程变为 6+2B+3C+D≡0 (mod 7)" 这一段——这是典型的"模型 in-text 心算"段。Rewriter 拿 Figure 8 prompt 后, 把它改写成:
... 先计算 1000, 100, 10, 1 各自的 mod 7:
<code>
```python
mod_1000 = 1000 % 7
mod_100 = 100 % 7
mod_10 = 10 % 7
mod_1 = 1 % 7
print(f"1000 mod 7 = {mod_1000}")
print(f"100 mod 7 = {mod_100}")
print(f"10 mod 7 = {mod_10}")
print(f"1 mod 7 = {mod_1}")
```
</code>
<interpreter>
1000 mod 7 = 6
100 mod 7 = 2
10 mod 7 = 3
1 mod 7 = 1
</interpreter>
所以方程变为 6 + 2B + 3C + D ≡ 0 (mod 7) ...
关键 design choice (论文 Figure 8 第 6 条): "Execution results should match the model's output exactly, with no extra or missing tokens"——意思是合成数据里的 <interpreter> 段被要求和真实 Python 输出 byte-by-byte 一致。这是为了让 cold-start 模型在真实 RL rollout (届时 interpreter 段来自真 sandbox) 时不会经历 distribution shift。
Cold-start 单独的效果: 只做 SFT (不做 RL) 的 ReTool 在 AIME2024 上 = 40.9%, 已经追平 text-only RL 1080 步训练的 40.0%。这说明 DCI 本身就装得下大部分"调工具该怎么调"的先验; RL 阶段做的事是从这个 prior 里 selection——挑出"哪种调用方式真的更可能答对"的子分布。
3.3 RL stage: PPO + interpreter mask + KV reuse + async sandbox
RL 阶段在 SFT 模型上继续, 用 PPO + outcome reward。核心改动有四:
| 组件 | 具体做法 | 为什么必要 |
|---|---|---|
| Rollout loop | 检测到 </code> 即暂停生成, 把 code 发去 sandbox, 把 stdout/stderr 包成 <interpreter> 拼回 context, 继续 generate | 让 rollout 时模型看到的轨迹与最终 inference 完全一致 (no train-test mismatch) |
| Interpreter Feedback Mask | <interpreter>...</interpreter> 段 loss-mask = 0 | 不让外部 token 干扰 policy 梯度 |
| KV-cache Reuse | 每次 </code> 触发后, code 之前的 KV cache 全部冻结, 只 incremental prefill interpreter feedback 部分 | 32B + 16k seq 多轮 rollout 否则 OOM |
| Async Sandbox | pod pool 异步拉任务, 慢 thread 不阻塞快 thread | RL throughput 不被 Python 执行时间拖累 |
</code> trigger 只增量 prefill, 不重算 history; (下) 多个 rollout 异步把 code 扔给共享 pod pool, 慢任务不阻塞快任务。训练超参 (论文 §2.3.2 直接抄)
- Framework: VeRL; Optimizer: AdamW, lr = 1e-6
- Cold-start: 2 epochs on DCI
- Max seq length: 16,384 tokens (含 interpreter 段)
- Mini-batch: 512; KL coef = 0.0 (没有 reference model 锚定)
- Backbone: Qwen2.5-32B-Instruct (主实验) / DeepSeek-R1-Distill-Qwen-32B (强化版)
反向论证 (KL=0 为什么不爆?) 通常 RLHF/RLVR 训练里 KL coef 不为零是为防 policy 远离 ref model 后 reward hack。ReTool 敢设 0, 我猜的原因有两条: ① outcome reward 是 binary, hack 空间小; ② <code>/<interpreter> 这个 deterministic 通道把"正确答案"锁死, model 无法靠改风格骗到 reward。但这也意味着不能直接拿 ReTool 的 reward+KL setup 套到 reward 不稳的任务上。
3.4 Worked example: 一道 AIME 题完整 rollout
用论文 Figure 4 的真实 AIME 题 (硬币贪心):
From an unlimited supply of 1-cent, 10-cent, and 25-cent coins, ... Find the number of values of N between 1 and 1000 inclusive for which the greedy algorithm succeeds. (答案: 610)
训练好的 ReTool 模型实际产生的 rollout (节选, 真实 token 取自论文):
Alright, let's tackle this problem step by step using code to ensure accuracy.
The goal is to determine for each value of N from 1 to 1000 whether the
greedy algorithm produces the minimum number of coins. If it does, we count
that N. ...
<code>
```python
def test_specific_N(N_values):
for N in N_values:
greedy_result = greedy(N)
...
test_results = test_specific_N(N_values_to_test)
print(test_results)
```
</code>
<interpreter>
Traceback (most recent call last):
File "/tmp/tmpbr3uj7zz/tmpdmj39k6o.py", line 11, in <module>
test_results = test_specific_N(N_values_to_test)
File "/tmp/tmpbr3uj7zz/tmpdmj39k6o.py", line 5, in test_specific_N
greedy_result = greedy(N)
NameError: name 'greedy' is not defined
</interpreter>
Oops, the functions need to be defined in the same scope. Let's correct that:
<code>
```python
def greedy(N):
denominations = [25, 10, 1]
...
def test_specific_N(N_values):
for N in N_values:
greedy_result = greedy(N)
...
test_results = test_specific_N(N_values_to_test)
print(test_results)
```
</code>
<interpreter>
{42: (9, 6), 25: (1, 1), 10: (1, 1), 1: (1, 1), 1000: (40, 40)}
</interpreter>
...
<answer> \boxed{610} </answer>
把这条 rollout 拆开看 (蓝=model token, 黄=interpreter token):
| Step | 段 | 谁产生 | 进 loss? | 关键决策 |
|---|---|---|---|---|
| 1 | "Alright, ...using code to ensure accuracy" | policy | ✓ | 决定调工具 |
| 2 | 第一段 Python (含 bug) | policy | ✓ | 写代码 |
| 3 | NameError traceback | sandbox | ✗ (masked) | — |
| 4 | "Oops, the functions need to be defined ..." | policy | ✓ | self-correction: 不重新解释问题, 直接修代码 |
| 5 | 修复后的 Python | policy | ✓ | — |
| 6 | 正确 dict 输出 | sandbox | ✗ (masked) | — |
| ... | ... | ... | ... | ... |
| N | \boxed{610} | policy | ✓ | verifier 给 r = +1 |
整条 trajectory 的 advantage 怎么分配: PPO 用 GAE 把 terminal reward +1 折回到每一个 model-token 上 (黄段 mask 不参与); 因为 KL=0 且 outcome reward 是 sparse +1/-1, 所有 model token 的 advantage 大致同号。这意味着"Oops, let's correct"那一句跟"Alright, let's tackle"那一句拿到几乎相同的 positive advantage——所以下次遇到 NameError 类似情况, 模型生成 "Oops" 风格 self-correction 的概率会上升。这就是 emergent self-correction 的训练机理。
反向论证 (为什么不需要 process reward?) 如果加 "code 跑通 +0.5, 跑不通 -0.5" 的 process reward——第一次 NameError 那条整体答对的轨迹反而会被 process 部分扣分。模型学到的 lesson 可能变成"宁可不调工具也别写出 bug code", 直接抹掉 self-correction 行为。所以论文坚持 outcome-only, 留出 trial-and-error 的空间。
4 · 实验关键结果
4.1 主表 (AIME 2024 / 2025, pass@1 = avg@32)
| 模型 | AIME 2024 | AIME 2025 | 说明 |
|---|---|---|---|
| Qwen2.5-Math-72B-Instruct | 30.0 | — | 纯 CoT, 大 backbone |
| Qwen2.5-Math-72B-Instruct-TIR | 40.0 | — | tool-integrated SFT, 72B |
| OpenAI o1-preview | 44.6 | 37.9 | 闭源 long-CoT |
| DeepSeek-R1-Zero-Qwen-32B | 47.0 | — | long-CoT RL, 同 scale |
| QwQ-32B-Preview | 50.0 | 33.5 | long-CoT, 同 scale |
| s1-32B | 56.7 | — | test-time scaling |
| CI-powered RL | |||
| ReTool (Qwen2.5-32B-Instruct) | 67.0 | 49.3 | 400 steps |
| ReTool (DeepSeek-R1-Distill-Qwen-32B) | 72.5 | 54.3 | 更强 backbone |
| Ablations (Qwen2.5-32B-Instruct) | |||
| w/o Training (base) | 26.7 | — | — |
| w/o CI (Text-based RL) | 40.0 | 36.7 | 1080 steps |
| w/o RL (only Cold-start, infer with CI) | 40.9 | 34.5 | SFT 单独已追平 text-RL |
三个最 load-bearing 的读法:
- 67.0 vs 40.0 (同 backbone)。同样 Qwen2.5-32B-Instruct + 同样 outcome RL, 加 CI 比不加 CI 在 AIME2024 上多 27 个绝对百分点——这是 tool 的边际贡献, 而不是 backbone / data 的功劳。
- 40.9 (SFT only) vs 67.0 (SFT+RL)。SFT 把"会用工具"装好后, RL 再加 26 个百分点——这是RL outcome reward从 SFT prior 里"选出更优 tool-use pattern"的功劳。两步缺一不可。
- 72.5 (R1-Distill backbone)。backbone 越强 ReTool 越强, 说明这套框架不是钉死在 Qwen 上, 而是一种 generic 后训练范式。
4.2 训练动力学 (Figure 3)
论文跑 400 steps 在 6 个指标上画曲线, 每个都讲一个有意思的故事:
4.3 涌现行为
- Code self-correction ("Aha moment")。见 §3.4 那条 NameError → "Oops, let's correct" → fix 的轨迹。注意训练数据 DCI 里几乎没有 self-correction 例子——这是 RL 自己长出来的。
- Code purpose 多样化。论文用 Doubao-1.5-pro 给 code 段分类, 训前主要是"calculation/verification"两大类, 训后出现了 enumeration / brute-force search / symbolic / hypothesis testing 等多种 purpose——adaptive tool selection 的证据。
- CI-powered vs text-only 同题对比 (Figure 6)。四位数 mod 7 题, text-only RL 模型推出 N=5624 (错), CI-powered 用 Python 暴力 enumerate 出 N=5694 (对)。差别在于 text-only 模型每一步模算术都有 small error probability, 一步错全错。
- Code pass rate 的分裂。对答对的轨迹, 最后一段 code 的 pass rate 始终 ~100%; 对答错的轨迹, 这个 pass rate 在训练中下降——意思是 model 在更复杂的题上更容易写错代码, 不是 code 能力倒退, 而是 model 在 reach for harder problems。
5 · 与同类工作对比
| 工作 | 类型 | 关键差异 |
|---|---|---|
| DeepSeek-R1-Zero / QwQ / s1 | long-CoT RL, no tool | 同 backbone 同算力, ReTool 32B 67.0 / 49.3 vs 47.0 / 33.5。证明 tool 是 token efficiency 的关键 |
| Search-R1 (arXiv:2503.09516) | RL+tool, 同 framework | 同一思想的搜索域对偶: tool = 搜索引擎, observation = 搜索结果片段。ReTool 是代码域实例。架构基本同构 (rollout pause/resume + interpreter mask) |
| ToolFormer / MathCoder | SFT-only tool use | 只学固定模式, 不会自适应"该不该调"; ReTool 的 cold-start 部分类似这条线, RL 是关键加法 |
| PoT / PAL (arXiv:2211.12588 / arXiv:2211.10435) | prompting-only | "把整道题翻译成 Python"——失去自然语言推理的灵活, 也不会动态决定调用时机 |
| Qwen2.5-Math-72B-TIR | SFT tool-integrated | AIME 40.0%——是同思路 SFT-only 的天花板, ReTool 32B 多 27 个点说明 RL 是收益放大器 |
| ToRL (arXiv:2503.23383) | concurrent: RL+tool 小模型 | 1.5B/7B Qwen-Math, 性能 suboptimal——ReTool 把这条线 scale 到 32B 并配齐 infra |
| OpenAI o1 + tools | 闭源 | o1-preview 在 AIME 2024 = 44.6%; ReTool-32B 67.0 / R1-Distill-Tool 72.5 都显著高——open-source 32B 在带 tool这条赛道上 cross 了 o1-preview |
| MathCoder / MAmmoTH | code-aug SFT | 更早期的 code-augmented math 数据 + SFT, ReTool 的 DCI pipeline 是其精神后继 |
| sibling 05 RethinkingAgenticRL | position paper | 把 ReTool 列为 "Agentic POMDP" 的实例——本文是具体训出来的 agent, RethinkingAgenticRL 是给它命名的 framework |
6 · 局限 / 个人 take / 待验证问题
论文的局限
- 只跑 AIME 一族 math 题。没测 GPQA / MMLU-Pro / 编程题 / 形式化证明。ReTool 的"调 Python 解决算术"在 AIME 上是 perfect fit, 换到推理而非计算主导的 task 上是否还成立, 未知。
- Cold-start data 构造的细节不透明。DCI 重写器是哪个 LLM, 用了多大规模, 多少条样本被淘汰, 全没披露。这是能不能复现的硬卡点。
- Code 仅限 Python。没讨论多语言/多工具 (SymPy / Wolfram / SAT solver / 数值库) 的混合调用——而真实 AIME 题里某些更适合 SymPy 解析解。
- Sandbox 安全 + 资源限制没讲清楚。对抗性 prompt 可能让模型写
os.system("rm -rf /")——pod pool 怎么 isolate, 是否有 timeout/memory cap, 论文几乎没提。 - KL=0 + outcome-only 的 stability claim 没在更长训练 / 更难任务上验证。400 步是不是恰好踩在崩之前的甜点上, 不知道。
我的疑问 (待验证)
- DCI 的 rewriter 如果用比 base model 更强的模型 (例如 R1) 蒸馏, vs 用 base model 自己蒸馏, 哪个 cold-start 效果更好? 论文回避了这个 ablation。
- ReTool 的token efficiency 收益(response length −40%) 在 inference cost 上有多大节省? 因为 sandbox 调用本身有 RTT 开销, 单看 token 数不公平。
- 如果把 PPO 换成 GRPO/DAPO/GSPO, ReTool 框架是否兼容? interpreter mask 在 group-normalized advantage 上的语义是不是改变?
- 对错题的 last-code-pass-rate 下降——是 model 在做更难的题 vs 是 model 学到"反正答错了不如赌一把没语法错"? 论文给出前者解读, 但没排除后者。
- ReTool 训出来的模型对"该不该调工具" 的判断, 在不需要工具的题 (例如简单算术、文字推理) 上是否会过度调用? Code ratio 98% 看起来可疑——如果训练集全是难题, 那 evaluation 是否会 over-tool simple problems?
- 同样 SFT data + 同样 RL setup, 把 Python 换成定理证明器 (Lean) 会怎样? 这是 Search-R1 → ReTool 的下一个自然 sibling。
个人 take
ReTool 的真正贡献不在 67.0 这个数字, 而在于给"RL + 异构 tool"这条技术路线找到了一份能 work 的菜谱: cold-start SFT 装格式 → outcome RL 选 pattern → interpreter mask 防梯度污染 → KV reuse 撑住多轮 → async sandbox 撑住 throughput。这五件事任何一个没做对, 整个流水线都会崩。它和 Search-R1 一起把 "training-time tool use" 从一个 idea 推成一个可复用的 recipe。下一步合理的事是把这个 recipe 复用到 (a) 多工具混合 (Python + 搜索 + 知识库), (b) 非数学任务 (SWE / GUI / formal proof), (c) 更小的 backbone (7B 量级)——这三条都是低悬果。
记忆点
配方 Cold-start SFT (DCI) → outcome PPO + interpreter mask + KV reuse + async sandbox
公式 JPPO 唯一改动: ratio 的 condition 里加 "; CI"; reward = ±1 only
关键数 AIME 2024: 67.0 vs 40.0 (text-RL); 400 steps vs 1080; response length −40%
涌现 code self-correction ("Oops, let me fix that") + adaptive tool selection
对偶 Search-R1 是搜索域的 ReTool; 同 framework 不同 tool
下一步 多工具混合 / 非数学任务 / 小模型复制 — 三条低悬果
精读笔记 v1 · 2026-05-11 · 配套论文 PDF: /tmp/retool_2504.11536.pdf · arXiv:2504.11536