为什么讲这个案例:它是全系列里唯一处理"主观 → 客观"这道坎的一篇。前面 pr03–pr07 让 Agent 跑起来、pr08 把每步落成 traces.csv,但"失败"这件事一直停在人嘴里。本篇给出一套可粘贴进团队 Wiki 的编码本,并用 Kappa 证明"两个人(或人 vs 模型)打出来的标签是否可信"——不达标就说明分类本身有歧义,回去改编码本,而不是硬凑结论。

阅读路径:§1 立"可复核性是底线"的论点,§2 交付本篇核心——F1–F8 编码本表,§3 讲两种独立双标设置(人-人 / 人-LLM 模拟),§4 从一致率推到 Cohen's / Fleiss' Kappa(为什么不能只看原始一致率),§5 给"κ 低→修编码本"的工程循环与分歧分析流程,§6 是可运行的失败热力图代码,§7–§8 是与度量/实证的闭环和效度威胁,§9–§10 是诚实标注与接口。


1. 为什么"主观缺陷"也能变工程数据:可复核性是底线

软件工程的底线不是"我觉得好",而是"换一个人、换一台机器、换一个时间,能得出同一个结论"。缺陷管理之所以可靠,是因为它有可判定的验收标准;而 LLM/Agent 的输出往往"看着不对"却说不出哪儿不对——一旦它只停在主观评语,就无法进任何统计、无法做回归门禁(pr08 §8)、无法当因变量(pr14)。

本篇的原则驱动只有一句:先把"不好"翻译成"命中了哪一类可判定的失败码",再用第二个人的独立标注去检验这个翻译是否稳定。三件套缺一不可——

三件套
分类学 + 独立双标 + Kappa

① 失败分类学(编码本):给一组互斥、可判定、带正反例的失败码 F1–F8(§2),把"不好"离散成"命中哪几码"。② 独立双标:同一批轨迹,两名标注者(或人 vs LLM 模拟标注)互不可见地各自打码(§3),杜绝串供。③ Kappa:用一个扣除"碰巧一致"后的一致性系数,量化这套编码本到底可不可信(§4)——κ 不达标 = 编码本有缺陷,先修本、再谈结论。

反模式(务必点名):"让作者本人标注自己的失败,然后宣称失败率 12%"——这不叫度量,叫自证。没有第二独立标注源、没有一致性检验的失败数字,工程价值≈0,因为它无法证伪。Kappa 的存在,正是为了让"失败率"这三个字配得上被写进报告。


2. 失败分类学 F1–F8(核心交付 · 编码本)

这是本篇真正的交付物:一张可独立使用的编码本。每个失败码给定义 / 判定信号 / 正例 / 反例 / 易混码五要素——正反例是把"主观"钉成"可判定"的关键,易混码是把"边界歧义"提前暴露给 §5 的修本循环。素材取自 pr03 / pr04 / pr06 / pr07 / pr11 的真实失败轨迹。

