逐Span评估Agent轨迹:用Jev拆解同样PASS背后的路径质量

发布时间:2026/11/4 13:45:01
逐Span评估Agent轨迹:用Jev拆解同样PASS背后的路径质量

最近排查Agent任务时,我撞上了一个很典型的场景:同一个任务连续跑七八轮,几乎每轮都能拿到PASS,但把执行轨迹拉出来一对比,发现每次走的路径都不一样。有的先调检索再调工具,有的先本地推算再到外部服务拿数据;有的只查了一次记忆就够了,有的来回检索了五六轮才收敛;甚至有一轮执行到一半明显走偏了,靠重试和自纠兜兜转转绕回来,最后依然是PASS。这件事让我意识到一个很实际的问题:既然结果都是PASS,那到底哪条路径才是真正稳定可靠的?光看最终结论根本回答不了这个问题。

要回答它,必须回到Agent Trace里的每一个Span去找线索。我最近在做一个类似“路径审计”的活儿——用Jev模型逐个Span去评估Agent执行轨迹,对比多轮运行的路径差异,把“同样PASS”背后的不同行为链拆出来看。这一套流程走下来,收获确实不小,这里把完整的思路、操作步骤和踩过的坑整理出来,希望能给同样在做Agent可观测性和质量评估的同行一些参考。

1. 先搞懂“同样PASS,路径为什么不同”

1.1 PASS只是结果,路径里藏着更多信息

Agent任务的PASS本质上是一个离散的验收结论——某个断言成立、某个目标指标达标、某个输出符合格式要求。但它完全不包含“这个结论是怎么来的”这一层信息。我拿考试打个比方:两个人同样考了80分,一个人是按部就班把会做的题做完,另一个是跳过了三道大题、靠蒙和检查时间硬凑出来的。分数一样,后续的可靠性完全不一样。Agent也是同一个道理:PASS可能是最优路径一次命中,也可能是走了大量弯路以后侥幸收敛。

现在的Agent系统普遍存在非确定性。LLM采样有随机性、工具调用的返回内容会变化、检索到的TopK结果会漂移、并发调度和重试机制还会改变执行顺序。这些因素叠加下来,哪怕输入完全相同的任务,每次生成的路径都可能不同。如果只盯着最终PASS,而不看路径,那有一天PASS率突然下降,你甚至连从哪一步开始查都不知道。

这也正是跟踪执行路径的核心意义:PASS不能作为质量的全部依据,路径才是。路径里藏着每一步的选择、每个工具调用的结果、每次异常处理的时机,以及这些行为之间的因果顺序。要想真正掌控Agent的行为质量,必须把路径当作一等公民来记录和分析。

1.2 从OpenTelemetry里读透Agent执行链路

说到路径分析,绕不开OpenTelemetry。Agent在执行过程中会生成一条完整的Trace,代表一次任务;这条Trace下面挂着若干个Span,每个Span代表一个具体操作单元。对Agent而言,一个Span可能是一次LLM推理调用、一次工具调用、一次内存检索、一次向量数据库查询,也可能是一次代码执行或文件读写。

每个Span里都有关键的元数据,做路径分析时需要特别关注几个字段:name告诉你是哪一步操作,parentSpanId给出父子关系,startTime和endTime记录这一步花了多长时间,status标记这一步是成功还是失败(包括错误码和错误信息),attributes里通常携带输入输出摘要、模型参数、工具参数等业务信息,events则记录运行中发生的异常事件或关键日志。

我推荐把所有Agent执行过程都纳入OpenTelemetry的标准化链路中,这样多个工具、多个服务的调用记录都能在同一个Trace体系里对齐时间线和因果关系。很多团队只是把LLM调用打点上去了,工具调用和决策过程没有打,最后Trace里只有孤零零的几个Span,路径就完全拼不起来。要让Run Trace真正可用,至少要把“每一步工具调用”“每一次LLM请求”“每一次失败重试”的边界和状态都落进Span里。

1.3 Jev在评估链路里的位置

