基于 Nemotron-Terminal-8B 的终端智能体,支持 TerminalBench 1/2 评测、GRPO 强化学习训练以及高质量数据集构建。
| 阶段 | 内容 | 状态 |
|---|---|---|
| 评测 | 在 TerminalBench 1/2 上验证 Nemotron-8B 基线 | ✅ TB1 完成(16.25%);TB2 完成(7.87%) |
| TB2 评测 | TermyLambda 在 TerminalBench 2.0 上的评测 | ✅ TB2 完成(10.11%,9/89) |
| SFT TB2 评测 | TermyLambda_sft 在 TerminalBench 2.0 上的评测 | ✅ TB2 完成(7.87%,7/89) |
| 训练 | GRPO + LoRA 强化学习,728 个 terminal 任务 | ✅ 完成(~18.5h,H100) |
| Stage 1 SFT | 多轮 SFT,OpenThoughts-Agent-v1-SFT(15,209 条),2 epochs | ✅ 完成(~16.1h,H100) |
| MT-GRPO 数据策展 | GPT-5-mini 四维分析 + 质量筛选 + 技能多样性 rebalance,100 任务子集 | ✅ 完成 |
| MT-GRPO 训练 | 多轮 GRPO + Dr.GRPO + turn-level reward,T=1.0,2 epochs | 🔄 训练中 |
| 数据集 | 从 17,472 rollouts 中提取 SFT / DPO / PRM 数据 | ✅ 已上传 HuggingFace |
| 数据集 | 任务数 | 正确率 | 结果文件 |
|---|---|---|---|
| TerminalBench 1 (v0.1.1) | 80 | 16.25% | results/baselines/tb1/nemotron_tb1_results.json |
| TerminalBench 2.0 | 89 | 7.87% (7/89) | results/nemotron_base_tb2/2026-03-12__02-40-27/result.json |
Nemotron 基线 TB2 通过任务(7):distribution-search, headless-terminal, hf-model-inference, modernize-scientific-stack, portfolio-optimization, pypi-server, sqlite-with-gcov
| 模型 | max_output_tokens | 完成 | 通过 | 正确率 | vs 基线 |
|---|---|---|---|---|---|
| Nemotron-Terminal-8B(基线) | 8192 | 88/89 | 7 | 7.87% | — |
| TermyLambda (GRPO, checkpoint-1092) | 4096 | 86/89 | 9 | 10.11% | +2.24% |
| TermyLambda_sft (SFT Stage 1) | 8192 | 87/89 | 7 | 7.87% | ±0% |
TermyLambda (GRPO) 通过任务(9):cancel-async-tasks, hf-model-inference, log-summary-date-ranges, modernize-scientific-stack, nginx-request-logging, portfolio-optimization, prove-plus-comm, pytorch-model-cli, sqlite-with-gcov
TermyLambda_sft 通过任务(7):configure-git-webserver★, log-summary-date-ranges, model-extraction-relu-logits★, modernize-scientific-stack, nginx-request-logging, prove-plus-comm, sqlite-with-gcov
Nemotron 基线通过但训练后丢失的任务:distribution-search△, headless-terminal△, pypi-server△
(★ = SFT 独有,GRPO/基线未通过;△ = 基线独有,训练后丢失)
三个模型共解决 14 个独立任务,但仅 2 个被全部模型解决:
| 类别 | 任务数 | 任务 |
|---|---|---|
| 三模型共解 | 2 | modernize-scientific-stack, sqlite-with-gcov |
| 仅基线 | 3 | distribution-search, headless-terminal, pypi-server |
| 仅 GRPO | 2 | cancel-async-tasks, pytorch-model-cli |
| 仅 SFT | 2 | configure-git-webserver, model-extraction-relu-logits |
| 基线 ∩ GRPO | 4 | +hf-model-inference, portfolio-optimization |
| GRPO ∩ SFT | 5 | +log-summary-date-ranges, nginx-request-logging, prove-plus-comm |
| 基线 ∩ SFT | 2 | 同"三模型共解" |
分析:
- GRPO 在基线之上净增 +2.24%,但丢失了 3 个基线能解的任务(
distribution-search,headless-terminal,pypi-server),同时新增 5 个(cancel-async-tasks,log-summary-date-ranges,nginx-request-logging,prove-plus-comm,pytorch-model-cli) - SFT 解决了格式问题(第一轮 JSON 合规率 100%,原 GRPO 约 0%),但任务解决能力与基线持平(7.87%)
- SFT 的 AgentTimeoutError 49 个(vs GRPO 3 个 / 基线 71 个错误):多轮 ReAct 格式每轮需一次 LLM 调用,累计时间开销大
- 三模型互补性强(14 个任务仅 2 个共解),表明 ensemble 或 GRPO R2 有较大提升空间
| 指标 | 值 |
|---|---|
| 硬件 | 1× NVIDIA H100 80GB |
| 训练时长 | ~18.5 小时 |
| Rollout steps | 546(每步 ~115s) |
| 初始 reward(20-step 滑窗) | ~0.204 |
| 峰值 reward(step ~530) | ~0.42(涨幅 +2×) |
| 最终 reward(last 20 steps) | 0.354 |
| Verifier 通过率(全量 rollouts) | 19.70%(3,442 / 17,472) |
推荐使用
checkpoint-750,滑窗奖励在 step 500–850 区间达峰,step 1000+ 后轻微退化。
模型已上传:zcheng256/TermyLambda
动机:TermyLambda(GRPO 版)TB2 仅得 10.11% 的根本原因是训练-评测不对齐——GRPO 在单轮格式(系统提示 + 用户任务 → 所有命令一次输出)上训练,而 TB2 评测使用多轮 ReAct 格式(每轮输出 JSON 命令,接收 terminal 输出,再决策)。SFT Stage 1 的目标是先让模型学会多轮 terminal 交互格式。
| 数据集 | 规模 | 格式 | 选择原因 |
|---|---|---|---|
| open-thoughts/OpenThoughts-Agent-v1-SFT | 15,209 条 | 多轮(Terminus-2 生成) | 与 TB2 评测格式完全一致:analysis + plan + commands[{keystrokes, duration}] + task_complete |
dataset_builder/outputs/api_sft_dataset.jsonl(内部) |
200 条 | 单轮 | GRPO rollout 中经人工 API 评分排名前 200 的高质量样本,混入增强格式多样性 |
为何不用 TerminalTraj(m-a-p/TerminalTraj,50,733 条):该数据集命令格式为 {"commands": ["shell_cmd"]} 字符串列表,与 Terminus-2 的 {"analysis", "plan", "commands": [{"keystrokes", "duration"}], "task_complete"} 结构根本不同,混入会破坏格式合规性。
训练仅对 assistant turns 的 token 计算 CE loss,系统提示和用户消息全部 mask(labels=-100)。实现方式:将响应模板 <|im_start|>assistant\n<think>\n\n</think>\n\n(token IDs: 151644, 77091, 198, 151667, 271, 151668, 271)作为边界,仅对每个 assistant 响应体内的 token 解除 mask。
这确保模型只学习"给定多轮上下文应该输出什么",而不是记忆 system/user 提示词。
| 参数 | 值 |
|---|---|
| 硬件 | 1× NVIDIA H100 80GB |
| Base model | nvidia/Nemotron-Terminal-8B |
| LoRA rank / alpha | 64 / 64 |
| LoRA target modules | q/k/v/o_proj + gate/up/down_proj(7 个) |
| 量化 | BF16(无 int4,SFT 不需要省显存) |
| 学习率 | 2e-5(cosine decay,10% warmup) |
| Epochs | 2 |
| Per-device batch size | 1 |
| Gradient accumulation | 16(effective batch = 16) |
| Max sequence length | 8,192 tokens(超出截断) |
| Packing | 关闭(多轮对话不跨轨迹拼接) |
| 训练脚本 | bash scripts/run_sft.sh configs/train/sft_openthoughts.yaml |
| 指标 | 值 |
|---|---|
| 训练时长 | ~16.1 小时(1902 步) |
| 总 token 量 | 177.5M(assistant tokens 约 17.7M) |
| 初始 MA loss(前 50 步) | 0.477 |
| Epoch 1 结束 MA loss(step ~951) | 0.220 |
| 最终 MA loss(后 50 步) | 0.193 |
| Loss 下降幅度 | -59.5% |
| 过拟合 | 无(最终 MA 0.193,阈值 ~0.1) |
- Loss 曲线:epoch 1 前半段快速下降(0.477 → 0.277),随后进入缓慢下降平台区;epoch 2 继续下降至 0.193,第二轮确有价值
- Epoch 2 效果:epoch 2 loss 直方图分布相比 epoch 1 明显左移(mean 0.277 → 0.193),证明模型在第二轮继续学习而非仅记忆
- Gradient norm:全程 < 0.5,无梯度爆炸,训练稳定
- Policy entropy:维持在 0.47–0.60 之间,模型输出多样性健康,未发生 entropy collapse
- 学习率:cosine decay 正常,最终步 lr ≈ 1.7e-11(几乎为零,充分消化数据)
训练图表:results/sft_plots/(loss_curve.png, training_dynamics.png, loss_distribution.png, loss_summary.png)
模型已上传:zcheng256/TermyLambda_sft(LoRA adapter,base: Nemotron-Terminal-8B)
方式一:从 HuggingFace 加载(自动下载到 ~/.cache/termylambda_sft/)
# 启动 vLLM 服务(首次运行自动下载 adapter)
bash scripts/serve_termylambda_sft_hf.sh
# 配置文件:configs/serve/vllm_termylambda_sft_hf.yaml方式二:从本地 checkpoint 加载
# 需先完成 SFT 训练:bash scripts/run_sft.sh configs/train/sft_openthoughts.yaml
bash scripts/serve_termylambda_sft.sh
# 配置文件:configs/serve/vllm_termylambda_sft.yaml(lora_local_path: ./checkpoints/sft-openthoughts)运行 TB2 全量评测(n_concurrent=4,89 题)
harbor run -d terminal-bench@2.0 -a terminus-2 -m openai/termylambda_sft \
-n 4 \
-o ./results/termylambda_sft_tb2 \
--ak api_base=http://localhost:8001/v1 \
--ak model_info='{"max_input_tokens": 24000, "max_output_tokens": 8192}' \
--ae OPENAI_API_KEY=dummy
# 评测配置:configs/eval/termylambda_sft_tb2.yaml内存说明:
max_model_len=32768(32k context)在单张 H100 80GB 上运行,GPU 显存利用率 0.85。 24000+8192=32192 < 32768,完全放得下。128k context 在单张 H100 上不可行(约需 197GB KV 缓存),需 tensor_parallel_size=2+。
terminalAgent/
├── configs/ # 配置文件
│ ├── eval/
│ │ ├── nemotron_tb1.yaml # TB1 评测配置(terminal-bench CLI)
│ │ └── nemotron_tb2.yaml # TB2 评测配置(Harbor 框架)
│ ├── serve/ # 模型服务配置(vLLM)
│ └── train/ # 训练配置(GRPO 超参、LoRA、DeepSpeed)
├── dataset_builder/ # 数据集构建工具
│ ├── main.py # 完整流水线入口
│ ├── utils.py # 公共工具(加载 rollouts、指令、prompt)
│ ├── sft_builder.py # 构建 SFT 数据集
│ ├── dpo_builder.py # 构建 DPO 偏好数据集
│ ├── api_analyzer.py # API 质量排序(gpt-4o-mini)
│ ├── prm_builder.py # 构建 Process Reward Model 数据集
│ ├── upload_to_hf.py # 上传至 HuggingFace
│ └── outputs/ # 生成的数据集文件(已 git 跟踪)
│ ├── sft_dataset.jsonl (428 条)
│ ├── dpo_dataset.jsonl (928 对)
│ ├── api_sft_dataset.jsonl (200 条)
│ ├── prm_sequence_dataset.jsonl (192 条)
│ └── prm_step_dataset.jsonl (2737 条)
├── patches/
│ ├── terminus_2_context_fix.patch # terminus-2 上下文溢出修复(TB1 / TB2 通用)
│ └── lite_llm_context_fix.patch # LiteLLM BadRequestError → ContextLengthExceededError
├── results/
│ ├── baselines/ # 已提交的基线结果
│ ├── GRPO_SUMMARY.md # 训练总结(详细)
│ └── grpo_plots/ # 训练曲线图表(5 张)
├── scripts/ # 运行脚本
│ ├── setup_env.sh # 环境安装
│ ├── serve_model.sh # 启动 vLLM 服务
│ ├── run_eval.sh # TB1 评测
│ ├── run_eval_tb2.sh # TB2 评测
│ ├── run_train.sh # 启动 GRPO 训练
│ ├── extract_rl_tasks.py # 解压 RL 数据集
│ └── plot_grpo.py # 生成训练曲线图表
└── src/ # 源代码(eval / serve / train 模块)
bash scripts/setup_env.sh
source .venv/bin/activate安装内容:Python 3.12 venv、torch、transformers、vllm、terminal-bench、Harbor、terminus-2 补丁。
Flash Attention 2(训练加速,需单独安装):
pip install "https://github.com/lesj0610/flash-attention/releases/download/v2.8.3-cu12-torch2.10-cp312/flash_attn-2.8.3%2Bcu12torch2.10cxx11abiTRUE-cp312-cp312-linux_x86_64.whl"适用于 torch 2.10 + CUDA 12.8 + Python 3.12 + Linux x86_64。未安装时自动回退到 SDPA,训练仍可正常运行。
# 终端 1
bash scripts/serve_model.sh默认端口 8000,max_model_len=40960(Nemotron 基线),gpu_memory_utilization=0.85。TermyLambda 服务使用 max_model_len=32768(LoRA 额外显存开销)。
前提条件:
# 确认 Docker 可访问
docker info
# 若出现 permission denied:
sudo usermod -aG docker $USER && newgrp docker
# 确认 GPU 可用(需 20GB+ VRAM)
nvidia-smi运行(全自动:下载 LoRA → 启动 vLLM → 跑 TB2 → 关闭 vLLM):
bash scripts/run_eval_termylambda.sh脚本内部流程:
- 从 HuggingFace 下载 LoRA 权重(
zcheng256/TermyLambda/checkpoint-750,首次运行约需几分钟,缓存至~/.cache/termylambda/) - 后台启动 vLLM(
nvidia/Nemotron-Terminal-8B+ LoRA,max_model_len=32768,端口 8000),等待就绪(最多 5 分钟) - 调用
run_eval_tb2.sh执行 Harbor 评测,并发数 4,共 89 题 - 评测结束后自动关闭 vLLM
结果输出:
results/termylambda_tb2/
如需中途清理:
bash scripts/cleanup_eval.shbash scripts/run_eval_tb2.shbash scripts/run_eval.sh前提:确认 Docker 可用
docker info # 若出现 "permission denied":
sudo usermod -aG docker $USER && newgrp docker准备数据:
python scripts/extract_rl_tasks.py启动训练:
bash scripts/run_train.sh configs/train/grpo_lora.yaml
# 自定义超参
bash scripts/run_train.sh configs/train/grpo_lora.yaml \
-o learning_rate=1e-5 -o num_train_epochs=5
# 多 GPU
NUM_GPUS=4 bash scripts/run_train.sh configs/train/grpo_lora.yaml加载 TermyLambda checkpoint:
from peft import PeftModel
from transformers import AutoModelForCausalLM, AutoTokenizer
model = AutoModelForCausalLM.from_pretrained(
"nvidia/Nemotron-Terminal-8B",
torch_dtype="bfloat16",
device_map="auto",
)
model = PeftModel.from_pretrained(model, "zcheng256/TermyLambda", subfolder="checkpoint-750")
tokenizer = AutoTokenizer.from_pretrained("zcheng256/TermyLambda")背景:TermyLambda(checkpoint-750)TB2 仅得 10.11%,低于 Nemotron 基线(16.25%)。 根本原因:训练是单轮(指令 → 所有命令 → reward),评测是多轮 ReAct(命令 → terminal 输出 → 命令 → ...)。
对两次评测的对话日志进行逐轮分析(TB1:13 个任务,TB2:89 个任务):
| 指标 | TB1(Nemotron 评测) | TB2(TermyLambda 评测) |
|---|---|---|
| 第一轮 valid JSON | 94%(16/17) | 1.2%(1/85) |
| 第一轮 plain text | 0% | 96.6%(83/88) |
| 主要失败原因 | timeout(69%)、error recovery 差 | 第一轮格式错误 |
| 失败发生时机 | 多轮积累后 | 第一轮就坏了 |
关键发现:GRPO 单轮训练导致多轮格式能力退化。
Nemotron 基础模型(TB1)在多轮 ReAct 下几乎全部给出合法 JSON,失败原因是中后期决策失误或超时。TermyLambda(TB2)经过单轮 GRPO 后,96.6% 的任务在第一轮就输出 plain text 而非 JSON——format reward 在单轮模式下只覆盖了"整批命令是否合法 JSON",多轮交互时的每轮格式遵守反而被破坏。
为什么会这样——从 token 和代码层面追溯根因:
-
Chat template 结构不一致(最深层原因):GRPO 训练的 prompt 使用
[system, user]两个角色(src/train/data.py:_format_prompt),token 序列以<|im_start|>system\n...开头。但 terminus-2 评测时不使用 system role——它把系统指令塞进第一条 user message,token 序列以<|im_start|>user\n...开头。训练和评测的 token 前缀从第 2 个 token 就开始分叉:训练: <|im_start|> system \nYou are an autonomous terminal agent... 评测: <|im_start|> user \nYou are an AI assistant tasked with solving...LoRA 在 17,472 条 rollout 上学到了
<|im_start|>system → ... → <|im_start|>user → ... → <|im_start|>assistant → JSON这一特定 attention pattern。评测时 context 以<|im_start|>user开头,LoRA 权重的贡献落入未见过的激活空间,基础模型的多轮 JSON 能力被干扰而非增强。 -
单轮 vs 多轮:训练中 prompt 固定为
(system, user)两轮,模型输出一个包含所有命令的单一 JSON,Docker 容器顺序执行并返回 verifier reward(见src/train/data.py:_format_prompt,17,472 条 rollout 全部如此)。训练中从未出现[user, assistant, user, assistant, ...]的多轮上下文。模型在 2 轮 context 下输出 JSON 的行为被高度强化,但对第 3、4、5... 轮完全没有梯度信号。 -
LoRA 覆盖面过广,放大了分布偏移的影响:LoRA target 覆盖全部 attention(q/k/v/o_proj)和 MLP(gate/up/down_proj),rank=64,训练参数多。17,472 次单一结构的强化足以将这些权重拉向窄分布,覆盖基础模型对多角色、多轮 context 的泛化能力。Nemotron 基础模型在 TB1 评测中 94% 第一轮正常输出 JSON,证明泛化能力存在于基础权重中,但被 LoRA 的窄分布拟合破坏了。
-
KL 正则化过弱:
beta=0.005,几乎不约束模型偏离基础策略(全程 KL 最大仅 0.049)。如果 beta 更高,模型会更多保留基础模型的多轮格式能力,但 verifier reward 的提升可能更慢。
本质上是三层分布偏移的叠加:chat template 结构(system role 有无)× 对话轮次(单轮 vs 多轮)× 系统指令措辞(训练用 You are an autonomous terminal agent vs terminus-2 用 You are an AI assistant tasked with solving command-line tasks),三者同时不一致,使得评测时模型几乎完全落在训练分布之外。
对 DPO 阶段的影响:
当前 DPO 数据(872 对)来自 GRPO rollouts,prompt 只包含第一轮 [system, task_instruction],chosen/rejected 是第一步的单轮 JSON 回复。这对 TB1 风格的失败(中后期决策)几乎没有帮助;对 TB2 的格式问题,Stage 1 SFT 已经可以修复,DPO 的边际价值有限。Stage 1 SFT 跑完后应先重新评测 TB2,若第一轮格式问题基本消失,可考虑跳过 DPO 直接进入 Stage 3 GRPO。
# 从 Nemotron-8B 基础模型开始(不是 checkpoint-750,避免单轮 LoRA 权重干扰)
bash scripts/run_sft.sh configs/train/sft_openthoughts.yaml- 数据:
open-thoughts/OpenThoughts-Agent-v1-SFT(15,209 条多轮对话,Terminus-2 格式) - 关键:
completion_only_loss=True(只对 assistant 轮计算 loss),packing=False(防跨轨迹污染) - 预期:TB2 从 10.11% 提升到 18–22%(多轮格式对齐是最大单点提升)
# 从 Stage 1 checkpoint 开始
bash scripts/run_dpo.sh configs/train/dpo_internal.yaml \
-o model_name=./checkpoints/sft-openthoughts- 数据:
dataset_builder/outputs/dpo_dataset.jsonl(928 对,同任务下 verifier_reward≥0.9 vs ≤0.3) ref_model=None+ PEFT:冻结 base 权重作为隐式参考策略,避免保留第二份显存副本- 预期:TB2 提升到 20–24%
# 从 Stage 2 checkpoint 开始
bash scripts/run_train.sh configs/train/grpo_lora_r2.yaml \
-o model_name=./checkpoints/dpo-internal- 改进(相比原始 GRPO):
num_generations=16(原 8),max_prompt_length=2048(原 1024),beta=0.01(原 0.005) - 预期:TB2 提升到 22–27%
| 阶段 | 预期 TB2 | 主要原因 |
|---|---|---|
| TermyLambda(当前) | 10.11% | 单轮训练-多轮评测不对齐 |
| Stage 1 SFT 后 | 18–22% | 多轮格式对齐 |
| Stage 2 DPO 后 | 20–24% | 偏好对齐 |
| Stage 3 GRPO 后 | 22–27% | 执行环境强化 |
从 17,472 条 GRPO rollouts 中提取五个数据集,每个面向不同的训练目标。
rollouts.jsonl ──┬──► sft_dataset.jsonl (428 条,SFT)
+ ├──► dpo_dataset.jsonl (928 对,DPO)
steps.jsonl ──┘ ├──► api_sft_dataset.jsonl (200 条,API 质量排序)
├──► prm_sequence_dataset.jsonl (192 条,PRM 序列)
└──► prm_step_dataset.jsonl (2737 条,PRM 步级)
# 从项目根目录运行,需要 OPENAI_API_KEY
python dataset_builder/main.py \
--rollouts checkpoints/grpo-lora/rollouts.jsonl \
--data-dir data/rl_tasks \
--output-dir dataset_builder/outputs
# 跳过 API 步骤(离线生成 SFT + DPO)
python dataset_builder/main.py --skip-api-sft --skip-prm验证器通过率 ≥ 0.9 的 428 个任务,每个任务选最优一条 rollout:优先最晚训练步(更成熟的行为) + 最高竞争度批次(step_reward_std 高,模型在该批次结果不一致,胜出的样本信息量更大)。
{
"task_id": "task_1683",
"training_step": 1087,
"prompt": [{"role": "system", "content": "..."}, {"role": "user", "content": "..."}],
"completion": "{\"analysis\":\"...\",\"plan\":\"...\",\"commands\":[...]}",
"verifier_reward": 1.0,
"step_reward_std": 0.35,
"step_frac_zero_std": 0.0,
"source": "grpo_rollout"
}同一训练步内,同一任务同时产生了通过(≥0.9)和失败(<0.3)的 completion,形成自然偏好对。这是 GRPO 实际学习信号的直接提取——相同模型状态、相同 prompt、不同结果,无跨步噪声。
{
"task_id": "task_7683",
"training_step": 1,
"prompt": [...],
"chosen": "{...通过的 completion...}",
"rejected": "{...失败的 completion...}",
"reward_chosen": 1.0,
"reward_rejected": 0.15,
"step_reward_std": 0.22
}DPO 数据质量过滤(dpo_dataset_filtered.jsonl)
原始 928 对中存在两类质量问题:
- JSON 解析错误(约 48 条):chosen/rejected 含转义错误等无效 JSON,无法加载
- chosen 伪造 fixture 数据(约 168 条):模型自行
touch或echo写入任务预期的输入文件,再对自造数据运行命令通过验证——这在 GRPO 中获得了高 reward,但会教模型在评测时伪造输入
使用 GPT 对每条 chosen 进行判断,过滤后得到 dpo_dataset_filtered.jsonl:
export OPENAI_API_KEY=sk-...
python dataset_builder/filter_dpo.py \
--input dataset_builder/outputs/dpo_dataset.jsonl \
--output dataset_builder/outputs/dpo_dataset_filtered.jsonl \
--model gpt-5-mini \
--concurrency 20被移除的记录及原因保存在 dpo_dataset_filtered.removed.jsonl,供审查。
358 个任务有多条通过的 rollout,仅靠奖励分数无法区分清晰推理和暴力通过。对每个任务,将最多 3 个候选(按最新训练步排序)发给 gpt-5-mini 进行质量排序,选出最适合 SFT 的那条。
{
"task_id": "task_2214",
"training_step": 987,
"prompt": [...],
"completion": "{...最优 completion...}",
"verifier_reward": 1.0,
"api_reason": "Completion 1 has clearest analysis and most direct commands",
"n_candidates_compared": 3,
"source": "api_ranked"
}对每对 DPO pair(优先 step_reward_std 最高的批次,信息最丰富),API 对两条命令序列的每一步打分(1–5),并标注失败序列在第几步出现了偏差。
{
"task_id": "task_5432",
"divergence_step": 2,
"divergence_reason": "Used wrong directory for searching",
"passing_scores": [4, 5, 5, 4],
"failing_scores": [4, 5, 2, 1],
"step_notes": [...]
}将序列记录展开为扁平步级记录,用于训练 Process Reward Model:给定(任务指令 + 前缀命令序列),预测当前步的质量分(1–5)。
{
"task_id": "task_5432",
"sequence_type": "failing",
"step_idx": 2,
"prefix_commands": [...],
"command": {"keystrokes": "cd /wrong/path"},
"step_score": 2,
"is_divergence_step": true,
"final_verifier_reward": 0.15
}所有数据集已上传至 zcheng256/TermyLambda。
reward = 0.05 × format_reward (合法 JSON,含 commands 字段)
+ 0.10 × script_reward (bash exit 0)
+ 0.85 × verifier_reward (test.sh exit 0 = 1.0;pytest 部分通过 = passed/total)
script_reward 权重极小,防止模型通过 || true 绕过执行失败获取高分。
| 参数 | 值 |
|---|---|
| LoRA rank / alpha | 64 / 64 |
| LoRA targets | q/k/v/o/gate/up/down_proj |
| per_device_batch_size | 4 |
| gradient_accumulation_steps | 4(effective batch = 16) |
| num_generations | 8 per prompt |
| max_completion_length | 2048 tokens |
| learning_rate | cosine,峰值 5e-6 |
| KL 系数 (beta) | 0.005 |
python scripts/plot_grpo.py \
--checkpoint-dir checkpoints/grpo-lora \
--out-dir results/grpo_plots| 文件 | 内容 |
|---|---|
reward_curve.png |
每步 reward + 20-step 滑窗 + ±1σ 阴影 |
reward_components.png |
format/script/verifier 三分量变化 |
kl_entropy.png |
KL 散度 + Policy entropy |
training_dynamics.png |
LR、grad norm、completion 长度、zero-std 比例 |
reward_distribution.png |
全量 17,472 rollouts reward 分布直方图 |
所有任务共享一个基础镜像(terminal-agent-task:base),每个任务的 seeds 在容器启动时动态挂载,避免为 728 个任务各自 build 镜像(节省 ~350GB 磁盘)。
- 每 batch 最多 8 个容器并发(
max_workers=8) - 容器限制:
--memory 512m --cpus 1.0 --pids-limit 64 subprocess.TimeoutExpired时主动docker kill,防止僵尸容器
- 初始 → 峰值涨幅 ~2×(0.204 → 0.42 滑窗均值),KL 极小(max 0.049),训练稳定
- step 1000+ 轻微退化,推荐使用
checkpoint-750而非最终权重 - 40% 批次 frac_zero_std = 1(所有 8 个生成奖励相同),无梯度信号,是收敛瓶颈
- Completion 长度增长 434 → 560 tokens,模型输出趋于冗长
详细分析见 results/GRPO_SUMMARY.md。
原始单轮 GRPO 只有一个标量 reward(最终 verifier 结果),所有 token 共享同一个 advantage,无法区分哪一轮的操作对最终结果贡献最大。研究表明 turn-level credit assignment 能显著提升多轮 agent 的训练稳定性和任务表现。
基于以下论文的核心思想:
| 来源 | 采用的技术 |
|---|---|
| MT-GRPO (arXiv 2505.11821) | 每轮独立 reward + 按 turn position 做组归一化 |
| Dr.GRPO / DeepSWE | 全局长度归一化,消除变长序列的 length bias |
| DeepSWE | Compact filtering:对超时/截断轨迹 mask loss |
r(t) = w_outcome · R_final · γ^(T-1-t) + w_exec · 𝟙[rc_t = 0] + w_format · 𝟙[json_valid_t] + w_efficiency · efficiency_bonus
R_final:最终 verifier reward(0–1),通过 γ 折扣分配到各轮(越靠后的轮次获得越高的 outcome credit)rc_t:Docker exec 返回码(命令是否执行成功),rollout 中已有json_valid_t:模型输出是否为合法 JSON,rollout 中已有efficiency_bonus:(max_turns - num_turns) / (max_turns - 1),仅当R_final > 0时生效,鼓励用更少轮次完成任务- 默认权重:
w_outcome=0.7, w_exec=0.15, w_format=0.15, w_efficiency=0.05(效率权重低以防模型偷懒)
对同一 prompt 的 G 个 rollout,在每个 turn position 上独立做组归一化:
A(i, t) = (r(i,t) - mean_g[r(·,t)]) / (std_g[r(·,t)] + ε)
最终 per-token advantage 是 turn-level 和 outcome-level 的混合:
A_token = (1 - α) · A_outcome + α · A_turn(t)
α = turn_advantage_coef(默认 0.5),A_outcome 是原始 GRPO 标量 advantage。
TRL GRPOTrainer 的 _compute_loss() 原生支持 (B, T) 形状的 advantages(内部检查 if advantages.dim() == 1),无需 fork TRL。只需在 _generate_and_score_completions() 中输出 (B, T) 张量。
| 参数 | 值 | 作用 |
|---|---|---|
loss_type |
dr_grpo |
用 B × max_completion_length 归一化 loss,消除长序列偏好 |
max_completion_length |
8192 |
Dr.GRPO 分母;设为实际 completion 长度的 P95(~4000),而非理论最大值(28672),避免梯度过度压缩 |
temperature |
1.0 |
Rollout 采样温度;覆盖 Nemotron 模型默认值 0.6。T=1.0 是 DeepSeek-R1、DAPO、DeepSWE 的共识,更高温度 → 更多样的 rollout → 更少的 zero_std group |
mask_truncated_completions |
True |
对未正常结束的轨迹 mask loss(DeepSWE compact filtering) |
scale_rewards |
none |
避免 double-scaling(turn-level 组归一化已处理) |
关于 Dr.GRPO max_completion_length 的调整:Dr.GRPO 用 B × max_completion_length 作为 loss 分母(而非每条 completion 的实际 token 数),目的是消除 length bias。但原始值 28672(6 turns × 4096 + headroom)远大于实际平均 completion 长度(~1400 tokens),导致 loss 和 grad_norm 被压缩约 20 倍,梯度信号过弱。调整为 8192 后,覆盖 P95 实际长度,同时保持合理的梯度量级。超过 8192 的 trajectory 由 mask_truncated_completions=True 处理。
# configs/train/grpo_lora_mt.yaml
temperature: 1.0 # rollout 采样温度(DeepSeek-R1/DAPO/DeepSWE 共识值)
max_completion_length: 8192 # Dr.GRPO 归一化分母(匹配实际 P95 completion 长度)
turn_advantage_coef: 0.5 # 0=纯 outcome, 1=纯 turn-level
turn_gamma: 0.95 # 时间折扣
turn_w_outcome: 0.7 # verifier reward 权重
turn_w_exec: 0.15 # 命令执行成功权重
turn_w_format: 0.15 # JSON 格式合法权重
turn_w_efficiency: 0.05 # 效率 bonus(少轮完成任务奖励,仅 R>0 时生效)
task_list: ./data/rl_tasks_mt100.json # 质量筛选后的 100 任务子集从 728 个 RL 任务中通过三步策展流程筛选出 100 个高质量训练任务:
基于 SFT 数据的轮次分布(median=6, P95=15)和 GRPO 评估的 reward 方差,使用联合权重采样:
weight = Normal(turns, μ=5, σ=2) × Normal(reward, μ=0.4, σ=0.25) × (reward_std + 0.05)
选出分布居中、reward 信号丰富(非全 0/全 1)的 100 个任务。
对 728 个任务全部进行 API 分析(scripts/analyze_tasks.py),评估四个维度:
| 维度 | 方法 | 用途 |
|---|---|---|
| 质量评分(1-10) | 多轮适配度:目标清晰度、步骤数、可验证性 | 移除 score≤6 的低质量任务 |
| 难度分级 | easy / medium / hard | 移除 easy 任务(过于简单无学习信号) |
| 技能标签 | 16 类技能分类(file_ops, git, networking, scripting 等) | 识别技能覆盖缺口 |
| 去重签名 | 5-10 词任务意图摘要 | 检测语义重复 |
分析结果:平均质量 8.07/10,移除 8 个低质量/简单任务(score≤6 或 easy),剩余 92 个。
分析发现技能高度集中于 file_ops (78) + text_processing (63),git/networking/system_admin 等严重不足。从剩余 636 个任务池中补充 8 个稀有技能任务(scripts/rebalance_tasks.py),恢复到 100 个:
| 技能 | 数量 | 说明 |
|---|---|---|
| file_ops | 80 | 文件操作(主体) |
| text_processing | 67 | 文本处理(主体) |
| git | 7 | 版本控制操作 |
| scripting | 7 | Shell 脚本编写 |
| system_admin | 2 | 系统管理 |
| process_mgmt | 2 | 进程管理 |
| networking | 1 | 网络操作 |
注:整个 728 数据集本身技能分布高度不均(511 file_ops vs 2 networking),在现有数据约束下已做最大化多样性选择。
最终数据集:data/rl_tasks_mt100.json(100 任务,avg quality 8.1+/10,全部 medium 难度)。
分析详情:data/task_analysis.json、data/task_analysis_remaining.json。
src/train/
├── turn_rewards.py # 新增:3 个纯函数(compute_turn_rewards, compute_turn_advantages, map_turn_advantages_to_tokens)
├── multi_turn_rollout.py # 修改:rollout 返回 turn_info(每轮的 exec_rc, format_valid, token 边界)
├── multi_turn_trainer.py # 修改:_generate_and_score_completions 计算 (B,T) advantages
└── trainer.py # 修改:_build_grpo_config 添加 dr_grpo + mask_truncated
scripts/
├── analyze_tasks.py # GPT-5-mini 四维任务分析
└── rebalance_tasks.py # 技能多样性 rebalance
# 多轮 GRPO 训练
python -m src.train.run_grpo_mt --config configs/train/grpo_lora_mt.yaml
# Smoke test(2 步,小 batch)
python -m src.train.run_grpo_mt --config configs/train/grpo_lora_mt_smoke.yaml
# 测试(CPU,48 个测试)
CUDA_VISIBLE_DEVICES="" python -m pytest tests/test_turn_rewards.py tests/test_multi_turn_rollout.py tests/test_smoke_grpo_mt.py -v| TB1 | TB2 | |
|---|---|---|
| 任务数 | 80 | 89 |
| 框架 | terminal-bench CLI | Harbor |
| 数据集版本 | terminal-bench-core==0.1.1 | terminal-bench@2.0 |
| 上下文管理 | 需 patch | 需 patch(原因相同) |
本项目的 TB2 评测与官方标准基本对齐,以下参数差异来自模型本身的硬限制,无法规避:
| 参数 | 官方标准 | 本项目 | 原因 |
|---|---|---|---|
| agent | terminus-2 | terminus-2 | ✅ 对齐 |
| dataset | terminal-bench@2.0 | terminal-bench@2.0 | ✅ 对齐 |
| temperature | 0.7 | 0.7 | ✅ 对齐 |
| enable_summarize | True | True | ✅ 对齐 |
| runs per task | 5 | 1 | |
| max_output_tokens | 8192 | 4096 | |
| max_input_tokens | 128K | 24K | ❌ 模型硬限制(LoRA + H100 显存决定 max_model_len=32768;减去输出和 margin 后为 24000) |
max_input_tokens 差距的影响:官方用 128K 上下文的前沿模型,TermyLambda 只有 32K,多轮对话更容易触发 context summarization(terminus-2 自带的压缩机制)。对长任务评分有一定不利影响,但不影响正确性——terminus-2 的 summarization 在上下文接近上限时自动压缩历史,任务仍可继续执行。
max_output_tokens 的选择:设为 4096 而非 8192,是为了给输入历史留更多空间(32768 - 4096 = 28672 可用于输入,而设为 8192 只剩 24576),在 32K 的紧张上下文下权衡后的选择。
补丁通过 scripts/apply_patches.sh 统一应用,TB1 和 TB2 均需。共涉及两个文件:
- context limit 感知:terminus-2 通过
litellm.get_max_tokens(model_name)获取上下文长度,对自定义本地模型(如openai/termylambda、openai/nemotron)该调用抛异常,回退值为 100 万,导致 proactive summarization 永不触发。patch 改为读取TERMINUS2_CONTEXT_LIMIT环境变量(默认 32768)。 - OLE 级联崩溃:输出超长(finish_reason=length)时,原代码将
error_msg同时追加进chat._messages又作为下一轮prompt传递,double-add 使上下文迅速膨胀。patch 移除重复追加,截断存储响应至 2000 chars,递归前检查剩余 token 空间。 - 初始 prompt 过大:
write-compressor等任务的 instruction 本身可超过 32768 token,在perform_task首次 LLM 调用前就会触发 400 错误。patch 在perform_task入口处截断超长 instruction,预留 4000 token 给模板开销和输出。
vLLM 对超长 prompt 返回 HTTP 400,LiteLLM 将其包装为 BadRequestError 而非 ContextWindowExceededError,导致 terminus-2 的 except ContextLengthExceededError 永不触发,任务直接崩溃。patch 在 LiteLLM.call() 的异常处理中额外检测 BadRequestError,当错误消息包含 "maximum context length"、"context window" 等关键词时,将其转换为 ContextLengthExceededError,使 terminus-2 能正常走截断/重试逻辑。
| 任务 | 最低要求 |
|---|---|
| 评测(vLLM 推理) | 1× GPU,20GB+ VRAM |
| GRPO 训练(BF16 + LoRA) | 1× A100/H100 80GB |
| 多卡训练 | 4× A100 80GB |
- 模型:Nemotron-Terminal-8B(基于 Qwen3-8B)
- 训练框架:TRL GRPOTrainer + PEFT LoRA
- 推理:vLLM
- 评测 TB1:terminal-bench v0.1.1
- 评测 TB2:Harbor + TerminalBench 2.0
- RL 数据:OpenThoughts-Agent-v1-RL(728 任务)
- 数据集 API:OpenAI gpt-5-mini(质量排序 + PRM 步级评分)
- 已训练模型:zcheng256/TermyLambda