# 黄金集回归框架(Golden Set) > 任务 T-P2-11 | 需求 FR-KB-11(检索质量可回归)、NFR-MAINT-04(指标退化阻断合并) > 架构 §17「黄金集是最重要的一道防线」| CI 防线 5c(`.github/workflows/ci.yml`) 黄金集 = **数据驱动的回归防线**:用例存 JSON、阈值存配置、指标低于阈值直接 fail。 与"散在单测里的固定样本"的本质区别是——**加用例 = 加 JSON 行,零代码改动**; 顾问/运营扩充规模不需要工程师改断言。 ## 目录 ``` tests/golden/ README.md # 本文档:用例格式 + 加用例流程 + 阈值口径 thresholds.json # 各子集指标阈值(pass_rate 等) cases/ rules_cases.json # 规则黄金集(当前 13 例种子) retrieval_cases.json # 检索黄金集(当前 13 例种子) runner.py # 用例加载 + 执行 + 指标计算 + 逐用例明细(不依赖 pytest) test_golden.py # pytest 薄壳(marker=golden,无 MEIDA_RUN_INTEGRATION 时 skip) ``` ## 四子集规划(知识库文档 §7,唯一事实源) | 子集 | 目标规模 | 当前 | 状态 | | --- | --- | --- | --- | | 检索 | 100 | 13 | ✅ 种子已就绪(内容轨 T-P2-16 扩充) | | 规则 | —(决策表回归,随单测+本子集维护) | 13 | ✅ 种子已就绪 | | Agent 问答 | 100 | 0 | ⏳ Sprint 3 占位 | | 诊断报告结构 | 30 | 0 | ⏳ Sprint 4 占位 | > 规模达标是内容轨 T-P2-16 的事;本框架的职责是让"加用例 = 加 JSON 行"。 ## 怎么跑 ```bash make golden # = MEIDA_RUN_INTEGRATION=1 uv run pytest -m golden --no-cov # 或直接: MEIDA_RUN_INTEGRATION=1 .venv/bin/pytest -m golden --no-cov ``` - 默认 `pytest`(`make test`)**不跑** golden(pyproject addopts 已排除),本地无 PG 不受影响; 未设 `MEIDA_RUN_INTEGRATION` 时本目录用例整体 skip,不拖累本地开发。 - 需要 PostgreSQL(pgvector) 已迁移到最新 schema(`make up && make migrate`)。 - CI 在集成测试之后强制跑(防线 5c),**指标低于阈值 = job 失败 = 阻断合并**。 ## 用例格式 ### 规则子集(cases/rules_cases.json) 每条用例是**自包含**的:规则数组 + 画像 + 期望。执行走 `packages.rules` 的 `dsl.Rule.model_validate` → `dsl.RuleContext` → `engine.evaluate`(纯逻辑)。 ```json { "name": "user-taboo-overrides-trend-recommendation", "rules": [ { "rule_key": "...", "category": "...", "priority": 95, "conditions": {"user_taboos": {"include": ["露脐"]}}, "effects": {"avoid": [{"slot": "top", "attrs": {"length": ["短款"]}}]} } ], "context": {"style": "甜酷", "user_taboos": ["露脐"]}, "expect": { "must_recommend": [{"slot": "top", "attrs_include": {"shoulder": ["垫肩"]}}], "must_avoid": [{"slot": "top", "attrs_include": {"length": ["短款"]}}], "forbidden_recommend": [{"slot": "top", "attrs_include": {"length": ["短款"]}}], "hard_blocks_nonempty": false } } ``` expect 判定语义(一条不符 = 该用例 fail): | 键 | 语义 | | --- | --- | | `must_recommend` | **子集语义**:实际 recommend 中存在同 slot 条目,且期望每个维度的取值都被其包含。`attrs_include` 为空 `{}` = 对整个 slot 的期望 | | `must_avoid` | 同上,但核对对象是实际 avoid 列表 | | `forbidden_recommend` | **红线语义**:实际 recommend 中任何与期望维度取值**相交**的条目 = 违规(用于验证禁忌/合规压制:被压掉的推荐绝不允许复活) | | `hard_blocks_nonempty` | `engine.evaluate` 的 `hard_blocks` 是否非空(is_hard 规则的 avoid 命中) | ### 检索子集(cases/retrieval_cases.json) ```json { "name": "taboo-body-shape-entity-never-leaks", "seed_entities": [ {"entity_type": "item", "name": "golden:r02-taboo", "attrs": {"occasions": ["wedding"], "avoid_body_shapes": ["apple"]}, "status": "published", "chunks": ["gq02 pear body dress styling guide"]} ], "query": "gq02 pear body dress styling guide for apple shapes", "user_ctx": {"body_shape": "apple", "occasion": "wedding", "entity_types": ["item"]}, "expect": { "must_hit_names": ["golden:r02-good"], "must_miss_names": ["golden:r02-taboo"], "first_hit_in": "top1" } } ``` - **种子实体契约**:`name` 必须以 `golden:` 开头(runner 强校验)。执行时 **跑前先删**自己 seed 的行(上次中断也能幂等重跑)、**跑后必删**(集成库长活, 留下假实体会破坏其它用例对结果列表的精确断言)。 - 判定语义: - `must_hit_names`:这些实体的 chunk 必须出现在结果里; - `must_miss_names`:**硬过滤红线**——draft / archived / 禁忌体型 / 场景外 / 类型外实体绝不允许出现在任何位置(零漏是 SQL WHERE 的结构保证,不是排序权重); - `first_hit_in`:`must_hit_names` 中最早出现的落位须满足 `top1`(第 1 位)或 `top3`(前 3 位)。 - **确定性策略**(校准新用例时遵守): 1. 嵌入用 `DeterministicEmbedder`(同文本同向量),**query 写成目标 chunk 的原文** → 余弦距离 0 → 向量路必排第 1,结果与库中其它数据无关; 2. chunk/query 带各自独立的 marker token(`gq01`、`gq02`…)→ 关键词路只命中本用例的行; 3. 需要非 top1 的落位时,用"两实体同 chunk 原文"构造并列(引擎按 chunk_id 稳定打破平局)。 - 检索参数钉死在 runner(维度 1024、top_k 50、默认 RRF 权重),不读 env—— 同一份用例集在任何机器上口径一致。 ## 加用例流程(零代码改动) 1. 在 `cases/*.json` 数组里追加一个用例对象(格式见上); 2. `make golden` 跑一遍校准 expect(种子用例必须 100% 通过——不过就是 expect 写错 或真有退化,二选一,搞清楚再提交); 3. 提交。CI 防线 5c 自动把它纳入回归口径。 注意:runner 对用例/expect 的未知键**一律判 fail**(封闭词汇表)——`must_recomend` 这类 typo 不会被静默忽略,这是防"断言悄悄失效"的刻意设计(与规则 DSL 同一哲学)。 ## 阈值口径(thresholds.json) | 子集 | 指标 | 阈值 | 说明 | | --- | --- | --- | --- | | rules | `pass_rate_min` | **1.0** | 硬结论子集,一条不许退(原因见下) | | retrieval | `pass_rate_min` | 0.8 | 顾问扩充用例后可逐步上调 | | retrieval | `first_hit_in_top3_min` | 0.8 | 首个 must_hit 落进前 3 的比例 | **为什么规则子集阈值必须 1.0**:产品根基本上是"结论由规则引擎产出,LLM 只负责表达" (防幻觉红线)。规则黄金集里的每一条都是**人工校准过的硬结论**——体型该推荐什么、 禁忌必须压过什么、合规必须拦什么。这些结论退化了任何一条,就意味着给用户的建议 直接变味,不存在"可以容忍 20% 出错"的余地;而检索是排序质量问题,允许有起步阈值、 随用例规模成熟逐步收紧。所以:规则 pass_rate < 1.0 = fail,没有商量。 ## runner 复用(不依赖 pytest) `runner.py` 是普通模块:`run_rules_subset()`(同步)与 `run_retrieval_subset()`(async) 各自返回 `SubsetReport`(含 `pass_rate`、`first_hit_in_top3_rate`、逐用例差异明细 `summary()`)。将来本地脚本、admin 后台"跑一遍黄金集"直接 import 即可, `test_golden.py` 只是薄壳。