Skip to content
Guides8 min read

逐步错误定位:用轨迹评估找出智能体究竟在哪一步出错

用 Potato 的 trajectory_eval 方案做逐步错误定位:分层错误分类体系、严重程度评分,以及贯穿整条智能体 trace 的连续评分。

Potato Team

问题所在:只知道智能体失败了,远远不够

A hierarchical agent error taxonomy with four categories and a severity scaleA trajectory error taxonomy

你在某个基准上跑了自己的智能体,任务完成率 63%。然后呢?

一个通过/失败的数字只告诉你有 37% 的任务失败了,别的什么都说明不了。它不会告诉你 trace 中哪个位置出了问题、智能体犯的是哪一类错误、错得有多严重。是第 2 步上一个灾难性的失误,还是十五个小推理错误一路累积?智能体是用错了工具,还是从一个错误前提出发推理?

没有逐步的错误定位,你既没法诊断失败模式,也没法判断该先修哪里,更没法为过程奖励模型攒训练数据。调超参数基本靠猜。

Potato 的 trajectory_eval 标注方案解决的正是这件事。标注者逐步走完一条 trace,并记录:

  • 正确性:这一步是对是错?
  • 错误类型:从你定义的分层分类体系中选取
  • 严重程度:轻微、严重或致命,分数权重可配置
  • 理由:对错误的自由文本说明(可选)
  • 连续评分:一个累计分数,按严重程度递减,给出整条 trace 的质量曲线

本文覆盖完整的搭建流程:定义错误分类体系、跑标注、分析收集到的数据。该方案的配置参考见源文档


trajectory_eval 方案概览

trajectory_eval 方案是为按顺序评估多步骤智能体 trace 而设计的。它不给一个总体质量评分,而是为每一步生成一条结构化的错误标注,最终得到一张详细的地图,标出智能体在哪里、因为什么失败。

标注界面在每一步的流程是这样的:

  1. 标注者看到当前步骤的内容(thought、action、observation、代码等)
  2. 把这一步标记为正确不正确
  3. 如果不正确,从分层分类体系中选出错误类型
  4. 指定严重程度(轻微、严重或致命)
  5. 可选地写一段理由说明这个错误
  6. 界面顶部的连续评分自动更新

标注者一步一步走完整条 trace,逐渐构建出完整的错误画像。

轨迹评估界面会在每一步旁边显示对应的分数:

Trajectory evaluation with running score trackerEach step gets a correctness rating, error type, and severity level with a running score that decrements based on severity


设计分层错误分类体系

分类体系是轨迹评估的价值所在。设计得好,你可以跨 trace 汇总错误、看出系统性的失败模式;设计得差,标出来的标签拼不出任何结论。下面是我会作为起点的一套体系,共四个顶层类别。

推理错误

即使智能体看到的和做的都没问题,推理本身也可能出错。

错误类型说明示例
logical_error无效的逻辑推断“A 蕴含 B,B 为真,所以 A 必为真”(肯定后件)
incorrect_assumption假定了证据并不支持的事情没做检查就假定某个文件存在
over_generalization从有限证据得出过宽的结论“这个函数失败了一次,所以整个 API 都坏了”
circular_reasoning把结论当作前提“答案是 X,因为 X 是正确的”
incorrect_calculation数学或逻辑计算错误推理循环边界时差一错误

感知错误

智能体读错、理解错或漏掉了观测中的信息。

错误类型说明示例
missed_element没注意到相关信息忽略了终端输出里的报错信息
misidentified_element误解了自己看到的东西把 404 错误当成成功响应
hallucinated_element引用了并不存在的东西引用了一个不存在的函数参数
outdated_reference沿用了前面步骤中已过时的信息使用了一个已被覆盖的变量值

动作错误

智能体做了错误的动作,或者用错误的方式做了正确的动作。

错误类型说明示例
wrong_tool为任务选了不合适的工具该用 find 时用了 grep
wrong_arguments工具选对了但参数不对给编辑命令传了错误的文件路径
premature_termination任务没完成就停下只找到部分信息就返回答案
unnecessary_action做了没有任何价值的动作重复读取刚刚读过的文件
destructive_action做了造成破坏的动作未备份就删除文件

沟通错误

这类错误出现在智能体回复用户的内容里,或者它对自己工作的叙述中。

错误类型说明示例
unclear_explanation解释含混或有歧义描述了修复方案却没说原来哪里坏了
missing_context回复中漏掉了关键上下文报告成功却不提附带的限制
incorrect_summary总结与实际动作不符只改了 2 个文件却声称改了 3 个
overconfident_claim把不确定说成确定对未经测试的改动说“这肯定能解决问题”

严重程度与分数权重