有了Trace和Span数据,下一步就是评估。这里就引入了Jev这个角色。Jev是一种面向执行轨迹设计的评估模型,它和我过去常用的LLM-as-judge方案不太一样。传统的做法是把Trace转成一段文字总结,塞给一个大模型让它打个分,这种做法高度依赖总结的完整性,而且模型感知不到前后顺序和层级结构。

Jev的核心定位是直接消费结构化的Span数据,对轨迹里的每个Span做语义级评估。它不只看某个Span执行成功或失败,还会判断这个Span在当前上下文里是否合理、是否是实现任务目标所必需、工具选择是否恰当、参数是否和任务目标一致、错误处理是否及时。也就是说,它评估的是“步骤的合理性”而不只是“步骤的成功率”。

我目前使用Jev的方式是通过API接入,需要提前申请密钥,项目在GitHub上也有开源仓库可以自行部署或微调。如果你只是做研发阶段的路径分析,直接用线上服务最快;如果出于数据安全考虑不想把Trace发到外部服务,可以拉它的开源版本本地跑。要注意评估结果和模型版本强相关,我后文会专门提这一点。

2. 为什么必须逐个Span评估,而不是整段一次性打分

2.1 整段评估的致命盲区

刚开始做Trace评估时,我犯过一个典型错误:把整条Trace简化成一段摘要文本,交给模型打一个综合分。比如“该Agent完成了用户请求,调用过搜索和代码执行,最终返回了正确结果,请评分”。表面上看流程没问题,实际用起来问题非常多。

第一,信息压缩必然导致因果链丢失。模型看到的只是一段叙述,无法感知哪个Span在前哪个在后、谁是父节点谁是被调用的子节点。第二,综合分无法定位问题。假设整条Trace得了7分,我完全不知道扣分点在哪,是LLM调用次数太多,是某个工具参数选择有问题,还是错误重试浪费了时间?第三,总结本身就带有主观性,如果是我写摘要,我可能把关键异常忽略掉,模型就更不可能发现了。第四,长Trace的文本化会突破上下文窗口,被迫做更激进的截断,结果就是评估彻底失真。

这也解释了为什么“逐Span评估”这条路非走不可。我们要的不是一句模糊的“看起来还行”,而是每一个环节的细颗粒度得分和理由,这样才能真正把路径上的问题钉死在具体步骤上。

2.2 逐Span评估的具体收益

把评估粒度下沉到单个Span之后,收益是很直接的。第一,能定位漂移拐点。同一个任务跑多轮,PASS率都一样,但路径不同,逐Span评分可以让你看到是哪一个Span的得分开始下降、在哪一步走了岔路,拐点一目了然。

第二,能发现“虽然成功但多余”的Span。Agent某些执行分支看起来没有报错,状态全是OK,但对任务目标并没有贡献。比如一个只需要查天气的任务,Agent却先调用了一个知识库检索、再调用一个网页摘要、最后才调天气接口。整体PASS,评估却能在中间Span上打低分,告诉你这一步是冗余的。

第三,能为多轮路径对齐提供基础。逐Span评分后,可以把多轮Trace的每个Span映射到同一套评估维度上,做横向对比。哪个环节波动最大,哪个环节一直表现稳定,数据会直接告诉你答案。第四,它能直接接入后续的流程控制,把分数作为门禁或回归指标。

2.3 逐Span评估的输入与输出

要让Jev按预期工作,输入格式非常关键。一个标准的识别单元至少应该包含几类信息:Span的基本身份信息(名称、类型、状态)、时间信息(开始时间、耗时)、父级关系(父Span ID)、关键属性和事件信息。其中attributes里的input和output建议做摘要处理,不要把超长文本全部塞进去,否则又会被上下文限制压垮。

输出建议要求JSON结构化格式,包含每个Span的评分、理由、问题类型和改进建议。我的经验是让评估模型输出四个字段:score(例如0到10的整数)、reason(简短解释评分依据)、issue_type(问题分类,如多余调用/参数错误/重试过频/工具选择不当/遗漏关键动作)、suggestion(具体的改进动作)。这样下游可以直接把JSON转成表格做聚合分析,也可以按问题类型分组做统计。

3. 实操:用Jev逐Span评估Agent Trace的完整流程

3.1 第一步:导出并清洗Trace数据

