Saito-Karuha's picture
|
download
raw
16.2 kB

基于 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_startstep_finish 标识。一个 Step Turn 中会出现模型输出的 textreasoning,以及由模型触发的 tool_usestep_finish 还会记录 token 使用情况,例如:

"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_countagent_idcommit_hashshared_state_hashexported_shared_content_hasheval_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=trueeval_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 并发扩展。首版使用一个固定的选择分数:

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 后的逻辑节点和逻辑边。

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_cleanlinessavg_knowledge 保留工具调用干净和共享知识行为充分的路径。对同一个 root 下的所有候选 root-to-leaf 轨迹,按 PathScore 从高到低排序,直接选择 Top-6;候选不足 6 条则全部保留。

被接受的路径会进一步导出为训练样本。OpenCode PR2 已经支持导出真实 request context,因此每个 assistant Step Turn 可以转换为 messages + tools 风格的 SFT sample;同时,路径级 metadata 需要保留 avg_cleanlinessavg_knowledgeavg_scoreavg_score_rankmax_scoremax_score_rankPathScore 等信息。方便后续分析。

最终数据集的主样本应来自通过筛选的 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.