你的轨迹日志记录的是你的外壳,不是一份「好行为」数据集——而其中值得留下的是环境,不是那些对话记录。
每个把智能体跑上生产的团队,最终都会提出那个飞轮:我们有几百万条真实轨迹,拿好的那些去微调吧。杀死它的那笔账比智能体本身更老——行为克隆的误差按时间跨度的平方累积,因为第一次偏离就把模型推进了一个你的数据从未包含过的状态,而轨迹日志里只有你旧策略去过的状态。请从轨迹里收割环境、评测集和失败标签;权重放到最后再碰,并且要预料到:无论你得到什么,外壳都被烤进去了。
步骤 1生产与你的数据集之间隔着三道筛子,而没有一道是谁挑选的。
一条轨迹不是「智能体应当做好的任务」的一份样本。它是一份活过了三道筛子的任务样本,而每道筛子都由一个各有自己算盘的系统施加。
用户尝试了什么。用户学你的智能体学得很快。一个月之内,他们就不再去要那些它做得差的事情——这意味着你正在记录的分布,正是你当前模型已经擅长的分布。产品里最难的那一类,同时也是日志里最罕见的那一类;而它罕见正是因为它难。
什么跑完了。撞上步数上限、超时、工具 500 或拒答的那些运行,在你的错误看板上被过度代表,而在任何训练流水线会称之为「可用」的东西里被严重欠代表——因为它们没有一个干净的最终答案可供监督。最像训练数据的那些轨迹,恰恰是什么有意思的事都没发生的那些。
你留下了什么。你的采样与保留策略如今成了一项数据选择策略。5% 的头部采样静默丢掉了你最想要的那条长尾的 95%;30 天的窗口删掉的恰恰是你造不出来的那些季节性用例;而对某个企业客户作出的零保留承诺,会把你要求最高的那份工作负载整个从语料里移走。
在有人动手写加载器之前,先把你的保留配置与训练集之间那次 join 写下来。诚实的那句话通常是「我们即将在一份按 5% 头部采样、保留 30 天、偏向成功、且来自我们最小客户所提需求的子集上做微调」——而这句话会改变这个项目。
步骤 2那个平方项:为什么每步 99% 的准确率并不等于 26% 的失败率。
下面是每份飞轮提案都会算的那笔账,以及它漏掉的那一笔。假设你微调后的策略在 1% 的步骤上偏离了被演示的动作。在一个 30 步的任务上,按「误差独立」的算法得到 1 − 0.99³⁰ = 0.26——至少出现一次偏离的概率是 26%,听起来还活得下去,于是团队就据此上线了。
但那是乐观上界,不是现实情形。模仿学习的那个经典结论——在 2026 年 5 月一篇重访 DAgger 的论文里针对 LLM 智能体被重新表述——是:行为克隆的误差并不随时间跨度 T 线性累积,而是按平方累积,因为这些状态并不独立。第一次偏离会把智能体推进一个演示从未覆盖过的状态分布,在那里它的错误率不是 1% 而是未知,而此后每一步都从那里继续复合。在由教师(你的旧策略,或你从中蒸馏的更强模型)诱导出的状态上训练,却在由学生诱导出的状态上评估,这就是整个领域以之命名的那种训练—测试错配。
由此得出两个后果,而且都很实际。第一,时间跨度是你数据集设计里的一等项:同一份语料,产出一个稳健的 5 步工具调用模型,却产出一个令人失望的 40 步研究智能体,而解法不是再多来点同样的数据。第二,你最需要的数据是关于恢复的数据——一个好策略从一个坏策略造出来的状态里会怎么做——而这恰恰是一份按成功过滤过的轨迹日志里没有的数据。DAgger 的解法是把状态分布逐步从教师诱导移向学生诱导:让候选策略跑起来,收集它到达的那些状态,并在那里标出正确动作。
# the two datasets are not the same shape teacher_states = traces.filter(success=True) # where the OLD policy went student_states = rollouts(candidate).collect() # where the NEW policy goes # round k: label the states the student actually reaches for k in range(rounds): s = mix(teacher_states, student_states, beta=0.9 ** k) labels = oracle(s) # verifier, stronger model, or human candidate = train(candidate, s, labels) student_states = rollouts(candidate).collect()
请注意这个循环所需要、而轨迹日志提供不了的东西:一个针对「尚无人见过的状态」的裁决者。如果你造不出一个——校验器、测试套件、更强的模型,或者一条人工标注队列——这个飞轮就转不起来;于是环境工程就是前置项目,而不是后面才做的事。
步骤 3拒绝采样丢掉的,恰恰是你需要的那个分布。
标准配方是拒绝式微调:采样轨迹、留下校验器判为正确的那些、在它们上面训练。它有效、便宜,也是该先试的那一招——Nebius 公开的 OpenHands 工作释出了约 6.7 万条轨迹以及 RFT 检查点,并报告它在不需要 RL 循环所要求的 rollout 基础设施的情况下,拿到了强化学习训练质量中的很大一部分。那是一项真实的结果,也是正确的起点;SFT、拒绝采样与蒸馏讲的是其中的机制。
这道筛子的代价值得精确地说出来。只留下正确的轨迹,等于移走了全部尝试的分布——这意味着调过的模型从未观察到它将在生产中遇到的那些失败模式;而在智能体循环里,观察到一次失败并从中恢复是一项有自己令牌预算的技能。一个只在「第一次工具调用返回 200」的运行上训练出来的模型,对于 429、对于一份不完整的结果、对于一个答得既自信又错误的工具,脑子里没有任何表征。
所以要改的是标签,不是筛子。一条轨迹至少有四种值得区分的结局——干净地成功、从一次错误中恢复后成功、败在环境上、败在模型上——而第二类才是有价值的那一类。把失败连同它们的结局标签一起留下,用于评测与作为负向偏好信号,而不要在入库时就丢掉——因为那一步之后这个决定就不可逆了。而在另一侧,把拒绝采样或偏好对齐的轮次推得太狠也有自己的代价:多样性坍塌——模型收敛到一种解法风格,于是第一条路走不通时,它再也拿不出第二种办法。
步骤 4外壳就在数据里,这让微调变成了对你自己工具面的一项依赖。
这是模型上线之后才让人意外的那笔成本。一条轨迹不是抽象的推理;它是一串工具名字、JSON 参数形状、系统提示词的措辞和错误字符串,而这些全都是写下那条轨迹的那天你那套外壳的属性。在它上面微调,你就把自己的工具目录编码进了权重。
于是寻常的维护变成了模型问题。你把 search_docs 改名成 search_knowledge_base;你把一个自由文本的 filter 收紧成枚举;你把一个太粗的工具拆成两个。每一项都是常规改进——一个靠提示词的模型在一轮之内就从工具定义里吸收了它,而一个在轨迹上调过的模型会跟它顶牛,因为它的先验现在偏好一种已经不存在的调用形状。团队的本能是冻住工具面来保护微调,而那一刻资产就变成了负债——你用「改动产品的能力」买了几个点的准确率。
在评测里还有第二个、更安静的版本:用于训练与用于打分的轨迹来自同一根管子,于是你的评测集抽自你的权重已经记住的那个分布,而在它上面测出的改进有一部分是污染。在任何东西碰到加载器之前,先按时间和按客户把语料切开,并把最新的那一片整块留出来。
如果部署方案要求工具定义不再变动,这次微调就没通过它自己的成本效益检验。请把这个模型当作「工具面一变就要重训」的东西来做预算;或者把那些行为留在提示词和上下文里——在那里,改一次 schema 是免费的。
步骤 5你被允许留下什么,是一项设计约束,不是法务脚注。
生产轨迹是多数智能体团队手上最富含个人数据的那件产物:用户真实的请求、代其检索到的文档、传给 CRM 的参数,有时还有一个收件箱的内容。随之而来的两项义务互相拉扯。脱敏保护了当事人,也降解了数据——一条把实体名字换成占位符的轨迹,用来测量工具调用结构仍然有用,用来教检索判断则几乎无用。而另一方面,一次删除请求必须抵达语料;一条轨迹一旦被烤进权重,就没有哪一行可以删了,唯一诚实的答案是重训——这正是智能体的数据治理必须预先决定「哪些语料压根可训练」的原因。
多数团队最后落到的实际形状是:一个短保留、未脱敏的运维存储,用于调试与重放;以及一个独立的、按同意范围界定、经过脱敏处理的语料,它是训练任务唯一可读的东西。两个存储、两套保留时钟,其中一套能写进合同里被点名。请在飞轮上线之前就定下这件事,因为给一份你已经训练过的语料事后补上同意,是本文里唯一一个在工程上没有补救办法的失败。
步骤 6真正划算的那个操作顺序。
轨迹确实有价值;只是它们的价值有一个顺序,而多数团队把这个顺序搞反了。按下面这个次序来,每一步都会为下一步买单。
第一,评测集。去挖失败——不是成功——找出你的智能体做错的那些用例,用一套失败分类法把它们聚类,再把每个簇变成一个可打分的用例。这是轨迹日志价值最高的用法,而且遥遥领先,还一块 GPU 都不需要。第二,环境。把那些难的轨迹重放进一个带校验器的沙箱,这就把一份对话记录变成了可以重跑的东西——也就是那件比你即将训练的模型活得更久的资产,正如环境工程所论证的。第三,提示词与工具。一套真实分类法里的多数簇,最后会化解成「某个工具的错误消息读不懂」或者「某个 schema 招来了错的调用」;这些修复今天就能上线,而且不花掉任何灵活性。第四,到现在才轮到权重。先在第二步建好的那个校验器上做拒绝式微调;如果时间跨度很长,再把学生诱导的状态加进来。
就这么做。在批准一次基于生产轨迹的微调之前,先把三个数字写在同一页纸上:你所瞄准的那些任务的时间跨度中位数与 p95(那个平方项就住在这里)、你的语料里按成功过滤的占比,以及将进入训练切分的最老那条轨迹的日期。如果跨度很长、语料只有成功、而最老的轨迹早于你现在的工具 schema,那你并不是在造一个飞轮——你是即将把上个季度的外壳编码进权重,并在第一次给工具改名时发现这件事。把那个季度花在校验器上吧;它是这条流水线里唯一一件在模型变了之后仍然保值的产物。