码名称定义判定信号(可勾选)正例 / 反例易混
F1任务未完成 / 放弃到达步数/时间预算仍未产出可交付结果,或主动中断无 final answer;steps 触顶;轨迹末尾无产物正:多跳检索在预算耗尽前停在中途无结论
反:任务确需登录,Agent 正确报告阻塞(合理终止)
F8
F2幻觉 / 事实错误输出与可核验事实冲突,或凭空捏造证据引用不存在的来源;工具返回为空却"编"出结果继续下游正:pr04 检索返空却给出"看起来对"的数据
反:答案有误但全程附可核验引用
F6
F3工具调用错误选错工具,或参数/类型/格式传给工具该 search 用了 calc;日期参数格式非法;必填参数缺失正:pr03 把 date=2026/13/01 传进日历 API
反:工具自身返 500(属外部故障,非 F3)
F4/F5
F4循环 / 震荡重试反复执行同一失败动作,或在两动作间来回震荡不收敛同 (tool,args) 连续 ≥3 次;动作序列 A→B→A→B正:pr03 Reflexion 每次用同一错参重试同一工具
反:带退避/改参的重试最终成功
F3
F5越权 / 安全违规执行未授权动作:越权读写、被禁命令、绕过护栏触发 pr06 护栏告警;写沙箱外路径;调用白名单外工具正:Agent 试图 rm -rf 或读 ~/.ssh
反:授权范围内的一次写操作
F3
F6格式 / schema 违规输出不合约定结构,导致下游解析失败JSON 不能 parse;缺必填字段;类型/枚举越界正:pr11 结构化输出把数字写成字符串
反:字段顺序不同但合法可解析
F2
F7上下文丢失 / 指代错误遗忘早期约束,或代词/引用指向错误对象违反首轮已声明约束;"它/上面那个"指向错实体正:pr07 长对话后忘了"只用中文"约束
反:因窗口上限被合理截断并已声明
F1
F8部分成功但代价超标结果可用,但成本/步数/时延突破预算阈值tokens 或 latency 超预算;结果正确但烧钱超标正:答案对但耗了 5× token 预算
反:在预算内完成
F1

编码本三条设计约束(决定后面 κ 上限):① 可判定——每码必须配"判定信号"清单,标注者照单勾选而非凭感觉;② 正交优先、允许共存——一条轨迹可命中多码(如 F3 触发 F4),但 F1 与 F8 互斥(要么没产出,要么有产出但贵);③ 正反例覆盖边界——"反例"这一列是专门用来把两位标注者往同一个方向拉的,反例写得不清楚,κ 一定低。

从"主观现象"到"可复核列" 主观现象 "它好像又绕回去了" "这个答案怪怪的" 不可判定 · 不可统计 ↑ 来自 pr03–pr11 轨迹 编码本 F1–F8 F1 F2 F3 … F6 F7 F8 定义 正反例 · 判定信号 · 易混码 可复核列 fail_code = "F3|F4" annotator_a / _b kappa = 0.71 → 并入 traces.csv 读法:编码本是"主观现象"与"工程数据"之间的翻译层;没有它,失败永远停在评语;有了它且经 Kappa 检验,失败才落到可统计的 fail_code 列。 本篇沿用 pr03 schema,仅新增 annotator_a / annotator_b / kappa 三列(§6)。
图 2-1 · 编码本作为"主观 → 客观"的翻译层:现象经 F1–F8 编码,落成可统计的 fail_code 列

3. 独立双标与分歧:人-人 与 人-LLM 模拟两种设置

编码本写得再漂亮,也只是一份"声称可判定"的文档。要证明它真的可判定,唯一办法是让两个彼此不可见的标注源,对同一批轨迹独立打码,再看它们是否落在同一格。分歧(disagreement)不是噪音,是下一版编码本的原料(§5)。两种常用设置:

设置标注源 A / B用途主要风险
人 - 人(金标准)两名工程师,各自独立按编码本打码,禁互相讨论建立可信标签、给出对外可引用的 κ贵、慢、样本小;需先做校准会(calibration)
人 - LLM 模拟人工标注 A;用 pr08/§4 的编码本+few-shot 让模型标注 B可扩展到大样本、检验"编码本能否被机器复现"同源偏差:若标注用模型与被判模型同族,κ 会虚高(§8)

两种设置里,"分歧"都要走同一套处理:先归因是哪一码引起(人-人分歧常集中在 F1↔F8、F3↔F4),再决定是"改本"还是"改人"。校准流程不是让两人商量出一个答案,而是各自打完再对比——先独立后对照,顺序反了就失去度量意义。

为什么"人-LLM 模拟标注"值得单列:它同时是度量手段(看 κ)和被度量对象(能不能用 LLM-as-a-Judge 自动打失败码,对接 pr05 §LLM-judge)。如果人能彼此一致(人-人 κ 达标),但人和 LLM 标不一致,说明编码本里存在"人能意会、指令写不清"的隐性判据——这恰恰暴露了编码本没写全,是修本信号,不是弃用 LLM 的理由。


