qinghuan
← 评测、复现与研究实践 / 全部笔记

单元 45 / 评测、复现与研究实践

P0 · 必会主线L3 · 深入

失败分类、轨迹诊断与反事实实验

先给失败分类,再删减轨迹或只替换其中一部分来定位问题,分清一起出现的现象、局部修复效果和因果结论。

本篇目录 · 按需要跳转

这篇重点学什么

本篇重点:逐步删减仍能触发同一失败的输入,固定其他条件后替换一个环节(反事实实验),检查结果是否变化,以及是否还有其他因素同时影响结果。

开始读正文 ↓
学过相近内容?查看哪些可以跳过

两条路线有些基础相同。已经学会的部分可以跳过,另一条路线只用来补你当前需要的内容。

失败定位与归因

共同基础保留完整的输入、执行步骤和结果;同时发生的变化,不一定就是导致失败的原因。

另一条路线补充什么?

沿一次请求的完整记录(trace)和各步骤记录(span)查找失败位置与耗时,再结合指标排查线上问题。

读完这篇,你能做什么

用 Java 删减仍会触发失败的事件集合,并说明“再删任一项就不失败”不等于全局最小;用 Python 展示观测关联与干预效果方向相反的例子。

学习理由、准备要求与知识点

为什么学它

失败过程很长时容易凭印象找原因。比较时不固定环境和检查器,换提示后的偶然成功也可能被误认成修复有效。

先读这些内容

知识点清单

  • 失败分类与证据
  • 最早偏离和传播
  • 确定性回放
  • ddmin一项最小
  • 反事实组件替换
  • 混杂与干预
  • 必要性和充分性
  • 分类一致性与修复回归

运行前先确认Delta Debugging TSE2002、Causality第二版2009;Java17与Python3.14标准库的确定性教学模型,未操作真实Agent环境。

01先弄懂它是什么

分析一次失败,先找出能证明“任务没完成”的观察,再判断问题可能发生在哪一步。可以检查是否理解错输入、计划不合适、工具参数有误、环境执行失败、证据用错、结果没校验,或基础设施出了问题。同一次任务可能同时有几个问题,没必要硬塞进唯一标签。更有用的记录是:怀疑哪个主因、依据是什么、还需要什么实验验证。

最早看见异常的地方,不一定就是真正原因。例如检索没有结果,可能是查询词不对,也可能是索引还没更新,或当前用户本来就没有读取权限。如果后续模型又编造答案,这是另一个可以单独修复的问题。把“哪里先触发了问题”和“后面哪里没有拦住问题”分开,才能提出具体改进。

反事实实验在同样条件下替换一个环节,观察结果是否变化。替换检索为人工确认的正确证据能检验“如果证据可用是否能完成”,却不证明原检索是唯一原因,也不代表系统已经会自主检索。若替换同时增加上下文、预算和隐藏答案,就混入多个变化,结果难以解释。

文中这几个词,先认一认

一项最小
当前失败输入已经缩到这样一个程度:再删掉任何一个保留元素,都不会触发同一种失败。但这不保证它是所有输入中最短的。
混杂
某个因素同时影响选择和结果,使两者看起来有关联,却不能证明改变选择就会造成对应结果。
反事实回放
尽量保持外部条件相同,只改指定的一步,再重新运行并检查结果,帮助判断这一步是否造成差异。

还有陌生词?到术语小抄里看一个具体例子 ↗

02看看它怎么工作

Delta Debugging把冗长失败输入拆成分块,尝试删除部分内容;若失败仍在就保留更小输入,再逐渐提高分辨率。最终通常得到一项最小输入,而不是元素数全局最少的输入。失败判定必须针对同一种症状,不能把原来的业务断言失败缩成一个无关语法错误后就算定位成功。

Java实现只做删除候选的ddmin变体,事件以唯一整数ID表示并保持相对顺序。失败oracle被约定为确定的纯函数,初始输入必须确实失败;空输入如果也失败直接返回空。测试故意构造两条不同失败路径:算法可能保留三项局部集合,即使另一个两项集合也会失败,清楚展示局部最小边界。

