# T7 · 知识库·规则引擎 > 对应任务:`T-P2-02`(规则求值 p95 < 50ms;硬约束可拦截)| 需求:`FR-KB-02`、架构 §8.2 rules-engine、`TD-10` | 分支:`task/t7-rules-engine` | worktree:`/home/xsl/meida_backend-t7` > 先读《README.md(协作规程)》,遵守全部纪律。 ## 1. 目标 自研数据驱动决策表(TD-10):`kb_rule` 的 JSON DSL 解析 + 纯域求值引擎。**核心结论由规则引擎产出,LLM 只负责表达**(防幻觉红线)——这是整个产品"专业可信"的根基,代码质量按"钱与正确性"标准写。 ## 2. 已就绪的契约(协调提交已提供,禁改 models/migrations) ### 表 `kb_rule`(ORM `KbRule` 已加) `rule_id` PK 雪花、`rule_key` varchar(64) UNIQUE(如 `R-BODY-A-002`)、`category` varchar(32)、`version` int、`status` draft/published/archived、`priority` int、`conditions` JSONB、`effects` JSONB、`is_hard` bool、`confidence` Numeric(4,3)、`evidence` varchar(255)、`author`/`reviewer` varchar(64)、`published_at`;Index(status, priority)。 (注:架构 §6.2 写的列名 `conclusions`,协调提交统一为 **`effects`**——与"决策表"术语一致,任务文档以 models.py 实际代码为准。) ### DSL 规格(知识库文档 §3.2⑩ 原文结构) ```json { "rule_key": "R-BODY-A-002", "category": "body_shape", "priority": 90, "conditions": { "body_shape": "A型", "features": { "include": ["溜肩"] }, "goals": { "include": ["修饰溜肩"] } }, "effects": { "recommend": [ { "slot": "top", "attrs": { "shoulder": ["垫肩","自然肩"], "sleeve": ["泡泡袖"] } } ], "avoid": [ { "slot": "top", "attrs": { "shoulder": ["落肩"], "silhouette": ["修身"] } } ], "explanation": "溜肩需要在肩部建立结构感……" }, "is_hard": false, "confidence": 0.95 } ``` ### 优先级分组(知识库文档 §3.2⑩,语义:**数字越大越优先**) 合规 100 / 用户禁忌 95 / 体型 90 / 色彩 85 / 场景 80 / 温度 75 / 风格 70 / 偏好 60 / 趋势 50。 ### 分层位置 `packages/rules` 已在 import-linter 第 4 层(kb|retrieval|rules|memory|profile),现只有 README + 空 `__init__`。只准 import `packages.shared`/`packages.domain`。 ## 3. 实现要点 允许改动:`packages/rules/**`、测试文件。 1. **`packages/rules/dsl.py`**——纯逻辑(无 IO,重点单测): - `RuleCondition` / `RuleEffect` / `Rule` pydantic 模型(解析校验 DSL;`conditions` 支持的字段:等值匹配(str)、`include`/`exclude` 列表、`min`/`max` 数值区间;未识别的键**拒绝**而非忽略——静默忽略会让规则悄悄失效); - `match(condition: RuleCondition, ctx: RuleContext) -> bool`:`RuleContext` 是 frozen dataclass(`body_shape/season/face_shape/temperature/features/goals/occasion/style/user_taboos…`,缺字段 = 不匹配该子句); - 所有子句 AND 语义。 2. **`packages/rules/engine.py`**——纯域求值: - `evaluate(rules, ctx) -> RuleVerdict`:返回 `recommend/avoid/explanation 汇总 + rule_hits`(每条含 rule_key/priority/category/is_hard/confidence/explanation); - 排序:priority 降序,同 priority 按 confidence 降序; - **硬约束语义**:`is_hard=true` 且 avoid 命中 → `RuleVerdict.hard_blocks` 非空,调用方必须用它做硬过滤(禁忌违反 = 0 是发布红线);**用户禁忌(priority ≥95 的 avoid)永远压过专业推荐**——同 slot 冲突时用户禁忌胜出并从 recommend 中剔除冲突项; - p95 < 50ms:求值是纯内存操作,规则量 400 条时远低于此;写一个 400 条规则 × 1000 次求值的基准单测(`time.perf_counter`,断言均值 < 5ms/千次的量级即可,CI 机器差异留裕量)。 3. **`packages/rules/service.py` + `repo.py`**——DB 侧:按 status='published' 载入规则(内存缓存 + `invalidate()`;量级 400,直接整表载入),CRUD(draft→published 状态机,同 T6 语义),DSL 写入前用 pydantic 校验。 4. **黄金集雏形**(架构 §1081 明确要求):`tests/unit/test_rules_golden.py` 固定 8~10 组「输入画像 → 期望 recommend/avoid/hard_blocks」样本,其中**必须包含**:一条硬禁忌场景(用户禁忌压过专业推荐的用例)、一条多规则叠加场景。 ## 4. 测试要求 - 单测:DSL 解析(合法/非法结构/未识别键拒绝)、match 矩阵(等值/include/exclude/min-max/缺字段)、排序、硬约束剔除、用户禁忌优先、基准测试、黄金集。 - 集成 `tests/integration/test_rules_engine.py`(新文件):插 3 条规则(含 1 条 is_hard)→ service 载入 → 求值命中与拦截正确;draft 规则不被载入。 ## 5. 禁改清单 通用禁改之外:`packages/kb/**`(表共享但包不共享——**repo 只碰 kb_rule 表**,通过 ORM 模型访问,不改 T6 的文件)、`packages/retrieval/**`、`packages/identity/**`。 ## 6. 验收标准(T-P2-02) 1. 求值 p95 满足 400 条规则量级远低于 50ms(基准测试证明); 2. 硬约束可拦截(hard_blocks 语义 + 黄金集用例); 3. 用户禁忌永远压过专业推荐(黄金集用例); 4. DSL 非法结构 100% 拒绝(不会静默失效); 5. 全部检查绿(mypy 范围同 T6 列表 + `packages/rules`)。 ## 7. 提交信息 ``` feat(rules): data-driven decision table engine with hard constraints kb_rule DSL 解析 + 纯域求值(优先级分组/硬约束/用户禁忌压过专业推荐) + 400 条基准 + 黄金集雏形(T-P2-02,TD-10)。 偏差说明:<如有写在这里,无则写"无"> ```