同一批 LoHoSearch 任务反复失败时,先别急着改提示词。更稳妥的判断顺序是:把失败样本按“任务需要几次检索”分桶,看失败是否集中在多步任务上;再对每个失败步骤回看原始检索结果,确认正确材料到底有没有被召回。如果材料根本没出现在结果里,问题在检索环节;如果材料在结果里但推理没采用,问题在推理环节。这两类失败要用不同方式修,混在一起调往往越调越乱。
判断 LoHoSearch 多步任务失败,建议先用失败样本做两件事:按任务需要几次检索分桶,确认失败是否集中在多步任务;再逐步核对正确材料是否出现在该步检索结果里。材料缺失属于检索类失败,应改查询改写和检索条数并回跑同一批任务;材料在但结论错属于推理类失败,应改步骤约束和上下文组织。样本少时先扩样本再下结论,不要用单条失败反推整体配置。
按任务需要几次检索分桶统计失败
分桶的目的是把“多步任务失败”从“所有任务都在失败”中拆出来。分桶依据可以来自任务标注(如果任务集自带 expected_steps 之类的字段),也可以是人工标注:对每条失败样本标注“完成该任务至少需要几次检索才能拿到必要材料”,按 1 次、2 次、3 次及以上分组。人工标注时只看任务本身需要什么材料,不要参考当时实际执行了几步,否则会把执行偏差带进分桶。
统计脚本只做聚合,不依赖具体 SDK。下面是一个通用骨架,输入是一份失败样本的 JSONL,每行至少包含 task_id、bucket(需要的检索次数)、status;把它放在能读到日志的机器上直接跑即可:
import json
from collections import defaultdict
# 输入:failures.jsonl,每行 {"task_id": "...", "bucket": 2, "status": "fail"}
# bucket 表示该任务“至少需要几次检索”,来自任务标注或人工标注
stats = defaultdict(lambda: {"fail": 0, "total": 0})
with open("failures.jsonl", encoding="utf-8") as f:
for line in f:
line = line.strip()
if not line:
continue
row = json.loads(line)
b = row["bucket"]
stats[b]["total"] += 1
if row["status"] == "fail":
stats[b]["fail"] += 1
print("bucket\ttotal\tfail\tfail_rate")
for b in sorted(stats):
s = stats[b]
rate = s["fail"] / s["total"] if s["total"] else 0.0
print(f"{b}\t{s['total']}\t{s['fail']}\t{rate:.2f}")
输出列只有四列:bucket、total、fail、fail_rate。看的是失败率随桶变化的走向,而不是某个具体数值。如果 1 次检索的任务失败率明显低、2 次及以上明显高,就可以先假设问题出在多步衔接处;如果各桶失败率接近,多步检索不一定是主因,需要回到单步执行日志再查。
样本太少时不要下结论。每个桶只有个位数样本,失败率波动很大,可以先把桶合并成“单步”和“多步”两组,或者补采一批同类任务再统计。合并后在日志里保留原始 bucket 字段,方便后续拆回来。
在失败样本里判断证据有没有出现在检索结果中
分桶只能说明失败和多步相关,不能说明是哪一环。下一步是对失败样本逐步回放:找到出错的那一步,看该步检索返回的原始结果里,是否已经包含能支撑正确判断的材料。判定规则建议只用一个:正确材料是否出现在该步检索结果里,出现在第几条可以一并记下。
为了减少主观判断,判定时只看检索结果原文,不看模型此前的推理过程。材料被改写过、只出现同义表述、或者需要跨两条结果拼接才能得到,都建议算“部分出现”,单独记一类,不要直接归到检索成功。
| 现象 | 归类 | 后续动作方向 |
|---|---|---|
| 该步检索结果里完全没有正确材料 | 检索缺失 | 改查询改写、检索条数,回跑同一批任务 |
| 正确材料在结果第 N 条,但模型选了别的依据 | 推理错误 | 改步骤约束、上下文组织,不改检索配置 |
| 材料只以被改写或碎片形式出现 | 部分出现,暂缓归类 | 先补样本,再决定算检索还是推理 |
判例建议从原始日志里各留一条,直接粘 task_id 和该步查询、结果片段,后续对照用。例如某条多步任务在第二步查询“某组件默认超时配置”,返回结果前三条都是同类组件的其他配置项,没有默认超时值,这条记为检索缺失判例;另一条任务同一步的结果第二条就写明了默认超时值,但模型在后续推理里引用了另一条无关结果,这条记为推理错误判例。两条判例都保留原文,不要只留改写后的归纳。
只换检索侧配置重跑同一批任务
确认存在检索缺失后,不建议同时改推理配置。更清晰的做法是固定推理侧参数和提示词,只替换检索侧变量,回跑同一批任务,看这类失败能不能被拉回来。变量建议一次只换一类,至少分两轮:第一轮只调查询改写方式,第二轮在改写方式固定的前提下调查检索条数。
- 查询改写方式:保留原查询、加同义词扩展、按上一步结果拼接查询,任选两种做对比。
- 检索条数:在现有值基础上上下各调一档,不要一次跳太多。
- 回跑范围:只跑分桶统计里失败集中的那几类任务,同时保留少量单步任务作对照。
前后对比口径要固定:同一个任务集、同一份判定规则、同一批标注人员。对比表只记三项——失败任务数、检索缺失条数、推理错误条数。如果替换检索配置后检索缺失条数下降、推理错误条数基本不变,说明修对了地方;如果两类都变,需要重新做判例抽查,确认不是判定口径漂移。
run_id,config_name,total_tasks,failed_tasks,retrieval_missing,reasoning_error
baseline,default_rewrite_topk5,...
run_a,synonym_rewrite_topk5,...
run_b,synonym_rewrite_topk8,...
脚本只负责把每轮结果汇总成上面这种表,具体检索接口参数按你环境里的字段替换,不要照抄示例里的名字。
把两类失败对应到不同的改动清单
最容易踩的坑是用提示词去补检索的洞:材料没召回,再强的提示词也只能让模型编。两类失败对应两份改动清单,改完分别验证。
检索类失败先动这些项:查询改写是否把关键实体保留完整、检索条数是否够覆盖多步任务所需材料、多步任务中上一步结果是否被正确并入下一步查询。验证方式是重跑同一批失败任务,看检索缺失条数是否下降,并抽查新召回的条目是否确实相关。
推理类失败动这些项:在步骤说明里明确“只能依据本次检索结果作答”、要求模型在给出结论前指出所依据的结果条目、上下文过长时按步骤裁剪而不是整体堆叠。验证方式是同一批任务、检索配置不变,看推理错误条数是否下降,并回看判例里模型引用的依据条目是否改对了。
两类改动不要放在同一轮。先修检索、回跑、确认缺失条数变化,再修推理;如果两轮都改完仍有过不去的任务,把它们单独列出来,看看是否属于“部分出现”那类,需要补充样本或调整任务标注,而不是继续加大提示词。