逐步错误定位:用轨迹评估找出智能体在哪一步出错
用 Potato 的 trajectory_eval 方案做逐步错误定位:分层错误分类体系、严重程度权重,以及贯穿整条智能体 trace 的连续质量评分。
通过率与失败模式
A trajectory error taxonomy
你在一个基准上跑自己的智能体,任务完成率 63%。然后呢?
一个通过/失败的数字只告诉你智能体在 37% 的任务上失败了,别的什么也没说。它不会告诉你 trace 里哪个位置出了问题、智能体犯的是哪一类错误、错得有多严重。是第 2 步上一次灾难性的失误,还是十五步小的推理错误一路累积?智能体是用错了工具,还是从一个错误的前提开始推理?
没有逐步的错误定位,你没法诊断失败模式,没法判断该先修什么,也没法为过程奖励模型攒训练数据。
Potato 的 trajectory_eval 方案解决的就是这件事。标注者逐步走完一条 trace,并记录:
- 正确性:这一步是正确、不正确,还是部分正确?
- 错误类型:从你自己定义的分类体系中选出一个类别和一个子类型
- 严重程度:轻微、严重或致命,各自带一个分数权重
- 理由:对这个错误的自由文本说明
- 连续评分:一个累计分数,每出现一个错误就按严重程度的权重往下扣
该方案的参考文档见源文档。
轨迹评估方案概览
这个方案按顺序评估多步骤的 trace。它不给一个总体评分,而是为每一步生成一条结构化标注,最后你得到的是一张地图,标出这次运行在哪里失败、为什么失败。
在每一步,标注者看到该步的内容,标记正确性;只要不是"正确",就选一个错误类别和子类型、指定严重程度,并可选地写一段理由。顶部的连续评分会随之更新。
Each step gets a correctness rating, error type, and severity level with a running score that decrements based on severity
配置选项
| 选项 | 说明 |
|---|---|
steps_key | 每个实例中存放步骤列表的字段 |
step_text_key | 一个步骤内部显示哪个字段 |
correctness_options | 正确性标签,默认是 correct 和 incorrect |
error_types | 分类体系,由若干类别组成,每个类别带一个 subtypes 列表 |
severities | 严重程度等级,每个带一个 name 和一个分数 weight |
show_score | 是否显示连续质量评分 |
max_score | 从多少分开始算 |
分类体系只有两层:一个类别配一份扁平的子类型名称列表。每个子类型具体是什么意思,这类较长的说明属于你的标注指南而不是配置,因为这个方案存的是名称,不是定义。
设计分层错误分类体系
你的分类体系决定了这些标签能不能汇总出结论。设计得好,你可以跨 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 崩掉或造成破坏,比如一次破坏性操作 |
分数从 max_score 起算,每遇到一个错误就往下掉。最后停在 85 的 trace 有几个小问题;停在 40 的则有好几处严重失败。
severities:
- name: minor
weight: -1
- name: major
weight: -5
- name: critical
weight: -10完整 YAML 配置
annotation_task_name: "Agent Trajectory Error Localization"
task_dir: "."
data_files:
- "data/traces.jsonl"
item_properties:
id_key: "trace_id"
text_key: "task"
instance_display:
fields:
- key: "task"
type: text
label: "Task"
- key: "trace"
type: agent_trace
label: "Agent trace"
display_options:
show_step_numbers: true
show_timestamps: false
step_type_colors:
thought: "#E8F0FE"
action: "#FFF3E0"
observation: "#F1F8E9"
code: "#F3E5F5"
annotation_instructions: |
Walk the trace one step at a time. Mark a step incorrect only when it is
wrong on the information available at that step, not with hindsight. When
several categories apply, choose the most specific one.
annotation_schemes:
- annotation_type: trajectory_eval
name: step_errors
description: "Mark each step and classify any error"
steps_key: trace
step_text_key: content
correctness_options:
- correct
- incorrect
- partially_correct
error_types:
- name: reasoning
subtypes:
- logical_error
- incorrect_assumption
- over_generalization
- circular_reasoning
- incorrect_calculation
- name: perception
subtypes:
- missed_element
- misidentified_element
- hallucinated_element
- outdated_reference
- name: action
subtypes:
- wrong_tool
- wrong_arguments
- premature_termination
- unnecessary_action
- destructive_action
- name: communication
subtypes:
- unclear_explanation
- missing_context
- incorrect_summary
- overconfident_claim
severities:
- name: minor
weight: -1
- name: major
weight: -5
- name: critical
weight: -10
show_score: true
max_score: 100
- annotation_type: radio
name: overall_success
description: "Did the run succeed overall?"
labels:
- success
- partial
- failure
sequential_key_binding: true
output_annotation_dir: "annotation_output/"
export_annotation_format: jsonl
user_config:
allow_all_users: true分步搭建
1. 准备你的智能体 trace
trace 放在 JSONL 里,一行一条,每条带一个 id、一段任务描述和一个步骤列表。steps_key 指定那个列表,step_text_key 指定每步里要显示的字段:
{
"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 calculate_discount to find the bug."
},
{
"type": "action",
"content": "search('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": "It returns the discount amount rather than the discounted price."
},
{
"type": "action",
"content": "edit_file('pricing.py:45', 'return price - (price * discount_pct / 100)')"
},
{
"type": "observation",
"content": "File edited successfully."
}
]
}如果你的 trace 是别的格式,先转换一下。转换器读入一个文件,写出规范的 JSONL:
python -m potato.trace_converter \
--input raw_traces.json \
--input-format react \
--output data/traces.jsonl--list-formats 会打印它支持的格式,其中包括 react、langchain、langfuse、webarena、openai、anthropic、swebench 和 otel。拿不准手上是哪种时,用 --auto-detect 代替 --input-format。
2. 调整分类体系
从上面那四个类别出发,按你的智能体做删减或扩充。编码智能体通常值得单列一个类别:
error_types:
- name: code_quality
subtypes:
- syntax_error
- runtime_error
- logic_bug
- style_violation收集一旦开始,子类型的名称就别再动了。中途改名会让同一种失败模式在导出结果里裂成两个标签。
如果是编码 trace,改用 coding_trace 显示类型,它会渲染 diff 和终端输出:
Coding trace display renders diffs, terminal blocks, and file reads alongside trajectory evaluation controls
3. 启动服务器
potato start config.yaml -p 80004. 撰写标注指南
这个方案存的是子类型名称而不是定义,所以定义必须放在标注者看得见的地方。把上面那些表格放进 annotation_instructions 或你的指南文档,并且至少把这四个问题定下来:
- 一步在什么情况下算不正确,而不是正确但不够好
- 多个类别都说得通时,以哪个为准
- 轻微、严重、致命之间的分界在哪,并给出例子
- 判断一步时依据的是当时可得的信息,还是事后回看
标注工作流程
任务在最上面,trace 在它下面,角落里的连续评分显示 100。
每一步,标注者结合前面各步的上下文来读它,标记正确性;遇到错误就先选类别、再选子类型、再选严重程度,然后写下理由。分数随之更新:第 3 步一个严重错误把它降到 95,第 7 步一个致命错误再降到 85。
走到最后,他们给出整体成功与否的评分并提交。
输出里有什么
每条标注过的 trace 带一个 steps 列表和最终的 score。正确的步骤只记录它的序号和正确性;出错的步骤在 error_type 里带类别,在 error_subtype 里带具体的那一个:
{
"steps": [
{"step_index": 0, "correctness": "correct"},
{
"step_index": 1,
"correctness": "incorrect",
"error_type": "action",
"error_subtype": "wrong_tool",
"severity": "major",
"rationale": "Should have used a more specific selector"
}
],
"score": 95
}error_type 是类别,error_subtype 才是叶子。指望在 error_type 里读到 wrong_tool,正是那种会让分析看起来"所有失败都是动作错误"的失误。
分析结果
载入标注数据
import json
import pandas as pd
from pathlib import Path
SCHEME = "step_errors"
traces = []
for f in Path("annotation_output/").rglob("*.jsonl"):
with open(f) as fh:
for line in fh:
rec = json.loads(line)
payload = rec.get("labels", rec).get(SCHEME)
if not payload:
continue
traces.append({
"instance_id": rec.get("instance_id"),
"annotator": rec.get("user_id"),
"steps": payload.get("steps", []),
"score": payload.get("score"),
})
print(f"Loaded {len(traces)} annotated traces")错误分布
errors = []
for t in traces:
for step in t["steps"]:
if step.get("correctness") == "correct":
continue
errors.append({
"instance_id": t["instance_id"],
"step_index": step.get("step_index"),
"category": step.get("error_type"),
"subtype": step.get("error_subtype"),
"severity": step.get("severity"),
})
error_df = pd.DataFrame(errors)
print(f"Total errors: {len(error_df)}\n")
print("By category:\n", error_df["category"].value_counts(), "\n")
print("Top subtypes:\n", error_df["subtype"].value_counts().head(10), "\n")
print("By severity:\n", error_df["severity"].value_counts())错误落在 trace 的什么位置
把位置归一化,能看出失败是聚在开头(智能体还在摸方向)还是聚在结尾(它正在收尾):
import matplotlib.pyplot as plt
positions = []
for t in traces:
n = len(t["steps"])
if n < 2:
continue
for step in t["steps"]:
if step.get("correctness") != "correct":
positions.append(step["step_index"] / (n - 1))
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)分数分布
分数已经替你算好了,直接读就行,不必重算:
score_df = pd.DataFrame([
{"instance_id": t["instance_id"], "score": t["score"]}
for t in traces if t["score"] is not None
])
print(score_df["score"].describe())按影响排序的失败模式
只看频次会高估轻微错误。乘上严重程度,排出来的才是真正让你付出代价的东西:
WEIGHT = {"minor": 1, "major": 5, "critical": 10}
modes = (
error_df.assign(w=error_df["severity"].map(WEIGHT))
.groupby(["category", "subtype"])
.agg(count=("w", "size"), avg_weight=("w", "mean"))
)
modes["impact"] = modes["count"] * modes["avg_weight"]
print("By frequency:\n", modes.sort_values("count", ascending=False).head(10).to_string())
print("\nBy impact:\n", modes.sort_values("impact", ascending=False).head(10).to_string())研究背景
逐步错误定位和智能体评估领域近来的几条线索是对得上的。
TRAIL(Patronus AI,2025)用一套 20 多种错误类型的分类体系,标注了来自 GAIA 和 SWE-bench Lite 的 148 条智能体 trace,共 841 个错误。值得注意的结果是定位有多难:他们测过的最好的长上下文推理模型,在错误类别加位置上的联合准确率只有 11%。这正是 trajectory_eval 交给人类标注者去做的活儿,也是这些标签值得花钱买的原因。
AgentRewardBench(McGill NLP,2025)盯的则是评判方。它在五个基准上收集了 1,302 条网页智能体轨迹,请专家逐条审查成功与否、副作用和重复行为,再拿十二个 LLM 评判器去对照这些审查打分。没有哪个评判器在所有基准上都领先,而各基准自带的基于规则的评估,低报了智能体实际成功的次数。如果你打算用模型把这套分类体系的一部分自动化,你需要的正是这种形状的检验。
步骤级的正确性和严重程度标签也可以直接喂给过程奖励模型的训练,因为每个标注过的步骤都是一个带有真值质量信号的训练样本。
Anthropic 的 Demystifying evals for AI agents 把同一个论点讲成了可操作的版本:评的是整份记录,而不只是结果;对智能体怎么调用工具、怎么跟用户说话,都用明确的量规去打分。它还提醒不要照着一套规定好的步骤序列去打分,因为智能体总能找到评估设计者没预料到的有效路径。套用这套分类体系时记住这一点:一步之所以是错误,是因为它错了,不是因为它出乎意料。
小结
trajectory_eval 把智能体评估从一次通过/失败的检查变成一项诊断。有了两层分类体系、严重程度权重和连续评分,你能看出哪一步出了错、是哪一类错误、错得多严重,以及错误在各条 trace 上聚集在什么位置。这些步骤级的标签也可以直接拿来当过程奖励模型的训练数据。
先用这里的四个类别起步,把定义放在指南里、标注者读得到的地方,等看清你的智能体实际会产生哪些失败之后,再细化子类型。