第 11 章:规则引擎:业务正确性的实现机制
11.1 工程问题的本质:规则从哪来、改得动、挡得住
规则驱动的前提是规则本身可靠。三个问题决定规则体系的可信度:规则从哪来(业务逻辑与代码分离,还是散落各处)、规则改得动吗(修改要发版还是改配置)、规则挡得住吗(能否被角色覆盖、被模型绕过、被训练篡改)。
设计原理: 规则体系的核心矛盾是表达能力与可控能力的平衡。规则要有足够的表达力覆盖业务逻辑,又不能有任意能力(否则规则即代码、可训练即可注入)。框架的解法是两层分离:动作能力不可训练(内置 action 集合为 frozenset,运行时不可修改),动作组合可训练(engine_steps 步骤序列存配置,经审批修改)。规则可以训练逻辑,但不能训练出任意代码执行能力——这是可训练的安全边界。
11.2 多态裁决:合规检查与人工决策的分离
规则结果采用三态:pass(通过)/ warn(警告,需审批可继续)/ block(阻断,不可继续)。硬规则(is_hard=True)的阻断不可被任何角色覆盖。
原理: 三态将"合规检查"与"人工决策"分离为两个独立环节。block 是系统的最终裁决——合规检查认为不可行,不再咨询任何人;warn 是系统的建议——合规检查认为有风险,将决策权上交给审批链(第 13 章)。approver_role 字段指明由谁决策,使"谁有权通过"成为规则的一部分而非临时决定。
11.3 数据驱动规则:配置即治理
普通规则逻辑以 engine_steps(JSON 步骤序列)存储在数据库,规则参数(阈值、折扣上限、审批金额线)同样存配置。引擎按步骤顺序执行,任何步骤异常默认返回 block(安全优先)。
原理: 数据驱动规则的意义不在"少写代码",而在把规则的变更纳入治理。规则逻辑是业务事实的一部分,业务事实的变更应当可审计、可回滚、可审批。将规则从代码移入配置,变更路径就从"发版"变为"审批后热更新"——变更记录落在业务表而非 git 历史,审批责任落在审批链而非代码评审。
失败防范: 引擎执行异常默认 block(fail-closed)。这是刻意的选择:规则引擎不确定时,宁可阻断业务,也不静默放行。放行的错误是"一次违规操作",阻断的错误是"一次请求被拒"——前者成本远高于后者。
11.4 沙箱:表达能力的边界
表达式求值在受限沙箱中执行(AST 白名单):禁止 eval/exec/import/任意函数调用,字符串方法白名单仅对 str 实例开放,属性访问拒绝私有与魔术属性(堵 __class__.__bases__ 属性链逃逸)。
原理: 沙箱回答"规则能表达什么、不能表达什么":规则可以比较、取值、分支、汇总——即业务逻辑;规则不能导入模块、执行代码、访问内部对象——即系统能力。可训练的是业务判断,不可训练的是系统权限。 这一边界是"配置可训练"不至于演变为"配置即漏洞"的保障。
11.5 规则引擎的经典理论与 DSL 设计
11.5.1 执行模型:前向链与步骤序列
经典规则引擎(产生式系统)以前向链(forward chaining)执行:工作记忆(working memory)中的事实反复触发规则,直到没有新事实产生。前向链适合"事实驱动、规则间有级联依赖"的场景,代价是执行过程难以预测(规则触发顺序影响结果)。
本框架选择显式步骤序列(engine_steps 顺序执行)而非前向链,这是刻意的工程取舍:
| 维度 | 前向链(产生式) | 显式步骤序列 |
|---|---|---|
| 执行可预测性 | 低(触发顺序影响结果) | 高(按序执行) |
| 调试难度 | 高(需追踪规则触发) | 低(逐步可断) |
| 表达力 | 高(隐式级联) | 中(显式编排) |
| 适合场景 | 复杂推理、专家系统 | 业务裁决、流程化校验 |
原理: 业务规则引擎的首要需求是可预测与可审计,而非表达力最大化。显式步骤序列牺牲了隐式级联的表达力,换取"每一步执行什么、产出什么"完全确定——这与第 8 章"可信结果 = 生成 × 裁决"的裁决确定性要求一致。RETE 算法优化的是前向链的匹配效率,在业务规则(数十条、级联有限)场景中,序列化的确定性价值高于匹配效率。
11.5.2 DSL 设计:语法受限与静态校验
规则 DSL(engine_steps)的工程要点是"语法受限 + 静态可校验":
- 动作受限:动作取自已定义的 frozenset,运行时不可扩展——新增动作须经代码评审,而非训练修改;
- 参数受限:每个动作的参数由引擎定义(如 lookup 的 mode/merge_into),规则只能填写合法参数;
- 表达式受限:条件表达式在沙箱子集内(比较、逻辑、字符串方法白名单),AST 静态检查在编译期拒绝非法节点;
- 可静态校验:规则入库前可做 schema 校验(步骤结构、动作名、参数类型),非法规则在提交时被拒,而非运行时才暴露。
原理: DSL 的价值不在"语法漂亮",而在把非法规则挡在运行时之外。运行时才暴露的规则错误是"配置型故障"——难定位、影响面大;提交时被拒的规则错误只是"一次编辑失败"。静态校验前置是配置治理的第一道闸门(第 14 章热更新之前)。
11.5.3 裁决路径的选择框架:规则 vs 决策表 vs 模型
业务裁决并非一律用规则。三类路径的适用边界:
| 路径 | 适用场景 | 不适用的场景 |
|---|---|---|
| 规则(if-then/engine_steps) | 决策条件明确、数量有限、需可解释 | 条件组合爆炸(需决策表/规则表) |
| 决策表/查找表 | 条件维度多但枚举有界(折扣率×客户等级) | 条件无界(自由文本判定) |
| 模型裁决 | 无显式规则、需泛化(语义相似判断) | 需确定性、需审计(不可解释) |
原理: 选择框架的依据是"决策是否可枚举、是否需要解释"。可枚举且有界 → 表驱动;可描述但复杂 → 规则引擎;不可描述 → 模型。先问可解释性需求,再选实现路径——审计需求为刚性约束时,模型裁决仅在无替代时引入,且须以 warn(人工复核)而非 block 形态参与(第 12 章审核链第 5 层)。
11.6 失败模式与权衡
| 失败形态 | 根因 | 设计应对 |
|---|---|---|
| 规则校验静默放行 | 注册表为空、规则未注册 | 进程级共享注册表 + 硬编码安全铁律 |
| 沙箱逃逸 | 属性链访问魔术对象 | AST 白名单 + 私有属性拒绝 |
| 规则逻辑漂移 | 双份实现(引擎与回退 check) | 引擎优先、回退兼容、参数签名适配 |
| 配置更新不生效 | 缓存未失效 | 进程级缓存 + 代次校验 + 热更新 |
权衡: 数据驱动换取可控性的代价是调试难度上升——规则错误不再表现为代码堆栈,而是步骤序列的失败。应对:引擎记录逐步执行日志与规则名,失败信息携带规则标识,将"配置型故障"的排查锚点还给规则本身。
11.7 本章小结
- 规则可信度取决于三个问题:从哪来、改得动、挡得住;
- 表达力与可控力的平衡:动作能力不可训练,动作组合可训练;
- 三态裁决分离合规检查与人工决策;
- 数据驱动使规则变更进入治理闭环;沙箱划定表达边界;
- 引擎失败默认阻断,放行错误的成本高于阻断错误。