# 03 — 两阶段分析 Agent + 存储 + 分析 API

**What to build:** 对终态活动可触发生成结构化分析报告。新表 `campaign_analyses`（每活动一行 upsert：status=generating/completed/failed、result JSON、model_config_id 快照、error、triggered_by、时间戳）。新模块 `evaluation/analysis.py`：输入复用 `generate_campaign_report` 聚合结果（不重算，ADR-0002/0004 口径）+ 每场景最多 3 条截断的代表性失败对话；阶段一每场景并行一次 LLM 调用产场景叙述+问题草稿，阶段二汇总产总体结论+跨场景问题+优先级建议；输出 JSON schema 约束，解析失败记 failed；证据 run_id 白名单校验，虚构引用落库前剔除。模型解析 = 活动覆盖 ?? 全局分析默认，两者皆无 → 明确错误。API：`GET /api/campaigns/{id}/analysis`（未生成返回空态）、`POST /api/campaigns/{id}/analysis`（触发/覆盖重跑；进行中/计划中 400；无模型 400 且 detail 引导配置）。后台 asyncio 任务 + try/finally 关 Session，失败落 error。

**Blocked by:** 02（模型解析依赖活动覆盖列）

**Status:** ready-for-agent

- [ ] Alembic 迁移：`campaign_analyses` 表（campaign_id 唯一）
- [ ] 仓储层 upsert / get_by_campaign
- [ ] `evaluation/analysis.py` 两阶段编排（阶段一 `asyncio.gather` 并行；输出 schema 见 spec）
- [ ] 失败样例选取：每场景 ≤3 条失败 turn（用户消息/回复/判定理由，截断）
- [ ] run_id 白名单：模型引用的虚构 id 落库前剔除
- [ ] 假 LLM 客户端单测：编排、解析失败→failed、白名单剔除、无失败样例场景仍产叙述、无模型→明确错误
- [ ] API 集成测试：GET 空态；POST→完成→GET 拿结果；进行中 400；无模型 400；重跑 upsert 不新增行
- [ ] `pytest` + `ruff` 全绿
