只看总体通过率的风险在于:它把不同任务类型混成了一个数。如果结果文件里简单类型的任务占了大部分,而少数困难类型几乎全错,总通过率仍然可能显得体面,但结论无法支撑“LoHoSearch 在这些任务上可用”。可行的做法是把两份材料对着读:一份是任务类型计数表,说明结果里各类任务各有多少;另一份是失败轨迹的首个失分步骤频次表,说明错误集中在哪个环节。两份表交叉后,再决定结论要写到多大范围。
总通过率无法回答“高分是否由少数任务类型支撑”。建议先用计数表核对任务规模与类型占比,再用失败样本的首个失分步骤频次表定位经常出错的位置,最后把两张表交叉成「类型 × 首个失分步骤」矩阵。行样本量过少的格子标注为样本不足,只作线索;类型字段缺失比例偏高时,所有按类型划分的结论都需要缩小范围。
用脚本把任务类型计数成一张表
计数脚本通常放在结果文件生成之后、撰写评估说明之前执行,输入是逐行的 JSON/JSONL 结果文件,输出是一张三列表格。脚本里的字段名是占位名,需要按实际结果文件替换:下面用 task_type、task_id、tags 表示类型来源,运行前先列一条记录确认字段确实存在。
# count_by_type.py —— 输出任务类型计数表并做总数对账
import json, sys, collections
TYPE_KEYS = ("task_type", "type", "category", "tags")
def pick_type(rec):
for k in TYPE_KEYS:
v = rec.get(k)
if isinstance(v, str) and v.strip():
return v.strip()
if isinstance(v, list) and v:
return str(v[0]) # 多标签只取首个,这个规则要写进表注
tid = str(rec.get("task_id", ""))
prefix = tid.split("-")[0] if "-" in tid else ""
return prefix or "unknown" # 兜底:按 task_id 前缀归类
counts = collections.Counter()
total = valid = 0
seen = set()
with open(sys.argv[1], encoding="utf-8") as f:
for line in f:
line = line.strip()
if not line:
continue
total += 1
try:
rec = json.loads(line)
except json.JSONDecodeError:
continue
valid += 1
seen.add(rec.get("task_id"))
counts[pick_type(rec)] += 1
print("| 任务类型 | 数量 | 占本份结果 |")
print("|---|---|---|")
for t, n in counts.most_common():
print(f"| {t} | {n} | {n / valid:.1%} |")
print(f"\n# 对账: 总行数={total} 可解析={valid} 去重task_id={len(seen)} 计数合计={sum(counts.values())}")
字段缺失在自建评估里很常见,替代归类的优先级建议是:显式类型字段 → tags 首个标签 → task_id 前缀 → 数据集目录名 → unknown。每一步降级都要写进表注,因为“按前缀猜类型”和“读显式字段”得到的结果可信度不同。unknown 行必须保留在表里,不能悄悄并入其他类型;unknown 占比明显偏高时,后面所有按类型的判断都只能当作线索。
对账做两件事:一是计数合计应等于可解析行数,可解析行数应等于总行数减去解析失败行数,不相等通常说明有记录被 pick_type 返回了空值后又被累加到别处;二是去重后的 task_id 数应与总行数接近,去重数明显少于总行数,说明结果文件被追加写过,需要先按 task_id 去重再统计,否则重复样本会把某些类型的占比放大。
统计失败轨迹里反复出现的同一步
失败轨迹的价值在位置而不在数量。做法是只取失败样本,在 trace 里顺序扫描,找到第一个被标记失败、或分数低于约定阈值的步骤,记录该步骤的名称并计频。步骤名通常来自 step_name、stage、name 一类的字段,具体字段需要打开一条失败记录的 trace 确认;如果整份结果没有步骤级标记,就退化到按 error_code、异常类型或错误信息前缀计频,并在表头注明退化口径。
# first_fail_step.py —— 只统计失败样本的首个失分步骤
import json, sys, collections
MIN_N = 5 # 频次下限,按失败样本总量自定
def first_failed_step(rec):
steps = rec.get("trace") or rec.get("steps") or []
for s in steps:
ok = s.get("passed", s.get("ok"))
score = s.get("score")
if ok is False or (isinstance(score, (int, float)) and score < 0.5):
return s.get("step_name") or s.get("stage") or s.get("name") or "unnamed"
return rec.get("error_code") or rec.get("exception") or "unknown"
counter = collections.Counter()
failed = 0
for line in open(sys.argv[1], encoding="utf-8"):
line = line.strip()
if not line:
continue
rec = json.loads(line)
if rec.get("passed") is True or rec.get("status") == "pass":
continue
failed += 1
counter[first_failed_step(rec)] += 1
print(f"# 失败样本数={failed}")
print("| 首个失分步骤 | 频次 | 占失败样本 |")
print("|---|---|---|")
for s, n in counter.most_common():
note = "" if n >= MIN_N else "(样本少,仅作线索)"
print(f"| {s} | {n} | {n / failed:.1%} {note}")
读频次表主要看三种形态。第一种,排名前一两位的步骤占了失败样本的多数,说明是单点缺陷,修掉这一处对结果影响最直接。第二种,同一步骤在不同任务类型里都出现,通常指向公共链路,比如检索、解析、超时控制,这类问题不看类型也值得先查。第三种,长尾里大量步骤频次只有一两次,基本不能单独解释,只能作为进一步抽样复查的入口。
样本量下限需要一个明确约定:建议单条目频次低于 5 时只标注为线索,不写进结论;失败样本总数本身很少时,连排名都不必解读。这些阈值要事先写进脚本,避免先看结果再调阈值。表里的占比一律以失败样本为分母,不要和总体通过率混用。
把类型分布和失败频次交叉
交叉表用于区分两类失败:某一类任务整体表现差,和某一类任务里某一步骤特别差。设计上以任务类型为行,以首个失分步骤为列,通过样本单独记一列 PASS。行的合计表示该类型样本量,单元格表示该类型在该步骤上首次失分的次数。类型字段的归类函数直接复用第一份脚本,保证两张表口径一致。
# cross.py —— 类型 × 首个失分步骤(通过样本记为 PASS)
import json, sys, collections
MIN_ROW = 10 # 行样本量下限,按任务总量自定
rows = collections.defaultdict(collections.Counter)
for line in open(sys.argv[1], encoding="utf-8"):
line = line.strip()
if not line:
continue
rec = json.loads(line)
t = pick_type(rec)
step = "PASS" if rec.get("passed") else first_failed_step(rec)
rows[t][step] += 1
freq = collections.Counter()
for r in rows.values():
freq.update(r)
steps = [s for s, _ in freq.most_common() if s != "PASS"][:8]
print("| 类型 | 样本数 | " + " | ".join(steps) + " |")
for t in sorted(rows):
r = rows[t]
n = sum(r.values())
mark = "" if n >= MIN_ROW else " ※样本不足"
print(f"| {t}{mark} | {n} | " + " | ".join(str(r.get(s, 0)) for s in steps) + " |")
某类型样本量低于下限时,用 ※ 标注并把它排除在横向比较之外,而不是照常参与排序。低于下限的行,单元格里的数字只代表这一批样本的分布,换成另一批任务完全可能变样;如果这类任务本身重要,应当先补充样本再下判断。
以下几种形态需要提高警惕。高位通过率集中在少数几行、且这几行占总样本很大比重,其余行普遍失败,说明总体数字由简单任务贡献的;某一行的失败几乎全部落在同一个步骤,说明该类型存在固定断点,修一处就可能明显改变结果,此时先确认该行样本量是否支撑这种结论;失败均匀散布在所有类型、且列方向集中在同一两个步骤,问题大概率在公共环节而非某个任务类型;unknown 行占比高,交叉表本身的可读性就要打折扣,先补类型字段再谈分布。
把交叉结果整理成复核清单
清单是给第二个复核条目用的,每一项都要能在结果文件里找到对应字段,并且给出明确判定,而不是“看起来还行”。建议把下面四项连同统计口径一起落到记录里,随报告一并留存。
- 任务覆盖是否均衡:看类型计数表的类型数与各类型占比。若头部一两个类型占了大多数任务,而结论描述覆盖了全部任务,应改写为按类型分别陈述;判断依据是计数表,不是主观印象。
- 失败是否集中:看首个失分步骤频次表,若靠前的一两个步骤占失败样本多数,把它记为待查单点;若失败分布在十个以上步骤、且每个频次都很低,记为问题分散,先扩大抽样再定位。
- 样本量是否足够:检查交叉表中带 ※ 的行和频次低于阈值的列。这些位置不进入结论,只作线索;需要时说明“该类型样本不足,暂不下判断”,而不是用现有数字外推。
- 结论是否需要缩小范围:交叉表若显示高通过率集中在少数类型、其他类型失败率偏高,最终表述应限制在这些通过率较高的类型上,并注明其余类型的已知失分步骤,避免用一句总体通过率覆盖全部场景。
复核时建议直接对着脚本输出操作:先核对计数表的合计与总行数是否一致,再确认频次表的分母是否等于失败样本数,最后逐条对照交叉表填清单。任何一项对不上,先修统计口径,再讨论结论;口径没对上的数字,不建议写进对外说明。字段名、阈值和归类规则都属于环境相关配置,换一份结果文件时需要重新确认,不能直接套用上一份的设置。