1. 从 GRPO 到 GVPOCoding Agent 强化学习里的奖励错位到底长什么样如果你正在用 GRPO 训练 Coding Agent大概率遇到过一种很别扭的情况模型明明把测试用例跑通了奖励却给得很低或者模型前面几步就写错了关键逻辑最后靠硬编码把样例骗过去反而拿到了高奖励。这个现象就是奖励错位Reward Misalignment。它不是说奖励函数写错了而是奖励信号和代码真实执行结果在时间维度上对不齐。Coding Agent 和普通对话模型不一样。它的输出是一条 trajectory读题、分析、写代码、跑测试、根据报错改代码、再跑测试。GRPO 的做法是把整条 trajectory 当成一个整体最后给一个 outcome reward。问题就出在这个「最后」上。一条 trajectory 里可能有十几步真正决定成败的往往只有中间两三步但 GRPO 把功劳和锅平均分给了所有 token。我实测下来奖励错位最典型的两种表现是第一种早错晚对。Agent 在第三步选错了数据结构但后面靠大量补丁和特判把测试糊过去了。GRPO 看到最终测试通过给整条 trajectory 高奖励。模型学到的是「先写错没关系后面能补」而不是「一开始就要选对」。第二种早对晚错。Agent 前几步思路完全正确但在最后格式化输出时多打了一个换行测试挂了。GRPO 给 0 奖励前面正确的推理过程被一起丢掉。模型学到的是「这条路径不能走」可实际上路径是对的只是收尾有问题。GVPOGroup Verification-based Policy Optimization要解决的就是这个。它的核心思路是不只在 trajectory 末尾给奖励而是在 trajectory 的中间过程也插入验证信号把奖励重新分配到真正该负责的步骤上。你可以把它理解成从「期末考试定终身」改成「平时作业加期末考试」。这篇会从可复制的训练配置讲起给出奖励对齐的验证动作帮你在自己的 Agent 任务上观察奖励错位有没有缓解。适合已经在跑 GRPO、想进一步优化 Coding Agent 训练效果的读者。如果你还没搭好训练环境后面也会给出接入和验证的完整步骤。2. TaoToken 前置给 Coding Agent 训练管线接上可验证的模型调用在讲 GVPO 配置之前得先解决一个前置问题你的 Coding Agent 在训练和验证阶段需要一个稳定的模型调用入口来生成 trajectory、跑验证脚本、做 reward 打分。很多人的做法是直接调某个模型 API但训练管线里往往要同时用到多个模型——一个负责生成代码一个负责验证一个负责打分。如果每个都单独配 key、单独处理限流管线会变得很脆。我试过用 TaoToken 作为统一入口把生成模型和验证模型都挂在同一个 Base URL 下训练脚本里只维护一份配置。它的 API 地址是 https://taotoken.net/api兼容 OpenAI 风格的调用格式所以现有的 GRPO 训练代码基本不用大改把 base_url 和 api_key 换掉就行。具体来说Coding Agent 的 RL 训练管线通常有三个需要模型的地方生成阶段Agent 根据题目生成代码 trajectory这一步需要模型有较强的代码能力建议用 Claude 系列或同级别模型。验证阶段跑单元测试、静态检查、或者用另一个模型做代码审查这一步需要的是稳定和低延迟因为每条 trajectory 都要验证。打分阶段GVPO 的过程奖励需要模型对中间步骤做判断比如「这一步选的算法复杂度是否合理」这一步对模型的理解能力要求高。把这三个阶段都指向同一个 API 入口好处是配置统一、计费统一、限流策略统一。你不需要在训练脚本里维护三套 key也不需要在某个模型限流时手忙脚乱地切换。接入方式很简单在训练脚本的环境变量或配置文件里设置export TAOTOKEN_API_KEY你的key export TAOTOKEN_BASE_URLhttps://taotoken.net/api然后在代码里这样初始化客户端from openai import OpenAI client OpenAI( api_keyos.environ[TAOTOKEN_API_KEY], base_urlos.environ[TAOTOKEN_BASE_URL], ) response client.chat.completions.create( modelclaude-sonnet-4-20250514, messages[{role: user, content: prompt}], temperature0.7, )这里要注意模型 ID 要和你实际使用的模型对齐。如果你在验证阶段想用更便宜的模型做初筛可以在这里换成对应的 model 名称。GVPO 的过程奖励验证对模型能力有要求建议验证模型不要选太弱的否则过程奖励本身就不准反而引入新的噪声。如果你还没拿到 key可以去 https://taotoken.net/api-keys 创建。创建时建议按用途分 key比如训练生成用一个、验证打分用一个这样后面排查问题时能快速定位是哪个环节的调用出了状况。对于长期跑 Coding Agent 训练的任务可以考虑 Coding Plan它在高频调用场景下更划算。模型对话入口可以用来快速测试 prompt 和验证脚本不用每次都跑完整训练管线。3. 可复制配置GVPO 训练片段与奖励重分配参数这一节给出可以直接抄的配置。GVPO 的关键改动在奖励分配上所以配置分两部分训练框架的 GRPO 基础配置以及 GVPO 的过程奖励插入点。先看训练配置。假设你用的是类似 TRL 或 OpenRLHF 的框架核心参数如下# gvpo_config.yaml training: algorithm: gvpo base_algorithm: grpo group_size: 8 kl_coef: 0.04 clip_range: 0.2 learning_rate: 1.0e-6 batch_size: 4 mini_batch_size: 1 gradient_accumulation_steps: 8 reward: outcome_weight: 0.6 process_weight: 0.4 process_verify_steps: [3, 6, 9] verify_model: claude-sonnet-4-20250514 verify_temperature: 0.0 early_error_penalty: 0.3 late_error_discount: 0.5 trajectory: max_steps: 12 step_reward_decay: 0.95 verification_window: 2这里几个参数需要解释。process_verify_steps是过程验证的插入点意思是第 3、6、9 步之后各做一次验证。这个不是随便定的要根据你任务的 trajectory 平均长度来。如果你的 Agent 平均 8 步完成那验证点放在 2、4、6 比较合适。early_error_penalty是早错晚对的惩罚系数当验证发现前面步骤有错但最终通过了从 outcome reward 里扣掉这个比例。late_error_discount是早对晚错的折扣前面步骤验证通过但最终失败时不直接给 0而是按这个系数保留部分奖励。然后是过程奖励的验证 prompt 模板这个直接决定 GVPO 能不能正确重分配奖励PROCESS_VERIFY_PROMPT 你是一个代码审查专家。下面是一个 Coding Agent 在第 {step} 步的输出。 题目要求 {problem} Agent 到目前为止的代码 {code_so_far} 请判断这一步的代码逻辑是否正确是否引入了后续难以修复的错误 只输出 JSON {{step_correct: true/false, error_type: logic|syntax|none, confidence: 0.0-1.0, reason: 简短说明}} 这个模板的关键是让验证模型只判断「这一步」不要让它看最终结果。很多人的过程奖励做不好就是因为验证 prompt 里带了最终测试结果验证模型会偷懒直接看结果反推过程奖励就失去意义了。再看 reward 计算的核心逻辑def compute_gvpo_reward(trajectory, outcome_reward, process_verifications, config): total_reward outcome_reward * config[outcome_weight] process_reward 0.0 step_count len(process_verifications) for i, verify in enumerate(process_verifications): step_weight config[step_reward_decay] ** (step_count - i - 1) if verify[step_correct]: process_reward verify[confidence] * step_weight else: process_reward - config[early_error_penalty] * step_weight if outcome_reward 0 and any(not v[step_correct] for v in process_verifications): total_reward - config[early_error_penalty] * outcome_reward if outcome_reward 0 and all(v[step_correct] for v in process_verifications): total_reward config[late_error_discount] * config[process_weight] total_reward process_reward * config[process_weight] return total_reward这段逻辑对应了前面说的两种错位。早错晚对时outcome 给的高奖励会被 early_error_penalty 扣掉一部分早对晚错时不会直接归零而是保留 late_error_discount 比例的奖励。step_reward_decay让越靠后的步骤权重越低因为 Coding Agent 里前面的决策通常更关键。如果你用的是 Claude Code 做 Agent 的代码生成可以在 settings 里配置验证模型的调用{ model: claude-sonnet-4-20250514, base_url: https://taotoken.net/api, api_key_env: TAOTOKEN_API_KEY, verification: { enabled: true, verify_steps: [3, 6, 9], verify_model: claude-sonnet-4-20250514, temperature: 0.0 } }这个 settings 片段可以直接放进你的 Agent 配置目录。注意 base_url 和 api_key 要和前面训练脚本里的一致否则验证调用会走另一个入口排查问题时容易混淆。4. 验证请求跑一条 trajectory 看奖励是否对齐配置写完之后不要直接开全量训练。先跑一条 trajectory手动看奖励分配对不对。这一步很多人跳过结果训练跑了几小时才发现奖励还是错的。验证脚本的核心是生成一条 trajectory记录每一步的代码和验证结果然后打印奖励分解。import json from openai import OpenAI client OpenAI( api_keyos.environ[TAOTOKEN_API_KEY], base_urlos.environ[TAOTOKEN_BASE_URL], ) def run_single_trajectory(problem, config): trajectory [] code_so_far process_verifications [] for step in range(config[trajectory][max_steps]): prompt build_step_prompt(problem, code_so_far, step) response client.chat.completions.create( modelclaude-sonnet-4-20250514, messages[{role: user, content: prompt}], temperature0.7, ) step_output response.choices[0].message.content code_so_far \n step_output trajectory.append({step: step, output: step_output}) if step in config[reward][process_verify_steps]: verify_result verify_step(problem, code_so_far, step) process_verifications.append(verify_result) print(fStep {step} verify: {verify_result}) outcome run_tests(code_so_far, problem) outcome_reward 1.0 if outcome[passed] else 0.0 final_reward compute_gvpo_reward( trajectory, outcome_reward, process_verifications, config[reward] ) print(fOutcome reward: {outcome_reward}) print(fProcess verifications: {json.dumps(process_verifications, indent2)}) print(fFinal GVPO reward: {final_reward}) return final_reward def verify_step(problem, code_so_far, step): prompt PROCESS_VERIFY_PROMPT.format( stepstep, problemproblem, code_so_farcode_so_far ) response client.chat.completions.create( modelclaude-sonnet-4-20250514, messages[{role: user, content: prompt}], temperature0.0, ) return json.loads(response.choices[0].message.content)跑这个脚本时重点看两个地方。第一process_verifications里每一步的step_correct是否和你的直觉一致。如果验证模型把明显错误的步骤判成正确说明验证 prompt 需要调整或者验证模型能力不够。第二final_reward和outcome_reward的差距。如果 outcome 是 1.0 但 final 只有 0.5 左右说明过程验证发现了早错晚对惩罚生效了。如果 outcome 是 0 但 final 还有 0.2 左右说明早对晚错的折扣生效了。成功的结果长这样Step 3 verify: {step_correct: true, error_type: none, confidence: 0.9, reason: 数据结构选择合理} Step 6 verify: {step_correct: false, error_type: logic, confidence: 0.8, reason: 边界条件处理遗漏} Step 9 verify: {step_correct: true, error_type: none, confidence: 0.85, reason: 补丁逻辑正确} Outcome reward: 1.0 Process verifications: [...] Final GVPO reward: 0.62这里 outcome 是 1.0但因为第 6 步验证发现逻辑错误最终奖励被压到 0.62。这就是 GVPO 在起作用测试虽然过了但过程有问题奖励不会给满。反过来如果 outcome 是 0 但前面步骤都对Step 3 verify: {step_correct: true, error_type: none, confidence: 0.9, reason: 算法选择正确} Step 6 verify: {step_correct: true, error_type: none, confidence: 0.88, reason: 实现逻辑正确} Step 9 verify: {step_correct: true, error_type: none, confidence: 0.9, reason: 仅输出格式有误} Outcome reward: 0.0 Final GVPO reward: 0.24最终奖励不是 0而是保留了 0.24。模型不会因为最后格式化错误就丢掉整条正确路径。验证通过后再开全量训练。训练过程中建议每 50 步打印一次奖励分解的统计观察 outcome 和 process 的分布有没有异常偏移。5. 常见报错排查401、local proxy failed、reading choices、OAuth训练管线跑起来之后报错基本集中在模型调用和验证环节。这一节按真实报错来排查。401 Unauthorized这个最常见。先检查TAOTOKEN_API_KEY环境变量有没有正确导出。在训练脚本里加一行print(os.environ.get(TAOTOKEN_API_KEY)[:8])确认 key 的前几位对得上。如果用的是配置文件而不是环境变量检查配置文件路径有没有被正确加载。还有一种情况是 key 创建后没有复制完整建议重新去 https://taotoken.net/api-keys 复制一次。local proxy failed / connection refused这个报错通常出现在训练机器有网络策略限制的场景。先确认TAOTOKEN_BASE_URL设置的是https://taotoken.net/api不要带多余的路径或斜杠。然后在训练机器上单独跑一个 curl 测试curl -X POST https://taotoken.net/api/chat/completions \ -H Authorization: Bearer $TAOTOKEN_API_KEY \ -H Content-Type: application/json \ -d {model:claude-sonnet-4-20250514,messages:[{role:user,content:test}]}如果 curl 能通但训练脚本不通说明是脚本里的客户端配置问题检查 base_url 有没有被其他环境变量覆盖。如果 curl 也不通检查训练机器的出网策略确认能访问 taotoken.net。reading choices 报错 / choices 字段为空这个报错说明 API 返回了非预期格式。常见原因是模型 ID 写错了比如把claude-sonnet-4-20250514写成了别的名称API 返回了错误信息而不是正常的 choices 结构。排查方法是把原始 response 打印出来response client.chat.completions.create(...) print(response.model_dump_json(indent2))看返回体里是 error 字段还是 choices 字段。如果是 error按 error message 处理。另外验证阶段如果 temperature 设得太高偶尔会返回空 content建议验证调用统一用 temperature0.0。OAuth 相关报错如果你用的是 Claude Code 或类似的 Agent 工具可能会遇到 OAuth token 过期或未授权。这类工具通常有自己的认证流程和 API key 是两套。排查时先确认你用的是 API key 模式还是 OAuth 模式。如果是 API key 模式在 settings 里明确配置{ auth_mode: api_key, api_key_env: TAOTOKEN_API_KEY, base_url: https://taotoken.net/api }如果是 OAuth 模式需要重新走授权流程。训练管线里建议统一用 API key 模式避免 OAuth token 过期导致训练中断。验证模型返回非法 JSON过程验证的 prompt 要求返回 JSON但模型有时会加 markdown 代码块标记。在解析前先做清洗import re def parse_verify_json(text): text re.sub(rjson\s*|\s*, , text).strip() try: return json.loads(text) except json.JSONDecodeError: return {step_correct: False, error_type: parse_error, confidence: 0.0, reason: text[:100]}解析失败时不要直接抛异常中断训练给一个保守的默认值让训练继续跑同时记录失败次数。如果失败率超过 5%说明验证 prompt 需要加更强的格式约束。奖励值异常全 0 或全 1如果训练日志里 final_reward 全是 0 或全是 1先检查 outcome_reward 的分布。如果 outcome 本身全是 0说明生成阶段就有问题不是 GVPO 的锅。如果 outcome 有区分度但 final 没有检查 process_weight 和 outcome_weight 是不是设反了或者 process_verify_steps 设成了空列表。6. 把 GVPO 接进你的 Coding Agent 训练管线GVPO 的价值不在于替换 GRPO而在于给 GRPO 补上过程奖励这一环。你不需要推翻现有的训练框架只需要在 reward 计算那里插入过程验证把 outcome reward 和 process reward 按权重合并。实际落地时建议先在一条 trajectory 上验证奖励分配符合预期再开小批量训练观察 100 到 200 步的奖励曲线。如果 process reward 的方差远大于 outcome reward说明验证模型太不稳定需要降低 process_weight 或者换更稳的验证模型。训练管线里的模型调用统一走 https://taotoken.net/api生成、验证、打分三个环节用同一个入口配置和排查都会简单很多。验证脚本和 prompt 模板可以直接用第 3、4 节的代码改一下模型 ID 和验证步数就能跑。如果你在跑长期训练任务Coding Plan 在高频调用下更合适。快速测试 prompt 和验证逻辑时模型对话入口可以省去搭脚本的时间。接入文档里有完整的参数说明和示例遇到配置问题可以先查那里。