返回 AICAP-180
B9 · Day 85部署 + 韧性 + 可观测 + 独立红队

把 eval harness 接入 OTel

Day 82 给单次模型调用打了一条 GenAI span;Day 83/84 补了韧性。今天把可观测性从「单条调用」拉升到「整套评测流水线」:把 29-task suite 批量埋点——每个 task 一条 span,span 内挂 gen_ai. 属性 + 该任务的评分(judge pass/fail + partial-credit)。

阶段: B9 · 部署 + 韧性 + 可观测 + 独立红队(Day 81-90) 标签: #eval-as-trace #observability #cohens-kappa #judge-calibration

今日导引(由浅入深)

Day 82 给单次模型调用打了一条 GenAI span;Day 83/84 补了韧性。今天把可观测性从「单条调用」拉升到「整套评测流水线」:把 29-task suite 批量埋点——每个 task 一条 span,span 内挂 gen_ai.* 属性 + 该任务的评分(judge pass/fail + partial-credit)。

这一步的意义是把「离线 eval 表」升级成「可观测的评测流水线」(业内叫 eval-as-trace)。一旦评分挂进 trace,你就能把评测口径——甚至 judge 与人工标注的 Cohen's κ——关联到每一条 trace 上去切片分析。这是 B1 评测方法论与 B9 可观测主线的合流点,也是为 Day 87「量产 trace → p50/p95 仪表盘」铺的最后一块底。

整条能力曲线在这里第一次「闭环」:B1 教会我们「怎么测」(tokenizer/judge/partial-credit/κ),B9 前几天教会我们「怎么把调用观测起来」(OTel span)。今天把两者焊在一起——评测不再是跑完导出一张静态表,而是边跑边产出带评分的 trace。这一焊接之后,B9 剩下的(Day 86 上线、Day 87 仪表盘)才有数据可聚合,B10 的 p95/cost 测量才有 trace 可切。

最小可判定产出:跑 pnpm eval:agentsrc/agent/eval/tasks.ts 全套),runner 外层包 tracer,29 task → ≥29 条 trace 入库计数截图。eval runner 本身已建并可跑(✅,key 已配置),但 OTel 导出层 = 待跑(需 key + 本地 Langfuse);judge-human κ 仍待跑(需 ≥50 手标)

1. 机理精读

eval-as-trace = 把每个 eval task 当成一条带评分的 trace。 传统离线 eval 产出一张表(task id × pass/fail × partial-credit × cost)。问题是这张表是「死的」——你看不到每个 task 内部的调用链,也无法按延迟/成本维度对评测切片。把它接进 OTel 后,每个 task 变成一条 trace:

  • span 挂 gen_ai.* 属性(model / input_tokens / output_tokens / cost,承接 Day 82)。
  • span 额外挂评测口径:judge 的 pass/fail、partial-credit 分数。

于是「这个 task 失败了吗」「它花了多少 token / 多少钱 / 多久」「judge 怎么判的」全在一条 trace 上对齐。资源:OpenTelemetry GenAI conventions (2026-01) + Anthropic — Demystifying evals (2026-01)。

为什么要把 κ 关联进来。 judge(LLM 评委)给出的 pass/fail 是自动的,但它可信吗?这要靠 Cohen's κ——judge 与人工金标的「超越随机的一致度」——来标定。把 κ 关联到 trace,意味着你不只看「judge 说 79.3% 通过」,还能看「judge 本身和人有多一致」。这正是诚信底线:judge 自动评分必须被人工标注校准过,否则就是循环自评。

「离线表 → 可观测流水线」的升级到底升了什么。 升的是可聚合性与可追溯性

  • 可聚合:29 条 trace 能按 model / category / 延迟分位聚合(Day 87 的仪表盘前提)。
  • 可追溯:某个 task 评分异常时,能下钻到那条 trace 看具体调用细节,而不是只看一个汇总数字。

与「judge 评分本身」的边界。 今天不改 judge 怎么评(那是 B1 的事),只把已有的评分结果埋进 trace。评分逻辑(judge pass/fail + partial-credit)是输入,trace 是承载它的可观测载体。

eval-as-trace 解锁的四类切片(量产后才显威力)。 一旦 29 条 trace 各带 model/token/cost/judge 评分,就能交叉切:

  • category 切:injection 类 task 的通过率 vs aml-detect 类——定位模型在哪类任务上弱。
  • cost 切:哪些 task 最烧 token,是否值得;为 Day 87 的「cost/run」维度铺底。
  • 延迟 切:哪些 task 最慢(Day 87 的 p95 尾延迟)。
  • judge 一致性 切:哪些 task 的 judge 判定与人标分歧大(κ 低),是 judge 校准的重点。