Python定义二十个合成任务,难度与外生编号影响是否调用工具及结果。困难任务更常用工具,所以直接比较调用组与未调用组,会观察到调用组成功率更低;但对同一组任务只干预工具开关,平均效果为正。因果效果在这里可计算,因为结构方程由我们明确定义;真实日志没有自动获得这些条件。

先跑通一个最小例子

Python3.14标准库执行experiment.py,用明示结构方程比较观察组差异与配对干预。Java另行验证缩减算法。

experiment.py · python
from statistics import mean
def outcome(difficulty,u,tool):
    if difficulty not in (0,1) or u not in range(10) or tool not in (0,1): raise ValueError("state")
    threshold=(9 if difficulty==0 else 2)+2*tool
    return int(u<threshold)
rows=[]
for difficulty in (0,1):
    for u in range(10):
        tool=int(u<(2 if difficulty==0 else 8))  # 困难任务更常调用,选择并非随机。
        rows.append((difficulty,u,tool,outcome(difficulty,u,tool)))
observed=mean(r[3] for r in rows if r[2])-mean(r[3] for r in rows if not r[2])
# 对同一组外生条件逐一干预工具开关,其他变量不动。
paired=[outcome(d,u,1)-outcome(d,u,0) for d,u,_,_ in rows]
causal=mean(paired)
assert observed<0
assert abs(causal-.15)<1e-12
assert all(delta>=0 for delta in paired)
assert outcome(0,9,0)==0 and outcome(0,9,1)==1
assert len(rows)==20 and len(paired)==20
try: outcome(1,10,0)
except ValueError: pass
else: raise AssertionError("invalid exogenous state")
print(f"observational_difference={observed:.6f} intervention_difference={causal:.6f}")
print("PASS 6 checks; effects hold only in the explicitly defined synthetic causal model")

核对运行结果observational_difference=-.1,intervention_difference=.15,PASS 6 checks;因果效果只在已定义合成模型中成立。

正在加载流程图…

先冻结可复现失败轨迹,再一次只替换一个环节;反事实修复不是自动的因果证明。
查看流程图源码(Mermaid)
flowchart TD
  N0["冻结失败轨迹环境和判定器"]
  N1["标注症状及最早异常证据"]
  N2["缩减输入并复现同一失败"]
  N3["一次替换一个环节作受控比较"]
  N4["用独立任务验证修复和回归"]
  N0 --> N1
  N1 --> N2
  N2 --> N3
  N3 --> N4

按顺序看,每一步做了什么

  1. 冻结失败轨迹环境和判定器
  2. 标注症状及最早异常证据
  3. 缩减输入并复现同一失败
  4. 一次替换一个环节作受控比较
  5. 用独立任务验证修复和回归

03对照实现代码

Delta Debugging的分块缩减机制 (新标签页)↗

Zeller与Hildebrandt,IEEE TSE 2002 · ddmin算法与1-minimal定义

原创ddmin删除模型,参考TSE2002原论文;不把轨迹缩减称为通用因果推断。

核心机制代码 · python
def try_removal(events, start, end, fails):
    candidate = events[:start] + events[end:]  # 删除一块,其他事件次序保持。
    same_failure = fails(tuple(candidate))  # 判定器必须检查原来的失败症状。
    return candidate if same_failure else events  # 只接受仍能复现的缩减。

原论文区分最小性层级并给出通过测试逐步缩减失败输入的算法。本文短代码仅展示一次删除尝试,完整Java调度分块大小与接受逻辑。它是原创的删除候选变体,没有实现论文所有差分定位、子集测试或未决状态处理。

作者维护的Debugging Book章节给出可交互的缩减示例,强调测试oracle在输入缩减中的作用。真实程序中某些候选无法编译或环境无法启动,可能属于UNRESOLVED;本文布尔接口把所有未复现目标症状的候选当作不接受,因此获得的最小性仅相对于这个二值判定协议。

