逐步错误定位:用轨迹评估找出智能体究竟在哪一步出错
用 Potato 的 trajectory_eval 方案做逐步错误定位:分层错误分类体系、严重程度评分,以及贯穿整条智能体 trace 的连续评分。
问题所在:只知道智能体失败了,远远不够
A trajectory error taxonomy
你在某个基准上跑了自己的智能体,任务完成率 63%。然后呢?
一个通过/失败的数字只告诉你有 37% 的任务失败了,别的什么都说明不了。它不会告诉你 trace 中哪个位置出了问题、智能体犯的是哪一类错误、错得有多严重。是第 2 步上一个灾难性的失误,还是十五个小推理错误一路累积?智能体是用错了工具,还是从一个错误前提出发推理?
没有逐步的错误定位,你既没法诊断失败模式,也没法判断该先修哪里,更没法为过程奖励模型攒训练数据。调超参数基本靠猜。
Potato 的 trajectory_eval 标注方案解决的正是这件事。标注者逐步走完一条 trace,并记录:
- 正确性:这一步是对是错?
- 错误类型:从你定义的分层分类体系中选取
- 严重程度:轻微、严重或致命,分数权重可配置
- 理由:对错误的自由文本说明(可选)
- 连续评分:一个累计分数,按严重程度递减,给出整条 trace 的质量曲线
本文覆盖完整的搭建流程:定义错误分类体系、跑标注、分析收集到的数据。该方案的配置参考见源文档。
trajectory_eval 方案概览
trajectory_eval 方案是为按顺序评估多步骤智能体 trace 而设计的。它不给一个总体质量评分,而是为每一步生成一条结构化的错误标注,最终得到一张详细的地图,标出智能体在哪里、因为什么失败。
标注界面在每一步的流程是这样的:
- 标注者看到当前步骤的内容(thought、action、observation、代码等)
- 把这一步标记为正确或不正确
- 如果不正确,从分层分类体系中选出错误类型
- 指定严重程度(轻微、严重或致命)
- 可选地写一段理由说明这个错误
- 界面顶部的连续评分自动更新
标注者一步一步走完整条 trace,逐渐构建出完整的错误画像。
轨迹评估界面会在每一步旁边显示对应的分数:
Each 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 的则出过好几次严重失败。
这些权重可以在配置中修改:
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,包含完整的分类体系:
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、一段任务描述和一个步骤列表:
{
"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 转换器:
python -m potato.trace_converter \
--input raw_traces/ \
--output data/traces.jsonl \
--input-format react2. 配置分类体系
先用上面这套完整体系,再按自己的智能体裁剪或扩充。比如针对编码智能体,可以加一个 code_quality 类别:
- 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 和终端输出:
CodingTraceDisplay renders diffs, terminal blocks, and file reads alongside trajectory evaluation controls
3. 启动标注服务器
potato start config.yaml -p 8000在浏览器中打开 http://localhost:8000,你会看到第一条 trace 以分步形式展示。
4. 写标注指南
要给标注者清楚的说明。至少要写明:
- 什么情况算不正确,什么情况算正确但不是最优
- 多个错误类别同时适用时怎么选(取最具体的那个)
- 各个严重程度分别在什么情况下使用,并给出具体例子
- 评估某一步时,是只依据当时可得的信息,还是可以事后回看
标注流程
标注者打开一条 trace 时,任务描述在最上方,第一步紧随其后。右上角的连续评分显示 100。
对于每一步,标注者要:
- 结合前面的步骤读懂当前步骤的内容
- 判定正确性,点击“Correct”或“Incorrect”
- 如果不正确,先选错误类别(例如“Reasoning Error”),再选具体类型(例如“Incorrect Assumption”)
- 指定严重程度:轻微、严重或致命
- 写理由(如果启用):“智能体没做检查就假定文件在当前目录下,但搜索结果显示它在 src/utils/ 里”
- 进入下一步,点击“Next Step”或按右方向键
每出现一个错误,连续评分就更新一次。把第 3 步标为严重错误(-5),分数从 100 降到 95;把第 7 步标为致命错误(-10),再降到 85。
走完整条 trace 后,标注者给出总体的成功/部分成功/失败评价并提交。
分析结果
加载标注数据
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")错误分布分析
# 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 中落在什么位置,常常能暴露出系统性的规律:
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")连续评分的分布
# 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)}")最常见的失败模式
# 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 中倾向于聚集在哪些位置。这些步骤级标签也可以直接用作过程奖励模型的训练数据。
先用本文给出的完整分类体系起步,再根据你的智能体和实际观察到的错误模式去打磨。最好的分类体系,是那个能指向你真正改得动的修复方向的体系。