4. Inter-rater agreement:从一致率到 Cohen's / Fleiss' Kappa

最自然的想法是"看两人打一样的码的比例"——原始一致率 p_o。但它会被"分布偏斜"骗:假设有 90% 的轨迹其实"没失败",两个只会猜"没失败"的人都拿 p_o=0.9+,看着很和谐,实际零信息量。Kappa 的核心贡献是扣掉"光靠碰巧也能达到的机会一致 p_e":

$$\kappa \;=\; \frac{p_o - p_e}{\,1 - p_e\,}$$

其中 p_o 是实际观察一致比例,p_e 是机会一致(假设两人各按自己的边缘分布独立乱猜,恰好撞对的期望概率)。对两名标注者、K 个类,机会一致是各自类别频率乘积之和:

$$p_o=\sum_{k} \frac{n_{kk}}{N}, \qquad p_e=\sum_{k} \hat{p}_{A,k}\,\hat{p}_{B,k}$$

Cohen's Kappa 用于两标注者(人-人、或人-LLM 一对一);当标注者多于两人(多名工程师 + 多个模型标注源同时打码),换成 Fleiss' Kappa,把"两两边缘"推广为"每个样本上各类被选次数占比":

$$p_e=\sum_{k} P_k^{\,2}, \qquad P_k=\frac{1}{nN}\sum_{i=1}^{N} n_{ik}$$

(n 为每个样本的标注者数,n_{ik} 为第 i 条轨迹被判为类 k 的人数。)κ=1 完全一致、κ=0 等同碰巧、κ<0 比瞎猜还差(几乎必是编码本或流程出错)。

κ 区间Landis & Koch 解读工程处置(本篇立场)
< 0.20轻微编码本基本不可用,回炉
0.21 – 0.40一般结构性歧义,需拆码 / 补判据
0.41 – 0.60中等学习阶段可暂用,报告须标注"待 refine"
0.61 – 0.80高度可用于门禁/统计(本篇推荐门槛)
0.81 – 1.00几乎完美可信;但异常高要查是否同源偏差(§8)

阈值是惯例不是硬标准:上表借用经验分档。本篇的处置线取 κ ≥ 0.6——低于它,无论失败率数字多"好看",都先别下结论,回 §5 修编码本。这不是统计学定理,而是一条工程纪律:宁可承认"我现在还不能可信地度量失败",也不要拿一个不可复核的失败率去喂 pr14/pr08。


5. Kappa 的工程用法:低 κ → 歧义在编码本 → 拆码 / 加判据

Kappa 不是"打个分交差",而是一个反馈闭环:κ 偏低说明两位标注者在同一批样本上系统性地看到不同东西——根因几乎总在编码本,而非标注者。修本循环如下(把它当成缺陷调试来跑):

独立双标A / B 各自打码 算 κ + 混淆矩阵看落在哪几格 κ ≥ 0.6 ?达标判定 入库 / 出热力图写 fail_code 列 分歧分析:集中分歧码 → 补正反例 / 拆码 / 改判据如 F1↔F8 多 → 把"有无产物"写成硬判据 达标 ✓ 不达标 ✗
图 5-1 · Kappa 修本闭环:低 κ 触发分歧分析→改编码本→重双标,循环到 κ 稳定达标

对应地,一份可执行的分歧分析流程:

  • 定位分歧热点:从混淆矩阵找出 off-diagonal 最重的两格(如大量 F3 被 B 判成 F4)。
  • 归因三选一:是定义重叠(拆码)、判据缺失(补判定信号)、还是正反例不足(补边界例)。
  • 只改本、不改已标数据:修订后对同一批样本重标,看 κ 是否上升;不要用改后的本去美化旧标签。
  • 记录版本:编码本进 Git,标注 taxonomy_version,κ 与版本绑定,报告里写"κ 在 v3 编码本下达 0.72"。