评估不是从模型开始的,是从数据入手的。首先要保证Agent的Trace被完整记录下来。执行端通常已经把OpenTelemetry数据发送到Collector或后端存储里,本地可以直接导出JSON格式的Trace数据。如果你的生产环境没有存Trace,那这一步要临时补,给执行端的打点逻辑加上Transform或SpanProcessor,把每个关键步骤的入口和出口都记录下来。

以我实际遇到的数据为例,导出的Trace JSON一般是嵌套结构,包含traceId、parentSpanId、name、startTime、endTime、status、attributes等字段。我写了一个简易脚本,把嵌套的Span拍平成列表,同时按父级关系保留一个children数组,方便后续构建树形结构做路径展示。

import json def flatten_spans(trace_json): spans = trace_json.get("spans", []) # 建立span id -> span 的映射 span_map = {s["spanId"]: s for s in spans} # 建立父子关系 for s in spans: pid = s.get("parentSpanId") s["children"] = [ c["spanId"] for c in spans if c.get("parentSpanId") == s["spanId"] ] # 提取评估需要的字段 output = [] for s in spans: output.append({ "span_id": s["spanId"], "parent_span_id": s.get("parentSpanId"), "name": s["name"], "kind": s.get("kind"), "start_time": s.get("startTime"), "end_time": s.get("endTime"), "duration_ms": s.get("durationMs"), "status": s.get("status"), "attributes": s.get("attributes", {}), "children": s.get("children", []) }) return output

清洗这步一定不能跳过。Trace里往往带着token数量、原始输入输出、密钥等敏感信息,直接送去评估既不安全也浪费上下文。我的做法是:对长文本做截断,超过1000字的部分用省略号替代;对密钥类字段直接打码;对工具返回值只保留结构化摘要和几个关键字段。清洗完之后再转成JSON写入文件,作为Jev的输入。很多时候“评估不准”不是模型问题,而是脏数据送进去了,这个教训我吃过亏。

3.2 第二步:设计评估维度和Prompt模板

Jev的评估质量,很大程度取决于评估维度的定义和模板的设计。我总结了一套适合大部分任务型Agent的评估维度,实际效果比较稳定:目标一致性(当前Span是否服务于用户最终目标)、工具选择合理性(在当前上下文中选择这个工具是否最优)、参数正确性(调用参数是否与目标一致,有没有缺参数或传错值)、错误处理质量(失败后是否能正确重试或降级)、执行效率(是否存在不必要的重复调用或低效路径)。对每个Span,都从这几个维度打综合分,再给出具体的理由。

Prompt模板我经过几轮迭代后固定成了下面这个结构,核心是两条:一是把当前Span和它的父Span、兄弟Span情况交代清楚;二是明确要求输出JSON格式,不要自由发挥。

你是一个Agent执行轨迹评估器。请输入Trace中的一个Span及其上下文,从以下维度进行评估: 1. 目标一致性:这个操作是否服务于用户最初的任务目标? 2. 工具选择合理性:在当前状态下,选择这个工具/动作是否最合适? 3. 参数正确性:本次调用的参数是否完整、正确? 4. 错误处理质量:如果发生错误,是否恰当处理? 5. 执行效率:该操作是否是达成目标所必需?有没有冗余? 参考上下文: 父Span:{parent_span_info} 兄弟Span:{sibling_span_summaries} 当前Span:{current_span_info} 请严格输出JSON: {"score": 0-10, "reason": "简短依据", "issue_type": "问题类型", "suggestion": "改进建议"}

这里有一个经验:每评估一个Span时,并不需要把整条Trace都发给Jev,而是只提供“局部上下文”。具体说就是当前Span、它的父Span、以及同层级的兄弟Span摘要。这样能大幅减少token开销,也能让模型更聚焦于当前节点本身,避免被全链路信息干扰。我测试过,带父Span和不带父Span的评分结果差异很大,所以父级信息一定要保留。

3.3 第三步:对每轮路径做差异对比

逐Span评估只是第一步,能真正回答“为什么PASS路径不同”的,是对比多轮Trace。我的做法是先把多轮执行记录按任务分组,然后对同一任务的多次执行做Span序列对齐。

