Buckets:
| # 基于 Student Snapshot 与 Token-Aware MCTS 的 Agentic SFT 数据构建方法 | |
| 本文记录新版 `coral + opencode` Agentic SFT 数据构建方法。它是一个顶层方法大纲,重点说明数据从哪里来、探索过程如何组织、以及最终怎样选择可用于训练的高质量轨迹;不涉及具体代码实现细节。更具体的算法规则、评分公式和默认超参数见 `PR_docs/algorithm.md`。 | |
| ## 背景说明与定义解释 | |
| Coral 可以被抽象为一个外层自进化 harness。它同时启动多个相互独立的 Agent 执行进程,每个 Agent 有自己的 git worktree,代码修改互不干扰;这些 Agent 之间唯一稳定共享的信息通道,是通过软链接挂载到各自 runtime 目录下的共享知识库,例如 `.opencode/notes/`、`.opencode/skills/`、`.opencode/attempts/`、`.opencode/roles/` 等。 | |
| 从单个 Agent 的视角看,它的工作循环是:读取任务说明、代码、共享知识和历史 attempt;调用模型进行一次或多次推理;通过工具修改代码、运行本地测试、写笔记或技能;最后使用 `coral eval -m "..."` 提交一次候选解。`coral eval` 会提交当前 worktree 的代码快照,写入 pending attempt,等待 grader 给出分数与反馈。随后 Coral manager 会根据 eval 结果、heartbeat 策略、超时或重启逻辑,把新的 prompt 注入到同一个 OpenCode session 中,让 Agent 继续下一轮优化。 | |
| 这里需要区分两个层级的“turn”。 | |
| **Step Turn** 是 OpenCode 内部的一次模型调用。在 `.coral/public/logs/*.log` 中,它通常由 `step_start` 和 `step_finish` 标识。一个 Step Turn 中会出现模型输出的 `text`、`reasoning`,以及由模型触发的 `tool_use`。`step_finish` 还会记录 token 使用情况,例如: | |
| ```json | |
| "tokens": {"total": 53596, "input": 53494, "output": 102} | |
| ``` | |
| 这里的 `tokens.total` 是一个关键观测量。它不是某个工具调用的局部长度,而是当前 OpenCode session 在该步模型调用时实际承载的上下文长度。随着同一个 session 不断工作,这个数通常会不断增大。OpenCode 自身的自动 compaction 也是在模型上下文接近上限时触发,因此 `step_finish.tokens.total` 可以作为判断某条轨迹是否接近压缩风险的主要代理指标。实际 pipeline 中,我们可以记录每个 Rollout Turn 内所有 Step Turn 的最大 token total,并把它作为树搜索中的上下文预算信号。 | |
| **Rollout Turn** 是 Coral / 数据构建算法层面的一条边。它表示从一次 eval 结果反馈之后开始,Agent 继续工作,直到下一次 `coral eval -m` 成功提交并拿到评分为止。一个 Rollout Turn 内部可以包含一个或多个 Step Turn,也可以包含很多本地 bash 测试、文件编辑、笔记更新和共享知识写入。也就是说,Step Turn 是模型调用粒度,Rollout Turn 是一次“提出新解并被 grader 评分”的实验粒度。 | |
| 在原始 Coral 运行中,`.coral/public/logs/agent-x.y.log` 往往近似对应一次 start / resume / heartbeat 后的执行片段,并且经常覆盖一个 Rollout Turn。但算法定义上,Rollout Turn 应该以 `coral eval -m` 的成功提交与评分为边界,而不是机械地以 log 文件名为边界。后续构建树结构时,每条边都应该明确记录:起始节点状态、该边执行过程中产生的 OpenCode session 轨迹、最终 attempt、评分结果、最大上下文 token、以及共享知识变化。 | |
| 新版方法保留“从真实中间状态出发进行分支式数据收集”的总体思想,但做两个关键改变。 | |
| 第一,snapshot 不再由 Teacher Model 的完整 Coral 运行产生,而是由 Student Model 先正常运行 Coral 产生。也就是说,对每个 task,我们先让目标学生模型或其同族模型在真实 Coral 多 Agent 环境中自进化,并在每次 `coral eval -m` 时导出 snapshot。这样得到的起点状态来自 Student 的真实 on-policy 分布,而不是 Teacher 走出的高能力、可能对 Student 完全 OOD 的状态。这个动机接近 OPD:Teacher 负责从 Student 真实会遇到的状态出发补充高质量行为,而不是把 Student 直接拉到 Teacher 自己的状态分布里模仿。 | |
| 第二,树搜索不再采用简单 beam search。固定宽度 beam search 隐含了一个不成立的假设:同一深度的多个 Agent 分支消耗了近似相同的思考预算,可以直接按这一轮 eval 分数比较。实际情况并非如此。一个分支可能花费大量 token 做深入分析、写脚本和本地验证,另一个分支可能很快提交一个浅尝试;如果只按同一轮的 grader 分数剪枝,就会系统性偏向“短期看起来更好”的分支,并误杀那些当前分数一般但保留了大量有效上下文、知识整理或后续潜力的路径。更合理的方式是把上下文 token 预算纳入树搜索,采用类似 MCTS 的异步、预算感知探索。 | |
| 在这个视角下,一棵探索树的 root 是一个 Student snapshot。每个节点表示一次 Teacher 探索过程中的可继续状态,包含代码状态、共享知识状态、最近一次 attempt 结果、OpenCode session 状态、当前累计上下文 token、路径长度和历史轨迹摘要。算法上的一条逻辑边由连续两个 Rollout Turn 合并而成:Teacher 从父节点出发连续完成两次 `coral eval -m`,并生成子节点。沿着同一条 root-to-leaf 路径,我们保留 Teacher 自己产生的 OpenCode 上下文;当要从同一个节点扩展多个候选子分支时,需要 fork 该节点的文件系统状态和 OpenCode session 状态,让多个子分支从同一上下文出发独立继续。 | |
| 注意,树的构建是从全新的Agent开始,这里不复用 Student 原始长会话;Student 只提供 root snapshot。Teacher 的 session 树是在数据收集阶段重新生成的。 | |
| ## Pipeline 执行流程 | |
| 整个 pipeline 分为三个核心问题:如何挑选 snapshot 作为 root,如何用 token-aware MCTS 执行探索,以及探索终止后如何选择高质量 root-to-leaf 轨迹。 | |
| ### 我们怎么挑选 snapshot? | |
| 对每个 task,第一阶段先运行 Student Coral。这个过程保持真实多 Agent 设置:多个 Student Agent 各自有独立 worktree,共享同一个知识库,通过 `coral eval -m` 不断提交尝试。PR3 中的 snapshot 导出机制已经解决了状态采集问题:每次 eval 后会导出包含 `code/`、`shared/` 和 `metadata.json` 的 portable snapshot,其中 metadata 记录 `eval_count`、`agent_id`、`commit_hash`、`shared_state_hash`、`exported_shared_content_hash`、`eval_ok` 等信息。 | |
| snapshot 选择的目标不是只挑“分数最高”的状态,而是挑“适合 Teacher 继续探索、且对 Student 学习有价值”的起点。一个好的 root 应该满足三点:它来自 Student 真实会到达的状态;它有足够的共享知识和尝试历史,使 Teacher 能展示如何利用 notes / skills / attempts;它仍然有可优化空间,使后续 Rollout Turn 能体现真实的自我改进过程。 | |
| 首版采用阶段分层的选择策略。首先收集所有 snapshot,只过滤掉 metadata 中没有 `eval_ok` 字段的 pending snapshot;`eval_ok=false` 的 snapshot 也会先保留,用于估计 Student run 的阶段分布。每个 snapshot 的当前提交分数通过 `metadata.commit_hash` 对应到 `shared/attempts/<commit>.json` 中读取;snapshot 的时间位次来自目录名里的全局保存序号,例如 `eval-000003-agent-3-...` 表示所有 Agent 合并后的第 3 个 snapshot。 | |
| 随后按全局保存顺序构造 score 序列,并用窗口约为 5 的移动平均做平滑。用前 5% 平滑得分均值作为初值 `S0`,后 20% 平滑得分均值作为终值 `S∞`,再用累计增益比例 `c=(s-S0)/(S∞-S0)` 将 snapshot 划分为前期、中期和后期:`c<0.5` 为前期,`0.5≤c<0.9` 为中期,`c≥0.9` 为后期。如果 Student run 没有明显上升趋势,则退化为按时间位次划分前 10%、中间 40%、后 50%。 | |
| 最终每个 task 默认选择 30 个 root snapshot,名额按 `5 / 15 / 10` 分配给前期、中期和后期。实际被选为 root 的 snapshot 必须满足 `eval_ok=true`;`eval_ok=false` 只参与阶段估计,不进入最终 root 列表。阶段内部不按 score 选最高分,而是从该阶段的 `eval_ok=true` snapshot 中抽取对应数量的点;如果某个阶段不足,则由其他阶段补齐,优先补中期,再补后期,最后补前期。这样得到的 root pool 覆盖 Student 真实优化过程中的不同阶段,但仍然更偏向中期状态。 | |
| ### 树结构 / MCTS 如何执行探索、每一步在干嘛、什么时候终止 | |
| 给定一个 root snapshot,第二阶段从它启动 Teacher 探索树。root 的文件系统来自 snapshot 的 `code/` 和 `shared/`:`code/` 物化为新的 worktree,`shared/` 物化为该探索树的初始共享知识库。随后从 root 启动多个上下文全新的 Teacher OpenCode session,形成第一层多个并行探索方向。(注意这里是并行探索,每个节点从root到该节点的轨迹,或者说opencode session是独有的,其共享知识库也是独有的,并没有共享关系)这个初始 session 是 Teacher 新建的,不继承 Student 的任何历史上下文。 | |
| 树中的每个节点都保存一个“可继续状态”。这个状态不仅包括代码和 shared 文件系统,也包括 Teacher 当前 OpenCode session 的位置。原因是我们希望一条 root-to-leaf 轨迹展示连续的自我改进能力:它读过哪些历史、做过哪些实验、如何理解上一轮失败、如何在下一轮调整。不要每条边都重新 fresh start。 | |
| 一次扩展节点时,系统从该节点 fork 出一个子工作副本。fork 的含义是复制该节点的代码状态、shared 状态和 OpenCode session 状态(基本上可以理解为把这一个session复制成两个一样的副本),然后在子副本中让 Teacher 连续执行两个 Rollout Turn。每个 Rollout Turn 都以一次 `coral eval -m` 作为边界;两次 Rollout Turn 共享同一个 fork 出来的 session。只有第二次 Rollout Turn 结束后的状态会成为树上的子节点;子节点的 score 取这两次评测中的最大值。 | |
| MCTS 的关键不是让所有分支同步走同样深度,而是每次从当前树中选择一批最值得继续扩展的 active nodes 并发扩展。首版使用一个固定的选择分数: | |
| ```text | |
| Select(n) = Q(n) + U(n) - T(n) | |
| ``` | |
| `Q(n)` 由当前节点自身 score 排名、该路径曾经达到过的最好 score 排名、以及路径平均 soft verifier score 组成;`U(n)` 是较弱的探索奖励,只轻微鼓励还没有充分扩展的节点;`T(n)` 是 token 压力项,随着节点当前 session 上下文长度接近 100K 而快速增大。 | |
| 这里的 token 不应该只看某条边的增量,而要看节点所在 Teacher session 到目前为止的整体上下文长度。实际记录时,可以使用该节点最新 log 中最后一个 `step_finish.tokens.total`,或路径上观测到的最大 token total。这个指标直接反映模型下一步继续推理时面对的上下文规模。首版将 70K 作为 soft token 起点,将 100K 作为硬停止阈值;到达 100K 的节点不再扩展,而是作为正常 leaf 进入最终路径选择。 | |
| 扩展完成后,需要对新逻辑边做 verifier。首版 hard prune 只保留两个标准:任意一次 `coral eval -m` 没有返回数值 score,或该逻辑边整体工具错误率超过 30%。为了避免早期剪枝导致探索路线过少,第一层有一次 fallback;第二层默认不做 hard prune;第三层及以后恢复正常剪枝。 | |
| 通过保留规则的逻辑边会计算 soft verifier score。soft score 由 outcome、efficiency、cleanliness 和 knowledge 四部分组成。outcome 看两次 Rollout Turn 的最大 score 是否超过父节点 score;efficiency 只在没有提升且 Step Turn 数超过路径平均 10% 时扣分;cleanliness 只由工具错误率决定;knowledge 要求两个 Rollout Turn 都发生过共享知识读取或写入。这个 soft score 用于后续节点选择中的 `process(n)`,也作为最终路径选择的过程指标来源。 | |
| 新节点产生后,MCTS 会更新该路径上的 score、soft verifier、token 长度和子节点数量等信息,并据此判断新节点是进入 active candidate set,还是成为正常 leaf。后续 selection 会自然偏向那些既有分数潜力、过程质量较好、又没有过早耗尽上下文预算的路径。 | |
| 探索终止有两类条件。节点级终止包括:上下文 token 达到 100K、或深度达到 4 条逻辑边、或节点没有继续扩展空间但路径本身有效。整棵树终止包括:已经得到 12 条正常结束 leaf、有效逻辑边数量超过预算 40、或没有 active candidate nodes。 | |
| 这套过程和 beam search 的差别在于,它不是“所有分支走一步、排序、砍掉大部分、再走一步”。它更像在一个异步树上分配有限上下文预算:某条路径如果每轮很短且持续产出有效改进,可以走得更深;另一条路径如果一次 Rollout Turn 就消耗大量 token,即使分数不错,也可能被视为接近终止;还有一些当前分数一般但上下文健康、知识行为好、探索方向新颖的路径,会保留继续尝试的机会。 | |
| 补充:root 节点一次性扩展 3 个方向,用来尽早制造多样探索路线;非 root 节点最多扩展 2 个有效子节点。每轮选择时不是只选 top-1,而是选择一批 Select 分数最高的 active nodes 并发扩展,具体并发宽度由 SGLang 服务能力决定。 | |
| ### 终止探索后怎么选择好的 root-to-leaf 轨迹 | |
| 当某个 root 的 MCTS 探索终止后,训练数据不是直接取所有边,也不是只取单步最高分 attempt。我们关心的是从 root 到 leaf 的完整路径,因为这条路径展示了一个 Agent 如何从 Student 的真实中间状态出发,连续读取知识、提出实验、提交 eval、吸收反馈、再继续改进。 | |
| 每个 leaf 对应一条候选 root-to-leaf trajectory。首版使用四个路径级指标选择数据:路径平均 cleanliness、路径平均 knowledge、路径平均节点得分的百分位排名、路径最大节点得分的百分位排名。这里的节点和边都是 MCTS 中合并两个 Rollout Turn 后的逻辑节点和逻辑边。 | |
| ```text | |
| PathScore(p) | |
| = 0.35 * max_score_rank(p) | |
| + 0.25 * avg_score_rank(p) | |
| + 0.20 * avg_cleanliness(p) | |
| + 0.20 * avg_knowledge(p) | |
| ``` | |
| `max_score_rank` 权重最高,因为我们希望训练数据中包含真正到达过高质量解的路径;`avg_score_rank` 用来避免只因为偶然高点而选择整体较差的轨迹;`avg_cleanliness` 和 `avg_knowledge` 保留工具调用干净和共享知识行为充分的路径。对同一个 root 下的所有候选 root-to-leaf 轨迹,按 `PathScore` 从高到低排序,直接选择 Top-6;候选不足 6 条则全部保留。 | |
| 被接受的路径会进一步导出为训练样本。OpenCode PR2 已经支持导出真实 request context,因此每个 assistant Step Turn 可以转换为 `messages + tools` 风格的 SFT sample;同时,路径级 metadata 需要保留 `avg_cleanliness`、`avg_knowledge`、`avg_score`、`avg_score_rank`、`max_score`、`max_score_rank`、`PathScore` 等信息。方便后续分析。 | |
| 最终数据集的主样本应来自通过筛选的 root-to-leaf 路径,而不是孤立 step。原因是我们的目标不是训练模型学会单次工具调用格式,而是激发小模型在 Coral/OpenCode 场景下的自我改进能力:它需要学会利用共享知识、提出可评估实验、根据 grader 反馈调整方向、控制上下文预算,并在多轮 eval 内持续优化。新版 Method 的核心就是用 Student snapshot 保持状态分布 on-policy,用 Teacher token-aware MCTS 补充高质量连续行为,再用路径级 verifier 选择真正值得蒸馏的轨迹。 |
Xet Storage Details
- Size:
- 16.2 kB
- Xet hash:
- 6b3183e73677e783ca232ffe9fe168eb4f721dea612ec9389cc632119ee241df
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.