Trace A/B 分析:耗时与效果
用途
把两批轨迹的「快慢」从一句感觉,变成可复核的账:每一秒归到五个因素之一,再把耗时差和得分差摆在一起,判断多花的时间是投资还是浪费。
何时使用
- 两个版本 / 两个端点 / 两次参数调整的轨迹批次,要比耗时和效果。
- 已经知道「这批慢」,需要回答慢在 token、速率、步数还是工具。
- 已有报告要复核:数字对不上、口径不清、结论越界。
不适用:只看分数不看轨迹(用评测报告本身);没有轨迹只有日志;单批体检——本技能以差异为核心,单批请把同一批同时作为 A 和 B 跑,此时 token 结构、工具分布、分场景量级仍可读,但五项差异恒为 0,不要据此写「更慢/更快」。
输入:三种来源
| 用户给的 | 用什么 |
|---|---|
本地批次目录;用户直接发的 trace(单条 .jsonl、一个 case 目录、压缩包) |
解开后 --a-dir / --b-dir |
Hubble 执行记录 ID(页面 /eval/task?id=385224,纯数字) |
--a-task 385224[,385575] |
Hubble 计划运行 ID(runId,一个 run 会展开成多条执行记录) |
--a-run 123456 |
两侧可以混用(A 给 ID、B 给本地目录)。ID 类型分不清时:先 hubble task <id> 当执行记录试,
失败再 basic-detail 当基础任务;只有执行记录 / 计划运行才有轨迹,基础任务只是模板。
用户直接发的 trace:压缩包解开即可用(Hubble 导出的 agent_outputs 包本身就是这个结构);
单条 .jsonl 要按 <批次>/<case>/trajectory/x.jsonl 摆好——只有一条时做不了 A/B,
先用 parse_traj.py 下钻,并问用户基线在哪。
目录内任意深度含 **/trajectory/*.jsonl 即可。评分与 durationMs 从 case 目录或其 runs/
同级目录的 report.json / detail_report.json / logs/eval_service/agent/workbuddy_response.json 读取。
结构与事件字段见 references/trace-format.md;从 Hubble 取数后的目录布局见 references/inputs.md。
配对键默认取 trajectory 的父目录名,同一 case 多轮各自成 run,先按 run 平均再进汇总。
缺输入先问,别猜
用户只说「帮我看看这批为什么慢」而没有 ID / 路径时,一次问全三件事:
- 比的是哪两批?(版本 / 端点 / 参数差异,用来给 A、B 命名)
- 两侧的 Hubble 执行记录 ID 或计划运行 ID?没有 ID 的话,trace 在哪(目录 / 压缩包 / 单条文件)?
- 要比耗时、比效果,还是两者都要?(只比耗时可以没有评分)
输入不够先确认,再开跑
run_ab.py 内置体检,任何一项不通过都会停下来;单独跑 preflight.py 也能看同样的结论:
| 体检结果 | 含义 | 话术 |
|---|---|---|
| 没有 trajectory / 没有主轨迹 | 路径给错、压缩包没解开、只给了子 Agent | 「这个目录里没有轨迹,路径是不是不对?」 |
| 两侧共同用例 0 | 批次给错,或命名规则不同 | 「两侧没有同名用例,是批次错了,还是要按某规则配对?」 |
| 一侧全无评分 | 只能出耗时结论 | 「这侧没有评分,08 章会留空,继续吗?」 |
| 缺评分 >10% | 效果结论只覆盖子集 | 「只有 x/y 例有分,效果结论按这个子集说,可以吗?」 |
| 未跑完 >20% | 墙钟被截断 | 「x/y 条没跑完,先剔除(--drop-incomplete)再比?」 |
| 只有单侧 / 单条 trace | 没有基线 | 「基线是哪批?单批只能做体检,不能写更慢/更快」 |
体检退出码:0 可跑;2 阻断(没轨迹、零重叠);3 需确认(缺评分、大量未跑完)。
run_ab.py 遇到 3 会打印问题并停下,确认可接受后加 --yes 继续。
执行
取数依赖 hubble skill:先 python3 ~/.workbuddy/skills/hubble/scripts/hubble.py whoami,
通不过就修鉴权(网关仅内网可达,别循环重试)。fetch_hubble.py 默认读该路径,
可用环境变量 HUBBLE_CLI 覆盖。
一条命令(Hubble ID 直接开跑,推荐)
python scripts/run_ab.py \
--a-task 385224,385574 --b-run 123456 \
--a-label hy4 --b-label hy4p \
--outdir out
--a-task/--b-task 可给多条执行记录(逗号分隔),--a-run/--b-run 给计划运行 ID 时脚本自动
展开成该 run 下终态成功的执行记录。两侧也可换成 --a-dir/--b-dir(本地目录),或混用。
只取数(想先看结构 / 分步调试)
python scripts/fetch_hubble.py --a-task 385224 --b-task 385575 \
--a-label hy4 --b-label hy4p --outdir data
产出:
data/raw/ detail 报告 JSON(评分来源,缓存)
data/attach/<taskId>.zip 报告附件包(轨迹来源,缓存)
data/work/A/<taskId>/agent_outputs/<taskId>/<case>/trajectory/*.jsonl
data/work/B/...
data/sources.json 两侧 root、任务清单、用例数与缺失情况
脚本做四件事:task 查状态 → download --which detail 取评分 → attach-zip 取轨迹附件 →
解包并把 final_score_100 / case_type / durationMs 写成每个 case 的 report.json。
缓存齐全时重跑很快;--refresh 强制重拉。执行记录没到终态会直接报错(不会拿半截数据算)。
本地目录
python scripts/run_ab.py --a-dir data/hy4 --b-dir data/hy4p \
--a-label hy4 --b-label hy4p --outdir out
可选参数
| 参数 | 用途 |
|---|---|
--category-map map.json |
覆盖场景归类;{"case_type": {...}, "rules": [{"pattern": "...", "cat": "..."}]} |
--key-regex '^(.+?)-r\\d+$' / --strip-regex |
用例名带轮次后缀时,抽/剥出配对键 |
--keep-score0 |
保留 0 分 run(默认剔除,0 分多为失败/超时) |
--drop-incomplete |
丢弃最后一个事件不是 assistant message 的轨迹 |
--no-detail |
dataset.json 不落逐步明细,体积更小 |
--yes / --skip-preflight |
体检有问题时继续 / 完全跳过体检 |
--refresh |
远程取数忽略缓存 |
分步执行(需要单步调试时):fetch_hubble.py → preflight.py → build_dataset.py → analyze.py
→ build_report.py,参数各自 --help。
单条轨迹逐请求下钻:
python scripts/parse_traj.py <trajectory.jsonl>
工作流
- 先确认输入来源。 本地目录 / 执行记录 ID / 计划运行 ID 三选一,缺就先问、不齐就先确认(见上文)。远程取数走
fetch_hubble.py,它已把评分注入到 case 目录,不需要另找 report。 - 先看配对是否成立。
build_dataset.py会打印A=x runs / y cases, B=..., common=z。common 明显小于两侧用例数时,先查命名差异(--key-regex),不要急着往下算。 - 跑 pipeline,读控制台摘要。 摘要里
W / steps / out / rate / tool / score与dec五项是后续所有结论的锚点,先自查量级是否合理(速率个位 tok/s 或上万 tok/s 都说明口径错了)。 - 抽查一条轨迹。 用
parse_traj.py打开步数最多的那条,确认Σllm + Σtool + 空转 ≈ span,且orphan非零时已知缺口多大。 - 按固定顺序读结果(见下节),不要跳步。
- 写结论。 报告 01 节是脚本生成的事实句,人工只需补「所以怎样」。每条结论必须带数字,并区分「观测」与「推断」。
- 交付
out/report.html与三个 CSV,并说明口径与局限。
解读顺序
| 步骤 | 看什么 | 判据 |
|---|---|---|
| 1 总差 | all.dec.total、ratio_mean / ratio_med |
均值与中位差距大 = 少数极端用例主导,看 slower_share |
| 2 主因 | dec 五项绝对值最大者 |
volume 主导=写得多;speed 主导=端点慢;step_fixed=轮次多;tool=外部依赖 |
| 3 量 vs 速 | out 倍数 与 rate 倍数 |
量涨得比速快,净差为正;反之更快 |
| 4 token 结构 | reason_share 与 text_out |
增量主要在 reasoning = 思考冗余;正文也涨 = 任务真的更大 |
| 5 步数与工具 | steps、fixed、tt 分工具 |
步数多且每步短 = 交互碎片化;某工具耗时断层 = 机制差异 |
| 6 效果 | score、quadrant 计数 |
更慢且分更高=有效投入;更慢分数没换来=冗余,是优化重点。报告先给四格计数,再看「耗时档位→涨分率」是否单调 |
判断阈值:ratio 在 0.9–1.1 视为持平;单例差异小于同侧多轮 CV(报告 10 节给出中位 CV)不可当结论;分数差小于 2 分视为噪声。
必须检查的坑
- hang 步:无 usage 且耗时 > 300s 的步多为上游超时,脚本已从
llm中扣除并单独计入hang;若hang占比高,先修数据再比。 - reasoning 误记:个别端把整步输出记成 reasoning token。脚本仅在步内有推理正文时才计 reasoning,
reason_field保留原值以便核对。 - 端点不可混比:不同 model 的速率只在本侧内比较;固定开销按端点单独回归,跨端点的
speed效应一律用基线端点速率折算。 - 多轮与噪声:同 case 多轮的墙钟波动可能到 20%+,报告给出 CV;不要用单轮差异下结论。
- 子 Agent:
agent-*.jsonl只汇总量级,不并入主轨迹墙钟,否则重复计时。 - 未评分用例:只进耗时口径,不进效果口径。报告会写明两侧都有评分的用例数,不要用这个子集代表全量耗时。
- token 缺口:
orphan非零说明有 usage 事件没匹配上步,累计 token 会偏低,需在报告中披露。
产物
| 文件 | 内容 |
|---|---|
out/report.html |
单文件报告:核心结论 / 口径 / 整体对比 / 五因素瀑布 / token 结构 / 工具 / 分场景 / 耗时-效果(四格计数 + 分档涨分率) / 逐用例明细 / 数据质量 |
out/dataset.json |
解析后的全量 run(含逐步明细),供二次分析 |
out/stats.json |
全部聚合指标与分解结果,报告的唯一数据源 |
out/case_rows.csv |
逐用例:耗时、倍数、步数、token、速率、五项分解、主因、分数与象限 |
out/category_summary.csv |
分场景汇总 |
out/run_level.csv |
逐 run:用于看同 case 多轮波动 |
报告写法
沿用 references/report-blueprint.md:结论先行、每条带数字、按「总差 → 主因 → 量速 → 结构 → 工具 → 场景 → 效果」推进;观测与推断分开写;局限单列一节。不要用「显著」这类统计词描述未做检验的差异。
资源
scripts/parse_traj.py:轨迹解析(usage 双格式兼容、步分组、工具墙钟、数据缺口标记)。scripts/build_dataset.py:扫描、配对、场景归类、子 Agent 汇总。scripts/analyze.py:端点回归、五项分解、分场景、四象限、CSV。scripts/build_report.py:HTML 渲染(内联 CSS/SVG,无外部依赖)。scripts/run_ab.py:取数 → 体检 → 扫描配对 → 归因 → 报告,全流程串联。scripts/fetch_hubble.py:按执行记录 / 计划运行 ID 从 Hubble 拉 detail 报告与轨迹附件,整理成批次目录。scripts/preflight.py:输入体检(轨迹数、评分覆盖、未跑完、配对重叠),给出待确认问题与退出码。references/trace-format.md:目录结构、事件类型、usage 字段、元数据取值。references/methodology.md:指标定义、分解推导、口径边界、判断阈值。references/report-blueprint.md:报告骨架与写作规范。references/inputs.md:Hubble ID 语义、取数后的目录布局、常见取数故障。