8.4 KiB
8.4 KiB
优化建议历史对比设计(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(新文件)
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. 数据流
- 用户打开报告详情页,run R(
scenario_name=S、run_id=R_id、finished_at=T、metrics=M) 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
- 复用现有
- 若找到上一次 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
- 读取 P 的
- 返回
AdvisorComparison(或None),挂到ReportData.advisor_comparison - 前端
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 的原始指标趋势线覆盖)