每个错误都要指定严重程度。默认权重如下:

严重程度权重说明
minor-1不会让整条 trace 跑偏的小问题(例如多余动作、解释不清)
major-5浪费工作量或产出部分错误结果的明显错误(例如用错工具、错误假设)
critical-10从根本上毁掉整条 trace 的错误(例如破坏性动作、给出错误答案后提前终止)

连续评分从 100 起算,每出现一个错误就按其严重程度权重扣分。最终停在 85 的 trace 只有几个小问题;停在 40 的则出过好几次严重失败。

这些权重可以在配置中修改:

yaml
severity_levels:
  - name: minor
    weight: -1
    description: "Small issue, does not derail the overall trace"
  - name: major
    weight: -5
    description: "Significant error that wastes effort or produces wrong intermediate results"
  - name: critical
    weight: -10
    description: "Fundamental failure that breaks the trace or causes harm"

完整 YAML 配置

下面是一份用于轨迹评估的完整 config.yaml,包含完整的分类体系:

yaml
annotation_task_name: "Agent Trajectory Error Localization"
 
data_files:
  - "data/traces.jsonl"
 
item_properties:
  id_key: "trace_id"
  text_key: "task"
 
# Display agent traces with step-by-step rendering
display:
  type: "agent_trace"
  trace_key: "trace"
  step_display:
    thought: { label: "Thought", color: "#E8F0FE" }
    action: { label: "Action", color: "#FFF3E0" }
    observation: { label: "Observation", color: "#F1F8E9" }
    code: { label: "Code", color: "#F3E5F5" }
 
annotation_schemes:
  - annotation_type: "trajectory_eval"
 
    # Per-step correctness check
 
    # Hierarchical error taxonomy (shown when step is marked incorrect)
 
      - category: "perception"
        label: "Perception Error"
        types:
          - name: "missed_element"
            label: "Missed Element"
            description: "Fails to notice relevant information in observations"
          - name: "misidentified_element"
            label: "Misidentified Element"
            description: "Misinterprets what it observes"
          - name: "hallucinated_element"
            label: "Hallucinated Element"
            description: "Refers to something not present in the context"
          - name: "outdated_reference"
            label: "Outdated Reference"
            description: "Uses stale information from a previous step"
 
      - category: "action"
        label: "Action Error"
        types:
          - name: "wrong_tool"
            label: "Wrong Tool"
            description: "Selects an inappropriate tool for the task"
          - name: "wrong_arguments"
            label: "Wrong Arguments"
            description: "Correct tool but incorrect parameters"
          - name: "premature_termination"
            label: "Premature Termination"
            description: "Stops before the task is complete"
          - name: "unnecessary_action"
            label: "Unnecessary Action"
            description: "Takes an action that adds no value"
          - name: "destructive_action"
            label: "Destructive Action"
            description: "Takes an action that causes harm or data loss"
 
      - category: "communication"
        label: "Communication Error"
        types:
          - name: "unclear_explanation"
            label: "Unclear Explanation"
            description: "Explanation is confusing or ambiguous"
          - name: "missing_context"
            label: "Missing Context"
            description: "Omits critical context from the response"
          - name: "incorrect_summary"
            label: "Incorrect Summary"
            description: "Summary does not match the actual actions taken"
          - name: "overconfident_claim"
            label: "Overconfident Claim"
            description: "States uncertain outcomes as certainties"
 
    # Severity levels with score weights
 
    # Running score configuration
 
    # Optional rationale field
 
  # Overall trace quality (optional, shown at the end)
  - annotation_type: "radio"
 
# Annotator settings
annotator_config:
  allow_back_navigation: true
  require_all_steps: true
 
# Output settings
output:
  path: "output/"
  format: "jsonl"

分步搭建

1. 准备智能体 trace

trace 数据用 JSONL 格式,一行一条。每条 trace 需要一个 ID、一段任务描述和一个步骤列表:

json
{
  "trace_id": "trace_042",
  "task": "Find the bug in the calculate_discount function and fix it",
  "trace": [
    {
      "type": "thought",
      "content": "I need to look at the calculate_discount function to find the bug. Let me search for it."
    },
    {
      "type": "action",
      "content": "search",
      "action_input": "def calculate_discount"
    },
    {
      "type": "observation",
      "content": "Found in pricing.py line 45:\ndef calculate_discount(price, discount_pct):\n    return price * discount_pct / 100"
    },
    {
      "type": "thought",
      "content": "The function calculates the discount amount but doesn't subtract it from the price. It should return price - (price * discount_pct / 100)."
    },
    {
      "type": "action",
      "content": "edit_file",
      "action_input": "pricing.py:45: return price - (price * discount_pct / 100)"
    },
    {
      "type": "observation",
      "content": "File edited successfully."
    },
    {
      "type": "action",
      "content": "finish",
      "action_input": "Fixed the calculate_discount function. It was returning the discount amount instead of the discounted price."
    }
  ]
}

