Files
siemens_ragas/docs/superpowers/specs/2026-07-02-advisor-comparison-design.md
T

160 lines
8.4 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 优化建议历史对比设计(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 的原始指标趋势线覆盖)