# v0.8 — 周期对比(Period Comparison) ## Problem Statement 运营者用评估活动对同一对象做周期性考察,每期末尾拿到一份智能分析(总体结论、问题诊断、改善建议)。但单期分析是孤立的:这一期的问题到底是新冒出来的、持续未愈的、还是已经消解的?上一期的改善建议落实了吗?要回答这些,人得自己打开两期报告逐条对照自由文本——而这正是平台该替人做的元层研判。 ## Solution 在相邻两期活动之间提供**周期对比**:机械 diff 给出两期聚合指标的客观变化(整窗与分场景的通过率/可用性/时延 delta,ADR-0004 口径,确定性计算),LLM 在两期分析结论之上产出结构化演进叙述(趋势判断、问题演化、建议追踪)。报告抽屉内新增「周期对比」区块,与智能分析并列。正式线活动全链自动(活动完成 → 分析 → 对比),加速调试线手动按需。 ## User Stories 1. 作为运营者,我希望打开本期活动报告时看到"与上一期相比"的指标变化表(整窗通过率/可用性/时延及分场景 delta),以便快速判断走势。 2. 作为运营者,我希望看到一个趋势徽章(改善/平稳/退化),以便不看数字也能感知方向。 3. 作为运营者,我希望看到问题演化列表(新增/持续/消解,含涉及场景与说明),以便知道该担心什么、可以放下什么。 4. 作为运营者,我希望看到上期建议的追踪状态(已落实/部分落实/未落实/新增),以便评估改进动作的有效性。 5. 作为运营者,我希望正式线活动的对比在分析完成后自动生成,以便零操作拿到结论。 6. 作为调试者,我希望加速线活动能手动点「生成对比」,以便验证功能而不等完整周期。 7. 作为运营者,当本期活动计划与历史不一致(无自动基线)时,我希望能手动挑选一个历史活动作基线,以便换计划后仍能对比。 8. 作为运营者,我希望对比生成失败时看到原因并可重试,以便区分模型问题与数据问题。 9. 作为运营者,我希望对比叙述与指标 diff 分开呈现(叙述是 LLM 判断、数字是确定性计算),以便知道哪部分可以较真。 10. 作为管理员,我希望对比复用「分析默认」模型(活动覆盖 ?? 全局默认),以便不引入新的配置面。 11. 作为运营者,我希望对比中引用的场景是真实存在的(白名单过滤),以便不被模型虚构误导。 ## Implementation Decisions ### 配对(计划指纹 + 自动基线) - **计划指纹** = 评测对象 + 计划条目集合(场景 id、偏移秒数、次数)+ 窗口秒数。指纹相等的正式线活动构成活动串 - **自动基线** = 同活动串中、完成时间早于本期、且已有 completed 分析的最近一期活动 - 指纹不一致 → 无自动基线;`POST /comparison` 接受可选 `baseline_campaign_id` 手动指定(跨串允许,可比性由用户负责) - 基线活动被删或分析被重置 → 视为无基线 ### 产出(混合:机械 diff + LLM 叙述) - **机械 diff**:纯函数,输入两期 `generate_campaign_report` 结果,输出整窗 + 分场景的通过率/可用性/时延 delta(读时现算,不存储) - **LLM 叙述**:单次调用(不做两阶段——输入只有两份已消化的分析 JSON + 机械 diff,无原始样本),输入上限轻量;输出结构化 JSON: ```json { "trend": "improving | stable | regressing", "summary": "总体演进结论", "problem_evolution": [{"status": "new | persisting | resolved", "title", "detail", "scenario_ids": []}], "suggestion_tracking": [{"text", "status": "addressed | partial | unaddressed | new", "note"}] } ``` - 叙述中的 `scenario_ids` 落库前过白名单(同 v0.7);非法 `trend`/`status` 枚举归一(trend→stable,status→persisting / unaddressed) - LLM 解析失败 → 行落 failed + error ### 存储与生命周期 - 新表 `campaign_period_comparisons`:当前活动 id 唯一 upsert、baseline_campaign_id、generating/completed/failed、result(叙述 JSON)、model_config_id 快照、error、triggered_by(auto/manual)、时间戳——镜像 `campaign_analyses` 模式 - 模型解析复用 v0.7 `resolve_analysis_model`(活动覆盖 ?? 全局分析默认);不可解析 → 自动链静默跳过 / 手动 400 引导 - 自动链:`execute_campaign_analysis` 成功落 completed 后,活动为正式线且存在自动基线 → enqueue 对比生成;任一前提不满足静默跳过 - 重新生成(手动 POST)= 同活动 upsert 覆盖,可借此更换基线重算 ### API - `GET /api/campaigns/{id}/comparison`:无行 → 空态 `{"status": "none"}`;有行 → 机械 diff(现算)+ 叙述行合并返回;附 `baseline_campaign_id` 与自动配对结果,前端据此渲染或引导选基线。`metric_diff` 跟随生效基线:有对比行按行内基线算,否则按自动基线算 - `POST /api/campaigns/{id}/comparison`:body 可选 `{"baseline_campaign_id": "..."}`;非终态活动 400;当前活动无 completed 分析 400(先生成分析);无模型 400 引导;成功 → 后台任务 + `{"status": "generating"}` - 两期都有 completed 分析是生成前提(基线无分析 → 400 提示先生成基线分析) ### 前端(报告抽屉「周期对比」区块) - 位置:「智能分析」区块之下,复用区块状态行范式(生成中 5s 轮询 / 失败重试 / 未生成按钮) - 已生成:趋势徽章 + 总结 → 指标变化表(整窗 + 分场景 delta,按好坏着色——比率升为绿、时延降为绿,反之红)→ 问题演化(新增红/持续橙/消解绿 Tag)→ 建议追踪(已落实绿/部分橙/未落实灰/新增蓝) - 无自动基线:基线选择器(同对象历史终态活动下拉)+ 生成按钮 - 展示基线活动名与完成时间、所用分析模型名与生成时间 ## Testing Decisions 好测试只测外部行为,不测实现细节;全部落在既有最高层 seam: 1. **机械 diff 纯函数**:手搓两份报告 dict,断言 delta 结构与数值、空桶/None 处理(先例如 `test_report_render.py`) 2. **计划指纹与自动基线**:内存 SQLite,指纹相等/不等、跳过无分析活动、跳过非正式线、取最近一期(先例如 `test_campaign_analysis.py` 的仓储测试) 3. **LLM 叙述编排**:FakeChatClient(v0.7 seam)覆盖成功编排、解析失败→AnalysisError 式失败、白名单剔除虚构 scenario_ids、非法枚举归一 4. **API 集成**:镜像 `test_campaign_analysis_api.py`——GET 空态、POST 非终态 400、无分析 400、无基线 400、手动指定基线成功、重跑 upsert 不新增行 5. **自动链**:spy 对比入口,断言分析完成后正式线自动触发、加速线不触发、无基线不触发(镜像 `test_campaign_analysis_auto_trigger.py`) 6. **迁移测试**:表存在 + campaign_id 唯一约束(先例如 `test_campaign_analyses_migration.py`) ## Out of Scope - 跨活动对比(多对象横切)——词汇表中另一个轴,本规格不涉及 - 活动列表趋势标记(↑↓)——后续增强 - 对比结果纳入 Markdown 导出——看使用情况再定 - 基线活动的选取 UI 优化(如按指纹分组展示) - 三期以上的趋势序列(只对比相邻两期) ## Further Notes - 设计决策源自 `/grill-with-docs` 访谈(6 问 6 决):配对 A、产出 C 混合、呈现 A 抽屉区块、生命周期 A 新表+联动触发、输入 A 轻量、输出 A 结构化 - 「周期对比」「正式线/加速调试线」已入 `CONTEXT.md` 词汇表 - 对比不重新消化失败样本:样本已被两期分析消化,对比是元层研判