ADR-012 · 多场景评委体系 · Scene 作为 Panel 的上层部署单元
| 字段 | 值 |
|---|---|
| 状态 | accepted · 2026-06-25 |
| 日期 | 2026-06-25 |
| 决策者 | CTO + 项目主理 |
| 承接 | ADR-001 (multi-anchor) · ADR-007 (REVIEW 模式) |
| 相关 | scripts/scene_loader.py · scripts/panel_loader.py · scripts/op2_output.py · scripts/smoke_e2e.py · scenes/ · panels/default.yaml |
1. Context
1.1 · 起源
boss 判断流水线最初是单场景架构: 一套 panels/default.yaml, 一个飞书机器人绑定, 一套评分维度。随着使用场景扩展, 出现了无法用单 panel 覆盖的结构性差异:
| 差异维度 | 常规评审 | 竞赛 / Workshop |
|---|---|---|
| 是否有锚点基准 | 有 (anchor_judge: tian) | 无 (anchor_judge: null) |
| 评分维度 | 固定 5 镜头 (推理质量) | 自定义 (业务价值 / 创新度等) |
| 成绩是否公开 | 内部 | 公开展示 |
| 机器人入口 | 独立 | 独立 |
| 总分制 | 通常 100 | 任意 (如 10) |
同时, 同一评审模式下也有多个并列场景: 公司级 / 各事业群 / 各专项评审 — 它们评委组合不同, 报告品牌不同, 但引擎完全一样。
核心矛盾: panel 是"评审规则", 但缺少"部署单元"的概念 — 没有地方描述"哪个飞书机器人触达哪套规则"。
1.2 · 在 Panel 上加 Scene 层的动机
直接往 panel.yaml 里塞机器人 app_id / 白名单 / scene_type 等字段不合适:
- panel 是评审规则, 理论上可以被多个入口共享
- 机器人凭据属于部署配置, 不应进入"判断引擎"核心
- scene_type (review vs competition) 是入口级约束, panel 层感知不到
2. Decision
引入 Scene 作为 Panel 的上层部署单元。
2.1 · Scene 文件结构
scenes/
└── <scene-slug>/
├── scene.yaml ← 场景级配置 (scene_type / bot 绑定 / 白名单 / 报告配置)
└── panel.yaml ← 评审规则 (可 extends 默认 panel)scenes/shared-judges/ 存放跨场景可复用的专项评委 SKILL.md (不绑定特定场景)。
2.2 · scene.yaml 最小结构
name: <scene-slug>
scene_type: review # review | competition
report:
anchor_judge: tian # competition 场景必须为 null
show_scores_publicly: false
feishu:
app_id_env: LARK_APP_ID_<ENV_VAR>
app_secret_env: LARK_APP_SECRET_<ENV_VAR>2.3 · scene_type 的强制约束 (scene_loader 在加载时校验)
| 约束 | review | competition |
|---|---|---|
anchor_judge | 任意 (默认 tian) | 必须为 null |
show_scores_publicly | 任意 | 必须为 true |
违反约束 → scene_loader.load_scene() 抛 ValueError, 阻断后续流水线。
2.4 · Panel 继承机制
场景 panel 通过 extends 字段继承基础 panel:
# scenes/<slug>/panel.yaml
name: <slug>
extends: panels/default.yaml # 继承, 未覆盖字段沿用
anchor_judge: tian
scoring_mode: sum_max_score
judges_override: # 完整替换评委列表 (非 merge)
- slug: tian
judge_category: anchor
skill_path: anchors/tian/perspective/SKILL.md
always_active: true
- slug: first-principles
judge_category: dimension
skill_path: scenes/shared-judges/first-principles/SKILL.md
scoring_lenses_override: # 完整替换评分维度
- { slug: strategic_fit, display_name_cn: 战略契合, max_score: 30 }
- { slug: feasibility, display_name_cn: 落地可行, max_score: 40 }
- { slug: one_page_quality, display_name_cn: 一页表达, max_score: 30 }panel_loader.resolve_panel() 负责展开继承链, 返回完整 panel dict。
2.5 · 评分模式与成绩渲染
当前仅支持 scoring_mode: sum_max_score:
total_max_score(panel)= 所有scoring_lenses的max_score之和- 等级计算:
pct = score / total_max * 100(与total_max无关, 兼容 10 / 100 等任意总分制) - 等级档: S ≥ 95 / A ≥ 85 / B ≥ 70 / C ≥ 55 / D < 55
op2_output.render_op2_6section() 渲染 6 段式报告:
- §1 总分 · §2 分项得分 · §3 必改项 · §4 补充建议 · §5 删减建议 · §6 一页汇总表
2.6 · Smoke 校验入口
scripts/smoke_e2e.py smoke-scene <slug> 对单场景执行静态全链路校验 (不调 LLM, 不写磁盘):
scene_loader.load_scene(slug)— 加载 + 约束校验panel_loader.resolve_panel(panel_path)— 继承链展开- 所有 judge
skill_path文件存在性检查 sum_max_score维度总分汇总anchor_judge一致性 (scene vs panel)
make smoke-scene SCENE=<scene-slug>3. Consequences
3.1 · 优势
| 维度 | 落地后能力 |
|---|---|
| 部署隔离 | 多个飞书机器人各自路由到对应场景, 互不干扰 |
| 配置差异可读 | panel.yaml 只写差异, 继承自 default.yaml, diff 即设计意图 |
| competition 约束硬编码 | scene_loader 在加载时校验, 无法绕过 |
| 共享评委复用 | scenes/shared-judges/ 评委 SKILL.md 跨场景引用, 不重复维护 |
| CI 静态校验 | smoke-scene 在任何 LLM 调用前完成全链路静态检查, 适合 PR gate |
3.2 · 接受的 tradeoff
judges_override是完整替换 (非 merge): 每个 panel 必须显式列出所有评委, 不能只追加一位。代价是配置稍冗长, 收益是配置可读、无隐含合并逻辑。- scene_type 仅 2 种 (review / competition): 不支持自定义枚举。如未来出现第 3 类场景, 需修改
scene_loader.py约束逻辑 + 本 ADR。 scoring_mode仅支持sum_max_score: 加权平均 / 取最高等其他模式未实现。现有场景均适用 sum, 延迟实现。
3.3 · 不变的承诺
- 无 scene.yaml 时回退到单机器人模式 — 完全向后兼容
- panel.yaml 直接使用 (不走 scene 层) 仍支持
- wiki 知识库、锚点心智模型、5 镜头、30/90/365 attribution 不变
- versions/ 不可变冻结不变
- redact_check + check_public_safe 闸不变
3.4 · Invariant impact ★
本变更让以下 invariant 失效:
- inv-old: "一个 boss 实例绑定一套 panel" → 失效 · Scene 层解耦后, 一个进程可同时服务多个场景 / 多个 panel
- inv-old: "anchor_judge 由 panel 决定, 无外部约束" → 失效 · competition scene_type 强制 anchor_judge=null, scene 层凌驾于 panel 层
- inv-old: "评分维度总和默认 100" → 失效 · 任意 total_max 均合法; 成绩等级按 pct 计算, 与总分无关
本变更引入以下新 invariant:
- inv-1:
scene_type: competition必须满足anchor_judge is nullANDshow_scores_publicly is true— scene_loader 强制校验 - inv-2:
scenes/shared-judges/<slug>/SKILL.md的评委必须包含adversarial_view三字段 (同 CLAUDE.md §4.5) — skill_lint 校验 - inv-3:
smoke_e2e.smoke_scene(slug)必须在任何该 scene 的 LLM 调用之前通过 — CI gate - inv-4: 所有 panel
scoring_mode: sum_max_score场景的total_max_score()必须与render_op2_6section()渲染的总分一致
4. Alternatives Considered
| 方案 | 为什么没选 |
|---|---|
| A1 · 直接在 panel.yaml 加 app_id / scene_type | 混淆"评审规则"与"部署配置"两个关注点; panel 理论上可多入口共享, 塞入 app_id 就绑死了 |
A2 · scenes/ 目录仅放 scene.yaml, panel.yaml 留在 panels/ | panel 与 scene 是一对一关系 (目前), 分目录存放让 diff 不直观; 同一 slug 下的 scene.yaml + panel.yaml 放在一起更清晰 |
| A3 · judges_override 改为 merge (只写增量) | 合并逻辑隐含继承顺序, 难以推理"这个 panel 实际有哪些评委"; 完整替换让 panel.yaml 即文档即配置 |
| A4 · 多个 scene_type 值 (review / competition / showcase / ...) | 过早泛化; review 与 competition 已覆盖所有现有场景, 其余类型留到出现真实需求时再加 |
| A5 · smoke 校验内嵌到 scene_loader 导入时 | 导入即触发校验会阻断 pytest collect 阶段; 独立 smoke_scene() 函数让测试与工具分离 |
5. Migration Plan
5.1 · 已实施 (v1.2.0, 2026-06-25)
| 步骤 | 文件 | 状态 |
|---|---|---|
| scene_loader.py | scripts/scene_loader.py — SceneConfig dataclass, load_scene(), list_scenes() | ✅ |
| panel_loader.py | scripts/panel_loader.py — resolve_panel(), total_max_score() | ✅ |
| op2_output.py | scripts/op2_output.py — render_op2_6section(), DimensionScore, Op2ReviewData | ✅ |
| smoke_e2e | scripts/smoke_e2e.py — smoke_scene() + smoke-scene CLI 子命令 | ✅ |
| shared-judges | scenes/shared-judges/ — 6 类专项评委 SKILL.md | ✅ |
| 多个场景 | scenes/ — review 场景 × N + competition 场景 × 1 | ✅ |
| e2e 测试 | tests/unit/test_scene_e2e.py — 40 tests (review / competition / smoke-all / server / isolation) | ✅ |
| Makefile | make smoke-scene SCENE=<slug> | ✅ |
5.2 · 待办 (M5)
- VM 联调: ≥2 飞书机器人分别触发 → 各自 panel → 报告用对应场景评委。验收见 PRD §12。
6. 关键反共识立场
"Scene 层多余, 直接给 panel 加 scene_type 字段就够了" — 错。
scene_type 是入口级约束, 它决定的不是"评审规则"(panel 的职责), 而是"这套规则允许出现在什么上下文里"。competition 场景的 anchor_judge: null 约束必须在 panel 被载入之前检查, 不能依赖 panel 内部自律。Scene 层是这条约束的自然归属地。
"panel 继承用 merge 更灵活" — 错。
灵活的代价是隐含性。"这个 panel 实际有哪些评委"如果需要追溯继承链才能回答, 就给排查问题带来了不必要的认知负担。judges_override 的完整替换语义让 panel.yaml 即文档即配置 — 打开文件即知真相。
"smoke-scene 应该调真 LLM 做端到端测试" — 错。
静态校验 (文件存在 / 字段完整 / 约束满足) 应该在 LLM 调用之前完成并且快速。smoke_scene() 的价值在于: CI 门控 + 本地开发时的即时反馈 + 不产生 LLM 费用。真实的 LLM 端到端测试是另一层, 不应混进 smoke。
7. 何时回看本 ADR
- 出现第 3 种 scene_type (如
showcase/audit) 时: 修订 §2.2 约束表 + scene_loader total_max_score计算逻辑变化时: 修订 §2.5 等级档说明- M5 VM 联调完成后: 验证 inv-1~4 是否在真实飞书环境下守住
- 任何 smoke_scene() 检查漏掉的静态错误在生产暴露时: 加 inv-N + 补 smoke 检查项
ADR-012 · accepted · 2026-06-25 · 多场景评委体系 · Scene 上层架构 · 10 场景 / 790 tests