对齐方式不复杂:先按Span名称生成一个操作序列,例如[retrieve, llm_generate, code_execute, llm_generate];然后用最长公共子序列算法找出多轮执行之间的相同部分,剩下的位置就是路径差异点。在分叉点两侧,我会特别关注Jev对这两个不同Span的评分差异。

举个例子:某轮PASS的Trace在主决策点先调用了search_web,另一轮同样PASS却先调用了search_docs。Jev对前者的评分是9分,理由是“最新信息需求优先匹配网络检索”,对后者的评分是6分,理由是“文档内容与当前问题相关性偏弱,选择不够直接”。两轮都PASS,但路径质量高下立判。把这种情况下所有差异点汇总起来,其实就是在给Agent的行为画“最佳路径基线”。后续再跑任务时,如果路径偏离这条基线,即使PASS也应该标记告警,因为这种PASS往往带有运气成分。

4. 路径漂移的根因排查

4.1 漂移最常见的五种来源

分析过多轮路径差异之后,我归纳出Agent路径漂移最常见的五个来源。

LLM采样的随机性是最容易理解的来源。温度不是0时,同样的Prompt和上下文,模型选出来的下一个动作可能就是不同的工具调用,这直接导致Span级别的路径分叉。工具返回内容变化也是一个常见来源,同一关键词在不同时间检索到的内容会变化,Agent看到的内容不同,后续决策自然分叉。第三是检索结果的TopK顺序变化,这会显著影响Agent从记忆或外部知识库里取到的上下文,哪怕只是相关条目的排序换了一下,决策顺序也会跟着变。第四是重试与并发调度,网络超时、工具报错后,重试机制会把任务引向另一个分支;并发执行时时序不确定,也会让Span的先后顺序改变。第五是上下文缓存的结果,如果某次执行命中了缓存,直接跳过某几个Span,路径长度就会骤减,但结果依然是PASS。

这五个来源里,模型随机性和工具输出变化是没法彻底消除的,能做的是通过路径评估去管理它们带来的不确定性,而不是试图消灭它们。

4.2 结合错误码定位路径拐点

热词里有一句“根据错误码定位路径的方法”,在Agent Trace评估里这个思路特别有效。当Agent在某一步调用外部工具或服务失败时,这个失败的细节通常会落在Span的status里,比如status.code = 2或status.message = "error: ...",异常信息会出现在events里。错误码是定位路径拐点的重要锚点。只要在Span的事件里找到错误Code,就能立刻知道这条路径是在哪个环节发生了转折。

我处理过一个实际案例:Agent在完成任务过程中调用了某个第三方服务,第一步返回429限流,Span的attributes里包含了http.status_code=429。接着Agent在下一个Span选择了一个备选工具,绕过了限流,最终返回PASS。从结果看任务成功了,但把Trace翻出来看,路径上有一次明显的“被迫换路”。如果后续这个第三方服务恢复正常,路径可能会回到原路;如果第三方服务彻底挂了,Agent就需要有别的备选方案。如果没有逐Span记录和错误码定位,这个隐患根本看不出来。

建议把每个错误码都作为路径分析的一个索引字段。你可以按span.status.message分组统计,也可以按attributes.error.code做聚合,把高频错误码对应的路径分布列出来。这样既能知道哪些错误码在引发路径分叉,也能判断后续是否需要增强特定环节的降级逻辑。

4.3 排查过程中的几个重要细节

排查路径漂移时,有几个细节容易踩坑,这里单独拿出来说。

第一个细节是必须保留跨服务关联ID。Agent的Trace要真正串起来,traceparent这类上下文传播头一定不能在调用链路上丢失。如果子服务没有从父Trace继承上下文,就会出现“两段操作分别有Trace,但无法判断谁先谁后”的尴尬状态。第二个细节是时间戳要对齐。多轮运行对比时,如果没有统一的时间基准(推荐全部转成UTC或Unix时间戳),排序一乱,路径差异分析就全错了。

