
最近我接了一个挺常见的排查任务同一批Agent评测任务结果表里有两行都是PASS。等我把对应的Agent Trace拉出来逐条看的时候发现情况比表面结果有意思得多——第一个任务只用了3个Span就完成了第二个任务却跑了整整11个Span中间经历了两次工具失败和一次上下文重写。最终判定都是PASS但执行路径完全不同。这个问题如果不深挖你永远不会知道自己的Agent流程里藏着多少隐性分岔。我这次用的分析工具是Jev一个专门针对Agent Trace做逐Span级评估的模型。它不同于传统评测里那种“只看最终结果给个分”的打法而是一段一段地把Trace切开给每个Span单独做语义评估和路径关联这样一来“同样PASS、路径为何不同”就不再是一个只能靠肉眼对比的玄学问题。这篇文章我会把整个排查思路、Jev的接入方式、逐Span拆解的实操步骤以及我踩过的几个坑完整记录下来。如果你也在做Agent评测、可观测性分析或者单纯想搞清楚自己的Agent执行的每一步是否合理这篇应该能给你一些可直接落地的参考。1. 同样PASS路径不同问题藏在评估指标的盲区里1.1 一个真实的对比场景3个Span与11个Span我在做这个排查之前评测体系里的PASS标记其实来自两套机制一套是规则匹配直接比对最终输出里的关键字段另一套是大模型评审让LLM根据任务目标判断结果是否达成。这两套机制的共同点是都只消费最终输出完全不在乎过程。那天我拿到两条Trace之后做了个简单标注。第一条的Span序列是plan → call_tool(search_file) → answer。工具调用一次成功答案直接检索得到干净利落。第二条的序列明显更复杂plan → call_tool(search_file) → error → call_tool(list_dir) → error → retry(search_file) → summarize → call_tool(read_file) → call_tool(read_file) → merge_context → answer。尽管最终输出都判PASS第二条里出现了错误Span、重试Span、上下文压缩Span。我把两条Trace并排放在Jev里做逐Span评估时得到的第一份输出就很有信息量第一个Trace的Span平均耗时0.8秒第二个平均耗时3.2秒失败型Span占到了总量的18%。此时“路径不同”这个现象已经量化了Span数量差、失败Span占比、总耗时、工具调用分布都成了可见的差异指标。1.2 只看PASS判定的三个盲区PASS标记本身没有问题问题在于“只看PASS”会漏掉三个层次的盲区。第一是效率盲区。同一个任务3个Span的Trace和11个Span的Trace前者可能只消耗了后者三分之一的Token和时间。在批量化评测里如果只统计PASS率这两条会被归进同一个桶里但线上运行时一个Agent如果频繁走第二条路径成本很快会被放大到不可忽视的程度。第二是稳定性盲区。如果一条Trace里出现错误Span说明Agent在某个环节失败过失败后能恢复并达到PASS仅代表这次运气好。下次同样的错误如果出现在更关键的步骤上可能就不会这么幸运了。PASS掩盖了这种“危险的稳定”。第三是策略盲区。同一个目标可以用多种路径完成直接搜索答案、逐步推理、调多个工具交叉验证。不同路径对应Agent不同的策略偏好也反映了Prompt和工具配置对行为的影响。只看PASS等于放弃了观察策略的机会。这三个盲区恰恰是逐Span评估存在的意义——把“过程”重新拉回评估视野。1.3 路径指纹让“路径不同”可以被量化既然要比较路径就得先定义路径。我用一个词叫“路径指纹”把一条Trace中所有Span的关键字段做序列化压缩形成一个短哈希码或紧凑序列字符串。每个Span代表一个执行单元路径指纹就是把“plan → tool_a → tool_b → answer”这类序列固定下来变成可以互相比较的字符串。例如第一条Trace的指纹大约长这样plan[1] → tool_search[1] → answer[1]。第二条的指纹就是plan[1] → tool_search[1] → tool_error[1] → ... → answer[1]。用最长公共子序列LCS去比对能算出两条路径的相似度也能精确定位到第几个Span开始分岔。路径指纹的价值在于三点可以在大量Trace里做聚类可以设定相似度阈值来自动告警异常路径可以在评测统计里把“PASS且路径正常”和“PASS但路径异常”分开统计。我后来把路径指纹纳入了评测报表PASS率旁边永远跟着一个“路径异常率”比只看PASS要踏实得多。2. Jev的实际工作机制与接入准备2.1 选择逐Span评估工具时要衡量什么市面上做Trace评估的工具其实不少但大多数是从可观测性角度出发的比如把Trace可视化、统计耗时、标红错误Span。这类工具的优势是“看得见”劣势是“看不懂”——它不会告诉你一个Span的内容是否合理、这个工具调用是否必要、这次LLM推理是否偏离了任务目标。我需要的是一个能“读懂语义”的评估层。这也是我选Jev而不是继续用光秃秃的可观测面板的原因。Jev的定位是Agent执行轨迹的语义评估模型它对每个Span不仅记录“做了什么”还会评估“为什么做、做得对不对、有没有更优路径”。也就是从“监控层”上升到“评估层”。选型时我主要看四点能否逐Span独立评分能否识别Span之间的依赖关系能否输出可解释的理由而不是一个黑盒分数能否批量跑毕竟评测会有一大批Trace需要处理。这四个条件框下来选择范围就很小了。2.2 Jev逐Span处理的核心逻辑Jev处理Agent Trace的方式一句话概括不把整条Trace当一个样本打总分而是把每个Span当独立评估单元先局部判责再全局归因。具体拆开讲Jev对每个Span主要做四件事识别Span类型是LLM推理、工具调用、等待轮询还是错误生成评估输入输出质量工具的输入是否命中任务目标、LLM输出是否与之前上下文断裂衡量必要性这个工具调用有没有增量信息还是重复检索记录副作用是否引入错误标记、是否触发重试、是否消耗超量Token。逐个Span评估完Jev会把结果汇总成一份“路径评估报告”里面除了每个Span的分值还有一条“关键路径预警”哪些Span是达成PASS的必要环节哪些Span是噪音环节或冗余环节。比如前面提到的11个Span那条TraceJev的评估报告中就明确标出了两个冗余Span——重复的search_file调用和一次无意义的list_dir枚举。这层“逐Span判责”的价值在于当Agent出问题时你可以精确到具体是第几个Span、哪个工具、哪段Prompt导致的而不是整个Trace打个叉然后从头猜。2.3 接入Jev的前置步骤与密钥配置接入这一步不算复杂但有几个前置条件我希望提前说明白。Jev目前以API服务的方式接入需要先去官网注册一个工作区拿到API Key它也提供本地部署的轻量版本适合Trace数据不想出内网的场景。我在实操中用的配置方式是在项目里加一个环境变量文件写入Jev的Endpoint和API Key然后在Agent执行框架的回调阶段做一层拦截每次Trace结束就自动把Spans POST到Jev的分析接口。拦截层不要直接发出同步请求先写到本地队列再由后台任务批量提交这样主流程不会被评估流程拖慢。另外Jev对输入格式有一定要求Span的字段名最好符合OpenTelemetry的约定。如果你的Agent框架导出的Trace不是这个结构需要先做一个标准化转换把字段名映射到统一的规范上。这个转换层我试过用简单的Python字典映射就能完成不用引入重型ETL。3. 用Jev逐Span拆解Agent Trace的四步实操3.1 第一步导出原始Trace并做字段标准化Agent框架的Trace导出格式五花八门有的直接给JSON日志有的要调框架内置exporter。我处理的这批任务用的是回调框架的默认日志输出是每一行的JSON格式事件。原始输出里没有一个完整Trace聚合需要先按trace_id做聚合把同一个Trace的Span拼装成一个大JSON。拼装完成后要做字段标准化。OpenTelemetry的Span核心字段是trace_id、span_id、parent_span_id、name、kind、start_time、end_time、status、attributes。如果你的框架给的是自定义字段名做一个映射表对齐。比如我这边原始的message_id对应span_idcaller对应parent_span_idevent_type对应kindstatus_code对应status。这一步不要偷懒因为Jev的语义评估依赖标准化的字段结构。我踩过的坑是attributes里埋了很深的嵌套JSON如果直接原样传过去Jev对工具参数和返回内容的解析会不稳定。需要把attributes打平成一级key-value再按约定前缀分组。标准化之后的Trace形如{ trace_id: 7f3a9b..., spans: [ { span_id: s1, parent_span_id: , name: plan, kind: internal, start_time: 2024-06-01T10:00:00.000Z, end_time: 2024-06-01T10:00:00.800Z, status: ok, attributes: { gen_ai.system: openai, gen_ai.usage.input_tokens: 520 } } ] }3.2 第二步根据parent_span_id重建执行拓扑标准化完成后的第一件事不是急着评估而是重建拓扑。Span之间的parent_span_id关系决定了执行顺序和因果链。如果直接按时间排序看Span列表你看到的是一条扁平日志但按parent关系整理后才能看到“plan下面挂了三个子Span”“某个工具调用对应哪次LLM推理的回填”这类结构信息。我一般用递归方式把Span列表转成树。要注意的是Agent场景下Trace往往不是严格意义上的树——因为存在循环调用。比如一个Agent在ReAct模式下反复调用同一个工具每次调用其实是同一个parent下的一串同名字节点又比如重试逻辑会在同一个parent下生成多个子Span。直接用树去组织会有失真我实际用的是“树循环标识”的方式保留兄弟节点之间的顺序同时给重试Span打上retry标记。拓扑重建完成后可以先把数打印出来看一眼。Jev的API里也提供拓扑校验功能它会检查Span的parent链是否完整、有没有孤儿Span、有没有parent链断开的节点。这些异常如果不修逐Span评估会在某些Span上给不出可靠的关联结论。3.3 第三步逐Span提交评估并解读四类结果打包好标准化的Trace之后就可以调用Jev的评估接口了。调用方式很简单POST一个JSON包含trace_id和spans返回结果是每个Span的评估记录。我把返回结果整理成一张表逐项看四类结果类型识别、合理性评分、必要性评分、副作用标记。拿到结果后我会先看类型识别是否准确。比如有的Span叫get_weather但Jev识别为tool_call还是llm_reasoning如果识别错误多半是标准化阶段的attributes没对齐。再看合理性评分和必要性评分低分通常意味着这个Span做了不该做的事或者参数与目标不对齐。副作用标记则是红点一旦出现error、retry、context_rebuild这个Span就是重点观察对象。解读时不要只看单个Span的分值要把父子关系带进去。一个低分子Span如果被高分的父Span成功纠正那么它在路径上的影响是“局部异常、全局正常”反之一个高分Span如果后面跟着三个低分子Span那这个高分很可能只是“局部自洽、全局迷失”。逐Span评估的意义恰恰体现在这种关联解读里。3.4 第四步路径指纹计算与批量对比单个Trace的评估解决的是“这步做得对不对”批量对比解决的是“这批任务里有多少条异路径”。我处理的方式是把每条Trace压缩成路径指纹字符串然后逐对计算编辑距离。路径指纹我用的是紧凑序列每个Span的评估类型首字母加序号。比如首次成功的Trace指纹是I1(plan)-T1(search)-A1(answer)失败重试那条是I1(plan)-T1(search)-E1(error)-T2(list)-E2(error)-T3(search)-I2(summarize)-T4(read)-T5(read)-I3(merge)-A1(answer)。用这类字符串做LCS或编辑距离计算能快速找到分岔点。有了相似度阈值之后我再给每条Trace分组路径指纹相似度高于90%的归为“正常路径”低于阈值的进入“异常路径待查”列表。这一步极大提升了我后续排查的效率。那次评测里我靠路径指纹把11个Span那条Trace和其他几十条正常Trace一键分开立刻锁定了问题Agent不用一条一条肉眼看。4. 路径分岔的三种典型形态与判读方法4.1 工具选择分岔同一个目标不同的工具实现第一种最常见的路径分岔是工具选择层面的分岔。比如让Agent完成“查某技术的最新版本信息”Agent A走的是search工具直接搜到结果Agent B走的是read_docs工具先打开官方文档页面再解析还有Agent C可能直接调用一个版本查询API。三者最终都返回了正确答案PASS全部成立但路径完全不同。用Jev逐Span评估时这类分岔的判读要看必要性评分。A路径的search结果如果足够准确B路径的read_docs就显得冗余但如果search返回的是二手内容、而read_docs访问的是权威源那从任务可靠性角度讲B路径反而更优。所以工具选择分岔并不总是“坏的路径差异”它有时是稳健性的体现。实操上我判断这类分岔的办法是把同类任务的多条Trace放在一起对比看是不是每次都固定走同一条工具路径。如果50次任务里有48次走search、2次走了read_docs那这2次大概率是Agent在search结果不满意时做的兜底切换属于合理应变如果50次里路径均匀分散说明工具的可用性提示词没约束好Agent在选择上过于自由。4.2 重试与回退分岔错误Span暴露的隐性成本第二种分岔是重试与回退导致的分岔。这类分岔最容易被PASS掩盖因为Agent最终成功了错误Span往往被略过。但我前面那个例子已经展示了一次重试就会让Span数从3变成6到8Token消耗翻倍时间翻倍。用Jev评估这类路径时我会重点看两个指标。一是错误Span的相邻上下文错误发生前那个Span的输入是否合理错误类型是工具本身返回异常、参数格式错误还是权限问题。二是重试策略是否收敛第一次错误后Agent是调整了参数再试还是原样重发、只是碰运气。原样重发能成功说明是瞬时故障否则就说明Agent没有从错误中学到任何东西。批量对比重试分岔的时候我一般会在路径指纹字符串上做标记在错误Span后面的重试Span上打上r(retry)后缀这样“无重试路径”和“有重试路径”在指纹序列上一眼可见。我顺便会统计重试率这个指标如果某个Agent的重试率长期超过20%基本可以判定是工具接口不稳定或者Agent对错误信息的利用不到位。4.3 上下文取舍分岔LLM如何分配有限的注意力第三种分岔来自上下文窗口限制下的取舍策略。Agent处理长文档、多文件任务时没法把全部内容塞进上下文必须决定哪些内容进、哪些内容不进、哪些需要压缩。不同的取舍会导致完全不同的路径。一个典型对比任务要求从10个文件里提取关键接口。Agent A的策略是把10个文件路径全部读一遍每个文件生成摘要Span然后再联合摘要做最终分析Agent B的策略是先读任务描述识别出可能相关的3个文件只精读这3个再确认其他文件不需要。两条路径都能PASS但A路径的Token开销可能是B路径的3倍。逐Span评估这类分岔我认为最有价值的指标是“信息增益”每个read Span的返回值里有多少内容被后续Span引用。如果某个read Span的摘要和最终答案没有任何关系Jev会把它标为低增益或零增益Span这就是可以优化的路径节点。这类分岔没有绝对的对错但至少应该让你知道Agent在长任务里是一股脑全读还是带着目标感在取舍。5. 逐Span评估实操中的常见问题与排查技巧5.1 Span不完整采样丢弃和字段丢失的处理我刚开始接入时最大的问题是Trace不完整。很多Agent框架默认做了采样比如5%的Traces会被记录排查单条问题时概率性丢Trace。我先关闭了采样确保全量记录然后在Jev提交结果里看到了好几条“parent链断裂、孤儿Span”的告警。字段丢失也很常见。尤其是我用的框架日志里有些Span的end_time没有写有些Span的attributes是空的导致Jev对耗时的统计全部归零对工具参数的分析也无法进行。我的处理办法是在标准化转换层做兜底如果end_time缺失就用父Span的end_time填充并打上flag如果attributes为空就根据Span的name做默认推断实在推断不了的标记为unknown。这里想提醒一句不要在评估阶段才补数据应该在Agent运行框架的输出端就把该埋的字段埋齐。逐Span评估的质量上限取决于Trace里每个Span的字段完整度。5.2 循环结构与时间戳异常让拓扑重建失败Agent Trace的循环结构是我在实操里最头疼的。同一个Agent可以多次调用同一个工具没有加序号区分的话重建出来的“树”会出现同一个父子关系的多胞胎节点顺序全靠猜测。我在标准化阶段给每个同父子节点加了序号后缀比如tool_search#1、tool_search#2才让拓扑重建稳定下来。时间戳异常是另一个高频问题。部分事件日志的时间精度不够多个Span共享同一毫秒导致排序时父子关系错乱。我的排查技巧是如果两个Span的时间戳完全一样不要依赖时间排序改用parent_span_id先组织父子关系时间戳只用来在同级兄弟节点之间排序。这个原则在重建拓扑时很重要能避免不少错位。还有一次遇到的比较极端的情况某个重试Span的start_time居然早于父Span的start_time一看是框架缓存的日志顺序写反了。这种情况靠逻辑修复不了只能在标准化层做时间校正把子Span的时间夹在父Span之后、下一个兄弟Span之前。5.3 Jev评估结果与人工经验判断不一致时怎么校准用过一段时间后你会发现Jev的打分并不总是跟你的直觉一致。最开始我遇到一个情况Jev把某个read_file Span标成了高必要性但我靠经验判断觉得那个文件根本不需要读。后来细看才发现那个文件里确实有一行关键配置是最终答案里的核心依据——是我的直觉错了不是Jev错了。第二种不一致是Jev过度惩罚长路径。有的Agent用更长的路径换取更低的Token消耗比如先做摘要再按需读Jev可能会因为Span数多而给较低评分但这实际上是个优秀的策略。这类不一致我处理的方法是在Jev的输出上叠加一个成本权重维度让评分同时考虑Token和时间成本而不是单纯看路径长短。校准的通用做法是维护一个“人工复核样本集”挑50条不同路径的Trace先用经验和上下文人工判读再对照Jev的评估结果。两边的偏差率如果低于10%说明配置基本可靠如果高于10%优先怀疑是标准化字段没有对齐而不是模型本身的问题。5.4 路径指纹批量对比的标准化建议最后分享一个批量对比时的标准化建议。路径指纹要真正做到“可比”前提是同一批Trace的标准化规则完全一致。包括Span类型名称统一tool_search、tool_read不能一会儿叫这个一会儿叫缩写字段值里的动态参数必须剔除比如file路径、搜索关键词它们不能进入指纹否则每次都不相同重试标记要有统一的编码规则。我实际用的编码方案是五类I表示LLM推理SpanT表示工具调用SpanE表示错误SpanR表示重试SpanA表示最终回答Span。字母后面跟序号区分同类型不同实例。这样编码出的指纹既保留了执行的类型顺序又留出了统计空间。别用UUID之类的东西编指纹那等于没编。把指纹计算做成一个独立函数挂在评测流水线里之后路径对比就变成了一次批量计算而不是人工逐条对。我后来还在报表里加了一条“路径一致性”曲线看一段时间内Agent是否稳定走同一类路径这个曲线对评估Agent行为漂移特别有用。不过这属于后话了先把基础的四步流程跑通更重要。整套流程跑下来我最大的感受是PASS只是一个及格线的结论逐Span评估才是看清Agent真实行为的手段。把Jev接入评测体系之后我现在看任何一份Agent评测结果都会习惯性地把PASS率和路径异常率并排放在一起看——结果对了但路径异常多得离谱那这个PASS大概率是运气好结果对了路径也稳定这个PASS才真正可信。如果你也在做Agent评测我的建议是别满足于那个绿色的PASS标记花点时间把Trace拆开看路径里藏着的分岔点往往才是Agent系统真正需要优化的地方。另外一个小技巧定好路径指纹的编码规则后把它养成评测报表的固定列长期积累下来你会对自家Agent的行为习惯有非常清晰的掌握。