离线一张表只能看「总通过率 79.3%」这一个数;接进 trace 后这个数能沿四个维度下钻——这就是「可观测流水线」相对「死表」的全部价值。

κ 的解读尺度(Landis-Koch 惯例)。 算出 κ 后怎么读:<0 比随机还差;0–0.20 极弱;0.21–0.40 弱;0.41–0.60 中等;0.61–0.80 强;0.81–1.0 几乎完美。一个能信的 LLM-judge 至少要落到 0.6 以上,否则它的 pass/fail 不比掷硬币可靠多少。但没有 ≥50 条手标,连点估计都不可信(CI 太宽)——这正是 κ「待跑」的卡点。

2. 代码走读

今天 eval runner 已可跑,但 OTel 导出层未建。走读相关真实文件:

src/agent/eval/tasks.ts(已建,18KB)—— 29-task suite 的真实来源:

  1. 文件头注释明确:任务是「Expert-authored from FATF/BSA AML typologies + known LLM-agent failure modes」——专家手写自 AML 类型学 + 已知 agent 失败模式,不是从生产 transcript 挖的(因为 AML Copilot 是确定性规则引擎,还没有真 LLM transcript)。
  2. EvalCategory 类型枚举了 11 类:aml-detect / aml-restraint / aml-typology / aml-compliance / format / honesty / robustness / planning / safety / injection / reasoning——故意瞄准模型最易失败处(过度标记、缺数据下编造、破坏输出格式、服从注入指令、越权自动执行)。
  3. 每个 task 带 prompt / reference / codeCheck(如 has(/structur|smurf/i) 是廉价启发式首过);注释说明真正的 outcome 评分 + partial credit 由 LLM-judge 做。
  4. 文件头的「CLOSED LOOP」注释正是今天的伏笔:跑完真模型后,把观测到的真失败补成新 task,并手标 ≥50 条agent-evals/labels.json 校准 judge(Cohen's κ)。这解释了为什么「κ 仍待跑(需 ≥50 手标)」。

src/agent/eval/cohensKappa.ts(已建并测)—— κ 关联的真实实现:

  1. cohensKappa(a, b)(第 22 行):两个等长评分数组的 κ 点估计,算 po(观测一致)、pe(随机期望一致)、kappa = (po−pe)/(1−pe)。处理了退化边界(双方都只用一个相同类别 → pe=1 → 完美一致返 1 否则 0)。
  2. cohensKappaWithCI(a, b, opts)(第 53 行):percentile bootstrap 95% CI(默认 2000 次重采样,rng 可注入)。注释点破要害:小 N → CI 很宽 → 这就是 P1 要 N≥50 的原因
  3. 纯/确定性、RNG 可注入、无需模型/网络——所以 cohensKappa 本身已测绿,缺的只是 ≥50 条真人工标注这个输入数据,不是代码。

手算一个 κ(看清「超越随机」的含义)。 设 judge 与人对 10 个 task 标 pass/fail,8 个一致:观测一致 po = 8/10 = 0.8。若两者都把 7 个标 pass、3 个标 fail,则随机一致 pe = 0.7·0.7 + 0.3·0.3 = 0.58。于是 κ = (0.8 − 0.58)/(1 − 0.58) = 0.22/0.42 ≈ 0.52——中等一致。

关键洞察:po=0.8 看着很高,但扣掉「光靠瞎猜也能蒙对 0.58」后,真实信号只剩 κ≈0.52。这就是为什么不能用裸 accuracy 评 judge——类别不均衡时 accuracy 会被「多数类」灌水,κ 才剥离了随机基线。cohensKappa.ts 第 42-48 行算的正是这个 pe(po−pe)/(1−pe)

3. 今日实战

按 seed,今天的实战是给 runner 包 tracer 并导出:

  1. pnpm eval:agentsrc/agent/eval/tasks.ts 全 29 task,key 已配置)。
  2. 在 runner 外层包 OTel tracer,每个 task 导出一条 trace 到本地 Langfuse 实例。
  3. 每条 trace 挂 gen_ai.*(Day 82 的属性)+ 该任务 judge pass/fail + partial-credit。
  4. κ 关联用 src/agent/eval/cohensKappa.ts——一旦有 ≥50 条手标,把 judge 评分与人标喂进 cohensKappaWithCI 出 κ + CI。
  5. 截图:29 task → ≥29 条 trace 入库计数。

为什么是「≥29」而非「=29」:一个 task 内若发生重试(Day 83 的 retry)或 fallback(Day 84),会产生多条子 span/子调用,trace 数可能多于 task 数。计数的下界是「每个 task 至少 1 条」,所以断言写成 ≥29——这也顺带验证了 runner 真的把每个 task 都埋上了点,没有漏埋。