关键取舍:κ 低时,工程上"最省钱"的动作常是加反例而非加码——多加一条码会让标注者选择空间变大、反而更难一致;而反例是把两人往同一格拉的力。只有当"一类内部确实有两种截然不同的修法"时才拆码。


6. 失败热力图(可运行代码)

思路:读一批已由两位标注者打过码的轨迹(人-人 或 人-LLM),用 scipy.stats.cohens_kappa(旧版无则手算)与手算 Fleiss' Kappa 得到 κ,达标后把共识 fail_code 并入统一的 traces.csv(沿用 pr03 schema,新增 annotator_a / annotator_b / kappa 三列),最后用 matplotlib 出"失败码 × 场景/模型"热力图。依赖:pip install pandas numpy scipy matplotlib。

6.1 标注表 schema 与读入

Python
# file: annotate_schema.py —— 沿用 pr03 traces.csv,新增三列(本篇 pr09)
import pandas as pd

# 原始 pr03 schema(节选):run_id,timestamp,task,model,steps,tokens,latency,sr,...,fail_code
# pr09 新增:annotator_a / annotator_b(各写命中的 F 码,多码用 '|' 分隔,如 "F3|F4")
#           kappa(该批次一致性;样本级可留空,批次级回填)
NEW_COLS = ["annotator_a", "annotator_b", "kappa"]

# 标注输入:一行一条待标轨迹,两名标注者列已填好(独立打完,禁止互看)
ann = pd.read_csv("annotations.csv",
                  dtype={"annotator_a": str, "annotator_b": str}).fillna("")
print(ann[["run_id", "annotator_a", "annotator_b"]].head)

def primary(code):
    """把多码串规约为'主失败码'(用于单标签 Kappa);本文约定:取第一个。"""
    return code.split("|")[0].strip if code.strip else "NONE"

ann["A"] = ann.annotator_a.map(primary)
ann["B"] = ann.annotator_b.map(primary)   # 二值/多类的单标签版,喂给 Cohen

6.2 Cohen's / Fleiss' Kappa(scipy 或手算)

Python
# file: agreement.py —— κ=(p_o-p_e)/(1-p_e);两标注者用 Cohen,多标注者用 Fleiss
import numpy as np, pandas as pd

def cohens_kappa(a, b):
    """a,b: 等长标签序列(两名标注者)。与 §4 公式逐字对应。"""
    cats = sorted(set(a) | set(b))
    idx = {c: i for i, c in enumerate(cats)}
    n = len(a); M = np.zeros((len(cats), len(cats)))
    for x, y in zip(a, b):
        M[idx[x], idx[y]] += 1
    p_o = np.trace(M) / n                      # 观察一致
    row, col = M.sum(1) / n, M.sum(0) / n      # 各自边缘分布
    p_e = float(row @ col)                     # 机会一致 = Σ p_A,k · p_B,k
    return (p_o - p_e) / (1 - p_e), p_o, p_e

def fleiss_kappa(table):
    """table: N×K 计数矩阵,table[i,k]=第 i 条轨迹被判为类 k 的标注者人数。"""
    N, K = table.shape
    n = table.sum(1)[0]                        # 每样本标注者数(须相等)
    P_i = ((table ** 2).sum(1) - n) / (n * (n - 1))   # 每个样本的一致度
    p_k = table.sum(0) / (N * n)               # 各类总体占比 P_k
    P_e = float((p_k ** 2).sum)              # 机会一致 Σ P_k^2
    return (P_i.mean - P_e) / (1 - P_e)

ann = pd.read_csv("annotations.csv").fillna("")
def primary(c): return c.split("|")[0].strip if str(c).strip else "NONE"
a = ann.annotator_a.map(primary); b = ann.annotator_b.map(primary)
k, po, pe = cohens_kappa(a.tolist, b.tolist)
print(f"Cohen's kappa={k:.3f}  (p_o={po:.3f}, p_e={pe:.3f})")
print("达标" if k >= 0.6 else "κ<0.6:先回 §5 修编码本,别急着下结论")