若oracle会随随机种子、时间或外部服务变化,同一候选的判定可能翻转,算法的不变量就不成立。实际回放需固定随机源和状态,或预先定义重复验证与置信规则;不能把一次偶然通过当作证据充分。这里的完整测试使用纯确定谓词,故没有模拟并发或网络不稳定。

因果推断资料用干预区分“观察某个变量为某值”和“主动设置它”。本文合成模型直接给定结构方程,替换工具开关可在相同外生条件下计算差异;这种计算不意味着从任意Agent日志都能识别真实因果关系,未观测混杂和组件依赖仍需独立设计。

代码先调用谁,再调用谁

  1. 确认初始输入复现指定失败
  2. 删除分块构造候选
  3. 用同一oracle决定是否接受
  4. 细化到不能再删单项

04动手写一个例子

保持事件顺序的一项最小失败缩减

输入唯一事件ID序列和确定性失败谓词,删除分块得到仍失败且不能再删除任一单项的序列;不保证全局最小。

输入 1
事件1至6,仅同时出现2和5失败
输出 1
[2,5]
输入 2
同时4,5,6或恰好1,2失败
输出 2
可返回[4,5,6],不是全局最短

算法维护一个始终失败的current序列,先用两个分块尝试删除,成功时缩小序列并稍降粒度;所有删除都无效则提高粒度,直到每块只剩一个事件。只接受失败候选,使任何中间结果都保留目标症状,不需要假设失败谓词单调。

若循环在粒度等于长度时停止,所有单项删除都已测试而不失败,所以返回结果是一项最小;长度一的情况由最开始空输入检查保障。这个证明依赖oracle确定且判定同一症状,不推出全局最小。不同删除顺序可能得到不同局部集合,测试保留非单调反例说明这一点。

ddmin删除变体最坏需要O(N²)次oracle调用;每个候选在本实现复制O(N)项,因此列表处理上界为O(N³)。若单次oracle成本为C,可写O(N³+N²C),额外活跃列表O(N),未包括oracle内部资源。把调用次数直接写成程序总时间会忽略真正的复制与执行代价。

为什么这样写是对的current始终复现目标失败;每次接受候选都严格减少元素且保持原次序。

时间 O(N²)次oracle调用,总时间上界O(N³+N²C)额外空间 O(N)额外活跃空间,不含oracle

最多二次量级删除测试,单次构造候选至多线性;C为一次确定性判定成本。

保存为 Main.java,运行 javac --release 17 -encoding UTF-8 Main.java,再运行 java Main。程序包含固定输入自测,断言失败时会抛出错误。

展开收起代码 · Main.java · 完整可运行程序Java · 48 行

长行可在代码区域内左右滚动,按 Tab 可把焦点移入代码区。

