跳到主要内容

Agent Trace 中的采样偏差:为什么你的调试数据集在悄悄排除你最关心的失败案例

· 阅读需 11 分钟
Tian Pan
Software Engineer

你团队每个周一盯着看的调试语料库并不是生产环境的代表性样本。它具有明显的偏差,而且偏差的方向完全错误。1% 的头部采样在保留一个罕见的灾难性轨迹之前,会先保留一百次中位数请求——大多数团队只有在某种静默循环了数月的失败模式最终导致退款或停机,并试图在追踪存储(trace store)中寻找示例却一无所获时,才会发现这一点。

这并不是什么罕见的边缘情况。这是所有专为无状态 Web 服务设计、随后又被用于长时程(long-horizon)Agent 的可观测性栈的默认行为。同样的采样算法在处理 HTTP 请求追踪时表现良好,但在处理 Agent 时却会系统性地抹除那些最重要的轨迹——因为在这里,每个“请求”都是一个包含三十个步骤的计划,可能会调用数十个工具,重新生成三个子计划,并在第 27 步发生细微错误之前消耗数万个 token。

解决方法不是“增加采样”。增加采样只会让账单爆炸,而不会改变偏差——你只会得到更多已经过剩的普通数据。解决方法是改变你采样的对象,以只有在轨迹结束后才能获知的预测结果为基准。这需要抛弃基于头部的默认设置,并围绕尾部信号、异常权重以及能在 Agent 执行的长尾效应中存续的有界蓄水池(bounded reservoirs)重新构建保留层。

为什么头部采样不适合作为 Agent 的默认方案

头部采样(Head-based sampling)在追踪开始时、任何事情发生之前就做出保留或丢弃的决定。它速度快、无状态,并且在追踪成本大致均匀、失败率大致恒定的情况下表现良好——但这两点在 Agent 身上都不成立。

Agent 工作负载的两个特性打破了头部采样的假设:

  • 追踪成本是双峰分布的,而非正态分布。 一个无所事事的 Agent 调用两次 LLM 就能回答;一个困惑的 Agent 则会尝试五十次。如果你均匀地对 1% 的启动进行采样,你保留了具有代表性的追踪数量切片,但保留的行为切片却严重倾斜,因为那些漫长、昂贵、有趣的轨迹在单位启动请求中是个体更为稀有的。包含最多信号的追踪反而是最不可能被保留的。
  • 失败率不是平稳的。 失败往往集中在工具新部署、特定的用户群体或周复一周漂移的内容分布周围。1% 的均匀采样通常需要某一失败模式在实际环境中发生 100 次,才可能捕获到一个示例——而到那时,失败已经波及了足够多的用户,造成了实际影响。此外,AI 工作负载生成的遥测数据通常比传统 API 调用多出 10–50 倍,因此增加采样的诱惑力随着稀有保留追踪价值的提升而同步增长。

这种组合是致命的。你保留了堆积如山的平庸、廉价且成功的追踪,而你用于调试的语料库过度代表了常见情况,却漏掉了那些值得修复的失败。团队对“生产环境长什么样”的心理模型逐渐与其中最无趣的部分融为一体。

尾部采样:在了解发生的情况后再做决定

尾部采样(Tail-based sampling)将保留/丢弃的决策推迟到追踪完成并获得结果信号之后。OpenTelemetry 社区几年前就确定将其作为分布式系统的正确默认设置,原因与现在对 Agent 至关重要的原因相同:最有用的信号往往在最后出现。一个以错误、异常 token 消耗或低评测分数结束的追踪,其价值远超一千次追踪启动。

在尾部采样之上一个可行的保留策略如下:

  • 100% 保留包含错误、超时或未处理异常的追踪。 这是整个流程中最廉价且最正确的决策。
  • 100% 保留超过成本阈值的追踪——例如,按 token 消耗排名前 5% 的追踪,或者任何超过单次请求预算的追踪。昂贵的追踪要么是伪装的失败,要么是承载压力的边缘案例;无论哪种,你都想保留它们。
  • 100% 保留在线 Judge 或评分标准运行后低于评估分阈值的追踪。 如果你有任何质量信号,保留机制就应该跟随它。
  • 少量的概率性基准采样——保留 1–5% 健康、廉价、通过的追踪——这样你就能保留一个覆盖层用于分布漂移检测。

这与头部采样产生的数据集形态完全不同。语料库有目的地向问题倾斜,而这正是重点所在。当你想要查找“Agent 在上周废弃的工具 schema 上产生困惑时是什么样子”时,答案就在语料库中,而不是永久丢失了。

尾部采样带来的折衷是缓冲(buffering)。你必须将 span 保存在内存中,直到追踪完成(或直到事件视界过期),这限制了收集器部署的拓扑结构。Hindsight(使事后追踪选择理念具体化的研究系统)运行时典型的事件视界约为 5 秒;长时程 Agent 需要更长的时间,而缓冲成本是实实在在的。你是在明确地用一些收集器 RAM 来换取一个不会对你撒谎的调试语料库。

