把 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:agent(src/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 的通过率 vsaml-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 的真实来源:
- 文件头注释明确:任务是「Expert-authored from FATF/BSA AML typologies + known LLM-agent failure modes」——专家手写自 AML 类型学 + 已知 agent 失败模式,不是从生产 transcript 挖的(因为 AML Copilot 是确定性规则引擎,还没有真 LLM transcript)。
EvalCategory类型枚举了 11 类:aml-detect/aml-restraint/aml-typology/aml-compliance/format/honesty/robustness/planning/safety/injection/reasoning——故意瞄准模型最易失败处(过度标记、缺数据下编造、破坏输出格式、服从注入指令、越权自动执行)。- 每个 task 带
prompt/reference/codeCheck(如has(/structur|smurf/i)是廉价启发式首过);注释说明真正的 outcome 评分 + partial credit 由 LLM-judge 做。 - 文件头的「CLOSED LOOP」注释正是今天的伏笔:跑完真模型后,把观测到的真失败补成新 task,并手标 ≥50 条到
agent-evals/labels.json校准 judge(Cohen's κ)。这解释了为什么「κ 仍待跑(需 ≥50 手标)」。
src/agent/eval/cohensKappa.ts(已建并测)—— κ 关联的真实实现:
cohensKappa(a, b)(第 22 行):两个等长评分数组的 κ 点估计,算po(观测一致)、pe(随机期望一致)、kappa = (po−pe)/(1−pe)。处理了退化边界(双方都只用一个相同类别 → pe=1 → 完美一致返 1 否则 0)。cohensKappaWithCI(a, b, opts)(第 53 行):percentile bootstrap 95% CI(默认 2000 次重采样,rng 可注入)。注释点破要害:小 N → CI 很宽 → 这就是 P1 要 N≥50 的原因。- 纯/确定性、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 并导出:
- 跑
pnpm eval:agent(src/agent/eval/tasks.ts全 29 task,key 已配置)。 - 在 runner 外层包 OTel tracer,每个 task 导出一条 trace 到本地 Langfuse 实例。
- 每条 trace 挂
gen_ai.*(Day 82 的属性)+ 该任务 judge pass/fail + partial-credit。 - κ 关联用
src/agent/eval/cohensKappa.ts——一旦有 ≥50 条手标,把 judge 评分与人标喂进cohensKappaWithCI出 κ + CI。 - 截图: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 / 需手标」,不写成已完成。
「待跑」的三个具体卡点,逐一点名(避免含糊):
- OTel 导出层:需要起一个本地 Langfuse 实例 + 把 tracer 接到 runner 外层——代码未写,是 wiring 工作。
- 真 trace 计数:需要 model key 真跑 29 task 才能产生 ≥29 条 trace——key 已配置,但导出层没接所以还没产出截图。
- 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)。