如果你的 trace 是别的格式(OpenAI messages、LangChain 运行记录、Claude 对话日志),可以用 Potato 的 trace 转换器:

bash
python -m potato.trace_converter \
  --input raw_traces/ \
  --output data/traces.jsonl \
  --input-format react

2. 配置分类体系

先用上面这套完整体系,再按自己的智能体裁剪或扩充。比如针对编码智能体,可以加一个 code_quality 类别:

yaml
- category: "code_quality"
  label: "Code Quality Error"
  types:
    - name: "syntax_error"
      label: "Syntax Error"
      description: "Generated code has syntax errors"
    - name: "runtime_error"
      label: "Runtime Error"
      description: "Code runs but produces an error"
    - name: "logic_bug"
      label: "Logic Bug"
      description: "Code runs without errors but produces wrong output"
    - name: "style_violation"
      label: "Style Violation"
      description: "Code works but violates project conventions"

对于编码智能体的 trace,评估界面会在评分控件旁边渲染 diff 和终端输出:

Coding agent evaluation with diff renderingCodingTraceDisplay renders diffs, terminal blocks, and file reads alongside trajectory evaluation controls

3. 启动标注服务器

bash
potato start config.yaml -p 8000

在浏览器中打开 http://localhost:8000,你会看到第一条 trace 以分步形式展示。

4. 写标注指南

要给标注者清楚的说明。至少要写明:

  • 什么情况算不正确,什么情况算正确但不是最优
  • 多个错误类别同时适用时怎么选(取最具体的那个)
  • 各个严重程度分别在什么情况下使用,并给出具体例子
  • 评估某一步时,是只依据当时可得的信息,还是可以事后回看

标注流程

标注者打开一条 trace 时,任务描述在最上方,第一步紧随其后。右上角的连续评分显示 100

对于每一步,标注者要:

  1. 结合前面的步骤读懂当前步骤的内容
  2. 判定正确性,点击“Correct”或“Incorrect”
  3. 如果不正确,先选错误类别(例如“Reasoning Error”),再选具体类型(例如“Incorrect Assumption”)
  4. 指定严重程度:轻微、严重或致命
  5. 写理由(如果启用):“智能体没做检查就假定文件在当前目录下,但搜索结果显示它在 src/utils/ 里”
  6. 进入下一步,点击“Next Step”或按右方向键

每出现一个错误,连续评分就更新一次。把第 3 步标为严重错误(-5),分数从 100 降到 95;把第 7 步标为致命错误(-10),再降到 85。

走完整条 trace 后,标注者给出总体的成功/部分成功/失败评价并提交。


分析结果

加载标注数据

python
import json
import pandas as pd
from collections import Counter
from pathlib import Path
 
# Load all annotation files
annotations = []
output_dir = Path("output/")
for f in output_dir.glob("*.jsonl"):
    with open(f) as fh:
        for line in fh:
            annotations.append(json.loads(line))
 
print(f"Loaded {len(annotations)} annotated traces")

错误分布分析

python
# Extract all errors across all traces
errors = []
for ann in annotations:
    for step_ann in ann.get("error_localization", []):
        if step_ann["correctness"] == "incorrect":
            errors.append({
                "trace_id": ann["trace_id"],
                "step_index": step_ann["step_index"],
                "category": step_ann["error_category"],
                "error_type": step_ann["error_type"],
                "severity": step_ann["severity"],
                "rationale": step_ann.get("rationale", ""),
            })
 
error_df = pd.DataFrame(errors)
print(f"Total errors found: {len(error_df)}")
print()
 
# Error distribution by category
print("Errors by category:")
print(error_df["category"].value_counts())
print()
 
# Most common specific error types
print("Top 10 error types:")
print(error_df["error_type"].value_counts().head(10))
print()
 
# Severity distribution
print("Severity distribution:")
print(error_df["severity"].value_counts())

错误位置分析

看看错误在 trace 中落在什么位置,常常能暴露出系统性的规律:

python
import matplotlib.pyplot as plt
import numpy as np
 
# Normalize step positions to [0, 1] range
for ann in annotations:
    trace_length = len(ann.get("error_localization", []))
    for step_ann in ann["error_localization"]:
        if step_ann["correctness"] == "incorrect":
            step_ann["normalized_position"] = step_ann["step_index"] / max(trace_length - 1, 1)
 