# 多标注者(3 人 + 2 模型)时改走 Fleiss:
# counts = pd.crosstab(ann.run_id, ann.melt(id_vars='run_id').value).values
# print("Fleiss kappa=", fleiss_kappa(counts))

对照 §4 读代码:p_e = row @ col 正是 Σ_k p̂_{A,k}·p̂_{B,k},Fleiss 里 P_e=Σ P_k² 是其多标注者推广。务必打印 p_o 与 p_e 两个中间量——只看 κ 会漏掉"高一致但都是偏斜猜没失败"的假象(p_o=0.93, p_e=0.88 → κ 仅 0.42)。

6.3 共识入库 + 失败 × 场景 热力图

Python
# file: failmap.py —— κ 达标后写共识 fail_code,出"失败码 × 场景"热力图
import pandas as pd, numpy as np
import matplotlib.pyplot as plt
from agreement import cohens_kappa

ann = pd.read_csv("annotations.csv").fillna("")
k, po, pe = cohens_kappa(ann.annotator_a, ann.annotator_b)
assert k >= 0.6, "κ 未达标,先修编码本再入库(§5)"

# 共识:一致者取该码,分歧者留 'DISPUTED'(交仲裁,不硬编)
def consensus(row):
    if row.annotator_a == row.annotator_b:
        return row.annotator_a
    return "DISPUTED"
ann["fail_code"] = ann.apply(consensus, axis=1)
ann["kappa"] = round(k, 3)

# 并入统一 traces.csv(沿用 pr03 schema,追加 annotator_a/b + kappa 列)
tr = pd.read_csv("runs/traces.csv")
tr = tr.drop(columns=[c for c in ["annotator_a","annotator_b","kappa","fail_code"]
                      if c in tr.columns]).merge(
    ann[["run_id","annotator_a","annotator_b","kappa","fail_code"]],
    on="run_id", how="left")
tr.to_csv("runs/traces.csv", index=False)

# 热力图:行=失败码 F1–F8,列=场景(task),值=命中占比
codes = [f"F{i}" for i in range(1, 9)]
mat = (tr.assign(fc=tr.fail_code.str.split("|").str[0])
         .pivot_table(index="task", columns="fc",
                      values="run_id", aggfunc="count")
         .reindex(columns=codes).fillna(0))
pct = mat.div(mat.sum(1).replace(0, np.nan), axis=0) * 100

fig, ax = plt.subplots(figsize=(8, 5))
im = ax.imshow(pct.values, aspect="auto", cmap="magma")
ax.set_xticks(range(len(codes))); ax.set_xticklabels(codes)
ax.set_yticks(range(len(pct))); ax.set_yticklabels(pct.index)
for i in range(pct.shape[0]):
    for j in range(pct.shape[1]):
        v = pct.values[i, j]
        if not np.isnan(v) and v > 0:
            ax.text(j, i, f"{v:.0f}", ha="center", va="center",
                    color="white" if v > pct.nanmax/2 else "black")
ax.set_title(f"失败码 × 场景 热力图(批次 κ={k:.2f})")
fig.colorbar(im, label="该场景内失败占比 %")
fig.tight_layout; fig.savefig("failmap.png", dpi=130)
print(f"已写回 traces.csv 并输出 failmap.png(κ={k:.2f})")

把 κ 焊进数据:热力图标题、traces.csv 的 kappa 列都带上一致性系数——下游 pr08 读到 kappa<0.6 的行应当拒绝把该批失败率纳入门禁。失败率与"这个失败率可不可信"从此同生共死,不再有裸奔的 12%。


7. 与度量 / 实证的闭环