Main.java · 完整可运行程序 · Java
import java.util.*;
public class Main {
    static void near(double expected,double actual) {
        if (!Double.isFinite(actual) || Math.abs(expected-actual)>1e-9*Math.max(1,Math.abs(expected)))
            throw new AssertionError(expected+" != "+actual);
    }
    static void check(boolean ok) { if(!ok) throw new AssertionError(); }
    static void bad(Runnable r) {
        try { r.run(); } catch(IllegalArgumentException e) { return; }
        throw new AssertionError("invalid input accepted");
    }
static List<Integer> minimize(List<Integer> input,java.util.function.Predicate<List<Integer>> fails) {
        if(input==null||fails==null||input.stream().anyMatch(Objects::isNull)||new HashSet<>(input).size()!=input.size()) throw new IllegalArgumentException("events");
        List<Integer> current=List.copyOf(input);
        if(!fails.test(current)) throw new IllegalArgumentException("not failing");
        if(fails.test(List.of())) return List.of(); // 空输入也失败时,已无需保留任何事件。
        int granularity=2;
        while(current.size()>=2) {
            int size=current.size(),chunk=(size+granularity-1)/granularity;boolean reduced=false;
            for(int start=0;start<size;start+=chunk) {
                int end=Math.min(size,start+chunk);List<Integer> candidate=new ArrayList<>(size-(end-start));
                candidate.addAll(current.subList(0,start));candidate.addAll(current.subList(end,size)); // 删除连续分块,同时保持其余事件顺序。
                if(fails.test(List.copyOf(candidate))) {
                    current=List.copyOf(candidate);granularity=Math.max(2,granularity-1);reduced=true;break;
                }
            }
            if(!reduced) {
                if(granularity==current.size()) break; // 单个事件删除均不能保留失败,得到一项最小而非全局最小。
                granularity=Math.min(current.size(),granularity*2);
            }
        }
        return current;
    }
    public static void main(String[] args) {
java.util.function.Predicate<List<Integer>> pair=x->x.contains(2)&&x.contains(5);
        check(minimize(List.of(1,2,3,4,5,6),pair).equals(List.of(2,5)));
        check(minimize(List.of(1,7,3),x->x.contains(7)).equals(List.of(7)));
        check(minimize(List.of(1,2),x->true).isEmpty());
        bad(()->minimize(List.of(1),x->false));
        java.util.function.Predicate<List<Integer>> nonmonotone=x->x.equals(List.of(1,2))||(x.contains(4)&&x.contains(5)&&x.contains(6));
        List<Integer> local=minimize(List.of(1,2,3,4,5,6),nonmonotone);
        check(local.equals(List.of(4,5,6))&&nonmonotone.test(List.of(1,2)));
        bad(()->minimize(List.of(),x->false));
        bad(()->minimize(List.of(1,1),x->true));
        check(minimize(List.of(5,2),pair).equals(List.of(5,2)));
        System.out.println("PASS 8 cases");
    }
}

这些特殊情况也要试一试

  • 初始不失败拒绝
  • 空输入失败直接返回空
  • 非单调oracle只保证一项最小
  • 唯一ID与原顺序保持
进阶选读 · 带状态依赖的轨迹缩减与反事实评估

场景删除早期动作会让后续动作参数失效,不能直接拼接日志重放。

做法用语义操作或依赖图构造合法候选,冻结环境快照;区分无效候选和目标失败,并对组件替换记录可见信息及预算变化。

复杂度任意最小失败子集搜索最坏可能枚举2^N种组合;结构约束可剪枝但无一般多项式最优保证。

好处与代价保留语义依赖提高解释力,却可能让结果更大;最短触发样例也未必是最方便修复的因果解释。

05项目里怎么用

失败分类表需要写清判定标准和例子,并让不同审阅者在一部分样本上独立标注,再检查分歧。若“规划失败”只是其他类别的垃圾桶,就很难据此决定下一步实验。可以先标事实事件,再标原因假设,保留不确定状态,而不是为所有日志编一个确定故事。

组件替换实验应有对照:原系统、只替换目标环节、以及同样增加预算但不替换的版本。若人工提供正确检索结果,报告要说明这是oracle检索上界,不能说已经修复自动检索。多个故障互相遮挡时还需组合干预,单项替换无效并不证明该组件没有问题。

最终修复应转成回归任务,并在独立保留任务中检查是否引入新失败。公开报告可保存脱敏轨迹、判定器版本和关键中间状态;工具响应包含不可信内容时,不把其中指令当作审阅操作。本文只执行合成模型,真实系统的根因仍须现场证据与受控实验支持。

容易踩的坑

  • 看到末端错误就宣布根因
  • 缩成不同症状也算成功定位
  • 随机oracle当确定函数
  • 一项最小被称全局最小
  • 观察相关被写成因果效果

几种做法,怎么选

表格有四列,窄屏可左右滑动查看代价和适用条件。

方案优势代价与限制适用条件
人工轨迹审阅能理解语义与业务主观且成本较高早期分类和复杂失败
自动缩减生成可复现小案例依赖oracle与有效输入确定失败的调试
受控组件替换检验具体改进假设需隔离混杂和替换语义定位瓶颈与设计消融

