多轮工具调用训练跑到第三、四轮就吐乱码、重要性采样比值暴涨、梯度范数瞬间拉满,这类雪崩十有八九是 void turn 没被过滤掉。用 TaoToken 拿模型通道,注册与创建 Key 在 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_content= 完成,然后让 Codex 帮你逐段核对那份「无效轮」检测代码漏在哪一行。这篇不聊排行榜,只做一件事:把 SimpleTIR 的启发式规则摊开,对着你自己的 rollout 源码、检测函数和训练日志,一个接入点一个接入点地查。
先讲清分工。SimpleTIR 的判定逻辑必须留在你自己的训练代码里,TaoToken 只负责提供模型通道的 Key 和 Base URL,Codex 通过这条通道读你贴过去的代码片段和日志,做静态对照与逐行解释。它不会连你的训练集群,也不会替你跑 rollout。回归测试、梯度检查、重跑训练,全都在本地执行,再把输出贴回对话——这条桥不能反过来搭。
1. 第三、四轮开始吐乱码:void turn 判定条件的现场还原
1.1 崩的链条从工具返回那一刻就埋好了
多轮工具调用里,模型的输出不是终点,而是一段会被拼回上下文的中间产物。Python 解释器的 stdout、搜索接口返回的 JSON、报错堆栈,这些内容的分布和模型预训练语料差得很远,属于典型的分布外 token。它们作为下一轮输入被喂回去之后,模型在采样时就开始偏离原本的语言分布。
偏一轮不致命,问题在于它会自我强化。第二轮偏离一点点,第三轮接着在偏离的上下文上再采样,到第三、四轮就会出现重复符号、半截标签、中英夹杂的乱码,甚至直接停止生成。与此同时,那些低概率 token 在新旧策略下的概率比会被放大成分母极小的分数,重要性采样比值从 1 附近窜到两位数,梯度范数跟着立起来,一次更新就能把前面几十步的收敛成果冲掉。
梯度裁剪和 KL 正则在这里只是止血。裁剪把超标的梯度按比例缩回去,等于把「这一轮学错了」这件事压住但没删掉;KL 惩罚则是在整体上拉住策略,管不了单条轨迹里的局部畸变。真正脏的数据还留在 batch 里,下一个 iteration 继续贡献噪声。
1.2 SimpleTIR 那 10 行到底判了什么
SimpleTIR 换了个思路:不去修正这条轨迹,直接判定它该不该进入策略更新。规则只有一句话——模型在某一回合既没有生成可执行代码块,也没有给出最终答案,这一轮就是 void turn,整条轨迹直接丢弃,不参与更新。
这个判定很粗暴,但正好切断了前面那条链。第一,无效轮几乎总是伴随极低概率 token,把它连同整条轨迹一起扔掉,等于从源头掐掉了高幅值梯度的来源。第二,无效轮之后的那次失败,不应该拿来惩罚前面几轮本来正确的推理步骤,整条丢弃就避免了对正确步骤的信用分配错位——你不需要去算「这两步该给多少分」这种没有标准答案的账。
要注意「整条」这两个字。不是把无效轮对应的 token 的 loss mask 置零,而是把这条轨迹从这一批样本里删掉。置零只是让这一步不产生梯度,但优势函数、比值计算仍然把这条轨迹算进分母,噪声还在。这一点是后面排障时最容易写错的地方。
2. 备齐三份材料,再去拿 Key 配 Codex
2.1 递给 Codex 的三份材料
在打开任何配置之前,先把材料准备齐,否则对话里只能靠描述,Codex 也只能给你泛泛的结论。三份材料分别是:
- rollout 生成函数:从第一轮拼 prompt、调工具、把返回值拼回上下文,到循环结束的整段代码。重点是工具返回结果的拼接位置。
- void turn 检测函数或片段:就是那段判「既无代码块又无最终答案」的逻辑,含它调用的代码块提取函数。
- 一段真实训练日志:包含每步的有效轨迹条数、importance sampling ratio 的 min/max、grad norm。有 ratio 曲线更好。
把这三份东西按顺序贴进对话,比丢一句「我的训练炸了」有用得多。日志不用全贴,把 ratio 出现尖刺的前后 20 步截出来就够定位。
2.2 创建 Key 与模型 ID 的取法
材料备好之后,打开 TaoToken 注册账号,进控制台创建一把 API Key,记作YOUR_API_KEY。模型 ID 不要凭记忆写,以站内模型广场当时的列表为准,把列表里的 ID 原样复制过去,不要自己加日期后缀。
一个常见的小坑:Key 只显示一次,创建完先存到密码管理器里,后面 Codex 的env_key要用到它。
2.3 在 config.toml 里把 Codex 指到统一通道
Codex 走的是~/.codex/config.toml,不是 Claude Code 那套ANTHROPIC_*环境变量,两者别混。按下面这份改,注意base_url是填进工具的接口地址,末尾不带/v1,也不要挂任何查询参数:
model = "YOUR_MODEL_ID" model_provider = "taotoken" [model_providers.taotoken] name = "TaoToken" base_url = "https://taotoken.net/api" env_key = "TAOTOKEN_API_KEY" wire_api = "chat"Key 通过环境变量传进去,别硬编码进文件:
export TAOTOKEN_API_KEY=YOUR_API_KEY codex如果你的通道按responses协议对接,把wire_api改成对应值即可,具体以接入文档说明为准。改完启动 Codex,随便问一句「读一下这段文件里的函数」,能正常返回就说明通道通了。
3. 逐段核对 10 行无误轮检测:三个最容易漏的接入点
3.1 判定端:代码块与最终答案必须是「双空」
把检测逻辑单独抽成一个纯函数,先保证它本身是对的,再去谈接入位置。下面这段只做一件事:判断这一轮是不是既没有代码块、也没有最终答案。
CODE_FENCE = re.compile(r"```(?:python|py|bash|sh)\s*\n(.*?)```", re.S) def has_executable_code(text: str) -> bool: return CODE_FENCE.search(text or "") is not None def is_void_turn(turn_text: str, final_answer: str | None) -> bool: no_code = not has_executable_code(turn_text) no_answer = not (final_answer or "").strip() return no_code and no_answer让 Codex 对照你原来的实现检查三处:代码块的正则是不是只认了 Python 围栏,导致其他语言的可执行代码被漏判成空;工具返回的代码块是不是也被同一段正则扫进去了,那属于误判;最终答案的判定是不是用了过于宽松的endswith匹配,把「我再确认一下」这种过渡句也算成了答案。
3.2 丢弃端:整条轨迹要在策略更新之前消失
判定函数只是零件,真正决定成败的是它被调用的位置。理想顺序是:rollout 生成完毕 → 逐条扫描是否存在 void turn → 剔除整条轨迹 → 计算优势与比值 → 策略更新。判定写在策略更新之后,等于这一批脏数据已经污染过参数了。
for traj in rollouts: traj["valid"] = not any( is_void_turn(t["text"], t.get("final_answer")) for t in traj["turns"] ) kept = [t for t in rollouts if t["valid"]] # 从这里开始才算 advantage、ratio、KL把这段代码贴给 Codex,让它指出你的训练脚本里 advantage 的计算位置在剔除之前还是之后。这一步是纯静态对照,不涉及任何执行。
3.3 日志端:从 ratio 尖刺倒推是哪一步漏滤
如果源码层面看不出问题,就回到日志。把尖刺前后的步数截出来,让 Codex 一起看三组数字:有效轨迹条数、单条轨迹的最大 token 长度、ratio 的极值。典型的漏滤特征是——ratio 极值和最大序列长度同时出现在同一步,而且那一步的有效轨迹条数没有下降。
反过来,如果有效轨迹条数确实在下降,但 ratio 依然会尖刺,那就要怀疑淘汰发生得太晚,脏轨迹已经参与了一次更新。这个判断不需要跑实验,把日志和源码并排贴出来讨论就够了。
4. 第三、四轮乱码与梯度炸裂的排障对照
| 现象 | 更可能的原因 | 先查哪里 |
|---|---|---|
| ratio 尖刺且伴随超长乱码轮 | 该轮未被判为 void,仍进了 loss | is_void_turn与剔除位置 |
| 有效轨迹数长期不变 | 检测函数恒返回 False | 代码块正则、最终答案取值来源 |
| 某步有效样本数为 0 | 一批轨迹被整组丢弃 | 是否需要重采样或放宽批次策略 |
| 报 401 | 环境变量名与env_key不一致 | config.toml与 shell 导出 |
| 请求路径重复 | base_url末尾多写了/v1 | 改回https://taotoken.net/api |
4.1 误判成「有效轮」的几种写法
最隐蔽的一类是把「我这就要调用工具了」这种过渡性发言当成有效轮。它既没有代码块,也没有最终答案,按规则就该判 void,但如果你的检测只看tool_calls是否非空,就会被放过去。工具调用本身是否算「可执行动作」,要按你自己框架对动作的定义对齐,别用一套标准写检测、用另一套标准算奖励。
第二类是把工具返回的内容当成模型的输出。解析上下文时如果没有把 role 区分开,正则就会扫到工具侧贴进来的代码块,于是明明空洞的一轮被判成了有代码。第三类是最终答案字段的取值时机,有些框架在生成结束前就把final_answer预填了一个空字符串以外的占位内容,检测自然失效。
4.2 配置侧的两个报错
401基本都是 Key 没被读到:env_key写的是TAOTOKEN_API_KEY,但 shell 里导出成了别的名字,或者新开一个终端窗口忘了再导出一次。验证时不要只看 Key 字符串有没有贴对,直接看请求返回的错误体更准确。
另一个高频问题是地址写错。填进 Codex 的base_url是https://taotoken.net/api,末尾的/v1不要加,也不要在这一项上挂任何查询参数。模型 ID 报错则先回模型广场核对,以当时列表里的写法为准,不要保留旧的记忆版本。想确认通道本身是否正常,可以先去 模型对话 用同一把 Key 发一条消息,绕过 Codex 先验证 Key 和模型 ID 这一层。
4.3 训练侧:整组被丢空的时候
整组轨迹都被判成 void,是这套启发式规则在大 batch 下的正常代价。处理方式不是放宽判定,而是把重采样当成流程的一部分:一批样本全丢,就重跑一次 rollout,或者把 batch 内有效轨迹的最小条数作为监控指标打出来。
还有一个容易被忽略的副作用:如果检测把「带正确最终答案的短轨迹」也判成 void,有效样本会莫名变少,训练看起来更稳定但学得更慢。查法很简单——统计一下被判 void 的轨迹里,有多少条的最后一轮其实给出了正确答案。这个比例长期高于预期,就该回头修判定条件了。
5. 小批量回归验证,再回控制台对账
5.1 用一条已知含 void turn 的轨迹做回归
准备一条你确定第三轮是乱码、且第三轮既无代码块也无最终答案的样本,单独跑检测函数,确认它返回 True;再打印剔除前后的轨迹条数,确认这条样本在 advantage 计算之前就消失了。这一步只跑数据,不跑训练,几秒钟就能出结果,但它能挡住后面几天的无效调参。
之后把剔除逻辑改动的 diff、加上这段验证输出一起贴回 Codex 对话,让它按前面三个接入点再对照一遍:判定条件有没有漏、丢弃位置够不够早、策略更新前有没有残留的引用。确认无误再放开批量训练,并盯住前 50 步的 ratio 极值曲线。
5.2 回到控制台看这次调用有没有记上账
通道跑通、检测逻辑也核完之后,值得顺手确认一下账目。打开 控制台 API Keys 看看刚才那把 Key 的调用记录有没有正常入账;如果打算长期用它做代码对照,Coding Plan 里能看清套餐额度够不够支撑你的日常读码量。Key 用完了记得回 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_content= 重建一把,别把旧 Key 硬编码进训练脚本里。
排障这件事,最贵的成本从来不是找到答案,而是把问题描述清楚。把 void turn 判定摊成三行代码、把接入点标成两个位置、把日志截成 20 步,这三个动作做完,往往不用等 Codex 给结论,你自己就已经看见漏在哪一行了。