
1. 从 Meta SSR 说起Self-play RL 在 Coding 场景到底在练什么Self-play RL 放到 Coding 场景里本质是让同一个模型分饰两角一个负责往能跑通的代码里“埋雷”另一个只拿着被弱化过的测试补丁去“排雷”。Meta FAIR 与 TBD Lab 那篇 SSRSelf-play SWE-RL最抓人的点是它把“人类写 Issue、人类写测试”这条数据供应链彻底绕开了——只给一个打好依赖的 Docker 镜像剩下的测试发现、解析、运行命令全由 Injection Agent 现场探索。这件事对做 AI 应用开发的我们意味着什么意味着你不需要先攒几千条带标注的修复数据也能在本地搭一个“模型自己出题、自己解题、自己记错题本”的最小闭环。我先把 SSR 的核心循环拆成能落地的三件事。第一件是 Bug 制品Artifact它不是一个模糊的“请修 bug”提示而是一组文件bug_inject.diff在业务代码里植入缺陷test_weaken.diff把会暴露缺陷的断言删掉或弱化test_script.sh负责跑测试并输出文本日志test_parser.py把日志转成{test_id: pass/fail}的 JSON 供奖励计算test_files.txt记录哪些测试文件参与验证防止 Solver 靠改测试作弊。第二件是双角色奖励Injection 端拿到的奖励是分段的一致性失败给 -1.0Bug 不可解solve-rate s0或太简单s1给 -α只有理想难度 0s1 才给 1-(1α)s目标是把 solve-rate 拉到约 0.2Solver 端则是二进制的全部测试通过 1否则 -1。第三件是参数共享加同时 RL 更新两个角色的梯度一起回传让模型同时学会“造难”和“解难”。为什么这套东西值得你在本地复现因为它的工程门槛比想象中低。旧范式依赖 GitHub Issue/PR、人类写的自然语言描述和测试用例数据标着天花板SSR 只需要一份能跑起来的镜像数据随训练无限增殖。你要做的不是复刻 Meta 的完整训练集群而是用统一 Key 把多模型对战、奖励回传、失败重试这条链路先跑通。下面我会用 TaoToken 作为统一 API 通道给出可复制的环境变量、Base URL 配置、双角色对战脚本以及验证请求和常见报错排查。适合谁想跑通多模型自博弈闭环的开发者、做 Coding Agent 评测的同学、以及想理解 Self-play RL 奖励设计但不想从零搭训练框架的人。2. TaoToken 前置统一 Key 与 Base URL 怎么配才不踩坑在动手写对战脚本之前先把通道打通。TaoToken 在这里扮演的角色是统一 Key 和统一 Base URL 的入口让你用同一套凭证去调用不同模型省掉为每个模型单独维护 endpoint 和鉴权的麻烦。官网入口是 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content API 地址是 https://taotoken.net/api 注意 API 地址后面不加 UTM 参数配置时直接写这个根路径即可。你需要准备的东西只有三样一个可用的 API Key、一个模型 ID、以及正确的 Base URL。模型 ID 的选取建议遵循“双角色可同源也可异源”的原则——SSR 论文里两个角色共享同一套权重但你本地复现时完全可以先用两个不同模型跑通链路再换成同模型自博弈。比如 Injection 端用擅长代码生成的模型Solver 端用擅长长上下文推理的模型等奖励回传稳定后再统一。环境变量建议这样组织避免把 Key 硬编码进脚本export TAOTOKEN_API_KEYsk-你的实际Key export TAOTOKEN_BASE_URLhttps://taotoken.net/api export INJECT_MODEL你的注入角色模型ID export SOLVE_MODEL你的求解角色模型ID如果你用的是 OpenAI 兼容的 SDKBase URL 直接填https://taotoken.net/apiSDK 会自动拼接/v1/chat/completions这类路径。这里有个容易踩的坑有人把 Base URL 写成https://taotoken.net/api/v1结果请求变成/api/v1/v1/chat/completions直接 404。正确做法是只写到/api让 SDK 自己补版本段。关于 Key 的获取和模型列表你可以走 API Keys 页面和接入文档这两个入口在 CTA 部分我会给出带归因参数的链接。这里先强调配置纪律Key 只放环境变量脚本里用os.environ读取Base URL 统一常量模型 ID 单独抽成变量方便你后面做 A/B 对战。如果你打算长期跑训练闭环建议直接上 Coding Plan因为自博弈会产生大量高频请求按量计费在调试阶段容易失控而套餐制更适合这种“跑一晚上看曲线”的场景。配置完成后先别急着写对战逻辑用一条最小请求验证通道是否通。下面这段 Python 代码可以直接复制作用是发一条最简单的对话请求确认 Key、Base URL、模型 ID 三者都对import os from openai import OpenAI client OpenAI( api_keyos.environ[TAOTOKEN_API_KEY], base_urlos.environ[TAOTOKEN_BASE_URL], ) resp client.chat.completions.create( modelos.environ[INJECT_MODEL], messages[{role: user, content: 回复两个字通了}], ) print(resp.choices[0].message.content)如果这行打印出“通了”说明通道没问题可以进入下一步。如果报 401检查 Key 是否有多余空格如果报连接错误检查 Base URL 是否写成了带/v1的版本。这一步看起来简单但后面所有对战脚本都依赖它先把地基打牢。3. 可复制配置双角色对战脚本与奖励回传骨架现在进入核心部分。我要给你的不是论文的完整复现而是一个能在本地跑起来的最小闭环骨架Injection 角色生成 Bug 制品Solver 角色尝试修复奖励函数根据测试结果回传失败尝试进入错题本喂给下一轮。为了让配置可复制我先给出一个 JSON 形式的角色配置你可以直接存成ssr_config.json{ base_url: https://taotoken.net/api, roles: { injector: { model: 你的注入角色模型ID, system_prompt: 你是一个 Bug 注入器。给定一段可运行代码和测试生成 bug_inject.diff 与 test_weaken.diff并输出 test_script.sh 与 test_parser.py。, temperature: 0.7 }, solver: { model: 你的求解角色模型ID, system_prompt: 你是一个 Bug 修复器。你只能看到被弱化后的测试补丁的逆以及报错仓库。请输出修复补丁。, temperature: 0.2 } }, reward: { inject_consistency_fail: -1.0, inject_too_easy: -0.3, inject_ideal: 1.0, solve_pass: 1.0, solve_fail: -1.0, target_solve_rate: 0.2 } }这个 JSON 里的target_solve_rate对应论文里 challenger 的最优解 p*≈0.2意思是让 Injection 端把难度控制在 Solver 大约两成能解出来的水平太简单或太难都会掉奖励。inject_too_easy我用了 -0.3 作为 α 的占位你可以根据实际训练曲线调整。接下来是对战脚本骨架。它的流程是读配置、初始化两个 client、循环若干轮、每轮先让 injector 产出制品、再让 solver 尝试修复、根据测试结果算奖励、把失败样本写入错题本。为了让你能直接跑我用伪代码加真实 API 调用的混合写法测试执行部分用subprocess调test_script.shimport os, json, subprocess from openai import OpenAI cfg json.load(open(ssr_config.json)) client OpenAI(api_keyos.environ[TAOTOKEN_API_KEY], base_urlcfg[base_url]) def call_role(role, user_content): r cfg[roles][role] resp client.chat.completions.create( modelr[model], temperaturer[temperature], messages[ {role: system, content: r[system_prompt]}, {role: user, content: user_content}, ], ) return resp.choices[0].message.content def run_tests(script_path): result subprocess.run([bash, script_path], capture_outputTrue, textTrue) return result.stdout def parse_reward(log_text, parser_path): out subprocess.run([python, parser_path], inputlog_text, capture_outputTrue, textTrue) return json.loads(out.stdout) wrong_book [] for step in range(150): artifact call_role(injector, 基于当前仓库生成 Bug 制品) # 这里应把 artifact 落盘为 diff 并应用省略文件写入细节 log run_tests(test_script.sh) result parse_reward(log, test_parser.py) solve_rate sum(result.values()) / max(len(result), 1) if solve_rate 0 or solve_rate 1: inject_reward cfg[reward][inject_too_easy] else: inject_reward cfg[reward][inject_ideal] fix_patch call_role(solver, 根据逆测试补丁修复仓库) # 应用 fix_patch 后重跑测试 final_log run_tests(test_script.sh) final_result parse_reward(final_log, test_parser.py) solve_reward cfg[reward][solve_pass] if all(final_result.values()) else cfg[reward][solve_fail] if solve_reward 0: wrong_book.append({step: step, artifact: artifact, patch: fix_patch}) print(fstep{step} inject_r{inject_reward} solve_r{solve_reward} s{solve_rate:.2f})这段脚本的关键设计点有三个。第一奖励回传不是简单看“修没修好”而是先算 solve_rate 再决定 Injection 端奖励这样能防止注入器摆烂或出太简单的题。第二失败尝试直接进wrong_book下一轮可以把它作为高阶 Bug 喂回去对应论文里“错题本”的机制。第三test_parser.py的输出必须是{test_id: pass/fail}的 JSON这是奖励计算的契约格式错了整个闭环就断了。如果你用的是 Cline MCP 或 Claude Code 这类工具来驱动配置三件套要写全Base URL 填https://taotoken.net/apiKey 填你的实际 KeyModel ID 填配置里的模型 ID。缺任何一个都会在调用时报鉴权或模型不存在。Codex 的auth.json同理把这三项对应字段填对即可。CC Switch 场景下切换配置时注意 Base URL 不要带尾斜杠否则部分客户端会拼出双斜杠路径。4. 验证请求与成功结果怎么确认闭环真的在转脚本写完不等于闭环跑通。你需要一组验证动作确认“注入—求解—奖励—重试”这条链路每一环都有真实输出。我建议按下面四步走每步都有可观察的成功标志。第一步验证 Injection 端能产出完整制品。单独调一次 injector把返回内容打印出来检查是否包含bug_inject.diff、test_weaken.diff、test_script.sh、test_parser.py、test_files.txt这五类文件。如果模型只返回了一段自然语言描述而没有 diff说明 system prompt 需要加强结构化约束比如明确要求“只输出 JSON字段为文件名到内容的映射”。第二步验证测试脚本能跑并产出可解析日志。手动执行bash test_script.sh看它是否输出文本日志再执行python test_parser.py看是否输出合法 JSON。这一步的成功标志是你能看到类似{test_add: false, test_sub: true}的结构。如果 parser 报 JSON 解析错误多半是日志里混入了非测试输出需要在脚本里加过滤。第三步验证奖励回传数值符合预期。跑 5 到 10 轮观察打印出的inject_r和solve_r。正常情况下你会看到 solve_rate 在 0 到 1 之间波动inject_r 在理想区间给正奖励solve_r 偶尔给 1 偶尔给 -1。如果 solve_rate 长期是 0 或 1说明难度控制失效需要调target_solve_rate或检查注入策略是否退化成“一行改值”这种 trivial 修改。第四步验证错题本真的被复用。在脚本里加一行日志打印len(wrong_book)跑 20 轮后这个数应该大于 0。然后手动取一条错题把它作为下一轮 injector 的输入上下文看 solver 是否仍然失败——如果仍然失败说明这条错题确实是高阶 Bug闭环在自我升级如果一喂就过说明错题难度不够需要提高注入策略的多样性。成功结果的形态是什么样的参考论文 Figure 8 的曲线你应该看到 solve_rate 稳定在 0.2 附近小幅震荡而不是一路飙到 1 或跌到 0。同时wrong_book持续增长但增速放缓说明模型在学会解决越来越难的题。如果你跑的是双模型对战还可以对比两个模型互换角色后的曲线观察哪个模型更适合当注入器、哪个更适合当求解器。这里有个实测经验奖励函数里的 α 不要设太大否则注入器会倾向于出“不可解”的题来骗 -α 以外的惩罚规避反而学不到东西。我试过把 α 从 0.3 调到 0.1solve_rate 的震荡幅度明显收窄曲线更接近论文里的稳定自我提升形态。5. 本篇常见错排查401、local proxy failed、reading choices、OAuth跑闭环的过程中报错基本集中在通道层和解析层。我把最常见的四类列出来对照真实报错给排查路径。第一类401 Unauthorized。报错原文通常是Error code: 401 - {error: {message: Invalid API key}}。原因有三个Key 复制时带了换行或空格、Key 已失效、或者你把 Base URL 写成了别的域名导致鉴权头没发到正确服务。排查动作先用echo $TAOTOKEN_API_KEY | wc -c看长度是否异常再确认 Base URL 是https://taotoken.net/api。如果都没问题去 API Keys 页面重新生成一个 Key 替换。第二类local proxy failed。这个报错一般出现在客户端配置了本地代理但代理没启动时原文类似local proxy failed: connection refused。注意这里说的是本地网络配置问题不是让你去搞什么网络工具。排查动作检查你的 HTTP_PROXY/HTTPS_PROXY 环境变量是否指向了一个没在跑的本地端口如果是就 unset 掉检查客户端设置里是否开了“使用系统代理”但系统代理本身不可用。把代理配置清干净直连https://taotoken.net/api通常就恢复了。第三类reading choices 相关报错。典型原文是KeyError: choices或TypeError: NoneType object is not subscriptable发生在resp.choices[0]这一行。原因通常是请求返回了错误结构而不是正常 completion比如模型 ID 写错时返回的是 error 对象。排查动作在取 choices 之前先打印完整resp看它到底是 completion 还是 error。如果是 error检查模型 ID 是否在可用列表里如果是空 choices检查 messages 是否为空或格式不对。第四类OAuth 相关报错。如果你用 Claude Code 或类似工具接入可能会遇到OAuth token expired或invalid_grant。这类报错说明你走的是 OAuth 鉴权路径而不是 API Key 路径。排查动作确认你在工具里选的是 API Key 模式Base URL 填https://taotoken.net/apiKey 填 API Key 而不是 OAuth token。如果你确实需要 OAuth 流程去接入文档看对应工具的配置说明不要混用两种鉴权方式。除了这四类还有一个隐蔽的坑test_parser.py输出的 JSON 里 test_id 重复导致sum(result.values())算出的 solve_rate 失真。排查动作是在 parser 里加一行去重断言确保每个 test_id 只出现一次。这个坑不报错但会让你的奖励曲线看起来很奇怪早发现早省事。6. 把闭环跑长从最小复现到可持续的自我升级最小闭环跑通之后你大概率会想把它跑得更久、更稳。这里给几个不涉及训练框架改造的实用调整方向。第一把单轮对战改成批量对战。一次让 injector 生成 4 到 8 个 Bug 制品solver 并行尝试修复奖励取平均后再回传。这样做的好处是降低单次请求波动对奖励信号的干扰曲线更平滑。批量大小受限于你的并发额度如果用的是 Coding Plan可以适当开大。第二给错题本加优先级。不是所有失败样本都值得回喂优先选那些 solver 连续两轮都失败的题这类题的信息量最大。你可以在wrong_book里给每条记录加一个fail_count字段回喂时按 fail_count 降序取前 N 条。第三定期做角色互换。跑 50 轮后把 injector 和 solver 的模型 ID 对调观察曲线是否还能维持。如果能说明两个模型都具备双向能力如果不能说明其中一个模型在某个方向上偏弱这本身就是有价值的评测信号。第四把奖励日志落盘成 CSV字段包括 step、inject_r、solve_r、solve_rate、wrong_book_size。跑一晚上后直接用 pandas 画曲线比在终端看打印直观得多。这一步不需要额外依赖Python 标准库的 csv 模块就够。如果你想把这条链路接到更完整的 Agent 工作流里比如让模型自己决定什么时候注入、什么时候求解、什么时候请求人工介入那就需要更细的奖励设计和状态管理。这部分可以先从模型对话里试提示词确认角色行为符合预期后再固化进脚本。长期跑编码类自博弈任务的话Coding Plan 在请求配额和稳定性上会比按量调用更省心尤其是你需要连续跑几百轮的时候。最后留一个我踩过的坑不要在脚本里用time.sleep硬等测试执行测试脚本本身可能卡住。给subprocess.run加timeout60超时就当失败处理并记录否则整个闭环会挂在一个死循环的测试上。这个改动很小但能让你第二天早上看到的是完整曲线而不是卡死的进程。