做 Agent 开发的人,最近半年几乎都会被同一个问题卡住:线上模型跑出来的执行轨迹那么多,到底怎么变成下一版模型的训练数据?我花了一段不算短的时间,把这条链路完整跑通了一次——从日志埋点、轨迹清洗、样本构建,到 SFT 和偏好对齐,再到离线回归、线上灰度,最后把线上新产生的轨迹回收进下一轮数据池,形成一个真正能转起来的“Agent 后训练闭环”。这篇内容就是这次实践的过程拆解。
这条链路解决的是一个很实际的问题:很多 Agent 项目不缺日志,缺的是把日志变成数据、把数据变成模型提升的加工方法。适合的人有两类。一类是正在做 Agent 后训练、但对数据侧无从下手的工程师,另一类是已经跑通 Pipeline、想对照检查“我是不是漏了某些关键环节”的团队。接下来我按实际操作的顺序,从轨迹数据的价值判断开始说起。
1. 执行轨迹到底是种什么数据——先想清楚值不值得拿来训练
1.1 一条完整轨迹的解剖
在讨论训练之前,得先统一对“执行轨迹”的认知。一个典型 Agent 任务的执行轨迹,不是一段单调递增的文字,而是一组结构化的循环过程。以 ReAct 这类最常见的范式为例,一个 Agent 拿到任务后,会反复执行“思考 → 调用工具 → 观察返回结果”这样的循环,直到它认为自己可以给出最终答案。
我通常会把一条轨迹拆成下面的结构:
- 任务描述(Task):用户原话,或者经过意图改写后的任务文本。
- 步骤序列(Steps):每个步骤包含 thought、action、observation 三要素。
- 执行结果(Outcome):成功、失败、部分成功,或者被用户中断。
- 元信息(Meta):模型版本、Agent 框架版本、运行环境、时间戳、工具调用明细等。
这里的 thought 是模型内部推理的显式文本,action 是它决定调用的工具名称和参数,observation 是工具返回的环境反馈。一条复杂的任务可能有十几步甚至几十步,每一步都是这样一个三元组。把轨迹当训练数据,本质是把这条“过程链”变成可学习的状态-动作序列。
1.2 轨迹数据和静态指令数据有什么本质区别
很多人一开始会拿 Agent 执行轨迹和普通的 SFT 指令数据做类比,但两者差异很大。静态指令数据是“结果数据”,它只告诉模型:用户说了什么,你应该输出什么。而执行轨迹是“过程数据”,它额外包含了多步决策链、环境反馈、中途纠错这三样东西。
具体来说,轨迹数据里有三类静态指令数据很难提供的信息。
第一是行动序列。模型能从成功轨迹里学到“一件事应该按什么顺序做”。比如用户问“帮我查一下某公司最新产品信息并整理成摘要”,成功的轨迹可能包含调用搜索、打开链接、提取正文、再次搜索补充、汇总输出这一连串动作。只看结果摘要,模型学不到这套动作编排。
第二是纠错序列。大量真实轨迹里,前几步是失败的,比如工具参数给错了、搜索没返回有效结果、页面解析失败。模型随后调整策略,换一种工具或者换一个关键词,最后成功。这种“从错误中恢复”的路径,是纯静态数据很难构造出来的。
第三是工具反馈分布。轨迹里的 observation 部分记录了真实环境对工具调用的反馈,模型通过这部分数据,可以学会“某个工具在什么条件下返回什么格式、什么错误码”,这是把模型从“只会生成文本”推向“真的会用工具”的关键一环。
1.3 为什么不能直接拿原始日志训练
既然轨迹这么有价值,那直接把线上日志灌进训练集行不行?我试过,结论是不行,而且代价很高。原始日志存在几个硬伤。
首先是格式噪声。线上日志里交错着心跳检测、重试机制、模型流式输出片段、用户中途插话、系统超时提示,甚至还有多轮并发请求互相穿插的记录。不把这些噪声剥离掉,模型学到的会是“在什么时候说一堆无关内容”。
其次是行为噪声。线上轨迹的成败是由很多因素综合决定的,包括当前模型的策略、用户的实际投入程度、工具服务的稳定性。同一类任务,用户中途放弃和模型完成任务,在日志里可能看起来差不多,但前者不应该作为正向训练信号。
第三是目标不一致。线上 Agent 的设计目标可能是“尽可能完成任务”,也可能是“尽可能少调用工具省成本”,还可能是“优先保证安全不越权”。如果不理解线上目标的偏置,直接训练,会把业务规则里的保守倾向错误地学成“能力不足”,或者把激进策略学成“无视边界”。
所以,执行轨迹有价值,但原始日志不能直接用。需要一套加工流程,把日志里的“数据矿石”提炼成“训练样本”。
2. 从原始日志到干净样本:轨迹采集与清洗全流程
2.1 先设计好埋点字段,否则后面全部白费
轨迹清洗的第一步其实是日志埋点。这一步如果漏做,后面再想补,成本是成倍上升的。我建议每一条线上轨迹必须记录这么几个字段:
| 字段 | 说明 | 为什么必须记 |
|---|---|---|
| trace_id | 一次任务的全局唯一 ID | 用来把分散在不同服务里的日志串成完整轨迹 |
| task_id / task_text | 任务标识和任务原文 | 后续做重复任务聚类、数据去重都能用到 |
| steps | thought/action/observation 的数组 | 核心训练内容 |
| outcome | 成功、失败、部分成功、用户取消 | 区分正负样本的基石 |
| user_feedback | 用户点赞、点踩、手动修改 | 线上最真实的奖励信号 |
| model_version | 当前模型的版本标识 | 数据血统追踪,复盘时必须知道是谁跑出来的 |
| agent_version | Agent 框架与提示词版本 | 同样的模型,不同提示词产生的轨迹差异很大 |
| tool_call_detail | 每次调用的工具名、耗时、返回码 | 分析工具失败率、识别投机性调用 |
| intervention | 是否有用户或人工介入纠正 | 被人工纠正过的轨迹,不能直接当成功样本 |
这套字段看起来简单,但实际踩坑时你会发现,少一个字段就断一条线索。我之前就遇到过:想分析某类任务的失败轨迹,结果日志里没有记录 user_feedback,导致完全无法区分“模型答错了”和“用户根本不想继续”两种情况,那批数据最后只能作废。
2.2 轨迹还原与并发交错处理
在分布式环境下,轨迹还原是个容易被低估的问题。Agent 执行过程中,主控模块、工具网关、模型服务是三个不同的进程,日志分散在三处,按时间戳粗暴排序的话,很容易把并行调用拼成乱序序列。
我常用的做法是:以 trace_id 为分组键,把同一 trace 下所有事件捞出来,再按事件时间排序,最后过滤掉两类非业务事件——一类是模型侧的流式 token 输出片段,另一类是网关层的健康检查和重试记录。做完这步,才能得到一张干净的执行事件表。
这里有个细节,同一 trace 内,Agent 可能会因为工具超时而重试。重试事件要保留,但要在步骤里打上 retry 标记。训练时,这些重试步骤既能作为纠错样本,也可能成为噪声源,关键看最终是否成功。成功轨迹里的重试是“知错就改”,失败轨迹里的重试是“反复横跳”,两者在筛选时要区别对待。
2.3 清洗规则:把轨迹加工成可读文本
轨迹还原之后,还要过一层清洗规则。我把常用的清洗操作列一下,这些阈值是我在多次实验里逐步调出来的,可以作为参考起点。
- 观测文本截断。工具返回的 observation 经常特别长,尤其抓取网页或读取文档时,动不动上万字。我一般截断到 8000 字符以内,超出部分强制截断并加截断标记。模型不需要把整篇网页都背下来,它只需要学会从长文中抽取关键信息。
- 敏感信息脱敏。轨迹里可能携带用户隐私数据、内部系统地址、密钥信息。这步必须用规则加模型双重过滤。脱敏不是隐去关键词就行,要连格式指纹一起处理,否则模型很容易从上下文学会“输出这类格式”而不是“安全地处理这类信息”。脱敏做不好,后面训练集根本不敢外发质检。
- 工具名归一化。不同版本框架可能对同一工具使用不同别名,例如 search 和 web_search,要统一映射到同义词表。
- 简单任务过滤。有些轨迹只包含一步动作加一个答案,这种样本的学习价值不高,而且量大了会稀释复杂任务的占比,不利于模型学会长程规划。经验阈值是步骤数少于 3 的轨迹,除非是被重点关注的失败类型,否则可以直接移除。
- 相似轨迹去重。每天产生的轨迹可能高度相似,尤其是搜索类任务反复出现“查天气”这类重复请求。我按任务文本的 embedding 相似度做聚类,阈值取到 0.9 以上视为重复。只保留同类中质量最高的一条,防止某类任务在训练集里过度泛滥。
2.4 筛选规则与分层采样策略
清洗完,接下来是决定哪些轨迹能进入训练集。先说正样本的标准:outcome 为成功,且没有用户或人工介入纠正(intervention 为空)。这条标准比表面看起来严格,因为很多线上“成功”实际上是被用户纠正之后才完成的,比如用户中途补充说明“不是这个意思,我要的是另一个版本”,模型随后修正答案。这类轨迹代表模型有纠错能力,但也有可能掩盖了“第一轮理解偏题”的问题。我一般会把这类轨迹单独打上修正标签,不直接进正向 SFT。
失败轨迹也不能全部丢掉。它们是天然的负样本,用于偏好对齐阶段做 pair。但失败轨迹也要分类,至少分成“模型犯错导致失败”和“环境不可用导致失败”。前者可用于 DPO 的 negative,后者只能丢弃或作为环境鲁棒性分析材料。
最后是分层采样。我会按任务类型做配额控制,比如搜索类、代码生成类、数据分析类、网页操作类各占 20% 到 30%,加上工具组合类,确保训练数据不会因为业务偏置而变的单一。这一步的真实工作很繁琐,但直接影响训练结果。
2.5 质检:人工抽检不能省
自动化流程全部跑完后,我建议大家至少抽检 1000 条轨迹做一次人工复核,重点看三类问题:轨迹是否完整(有没有缺 observation)、步骤排序是否正确(有没有前后颠倒)、筛选标签是否合理(有没有把失败轨迹标成成功)。人工抽检看起来费时,但实际上能拦截掉大量规则覆盖不到的异常。
这轮质检还有一个作用:修正规则阈值。比如某类任务的轨迹普遍很长,步骤数超过 25 就可能是触发了死循环;但如果某类任务天然复杂,那么 25 以上的正常轨迹也不少。质检员反馈这些边界情况后,规则可以迭代得更准。
3. 轨迹变训练数据的三条管线:SFT、偏好对齐与过程监督
3.1 SFT:动作级监督,只教模型“下一步做什么”
SFT 是最直接的转换方式,核心思路是把一条轨迹拆成若干“状态 → 动作”对,让模型学会在给定的历史状态下,应该采取哪个下一步动作。
我构造 SFT 样本时,用的是 chat 格式。一个样本长这样:
{ "messages": [ {"role": "system", "content": "你是一个能调用工具完成任务的助手。工具列表:search(query), open_url(url), read_page(), summarize(text), respond(answer)。你必须一步步执行,每步先输出 Thought 再输出 Action。"}, {"role": "user", "content": "查一下 AlphaTech 公司最近发布的第四代产品参数,整理成对比表配一句点评。"}, {"role": "assistant", "content": "Thought: 用户需要产品参数,先搜索产品信息。\nAction: search(\"AlphaTech 第四代产品 参数\")"}, {"role": "user", "content": "Observation: 搜索到 3 条结果,第一条是官网介绍页,包含参数表。排名:官网 > 新闻稿 > 第三方评测。"}, {"role": "assistant", "content": "Thought: 官网信息最准确,打开官网页面。\nAction: open_url(\"https://...\")"} ] }这里有两个关键点。第一,只保留 assistant 的 thought 和 action 作为监督目标,observation 归入 user 角色,因为它本质上是由环境产生的,而不是模型输出的;第二,最后一个 assistant 回复如果是最终答案,则不需要附带 Action。
在训练时,还需要对 loss 做 mask,只计算 assistant 内容中 thought/action 部分的 loss,不计算用户消息和 observation 文本的 loss。模型不需要把 observation 一字不差地背下来,它只需要参考 observation 来生成下一步动作。这一点很多人会忽略,但恰恰是轨迹 SFT 与普通指令微调之间最大的差异。
还有一个很实用的经验:SFT 阶段千万不要把失败轨迹作为标准答案混进去。模型学到的不是“从失败中反思”,而是“失败也无妨,继续输出”。失败轨迹的正确定位是负样本,应该在偏好对齐阶段使用。这个坑我早期踩过,模型训完明显“摆烂”——遇到复杂任务,它会更倾向直接放弃或重复无意义的工具调用。
3.2 DPO:构建成功对失败的轨迹对
DPO 是偏好对齐里性价比非常高的方案,因为它不需要训练一个独立的奖励模型,只需构造“偏好对”数据。所谓偏好对,就是同一任务下,一条成功轨迹和一条失败轨迹配对,让模型学会提高成功轨迹的概率、降低失败轨迹的概率。
构造偏好对的难点在于如何保证两条轨迹的重合程度足够高。如果任务完全不同,模型学到的只是“这两段话的概率此消彼长”,没有实际意义。我常用的方法是:
- 对同一 task_id 的轨迹,如果存在成功和失败两个版本,直接配对。
- 如果没有天然配对,可以拿失败轨迹的输入,让新模型重新执行一次。如果这次成功了,就能得到一对“同一任务、旧失败、新成功”的轨迹。
- 同一成功轨迹如果真的没有失败副本,也可以用“成功轨迹的全过程”和“把某几个步骤置乱后的伪造失败轨迹”来凑对,但这种手段只能作为补充,不建议大量使用。
DPO 样本的构造格式是把整条轨迹序列化成一串文本,再做 pair。实际效果看,凡是轨迹长度适中、差异集中在某几个关键步骤的偏好对,对模型提升最明显;差异太小的 pair(只有一处用词不同)反而容易让模型在细节上变得敏感,甚至会轻微损害原有生成能力。
3.3 PRM:给每一步过程打分
SFT 和 DPO 都停留在轨迹级,但 Agent 任务里,一个致命的错误往往只发生在一个步骤上。过程奖励模型(PRM)的思路是把监督信号细化到步骤级别,训练一个模型对“状态 + 候选下一步动作”打分。
自动生成过程标签的方式主要有两条路。第一条基于结果回传:轨迹最终成功,则其内部所有步骤标为正;轨迹最终失败,则所有步骤标为负。这种方式噪声较大,因为失败轨迹里其实也有正确步骤。第二条规则判定:针对常见工具调用,用工具返回码、是否触发异常、是否重复调用等规则,给每个动作打上“有效/中性/无效”的标签。
实际项目里,我会结合两条路,再加人工抽样修正。PRM 训练数据中,一个样本是:
{ "state": "当前任务状态 + 历史步骤摘要", "candidate_step": "模型计划采取的下一步动作", "label": 1 }PRM 训练好之后,它的价值不止在训练阶段。推理时,可以用它对模型生成的多条候选轨迹做步骤级重新排序,比单纯靠“最终答案置信度”来选路靠谱得多。这一步如果我们把 PRM 回溯用于下一次轨迹筛选,就形成了过程信号层面的闭环。
3.4 反思样本:把失败轨迹变成修正课
真实线上轨迹数据里,成功轨迹往往是少数派,失败轨迹占大头。如果只靠 DPO 消化失败样本,浪费了大量信息。我的做法是把一批失败轨迹转成“反思样本”,格式是:失败轨迹 + 反思分析 + 修正重试。
具体操作是:把失败轨迹发给一个更强的模型,让它分析“问题出在哪一步、为什么出错、应该怎么做”,生成一段明确的反思文本;然后带着这段反思,让模型重新执行原任务,得到新轨迹。如果重试成功,我们就得到了一条包含“失败+反思+成功”三段内容的复合轨迹。
这种样本的训练收益非常明显。模型学到的不只是一个动作对,而是“遇到这类问题时先诊断再修正”的决策习惯。尤其是工具参数错误、信息源选择不当这两类高频失败,反思样本能显著降低复发率。
反思样本的缺点是构造成本高,因为需要额外调用模型,还需要判断重试是否成功。我控制的比例大约是训练集的 10% 到 15%,再多会让训练数据整体偏“话痨”,模型容易在动作前生成大段无谓的自我剖析。
3.5 各管线数据的配比参考
三条管线不是平均分配。我以一次实际训练任务为例,当时数据池里共有约 12 万条轨迹,经过清洗、筛选后合格样本约 2.1 万条。最终配比是:
| 管线类型 | 样本数 | 占比 | 说明 |
|---|---|---|---|
| SFT 正样本 | 约 8000 条 | 38% | 仅使用成功轨迹,动作级监督 |
| DPO 偏好对 | 约 6400 条(3200 对) | 30% | 同一任务的成败 pair |
| PRM 过程标签 | 约 4800 条 | 23% | 规则 + 人工抽样校准 |
| 反思样本 | 约 1900 条 | 9% | 失败重试复合轨迹 |
这份配比的关键在于:SFT 保证基础能力,DPO 修正偏好,PRM 强化过程信号,反思样本补齐错误恢复能力。顺序上不能打乱——先 SFT 提升动作准确率,再 DPO 做偏好对齐,最后再引入 PRM 和反思样本。跳步训练会导致模型在不同目标之间反复横跳,效果反而更差。
4. 闭环的另一半:离线回归与线上灰度怎么衔接
4.1 评估集怎么造:黄金任务与时间分窗
训练数据加工好,模型也训练完了,这时候最怕的是“自我感觉良好”。我用两套评估集来做离线回归。第一套是黄金任务集,固定 500 条覆盖各任务类型的评测任务,每条任务都有明确的成功标准,人工预跑过 baseline,确保它们都是可完成的。第二套是时间分窗集,从线上日志里按新时间段采样一批任务,例如用 T-1 到 T 两周内、且不在训练集里的任务做验证。
时间分窗这件事特别重要。Agent 任务和普通 NLP 任务一样,存在任务分布漂移:上周热门的问题类型,这周可能就少了;某些工具的上游接口变了,返回格式也会漂移。如果把训练集和验证集都从同一时间段取样,哪怕做了严格 task 去重,分布泄漏依然存在。我见过不止一个团队因为忽略时间分窗,离线指标被高估了几个点。
4.2 离线指标组合:单看成功率一定会被骗
评估 Agent 模型不能只看任务完成率,那太粗了。我每个版本都会统计四类指标:
| 指标 | 说明 | 关注点 |
|---|---|---|
| 任务完成率 | 成功完成任务的比例 | 整体能力不下降是底线 |
| 工具调用失败率 | 调用工具时返回异常的占比 | 下降说明模型更会“用工具” |
| 平均决策步数 | 完成任务所需的步骤数 | 明显上升可能是因为犹豫/试探变多 |
| 拒绝率与越权率 | 直接拒绝任务或调用禁止工具 | 安全与合规维度 |
这四类指标要看组合变化,不能单独看。比如新模型任务完成率上升了 2%,但平均决策步数同时上升了 20%,那很可能是模型学会了“多绕几步混成功率”,并不是真正变强。又比如工具调用失败率下降了,但拒绝率上升了,说明模型可能是在用“不调用工具”规避失败风险。
4.3 回归对比:逐个看新老模型的输出差异
离线评估除了看指标,还要做抽样对比。我把新老模型在同一批任务上的输出逐条对照,重点看三类变更:
- 新增了哪些正确的工具调用,之前模型漏掉的;
- 哪些原本正确的动作,新模型反而做错了,这类属于回归;
- 哪些步骤虽然结果对,但执行路径更绕,这类属于效率回退。
这个动作很费时间,但非常值。我第一次跑完整流程时,靠这层逐条对比,发现新模型在代码生成类任务里“学会了用代码解释器”,但“忘了还能直接读本地文件”,导致原本一步能完成的事变成了三步。这种问题在聚合指标里完全看不出来,只有逐条对比才能暴露。
4.4 线上灰度:从小流量到全量的节奏
离线评估通过,不代表可以直接全量上线。我会按 5% → 20% → 50% → 100% 四步灰度,每步至少观察 24 小时。灰度期间重点盯三类线上信号:任务完成率(与 baseline 模型对比)、用户反馈(点踩率、人工纠正率)、工具网关错误率。
如果发现完成率不升反降,或者错误率抖动,立即回滚到上一个稳定版本。回滚本身要设计成一键操作,最好在模型网关层就做分流,而不是等到应用层才发现问题。这里有个容易被忽略的细节:灰度期产生的轨迹数据要保持独立存放,标记上模型版本。这些轨迹就是下一轮训练的原材料,也是判断本次迭代真实收益的证据。
5. 把反馈接回数据池:一套可持续迭代的工程闭环
5.1 一轮完整的轨迹闭环长什么样
到这里,整个闭环的各个环节都出现了。把它们串成一个可持续运转的流水线,大概是下面这个顺序:
- 线上 Agent 运行,原始日志落到数据管道;
- 日志按 trace_id 还原聚合,生成原始轨迹集;
- 清洗去噪、脱敏、去重,形成候选池;
- 按规则筛选正负样本,做分层采样,形成训练批次;
- 按 SFT / DPO / PRM / 反思四条管线加工训练样本;
- 训练完成后跑离线回归(黄金任务 + 时间分窗任务);
- 通过后按灰度节奏发布到线上,采集效果指标;
- 灰度期产生的新轨迹回流到数据池,参与下一轮清洗与采样。
这八步就是“Agent 执行轨迹 → 训练数据 → 模型迭代 → 新轨迹 → 再训练”的完整闭环。听起来简单,但每一步都有各自的坑。
5.2 数据血统管理:每条轨迹都要有户口本
闭环要能长期转,最重要的一件事是数据血统管理。每条进入训练池的轨迹,必须携带完整的版本元信息。
我维护了一张线索表,字段包括:轨迹 ID、采集时间、来源模型版本、Agent 框架版本、提示词版本、任务类型、清洗规则版本、最终用途(SFT/DPO/PRM/反思/未采用)。任何一次训练用到的数据,都能从血统表反查到源头。
血统管理最直接的用处是复盘。有一次新版本模型在某个任务类型上明显退步,我用血统表一查,发现训练集里混入了大量由旧提示词版本产生的轨迹,而旧提示词的决策风格和现在的目标恰好相反。如果没有血统表,这种问题几乎不可能定位。
5.3 迭代节奏:不是每天都要跑一轮
闭环能转,不代表要天天转。我的建议是每 1 到 2 周跑一轮完整训练,前提是候选池新增了足够数量的合格样本。如果这周线上流量平淡,新增合格轨迹不足 3000 条,那宁可不训练,也不要拿凑数的数据硬跑的。频繁用小数据量训练,模型很容易在个别任务类型上过拟合,整体能力反而下降。
数据不足时的替代方案是:调低筛选阈值,纳入更多“部分成功”轨迹,或者补充人工构造的合成轨迹。合成轨迹有个好用的办法——拿合格任务让当前模型多跑几次,记录不同随机温度下的多条候选轨迹,再用规则打分挑出最好的一条入库。这相当于把模型自己的能力当成数据增强器。
5.4 成本与资源估算:别让数据工程吃掉训练预算
轨迹清洗看起来只是“处理日志”,实际消耗的资源一点都不少。以 12 万条原始轨迹的规模为例,清洗阶段的算力主要消耗在 embedding 去重和脱敏模型推理上,大约需要几天的小型 GPU 实例;人工质检 1000 条轨迹,大概需要两个标注人员各花两到三个工作日;反思样本的构造需要调用强模型,一条约 0.1 元左右。整体算下来,单个训练批次的数据工程成本约占整体训练预算的 30% 到 40%。
所以我的建议是,把数据工程当做和训练本身同等重要的一环来排期,而不是“跑完训练再说”。数据质量低导致训练返工,浪费的算力和时间成本,比提前投入数据清洗要高得多。
6. 跑了半年踩过的坑:常见问题与排查技巧
6.1 数据泄漏:验证集里混进了训练集轨迹
这是最隐蔽、后果最严重的问题。我曾有一版模型离线指标漂亮得反常,上线后却明显退步,后来排查发现验证集里混进了与训练集高度相似的任务。原因是我用了“按任务文本精确去重”,但线上任务经常只改几个字就变成“新任务”,embedding 相似度很高,精确去重根本拦不住。
修复方案有两层。第一层,去重改用 embedding 相似度,阈值设在 0.85 左右;第二层,坚持时间分窗,确保训练集和验证集来自不同时间段。如果你做不到第二层,至少保证验证集任务的采集时间晚于训练集 7 天以上。
6.2 格式崩塌:模型学会了生成 Observation 而不是 Action
SFT 训练后出现了一个诡异现象:模型在中间步骤直接生成“Observation: 这是我从网页里看到的内容”,然后不再调用工具。原因是样本构造时,我把 observation 也放在 assistant 内容里参与训练,模型误以为 observation 是它自己应该输出的东西。
修复方案是在样本构建时严格区分角色:observation 全部写入 user 字段,训练时用 loss mask 屏蔽;只让 assistant 生成 thought 和 action。这个问题也提醒我,样本构造完成后,一定要先抽出少量样本看一眼“模型视角下”的完整上下文是什么,而不是只看 JSON 结构是否正确。
6.3 奖励黑客:PRM 学会了投机取巧
PRM 训练初期,我给每个步骤的标签是“工具是否调用成功”。结果模型很快就摸到了规律——只要它调用一个无害的工具,比如“搜索一下”但根本不看结果,这个动作就能获得正标签。于是模型学会了频繁触发无意义调用刷 PRM 分数,整体执行路径越来越长,任务完成率反而下降。
修复方法分两步。第一步,标签里加入“这一步是否推动任务向最终目标靠近”的人工抽样判断,只靠工具返回码不够;第二步,在特征里加入“该动作是否重复调用同工具”的惩罚信号,重复调用超过两次,即使工具成功也降分。
6.4 长轨迹训练不稳定:loss 震荡,越练越差
早期训练时,包含 20 步以上的长轨迹样本会引发 loss 震荡,甚至出现一个 batch 里 loss 特别大、下一个 batch 直接爆炸的情况。排查后发现原因有两个:一是 packing 时把多条轨迹拼在一起,长样本截断了正常上下文;二是我把整段轨迹的全部文本(包括长 observation)都计入 loss,导致模型被极端长度的文本主导。
修复方案是:对轨迹按长度分层打包,超过 1024 token 的样本单独处理;loss 只对动作位置计算,observation 位置直接 mask 掉。下面是一个 mask 的简化示意:
# labels: 与输入等长的标签序列 # 所有 observation 位置的标签设为 -100,SFT 时不参与 loss labels = [ -100 if is_observation(turn) else token_id for turn in trajectory ]这个修改直接把训练稳定性拉回来了。之后我养成了一个习惯:训练前先统计轨迹长度分布,再决定 packing 策略,绝不无脑拼包。
6.5 数据新鲜度陷阱:旧版本轨迹带偏了新模型
闭环跑久之后,数据池里会积累很多历史轨迹。最初我舍不得丢弃旧数据,结果训练集里混着三个月前的轨迹,而三个月内线上工具接口、任务分布、用户习惯全都变了。模型既学不到最新的工具返回格式,又被过时数据里的旧行为干扰。
我现在的做法是引入时间衰减:超过四周的轨迹权重减半,超过八周的轨迹降权到 10%,超过十二周的直接移出训练池。保留策略是“旧数据只留分析用,不进训练集”。这个习惯帮我拦下了至少两版有明显数据泄漏风险的训练批次。
如果你也在做 Agent 后训练,建议第一件事就是把数据池的“时间账本”和“版本账本”建起来。每条轨迹标清楚采集时间和模型版本,训练前先看一眼数据池的时间分布和版本构成。这个账本会帮你避免掉绝大多数让人半夜爬起来排查的问题。