From 81996f8aa1fe22762b5b5c731b4fbf20bbd404d3 Mon Sep 17 00:00:00 2001 From: wangwei Date: Thu, 2 Jul 2026 16:28:13 +0800 Subject: [PATCH] docs: add advisor comparison design spec Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../2026-07-02-advisor-comparison-design.md | 159 ++++++++++++++++++ 1 file changed, 159 insertions(+) create mode 100644 docs/superpowers/specs/2026-07-02-advisor-comparison-design.md diff --git a/docs/superpowers/specs/2026-07-02-advisor-comparison-design.md b/docs/superpowers/specs/2026-07-02-advisor-comparison-design.md new file mode 100644 index 0000000..14919f3 --- /dev/null +++ b/docs/superpowers/specs/2026-07-02-advisor-comparison-design.md @@ -0,0 +1,159 @@ +# 优化建议历史对比设计(Advisor Comparison) + +**日期**: 2026-07-02 +**状态**: 已批准,待实现 +**范围**: 报告详情页「优化建议」区域新增"相比上次运行"的自动对比摘要,落地架构设计 §11"单变量变更、回归复测"方法论。仅涉及 webapp 读取/展示层,不改动 `rag_eval/advisor/` 的任何写入逻辑。 + +--- + +## 1. 背景与目标 + +架构设计 §11 明确优化方法论: + +> 优化由评估结果驱动,遵循**单变量变更、回归复测**,避免一次性引入多项变更导致归因困难……每批经回归复测确认后再推进下一批。 + +当前平台已有两类"跨 run 对比"能力: +- **Dashboard**(`dashboard.js`):多 run 指标均值趋势线图 + 阈值柱状图,用户手动勾选 run 对比 +- **问题历史**(`question_history.py`):同一问题在不同 run 中的分数对比,展示在最低分样本详情里 + +但**顾问诊断层面**没有对比能力:`Diagnosis`(metric/severity/threshold)目前只以自由文本嵌入 `optimization_advice.md`,从未结构化保留,无法程序化判断"上次标记的问题这次是否真的解决了"。 + +**成功标准**: +- 打开报告详情页时,若存在同 `scenario_name` 的上一次运行,优化建议区域自动显示一个精简的"相比上次运行"摘要,只列出有变化或仍有问题的指标 +- 找不到上一次运行(首次评测、或 `/api/score/async`/`session_async` 这类每次 scenario_name 唯一的场景)时,静默不显示,不影响现有优化建议区域 +- 不新增任何持久化文件;对比结果始终基于当前代码里的最新阈值规则重新计算 +- 现有 `rag_eval/advisor/` 三个文件(rules.py / llm_analyzer.py / writer.py)与三条写入入口(runner.py / score_job_manager.py / session_score_manager.py)零改动 + +--- + +## 2. 架构 + +新增的对比逻辑完全是 **webapp 层的只读附加计算**,对齐 `question_history.py` 的既有模式: + +``` +report_builder.build_report(run_dir, metrics) + ├── 现有逻辑:读取 scores.csv、构建 metric_means / distributions / groupings / lowest_samples + ├── 现有逻辑:question_history.build_question_history_index(...) + └── 新增:advisor_comparison.build_advisor_comparison(run_dir, scenario_name, metrics) + ├── find_previous_run(...) → 复用 run_reader.list_run_summaries() + ├── 读取上一次 run 的 scores.csv + metrics(复用 run_reader 既有 helper) + ├── 对两次分别调用 rag_eval.advisor.diagnose()(已公开,零修改) + └── 按指标名分类差异状态,返回 AdvisorComparison | None +``` + +不引入新的持久化文件;每次报告详情页请求都会现场重新计算,天然保证"始终用最新阈值规则重新评估历史数据"。 + +--- + +## 3. 数据模型 + +### 3.1 `webapp/services/advisor_comparison.py`(新文件) + +```python +def find_previous_run( + scenario_name: str, + current_run_id: str, + current_finished_at: str, +) -> RunSummary | None: + """在所有 run 中找到同 scenario_name、时间上最近的前一次运行。""" + +def build_advisor_comparison( + run_dir: Path, + scenario_name: str, + metrics: list[str], +) -> AdvisorComparison | None: + """构建当前 run 相对上一次同名 run 的顾问诊断差异,找不到上一次 run 时返回 None。""" +``` + +内部指标状态分类(4 种,两次都健康的指标不生成条目): + +| 状态 | 含义 | +|---|---| +| `resolved` | 上次触发诊断,本次不再触发 | +| `regressed` | 上次未触发(该指标当时也被评测),本次新触发 | +| `still_triggered` | 两次都触发,展示分数与严重度变化 | +| `new_metric` | 上次运行未评测该指标(无基线),本次触发——不算回归,只是新指标 | + +### 3.2 `webapp/models.py` 新增 + +```python +class AdvisorComparisonEntry(BaseModel): + metric: str + status: Literal["resolved", "regressed", "still_triggered", "new_metric"] + previous_score: float | None = None + previous_severity: str | None = None # "critical" | "warning" | "low" | None + current_score: float | None = None + current_severity: str | None = None + + +class AdvisorComparison(BaseModel): + previous_run_id: str + previous_finished_at: str + previous_judge_model: str = "" + current_judge_model: str = "" + judge_model_changed: bool = False + entries: list[AdvisorComparisonEntry] = Field(default_factory=list) +``` + +`ReportData` 新增字段: + +```python +advisor_comparison: AdvisorComparison | None = Field( + default=None, + description="相比同场景上一次运行的顾问诊断差异;无可比对象时为 None。", +) +``` + +--- + +## 4. 数据流 + +1. 用户打开报告详情页,run R(`scenario_name=S`、`run_id=R_id`、`finished_at=T`、`metrics=M`) +2. `build_report()` 调用 `find_previous_run(S, R_id, T)`: + - 复用现有 `run_reader.list_run_summaries()` + - 过滤 `scenario_name == S 且 run_id != R_id 且 finished_at < T` + - 按 `finished_at` 倒序,取第一个(时间上最近的前一次) + - 无匹配 → 返回 `None` +3. 若找到上一次 run P: + - 读取 P 的 `scores.csv`(`run_reader.read_scores_frame`)与其 metrics 列表(快照或推断,复用现有 helper) + - 对 P 和 R 分别调用 `rag_eval.advisor.diagnose(score_rows, metrics)`(现有函数,零修改) + - 取两次触发指标名的并集,按 §3.1 表格分类状态,跳过两次都未触发的指标 + - 记录两次的 `judge_model`/`embedding_model`(来自各自的 `metadata.json`),设置 `judge_model_changed` +4. 返回 `AdvisorComparison`(或 `None`),挂到 `ReportData.advisor_comparison` +5. 前端 `report.js` 新增 `renderAdvisorComparison(report)`: + - `advisor_comparison` 为 `None` 或 `entries` 为空 → 不渲染任何内容 + - 否则在「⑤ 优化建议」区域上方渲染"相比上次运行(run_id,时间)"精简列表,每条用 `MetricPresenter.deltaInfo()` 计算涨跌箭头,复用 `.delta-good`/`.delta-bad` 样式;`new_metric`/`regressed` 状态额外给一个醒目标记 + - `judge_model_changed=true` 时追加一行小字提示"judge_model 不同(X → Y),对比仅供参考" + +--- + +## 5. 错误处理 + +| 情况 | 处理 | +|---|---| +| 找不到同 scenario_name 的历史 run(含 `/api/score/async`、`session_async` 这类 scenario_name 唯一的场景) | `find_previous_run` 返回 `None`,整个对比区域不渲染 | +| 上一次 run 的 `scores.csv` 缺失/损坏/无法解析 | 捕获异常,记录日志,`build_advisor_comparison` 返回 `None`(不影响报告详情页其余部分) | +| 两次指标完全不重合、且都没有 `new_metric` 触发 | `entries` 为空列表,前端按"无内容"处理,不显示对比区域 | +| `diagnose()` 内部抛出任何异常 | 在 `build_advisor_comparison` 顶层 try/except 兜底,返回 `None`,绝不让报告详情页 500 | + +设计原则与 `run_advisor()` 一致:辅助性/可观测性功能失败必须静默降级,不能影响核心报告渲染。 + +--- + +## 6. 测试策略 + +- `tests/test_advisor_comparison.py`(新文件): + - `find_previous_run`:同名匹配、排除自身、按时间取最近一次、无匹配返回 `None`、多个候选按时间正确排序 + - `build_advisor_comparison`:用 `tmp_path` 构造两个假 run 目录(`scores.csv` + `metadata.json` + `scenario.snapshot.yaml`,复用 `tests/test_webapp_report_builder.py` 里已有的构造模式),覆盖 4 种状态分类、`judge_model_changed` 检测、无上一次 run 时返回 `None`、两次都健康时 `entries` 为空 +- 扩展 `tests/test_webapp_report_builder.py`:验证 `build_report()` 正确挂载 `advisor_comparison` 字段(有上一次 run / 无上一次 run 两种情形) +- 不修改任何现有 `test_advisor_*.py`(rules/llm_analyzer/writer 行为完全不变,零回归风险) + +--- + +## 7. 非目标(本轮明确不做) + +- 不支持手动选择任意两个 run 做对比(只做"自动对比同场景上一次") +- 不持久化结构化诊断数据(`diagnoses.json` 或类似文件)——每次现场用 `diagnose()` 重新计算 +- 不改动 `rag_eval/advisor/` 内部规则、LLM 分析 prompt 或写入逻辑 +- 不支持跨 scenario_name 的对比(不同场景之间的指标不具可比性) +- 不做多跳历史链(只对比"最近一次",不做完整时间序列的顾问诊断趋势图——这块需求已由 Dashboard 的原始指标趋势线覆盖)