# Collect normalized positions
positions = [
    step_ann["normalized_position"]
    for ann in annotations
    for step_ann in ann.get("error_localization", [])
    if step_ann["correctness"] == "incorrect"
    and "normalized_position" in step_ann
]
 
plt.figure(figsize=(10, 4))
plt.hist(positions, bins=20, edgecolor="black", alpha=0.7)
plt.xlabel("Normalized Position in Trace (0 = start, 1 = end)")
plt.ylabel("Error Count")
plt.title("Where Do Agent Errors Occur?")
plt.tight_layout()
plt.savefig("error_position_distribution.png", dpi=150)
print("Saved error_position_distribution.png")

连续评分的分布

python
# Extract final running scores
final_scores = []
for ann in annotations:
    score = 100
    severity_weights = {"minor": -1, "major": -5, "critical": -10}
    for step_ann in ann.get("error_localization", []):
        if step_ann["correctness"] == "incorrect":
            score += severity_weights.get(step_ann["severity"], 0)
    score = max(score, 0)
    final_scores.append({
        "trace_id": ann["trace_id"],
        "final_score": score,
        "overall_success": ann.get("overall_success", "unknown"),
    })
 
score_df = pd.DataFrame(final_scores)
 
print("Score statistics:")
print(score_df["final_score"].describe())
print()
 
# Score distribution by overall success
for label in ["success", "partial", "failure"]:
    subset = score_df[score_df["overall_success"] == label]
    if len(subset) > 0:
        print(f"{label}: mean={subset['final_score'].mean():.1f}, "
              f"median={subset['final_score'].median():.1f}, "
              f"n={len(subset)}")

最常见的失败模式

python
# Group errors by category + type for a failure mode analysis
failure_modes = (
    error_df.groupby(["category", "error_type"])
    .agg(
        count=("severity", "size"),
        avg_severity_weight=("severity", lambda x: x.map(
            {"minor": 1, "major": 5, "critical": 10}
        ).mean()),
    )
    .sort_values("count", ascending=False)
)
 
print("Top failure modes (by frequency):")
print(failure_modes.head(15).to_string())
print()
 
# Impact-weighted failure modes (frequency x average severity)
failure_modes["impact"] = failure_modes["count"] * failure_modes["avg_severity_weight"]
print("Top failure modes (by impact):")
print(failure_modes.sort_values("impact", ascending=False).head(10).to_string())

研究背景

逐步错误定位与近年智能体评估的几条研究线索是对得上的:

TRAIL(Patronus AI,2025)用一套超过 20 种错误类型的分类体系,标注了取自 GAIA 与 SWE-bench Lite 的 148 条智能体踪迹,合计 841 处错误。值得记住的结果是定位有多难:他们测过的最好的长上下文推理模型,在错误类别加位置上的联合准确率只有 11%。这正是 trajectory_eval 交给人类标注员的活儿,也是这些标签值得花钱的原因。

AgentRewardBench(McGill NLP,2025)盯的则是裁判本身。它收集了跨五个基准的 1302 条 Web 智能体轨迹,让专家逐条从成功、副作用和重复三方面复核,再拿这些复核去给十二个 LLM 裁判打分。没有哪个裁判在所有基准上都领先,而基准自带的规则式评估还低报了智能体实际成功的次数。如果你打算用模型自动化这套分类体系的一部分,你需要的就是这种形式的核对。

trajectory_eval 产出的步骤级正确性与严重度标签,也能直接喂给过程奖励模型的训练:每个标注过的步骤都是一个带有真值质量信号的训练样本。

Anthropic 的 Demystifying evals for AI agents 给出了同一主张的操作版本:不要只评结果,也要评整段记录;对于智能体如何调用工具、如何与用户对话,使用带明确评分细则的模型评分器。它同时提醒不要照着预设的步骤序列打分,因为智能体总能找到评测设计者没料到的有效路径。套用这套分类体系时请记住这一点:某一步算错,是因为它做错了,而不是因为它出乎意料。

按严重程度加权的连续评分也能对应到 RLHF 中使用的奖励信号。一条 20 步的 trace,如果分数曲线在第 5 步陡然下跌,就直接指明了智能体需要改进的地方,这比整条 trace 结束时给一个总奖励要有用得多。


小结

trajectory_eval 方案把智能体评估从一次通过/失败的检查变成了一次诊断。有了分层分类体系、严重程度评分和连续评分,你能看清是哪一步出了错、错的类型是什么、有多严重,以及错误在不同 trace 中倾向于聚集在哪些位置。这些步骤级标签也可以直接用作过程奖励模型的训练数据。

先用本文给出的完整分类体系起步,再根据你的智能体和实际观察到的错误模式去打磨。最好的分类体系,是那个能指向你真正改得动的修复方向的体系。