06检验是否读懂了

先自己想、自己写,再展开答案核对。场景题是假设的练习情境;遇到还没学过的内容,可以做完基础后再回来。

先用自己的话解释

01一项最小等于最短吗?

展开参考答案与推导

不是,它只排除单个元素删除,另一个更短组合仍可能失败。

02调用工具的题更差说明工具有害吗?

展开参考答案与推导

不能直接说明,困难任务可能更常调用,存在选择与混杂。

03人工证据让任务成功证明什么?

展开参考答案与推导

说明在该替换条件下下游可完成,不证明自动检索已修复或原检索是唯一原因。

换个情况,还会处理吗

01回放有时成功有时失败?

展开参考答案与推导

先固定状态和随机源,或定义重复判定协议,再解释缩减结果。

02删除步骤后参数全无效?

展开参考答案与推导

按依赖和语义重建合法候选,区分未决输入与同一业务失败。

03两个组件分别替换都没用?

展开参考答案与推导

检查组合故障及相互遮挡,做预定义组合干预,不能据此认定二者都正常。

写代码试一试

01构造局部最小反例。

输入 / 条件
失败条件为包含456或恰好12
输出 / 预期
可返回456

提示 局部不能删不代表别处无短解

展开参考答案与推导

分别验证456任何单项删除不失败,而12仍失败。

02算干预差值。

输入 / 条件
同一外生条件开关工具
输出 / 预期
平均差.15

提示 保持其他输入不动

展开参考答案与推导

逐任务求Y工具开减Y工具关后平均。

03记录失败分类。

输入 / 条件
超时、参数错、错误答案日志
输出 / 预期
不同事实标签及证据

提示 先事实后假设

展开参考答案与推导

保留原始错误和阶段,允许多标签与未确定原因。

读完之后,独立完成

课后作业:自己做一遍

本篇共 12 道作业,先完成 9 道基础与综合任务,再按前置知识挑战进阶题。其中包含 2 道思维拓展,试着换个条件,看看原来的办法是否还管用。不附参考答案。

这次练什么

用 Java 删减仍会触发失败的事件集合,并说明“再删任一项就不失败”不等于全局最小;用 Python 展示观测关联与干预效果方向相反的例子。

本篇重点加练

删减最小性和因果解释是不同证据,重点研究非单调失败与多因素依赖。

  • ddmin一项最小
  • 混杂与干预
  • 必要性和充分性

重点内容会多练几次。先说清楚怎么做,再试试空数据、出错或条件改变时会发生什么。

先自己做,卡住时记下问题,再回来查。问答题写清“为什么、什么时候适用”;动手题留下输入、输出和运行步骤,方便下次重做。

基础必做

  1. 作业 01原理问答

    为多步骤研究环境定义失败分类与证据字段,区分最早偏离、后续传播和最终症状;设计两名标注人可独立使用的规则。

    提交内容
    分类手册与标注表
    怎样算完成
    • 类别可由证据判定
    • 分歧处理明确

    练到的知识点失败分类与证据 · 最早偏离和传播 · 分类一致性与修复回归

  2. 作业 02代码推演

    给出一个由六个事件组成的失败输入,自定义可执行失败谓词,手工记录一次删减过程并核验最终保留项的最小性声明。

    提交内容
    删减轨迹与验收记录
    怎样算完成
    • 失败谓词保持一致
    • 最小性范围准确

    练到的知识点确定性回放 · ddmin一项最小

  3. 作业 03原理问答

    自行定义一个非单调失败谓词:添加某项可使失败消失,再添加另一项又恢复。说明一项最小结果与全局最短结果的检查要求。

    提交内容
    谓词、真值表与最小性定义
    怎样算完成
    • 所有声明可由穷举核验
    • 不同最小性概念分开

    练到的知识点ddmin一项最小