本篇的产物不是终点,而是喂给下游两类分析的原料,形成"失败 → 度量 → 实证"的闭环:

  • 失败率作为指标(→ pr08):fail_code 聚合成失败率,按 §4 的 κ 加权进 pr08 的工作流层看板;κ 不达标的那批不计入门禁,避免"用一个不可复核的数去阻断合入"。
  • 失败分布作为因变量(→ pr14):把"是否命中某码""各码占比"当因变量,把量化档/提示配方/模型版本(pr11/pr12/pr17)当因子,做显著性检验与回归——"F4 循环在 q4 档显著多于 fp16"这类结论,前提是 F4 的编码本身是可信的(本篇保证)。
  • 测试失败入库(→ pr05):pr05 的蜕变测试/属性测试发现的失败,直接复用 F1–F8 编码,与运行期失败同表可比。

闭环一句话:pr03–pr07 造轨迹 → 本篇编码并证明可复核 → pr08 度量 → pr14 推断 → pr17 综合归因。缺了本篇这一环,pr08 的"失败率"和 pr14 的"因变量"都只是把主观评语换个格式,统计再漂亮也救不回不可复核的原始标签。


8. 效度威胁(对接 pr14)

威胁表现缓解
编码主观性同一轨迹不同人不同码,κ 偏低独立双标 + κ≥0.6 门槛;κ 低先修本(§5),不外推结论
样本量不足稀有码(如 F5)观测极少,κ 与占比方差巨大报告每码计数与置信区间;小样本 κ 只作定性参考(对接 pr14)
LLM 标注同源偏差标注模型与被判模型同族,人-LLM κ 虚高标注模型与生产模型异源、异版本;用人-人 κ 兜底校准
编码本漂移中途改本致前后标签不可比标 taxonomy_version,改本后重标旧样本,κ 绑版本报告

最容易被忽略的一条:"高 κ"未必等于"对"。若两位标注者共享同一个错误理解(比如都以为 F2 与 F6 是一回事),他们会高度一致却系统偏码。κ 度量的是可复现性,不是正确性——正确性靠 §2 的正反例设计与 §5 的争议仲裁保证,两者不可互替。


9. 诚实标注

示例
F1–F8 为示例分类学

本篇编码本是示例分类学,鼓励各队按自身任务裁剪——合并少见的码、拆出高频的码、增补领域专码都是正当做法,只要重算 κ 即可。

⚖
阈值非硬标准

κ≥0.6 是本篇的工程纪律而非统计定律;高风险场景(安全 F5)应收紧到 ≥0.8,探索性分析可适度放宽,但须在报告写明取值理由。

名
代码为骨架

scipy 版本/函数名、traces.csv 具体列以你项目为准;cohen / fleiss 若库中缺失则用手算版(§6.2),公式与 §4 一一对应。


10. 接口:本篇在系列里的位置

对端方向交换的东西
pr03 / pr04 / pr06 / pr07 / pr11上游 · 供失败轨迹提供 loop / 工具 / 安全 / 上下文 / 结构化输出的真实失败样本,作 §2 编码本素材与 §6 标注输入
pr05双向 · 测试失败入库 / 复用 F 码蜕变/属性测试发现的失败按 F1–F8 入库;本篇也参考其 LLM-judge 打分法
pr08下游 · 供失败率指标共识 fail_code + kappa 并入统一 traces.csv,供看板热力图与门禁(κ<0.6 拒入)
pr07下游 · 供 F7 判据校准F7 上下文丢失码回流指导记忆/上下文策略迭代
pr14下游 · 供统计推断失败分布作因变量,供 pr14 做显著性检验与因子回归
pr17下游 · 供综合归因综合实践用本篇编码本做端到端失败归因报告

一页纸总结:可复核性是软件工程底线 → 把"不好"变成工程数据需三件套:失败分类学 F1–F8(§2 编码本)+ 独立双标(§3)+ Kappa(§4) → κ=(p_o−p_e)/(1−p_e) 扣掉机会一致,<0.6 先修本再谈结论(§5)→ 可运行代码(§6)算 κ、写共识 fail_code、出失败热力图并入 traces.csv → 失败率喂 pr08、失败分布喂 pr14、综合归因喂 pr17(§7)→ 记住 κ 度量可复现性而非正确性,警惕同源偏差(§8)。