第三个细节是评估结果不要当成绝对真理。Jev的评分是语义层面的推断,不是数学证明。同样的Span,上下文稍有不同,分数就可能浮动。所以在做多轮对比时,我通常只看趋势而不纠结单个分数:分数连续下降的路径优先级最高,单个Span偶尔飘低分则可能是上下文噪声。第四个细节是固定评估模型版本。Jev这类模型升级后,对新旧版本之间的评分差异会很大,如果评估基线一直在变,对比就没有意义。我会在每次评估时记录一个模型版本标识,保留当时的结果和Prompt版本。

5. 常见问题速查与避坑经验

5.1 问题速查表

把这段时间实践中遇到的典型情况整理成一个速查表,方便直接参照排查。

现象可能原因排查方向解决建议
所有Span分数都高,但整条任务失败Span评估维度没包含最终结果校验检查最后一个Span及整个Trace的终态在最后一个节点增加结果正确性评估
两次运行Span名称相同,但路径截然不同只比对Span名称,没比对参数和输入输出对比attributes中的参数摘要将输入输出的哈希或摘要纳入路径指纹
Jev评分忽高忽低评估模型版本不稳定,或Prompt缺少few-shot核对模型版本和Prompt版本固定版本,使用结构化Few-shot示例
路径分叉点定位不到Trace没有记录决策过程对应的Span查看Agent框架里是否有Decision Span在决策点显式创建一个Span
实际耗时高的Span反而评分高评价维度没纳入成本指标看一下耗时与评分的相关性在评估维度中加入效率权重
失败路径最终也PASS存在错误重试和降级逻辑查看重试Span和降级判断Span将Fail后再PASS的路径单独标记

5.2 评估成本控制与工程落地建议

逐Span评估比整段评估的成本要高,这是绕不过去的现实。我建议不要对每一条Trace全量评估,而是采取“先粗筛、再细评”的办法。第一层粗筛用规则和简单的统计指标来判定:耗时是否异常、错误码是否出现、路径和最佳基线是否偏离;命中规则的Trace才进入Jev逐Span评估环节。这样既可以把成本压下来,又能保证异常路径一个不漏。

工程落地时,我会把Jev的评估结果回传到Trace系统里,作为Span的attributes额外字段,例如jev.score、jev.issue_type。这样后续在观测平台里看Trace时,能直接看到每个Span的评分,不需要单独拉起一套评估系统。更进一步,可以把逐Span评分聚合到任务级别,生成一个path_health_score,作为CI或发布流程的准入门槛之一。

我曾见过团队直接把所有Trace全部送去做模型评估,结果云账单翻了几倍,评估结果反而因为上下文压缩变差了。所以务必先做抽样和筛选,让高价值的数据进入模型评估流程,这比一味追求“全量”要实际得多。

5.3 从“逐Span评估”到“路径回归”的扩展思路

做完逐Span评估以后,我建议向前再走一步:把“最佳路径基线”固化成回归用例。做法是选几条历史表现最好、多轮PASS且评分稳定的路径,把它们的关键Span序列和关键参数记录成基线。此后每次改动Agent的Prompt、工具定义或模型版本时,跑一批回归任务,逐Span对比当前路径和基线路径的偏差,结合Jev的评分变化来判断改动是否引入了行为退化。

这个思路本质上是在为Agent做行为层面的回归测试。传统回归测试只能保证“输出仍然正确”或“断言仍然通过”,而路径回归能保证“行为方式没有发生不可解释的漂移”。把Jev的逐Span评估结果接入这个流程后,Agent的一举一动都会被量化,PASS不再是一个黑盒结论。

我在实际使用中还发现一个细节:路径基线的维护也要控制版本。Agent涉及的模型版本、外部工具版本、数据索引版本一变更,基线路径可能就不再是“最佳路径”了。这时不能机械地拿旧基线去约束新行为,而是要结合新的评分重新生成基线。这就像代码仓库的基准分支一样,要定期刷新,才能让路径回归不变成刻舟求剑。在我自己团队里,已经把这个逻辑做成了一个小任务流:每周自动对主路径任务跑一轮逐Span评估,刷新基线并输出路径漂移报告。报告不追求复杂,一张表格列出“路径拆分点”“Jev评分变化”“错误码命中数”就足够用了。这套东西从原理上一点都不玄乎,但它确确实实让“同样PASS”从一句结论变成了一组可以反复审视、对比和优化的证据链。