综合应用

  1. 作业 04编程实践

    实现 CPU ddmin 测试器,对确定性失败函数及偶发失败函数分别运行;记录调用次数、缓存和不稳定结果处理。

    提交内容
    程序与两类实验报告
    怎样算完成
    • 不稳定性未被当确定结论
    • oracle 调用可审计

    练到的知识点确定性回放 · ddmin一项最小

  2. 作业 05排错实验

    构建含检索和决策两个组件的小环境,在同一初始状态进行四种组件替换,观察单独替换与同时替换的差异。

    提交内容
    四条件干预表
    怎样算完成
    • 配对初始条件一致
    • 观察相关与干预结论分开

    练到的知识点反事实组件替换 · 混杂与干预 · 必要性和充分性

  3. 作业 06面试场景

    失败轨迹缺少中间观察,团队将最后报错组件认定为根因。制定补证计划并写出当前证据允许的最强结论。

    提交内容
    证据缺口与诊断计划
    怎样算完成
    • 不完整日志限制明确
    • 替代原因有可检验方案

    练到的知识点最早偏离和传播 · 混杂与干预 · 分类一致性与修复回归

  4. 作业 07编程实践

    对不超过八个元素的失败输入同时运行删减程序与穷举,比较保留集合;分别审计某元素对当前集合是否必要与单独是否足以失败。

    提交内容
    CPU 双方法核对与条件表
    怎样算完成
    • 参考穷举独立
    • 必要充分性带上下文条件

    练到的知识点ddmin一项最小 · 必要性和充分性

  5. 作业 08设计评审

    替换检索组件时同时改变了工具延迟与输入长度。设计能分离这三个因素的干预表,并说明预算只容许少量运行时可支持的结论。

    提交内容
    干预矩阵及证据边界
    怎样算完成
    • 变化因素全部列出
    • 未识别的效应不硬归因

    练到的知识点混杂与干预 · 必要性和充分性

  6. 作业 09排错实验

    删除轨迹某一步后后续参数引用失效,失败现象改变。给删减工具增加“输入仍合法”验收,并比较有无该约束时的最小化结果。

    提交内容
    合法性检查与双结果报告
    怎样算完成
    • 语义有效与仍然报错区分
    • oracle 目标保持一致

    练到的知识点ddmin一项最小 · 混杂与干预

进阶挑战 · 掌握前置后选做

  1. 作业 10设计评审

    从十个自建失败案例中挑选一种修复,冻结分类规则与回归集后评估新旧版本;报告新增失败及未解决子类。

    提交内容
    修复评估与逐例证据
    怎样算完成
    • 回归集不只含已知错误
    • 必要性充分性用语准确

    练到的知识点失败分类与证据 · 反事实组件替换 · 必要性和充分性 · 分类一致性与修复回归

思维拓展

先写出你的猜想,再用例子或实验验证。答案可以不同,但要说清依据、适用情况,以及还有什么没弄懂。

  1. 作业 11思维拓展 · 反例构造

    构造两套二元机制,使日志中的组件异常与任务失败频率相同,但人工干预该异常后结果不同;分析仅凭观测能否判断该组件的必要性或充分性。

    提交内容
    二元机制真值表与干预分析
    怎样算完成
    • 观测等价有完整分布依据
    • 干预差异经独立计算核验

    练到的知识点混杂与干预 · 必要性和充分性

  2. 作业 12思维拓展 · 跨域迁移

    把失败输入删减迁移到需求或配置最小化:删去某项后系统更简单,但是否仍保持原问题?设计“最小且保持语义”的验收规则。

    提交内容
    迁移实例与语义契约
    怎样算完成
    • 目标行为可检验
    • 简短不自动等于有效解释

    练到的知识点ddmin一项最小 · 必要性和充分性

完成后,再检验一次

给每题标记“独立完成 / 查阅后完成 / 尚未完成”。查过资料或没做出的题,记下卡在哪里。隔一天换一组输入再试,能解释结果和出错原因,再把这个知识点标为掌握。

继续阅读

先看能帮你完成当前练习的部分。例子跑通后,再读更深入的解释。

打开资料库,找下一份学习材料 ↗