4. 今日实测 / 产出

  • src/agent/eval/tasks.ts + eval runner:已建并可跑(✅,key 已配置)
  • OTel 导出层:待跑(需 key + 本地 Langfuse)
  • 目标产出 = 29 task → ≥29 条 trace 入库计数截图(待出)。
  • judge-human κ:仍待跑(需 ≥50 手标)

诚实状态不升级:runner 是真能跑的,但「trace 导出」和「κ 标定」都还卡在「需 Langfuse / 需手标」,不写成已完成。

「待跑」的三个具体卡点,逐一点名(避免含糊):

  1. OTel 导出层:需要起一个本地 Langfuse 实例 + 把 tracer 接到 runner 外层——代码未写,是 wiring 工作。
  2. 真 trace 计数:需要 model key 真跑 29 task 才能产生 ≥29 条 trace——key 已配置,但导出层没接所以还没产出截图。
  3. judge-human κ:需要 ≥50 条人工标注喂进 cohensKappaWithCI——cohensKappa.ts 代码已测绿,缺的纯粹是标注数据这个输入。

三者都是「数据/接线缺口」而非「能力缺口」,但在它们补齐前,今天的产出诚实地停在「runner 可跑」这一档。

5. 常见误区 / 陷阱

  • evalBaseline.ts 的循环自评当独立信号:judge 评 judge 自己跑的结果,是自证循环。κ 与独立红队才是可信口径——这是这套笔记的诚信底线。
  • N 太小就信 κ 点估计:小 N → bootstrap CI 很宽,点估计不可信。cohensKappa.ts 的 CI 就是用来暴露这点的,P1 要 N≥50 正因如此。
  • trace 只挂 token/cost、漏挂评分:那样 trace 只能算延迟/成本,无法做 eval-as-trace 的核心——评测口径关联。评分必须一起挂。
  • 把 29 当作足够的统计样本量:29 task 偏小,扩到 ~70 task 才有统计 power(承接 Day 80 SOTA 的待办)。
  • judge 和被测是同一个模型/同一次调用链:若 judge 用的模型与被测模型同源、甚至看得到被测的内部状态,评分会有系统性偏向。judge 应独立、且最终要被人工标注校准(κ)。
  • 手标数据混进 tasks.ts 训练 judge 又拿同批数据测 κ:标定集与评测集必须分开,否则 κ 虚高。agent-evals/labels.json 的手标专用于校准,不能回流污染评测。

6. 学习资源(每条带 YYYY-MM)

  • OpenTelemetry — GenAI semantic conventions,2026-01。
  • Anthropic — Demystifying evals(评测方法论,seed 引用),2026-01。
  • Langfuse docs — trace + scores / datasets 闭环,执行当周 pin 版本,2026-01。
  • Cohen, J. — A Coefficient of Agreement for Nominal Scales(κ 原始定义),1960,机理常青。
  • Landis & Koch — The Measurement of Observer Agreement for Categorical Data(κ 强弱分档惯例),1977,机理常青。
  • Arize Phoenix / Langfuse — Evaluation traces & online scoring 文档,执行当周 pin 版本,2026-01。

与「独立红队」(B9 后半)的接口。 今天把 judge 评分埋进 trace,是为了让评分可被审计;而 judge 评分本身是否可信,最终要靠两条独立信号——人工标注(κ)和独立红队对抗测试——来背书。eval-as-trace 是把这两条信号「挂得上去」的载体:κ 落点、红队发现的失败 task,都能关联回具体 trace 下钻。所以今天这一步不只是「好看的可观测」,而是 B9 后半「独立红队」可信口径的前置基础设施。

SOTA检查 (2026-06 更新)

  • 当前主流:评测 + 可观测合流(eval-as-trace)是 2026 主线,仍是 SOTA 方向。
  • 诚信口径:避免把 evalBaseline.ts 的循环自评当独立信号——κ 与独立红队才是可信口径。
  • 过时黑名单:循环自评当 baseline;小 N 信点估计不看 CI。
  • 下次复查点:拿到 ≥50 条人工标注后,复查 κ 落点 + CI 宽度;OTel 导出落地当周复查 GenAI spec + Langfuse 版本。

衔接

  • 昨天:Day 84 — 熔断 + fallback 路由实战(韧性原语组合成主→备路由)。
  • 今天:把 29-task suite 批量埋点,每 task 一条 trace + 评分,离线 eval 表升级为可观测评测流水线;导出层与 κ 标定待跑。
  • 明天:Day 86 — 部署上线 + 公网可观测(容器上 Cloud Run、key 走 env、trace 上报托管 Langfuse、公网 URL 接住 B7 OAuth 401/200)。