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

8.4 KiB
Raw Permalink Blame History

优化建议历史对比设计(Advisor Comparison

日期: 2026-07-02 状态: 已批准,待实现 范围: 报告详情页「优化建议」区域新增"相比上次运行"的自动对比摘要,落地架构设计 §11"单变量变更、回归复测"方法论。仅涉及 webapp 读取/展示层,不改动 rag_eval/advisor/ 的任何写入逻辑。


1. 背景与目标

架构设计 §11 明确优化方法论:

优化由评估结果驱动,遵循单变量变更、回归复测,避免一次性引入多项变更导致归因困难……每批经回归复测确认后再推进下一批。

当前平台已有两类"跨 run 对比"能力:

  • Dashboarddashboard.js):多 run 指标均值趋势线图 + 阈值柱状图,用户手动勾选 run 对比
  • 问题历史question_history.py):同一问题在不同 run 中的分数对比,展示在最低分样本详情里

顾问诊断层面没有对比能力:Diagnosismetric/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(新文件)

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 新增

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 新增字段:

advisor_comparison: AdvisorComparison | None = Field(
    default=None,
    description="相比同场景上一次运行的顾问诊断差异;无可比对象时为 None。",
)

4. 数据流

  1. 用户打开报告详情页,run Rscenario_name=Srun_id=R_idfinished_at=Tmetrics=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.csvrun_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_comparisonNoneentries 为空 → 不渲染任何内容
    • 否则在「⑤ 优化建议」区域上方渲染"相比上次运行(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/asyncsession_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_*.pyrules/llm_analyzer/writer 行为完全不变,零回归风险)

7. 非目标(本轮明确不做)

  • 不支持手动选择任意两个 run 做对比(只做"自动对比同场景上一次")
  • 不持久化结构化诊断数据(diagnoses.json 或类似文件)——每次现场用 diagnose() 重新计算
  • 不改动 rag_eval/advisor/ 内部规则、LLM 分析 prompt 或写入逻辑
  • 不支持跨 scenario_name 的对比(不同场景之间的指标不具可比性)
  • 不做多跳历史链(只对比"最近一次",不做完整时间序列的顾问诊断趋势图——这块需求已由 Dashboard 的原始指标趋势线覆盖)