超越错误的异常偏置保留

错误和超时是最简单的维度。更难也更有价值的是隐性失败——那些成功完成、返回了看似合理的结果,但仍然代表你需要修复的问题的追踪(trace)。这些失败不会自动标记。你必须定义能让它们浮现的异常信号。

一些能迅速带来回报的信号:

  • 执行结果与声明计划的偏离。 如果你的智能体在第一步生成了一个计划,而执行的动作序列超过了某个编辑距离阈值,请保留该追踪。这能捕捉到执行过程中的引导失败以及那些虽然完成但极其浪费的静默重新规划循环。
  • 高于基准的工具重试次数。 一个在“成功”之前以变化的参数调用了四次相同工具的追踪,几乎总是值得研究的,即使最终的响应是可以接受的。
  • 每个有用输出的 Token 比例。 消耗的 Token 数与最终答案字符数的比例处于长尾右侧边缘的追踪,通常代表模型正在进行无效尝试(thrashing)。保留所有这些追踪。
  • 与更廉价基准模型的不一致。 在一部分追踪上运行 Haiku 级别的评测器或确定性规则检查;保留所有评测器与智能体自我报告不一致的追踪。这些往往是事后合理化和幻觉式的成功——正是用户抱怨而追踪存储似乎从未包含的失败。

Datadog 的追踪代理长期以来一直提供这种思路的一个版本:即使启用了激进的前端采样(head-based sampling),它也会覆盖一条单独的错误追踪摄取路径,以确保错误覆盖的底线(默认每秒十个错误)。这个原则是通用的——你的保留层应该是概率性底线和一组基于结果触发的过滤器的并集,永远不要只选其一。

针对长周期智能体的成本感知蓄水池采样

剩下的问题是控制账单。如果你盲目地保留“每一个错误追踪”,一个损坏的下游依赖可能会在一夜之间变成一百万个相同的保留追踪,你的观测成本也会随之飙升。关于加权蓄水池采样(weighted reservoir sampling)的文献恰好解决了这类问题:维护一个固定大小的蓄水池,每个候选项目进入的概率与其权重成正比,而池中已有的项目会被新加入的项目替换,其方式保持了加权采样的特性。

映射到智能体追踪上,就变成了:

  • 基于每个故障模式的蓄水池,而非单一的全局蓄水池。 根据错误类别 + 工具 + 提示词版本(或任何你感兴趣的维度)对追踪进行哈希处理,并为每个桶(bucket)运行一个受限的蓄水池。一个嘈杂的桶不能淹没一个安静的桶。你总能覆盖到生产环境中的每种故障模式,而且你不用再为第一千零一个相同的重试风暴付费。
  • 按异常强度加权,而非均匀加权。 比基准成本高出三个标准差的追踪,在蓄水池中的权重应该比仅稍微超过阈值的追踪更高。在调试过程中,蓄水池会趋向于保留每类故障中最极端的样本,这正是人类真正想要查看的内容。
  • 对权重进行时间衰减。 昨天的追踪权重应该高于三个月前相同故障类别的追踪,因为生产环境已经发生了变化。对蓄水池权重进行指数衰减可以处理这个问题,而无需单独的淘汰策略。

这种组合——受限的分桶蓄水池、异常加权的准入、时间衰减的权重——为你提供了一个你可以控制其规模、其构成能追踪当前生产状况、且其内容系统性地过度代表了你应该调试的失败的调试语料库。账单变得可预测:它随着你关注的桶的数量而扩展,而不是随着最糟糕的那个桶触发的次数而扩展。

本周要问你的追踪存储的一个诊断性问题

你可以运行一行审计代码来检查现有的采样是否正在生成可用的调试语料库:提取你的团队最近处理的一个用户报告的故障,并向追踪存储索要用户报告前七天内看起来类似的例子。如果答案是零,说明你的采样偏离了那些你受雇去发现的故障。如果答案是“我们不得不手动重新开启详细日志记录并等待它再次发生”,同样的问题,诊断速度更慢。

大多数第一次提出这个问题的团队都会发现,他们一直在根据一个在结构上排除了最难故障模式的语料库进行调试,同时却在支付全价来大量保留最容易的故障。解决方法不是增加更多的存储空间。而是将“保留或舍弃”的决定移到追踪的末尾,根据结果异常对其进行加权,并将其限制在能够存在足够长的时间、直到有人最终去寻找它的故障模式蓄水池中。

调试数据集的形态是一个设计选择。目前大多数团队都是在无意中做出的选择。请有目的地做出选择,以免下一次事故复盘揭示你需要的追踪在事故发生前一周就被采样过滤掉了。

References:Let's stay in touch and Follow me for more thoughts and updates