runTaskEval 打通
B3 这一批的主题是「把 agent loop 跑起来 + 把它量出来」。
阶段: B3 · agent loop + 评测统计(Day 21-30) 标签: #agent-eval #llm-judge #completion-rate #tool-calls
今日导引(由浅入深)
B3 这一批的主题是「把 agent loop 跑起来 + 把它量出来」。
- Day 21-22 厘清了 workflow vs agent 的边界,定下 tool 契约;
- Day 23 用 Wald CI 提前感知「29 任务太少」;
- Day 24 写了 framework-free 的
loop.ts; - Day 25 把 loop 接到 AML 的 typology→SAR 三步任务上。
今天 Day 26 是这条线的「打通日」:把那个 framework-free 的 agent loop 挂进评测 harness runTaskEval,让每个任务同时被两把尺子量——completion(任务做对没有,code grader + LLM-judge 判)和 tool-call 数(效率/啰嗦度)。
在 B1→B18 的能力曲线上,这是从「能跑」走向「能量化地跑」的关键节点:没有这一步,后面 D27 的横向对比、D28 的 CI、D30 的选型结论都无从谈起。今天的最小可判定产出是:pnpm eval:agent 能在离线 fixture 下跑通、吐出报告骨架并 commit;真实 completion% 与 avg tool-calls 要等 key(待跑)。
1. 机理精读
为什么要双指标,而不是只看一个「分」。
单一 completion 率会把两种截然不同的失败混在一起:
- 「做错了」——结论错、格式坏、被 prompt 注入劫持、幻觉了一个数字;
- 「做对了但绕远路」——多调了 5 次工具、来回试探才收敛、触发 step cap。
第一类是质量问题,第二类是成本/效率问题,二者处理手段完全不同(前者改提示/约束,后者改工具设计/早停)。把 completion 与 tool-call 数 分离,就能在同一张报告里区分「对不对」和「贵不贵」——这正是 Anthropic《Demystifying evals》(2026-01) 强调的判分口径:grade the OUTCOME(判结果不判过程),但要单独度量过程成本。
completion 怎么判:code grader + LLM-judge 双层。
runTaskEval 对每个任务先跑一个确定性的 codeCheck(廉价、启发式,如正则 has(/structur|smurf/i)),再跑一次 LLM-judge:
- code grader 是第一道便宜的粗筛,确定性、可复现、零成本;
- LLM-judge 才是真正的 outcome 判分,遵循三条纪律:
- (1) 判 OUTCOME 不判步骤;
- (2) 给 partial credit(score∈[0,1],pass≈1 / fail≈0 / 中间为部分分);
- (3) 信息不足时明确返回 unknown 而非硬猜。
unknownRate 被单列成一个指标,因为「裁判不敢判」本身是信号——往往意味着任务描述不清或证据缺失,而不是模型对错。
为什么 code grader 与 LLM-judge 不能互相取代。
code grader 用正则,只能抓「关键词在不在」,抓不住语义对错(比如模型说了 "structuring" 但结论是「不可疑」,正则会误判 pass)。LLM-judge 懂语义但有主观偏差且要花钱。两者并存:code grader 当廉价烟雾测试和交叉验证,judge 当主判。报告里 codePassRate 与 completionRate 若大幅背离,本身就是一个该深查的信号。
为什么 LLM-judge 必须配人标 κ 校准(但今天还没有)。
judge 是模型,模型会系统性偏差(偏松/偏严/对自己输出风格宽容)。所以 runTaskEval 在传入 humanLabels 且配对数 ≥2 时,会顺手算 judge↔human 的 Cohen's κ + bootstrap CI(复用 B2 已建的 cohensKappa.ts),把「裁判本身可不可信」也量出来。今天先把 harness 接通,κ 校准(≥50 条人标、目标 κ≥0.6)是后续的事——在拿到 κ 之前,不能单凭 judge 数字下选型结论,这是 SOTA 段的硬约束。
与相邻概念的边界。
- 今天打通的是「评测 harness ←→ agent loop」这段管道,不是评测方法学本身(那是 Day 23 的 Wald CI、Day 28 的 paired bootstrap)。
- 也不是真实多步轨迹的产生——
tasks.ts是单轮 prompt-in/output-out 的任务集,runAgentLoop的多步闭环要在带工具的真实运行里才触发。 - Day 26 的角色就是把两者接上线、让
pnpm eval:agent端到端跑起来。
2. 推导 / 手算 / 代码走读
runTaskEval 在 src/agent/eval/agentEval.ts,关键走读:
- 核心签名:
runTaskEval(tasks: EvalTask[], opts: RunTaskEvalOpts): Promise<TaskEvalReport>。opts里generate: GenerateFn与judge: JudgeFn都是注入的——这就是「逻辑用 fake 单测、真实跑由scripts/run-agent-eval.ts注入 DeepSeek/OpenRouter 模型」的解耦点。 - 逐任务三步(第 78–92 行):对每个 task,
- 先
g = await opts.generate(task)拿模型输出; - 再
task.codeCheck ? task.codeCheck(g.output) : undefined跑确定性码检; - 最后
judge = await opts.judge({ task, output: g.output })拿 judge 裁决; - 三者打包成
TaskRunResult(含costUsd、latencyMs)。
- 先
- 聚合(第 94–100 行):
completionRate = passes/n(judge.label==='pass' 的占比);partialCreditMean(judge.score 的均值,先逐个clamp01);unknownRate(judge.label==='unknown' 占比);codePassRate(只在声明了 codeCheck 的任务上求均值,否则null而非 0)。
- κ 校准(第 102–115 行):仅当
opts.humanLabels存在且配对paired.length >= 2时,调cohensKappaWithCI(...)算judgeHumanKappa;否则为null——诚实表示「没人标就没校准」。 - 真实模型接线:
makeModelGenerate(model, { modelName })用generateText跑 prompt,从usage取 in/out token,只有当MODEL_PRICES[modelName]存在时才estimateCost算costUsd,否则留undefined而非静默 0(注释明说 "never a silent 0")。 - 判分提示:
JUDGE_SYSTEM把「grade the OUTCOME / give partial credit / unknown rather than guess / return ONLY JSON」写死进系统提示。 - 容错解析:
parseVerdict(text)用text.match(/\{[\s\S]*\}/)抓第一个 JSON 块、clamp01限分、解析失败默认{label:'unknown', score:0}——judge 偶尔不守 JSON 也不会让整轮崩。
最小手算(验聚合直觉):假设跑了 4 个任务,judge 给出 [pass(1.0), pass(0.9), fail(0.1), unknown(0)]:
completionRate = 2/4 = 0.50(两个 pass);partialCreditMean = (1.0+0.9+0.1+0)/4 = 0.50;unknownRate = 1/4 = 0.25。
注意 completionRate 与 partialCreditMean 这里恰好都 0.50,但语义不同——前者是「几个全对」,后者是「平均完成度」。当模型大量「半对」时两者会拉开,这正是 partial credit 存在的意义。
3. 今日实战
- harness 已建(事实):
src/agent/eval/agentEval.ts(含 code grader + LLM-judge,支持 unknown + partial-credit)、src/agent/eval/tasks.ts(任务集)、scripts/run-agent-eval.ts、pnpm eval:agent脚本,OpenRouter/OpenAI/DeepSeek provider 均已接线。 - 把
src/agent/loop.ts的 framework-free 循环作为「带工具的被测体」接进runTaskEval的generate(注入式),跑全 29 任务pnpm eval:agent。 - 输出两个数:completion 率 + 平均 tool-call 数,报告写入
agent-evals/(runner 默认落agent-evals/reports/run-<stamp>.json)。 - 无 key 时:
scripts/run-agent-eval.ts检测到DEEPSEEK_API_KEY/OPENROUTER_API_KEY缺失会打印 dry-run("Would run N tasks…")并 exit 0——可先用离线 fixture 跑通 harness 出报告骨架并 commit。
4. 今日实测 / 产出
- harness 已建:agentEval(含 code grader + LLM-judge:unknown + partial-credit)/ tasks /
scripts/run-agent-eval.ts+pnpm eval:agent已建,OpenRouter provider 已接线。已完成。 agent-evals/新报告:completion% + avg tool-calls —— 待跑(需 OPENROUTER_API_KEY);可先用离线 fixture 跑通pnpm eval:agent出报告骨架 + commit。- 真实 completion 率与平均 tool-call 数依赖真模型运行,未产出真实数字——不臆造。
5. 常见误区 / 陷阱
- 只看 completion 不看 tool-calls:会把「绕远路」的高成本行为当成功,掩盖效率退化。
- 把 codePassRate 当最终分:codeCheck 是廉价启发式正则,只做粗筛;真正的 outcome 判分是 LLM-judge。
- 拿 judge 数字直接下结论:judge 未经 κ 校准前不可单独采信(κ 是后续才有的)。
- 把 dry-run 的「Would run …」误当成真跑出了数字:无 key 时 runner 只打印计划并 exit 0,没有任何 completion% 产生。
- 把 costUsd 的 undefined 当 0:模型未在
MODEL_PRICES里定价时成本是 undefined(未知),不是免费。
6. 学习资源(每条带 YYYY-MM)
- Anthropic, Demystifying evals(2026-01)—— 判分口径:grade the outcome、partial credit、unknown,本日双指标设计主引用。
- 本仓
src/agent/eval/agentEval.ts(runTaskEval / makeLlmJudge / makeModelGenerate / parseVerdict)—— 代码走读底座。 - 本仓
src/agent/eval/cohensKappa.ts(2026,AICAP B2 产出)—— judge↔human κ + bootstrap CI 实现。 - Vercel AI SDK
generateText(ai包,2025-2026 持续维护)—— 真实模型调用与 usage 取数接口。
SOTA检查 (2026-06 更新)
- 当前主流:Anthropic《Demystifying evals》(2026-01) 仍是 current 的 LLM-eval 方法论基线;双指标(outcome 判分 + 过程成本)+ partial credit + unknown 三件套仍是 SOTA。
- 是否仍 SOTA:是。outcome-grading + 独立 grader + 可校准 judge 仍是 2026 评审硬要求。
- 硬约束:LLM-judge 必须配人标 κ 校准(后续落地),目标 κ≥0.6——勿单凭 judge 数字下结论。
- 过时黑名单:不要用「只报单点 completion 无 CI」的旧做法;不要把 judge 当无偏裁判直接采信;不要把未定价模型的成本静默记 0。
- 下次复查点:Day 28(paired bootstrap + CI)、Day 30(接真实报告判 CI 是否含 0);OpenRouter 上 DeepSeek/Qwen3 可用性按当周重验(agent 半衰期 ~6 月)。
衔接
- 昨天:Day 25 — 多步 AML 任务接入(loop 接 typology+sarDraft,跑三步轨迹)。
- 今天:把 agent loop 挂进
runTaskEval,用 completion + tool-call 双指标度量;harness 全接通,真实数字待 key。 - 明天:Day 27 — Qwen3 对比与稳健性(同任务集、同 temperature,换模型横向比 Δ)。