规则引擎与决策表

本文讲解规则引擎与决策表的建模与落地,回答业务规则该写死在代码里还是抽成决策表、DMN 的命中策略怎么选、规则变更如何灰度与回滚。覆盖 DMN 决策表、FEEL 表达式、决策需求图、Drools 与 DRL、规则版本化与测试、与工作流引擎和策略即代码的边界、评分卡建模、规则冲突与性能优化,并给出可运行的配置与代码。

引言

任何业务系统里都有一批「如果这样就这样,如果那样就那样」的判断:贷款额度超过 50 万要人工复核、客户等级是金卡就打 9 折、风险评分大于 80 就拒绝。这些判断最初写在 if-else 里,随着业务变化越堆越多,最终变成一个没人敢改的 decide() 方法。

规则引擎要解决的就是这件事:把业务规则从代码里抽出来,用表格或 DSL 表达,让业务方能读、能改、能测试、能版本化。它和工作流引擎是互补的:工作流回答「流程怎么走」,规则引擎回答「这个条件下该走哪条路」。

但规则引擎也是最容易被滥用的技术之一。把 20 行 if-else 搬进规则引擎,只会增加一个需要运维的组件和一层需要穿透的抽象,收益为负。判断标准是「规则的变更频率与变更主体」:如果规则每月都要改且由业务方提出,就值得抽出来;如果一年改一次且由工程师改,留在代码里更好。

本文先讲规则的三种表达形式与选型,再深入 DMN 决策表与命中策略,然后讲 Drools 与 DRL、规则版本化、测试策略,最后讲与工作流引擎和策略即代码的边界。想先看流程侧的内容,可以从 BPMN 2.0 与 Camunda 实战 开始。

目录

  1. 规则引擎要解决什么问题
  2. 规则的三种表达形式
  3. DMN 决策表基础
  4. 命中策略的选择
  5. FEEL 表达式与数据类型
  6. 决策需求图与多级决策
  7. Drools 与 DRL
  8. 事实、会话与议程
  9. 规则版本化与灰度
  10. 规则的可测试性
  11. 与工作流引擎的集成
  12. 与策略即代码的边界
  13. 评分卡与风险模型
  14. 规则的冲突与优先级
  15. 规则的性能优化
  16. 规则的可观测
  17. 落地路线图
  18. 权衡取舍
  19. 常见坑清单
  20. 小结

1. 规则引擎要解决什么问题

规则引擎解决四个具体问题,只要命中一个就值得评估引入:

  • 规则频繁变更,且变更由业务方提出,走研发排期太慢。
  • 规则数量多(超过 50 条)且互相有关联,代码里的优先级难以维护。
  • 规则需要可解释:能回答「为什么这个申请被拒绝了」。
  • 规则需要审计:监管要求能提供「当时的规则是什么」。

反过来,如果规则稳定、数量少、不需要解释与审计,那么一个带注释的 if-else 就是最好的规则引擎。引入规则引擎的成本是「多一个组件、多一门 DSL、多一层调试穿透」,这些成本在规则简单时完全无法被收益覆盖。

还有一个中间态值得考虑:把规则抽成独立的模块或配置类,用清晰的数据结构表达,而不引入引擎。这样既获得了「规则集中、可测试」的收益,又避免了引擎的复杂度。很多项目真正需要的只是这一步。

2. 规则的三种表达形式

形式表达力业务方友好度可测试性典型工具
决策表中(条件组合)高(就是表格)高(逐行可测)DMN、Excel
DSL 规则高(支持推理)中中Drools DRL
表达式低到中低高CEL、SpEL、JS

决策表是最实用的形式:一行一条规则,一列一个条件,最后一列是结论。业务方能直接读,工程师能逐行写测试。它的局限是「表达能力有限」——不支持跨规则的推理与事实传播。

DSL 规则(Drools 的 DRL)支持前向推理:规则 A 的结论可以成为规则 B 的条件,适合复杂场景(比如风控里的多轮推导)。代价是可读性下降,且规则的执行顺序不直观(由引擎的议程决定)。

表达式(CEL、SpEL)适合「单条判断」,比如 API 网关的路由条件、特性开关的开关条件。它们不适合表达多条规则的组合。

选择建议:先用决策表覆盖 80% 的场景,只有确实需要推理时才上 DRL。不要一开始就选表达力最强的工具,那通常意味着最长的学习曲线与最高的维护成本。

3. DMN 决策表基础

DMN(Decision Model and Notation)是 OMG 的决策建模标准,与 BPMN 同源。一个决策表由输入列、输出列和规则行组成。

<definitions xmlns="https://www.omg.org/spec/DMN/20191111/MODEL/"
             id="riskDecision" name="风险评估">
  <decision id="riskLevel" name="风险等级">
    <decisionTable id="riskTable" hitPolicy="FIRST">
      <input id="in1" label="客户等级">
        <inputExpression typeRef="string"><text>customer.tier</text></inputExpression>
      </input>
      <input id="in2" label="申请金额">
        <inputExpression typeRef="number"><text>amount</text></inputExpression>
      </input>
      <output id="out1" label="风险等级" name="level" typeRef="string"/>
      <output id="out2" label="是否人工复核" name="manual" typeRef="boolean"/>
      <rule id="r1">
        <inputEntry><text>"PLATINUM"</text></inputEntry>
        <inputEntry><text>&lt; 100000</text></inputEntry>
        <outputEntry><text>"LOW"</text></outputEntry>
        <outputEntry><text>false</text></outputEntry>
      </rule>
      <rule id="r2">
        <inputEntry><text>"PLATINUM","GOLD"</text></inputEntry>
        <inputEntry><text>[100000..500000]</text></inputEntry>
        <outputEntry><text>"MEDIUM"</text></outputEntry>
        <outputEntry><text>true</text></outputEntry>
      </rule>
      <rule id="r3">
        <inputEntry><text>-</text></inputEntry>
        <inputEntry><text>&gt; 500000</text></inputEntry>
        <outputEntry><text>"HIGH"</text></outputEntry>
        <outputEntry><text>true</text></outputEntry>
      </rule>
    </decisionTable>
  </decision>
</definitions>

输入条目支持多种写法:单值("PLATINUM")、枚举("PLATINUM","GOLD")、区间([100000..500000])、比较(< 100000)、通配(-)。这些写法让表格保持简洁,但也意味着输入列的类型必须正确,否则区间比较会退化成字符串比较。

4. 命中策略的选择

命中策略(Hit Policy)决定「多条规则同时匹配时取哪一条」,是 DMN 里最重要的语义选择:

策略含义适用场景
UNIQUE只允许一条匹配,多条则报错规则互斥的严格场景
FIRST取第一条匹配(按顺序)有优先级的场景(推荐默认)
PRIORITY取输出值优先级最高的输出可排序的场景
ANY允许多条匹配,但结论必须相同交叉校验规则一致性
COLLECT收集所有匹配的输出需要汇总(求和、计数)
RULE ORDER按规则顺序收集需要有序结果

FIRST 是最常用的,因为它符合人的直觉:从上往下看,第一条匹配的生效。代价是「规则的顺序变得重要」,插入一条规则可能改变已有行为,所以规则表要版本化并做回归测试。

UNIQUE 看起来最安全,但它在规则有重叠时会直接报错,而规则重叠在业务上很常见(比如「金卡且金额大」同时匹配两条)。用它需要把规则设计成完全互斥,维护成本高。

COLLECT 常用于需要汇总的场景,比如「所有适用的折扣规则,累加折扣率」。

5. FEEL 表达式与数据类型

FEEL(Friendly Enough Expression Language)是 DMN 的表达式语言,语法接近自然语言:

// 条件判断
amount > 100000 and customer.tier = "PLATINUM"

// 区间与列表
amount in [100000..500000]
tier in ("GOLD", "PLATINUM")

// 日期计算
now() - order.createdAt < duration("P30D")

// 条件表达式
if amount > 500000 then "HIGH" else "LOW"

FEEL 的类型系统有三个容易踩的坑:

  • 数字与字符串不自动转换。amount > "100000" 会报类型错误,而在 JSON 里传入的数字如果被解析成字符串就会失败。
  • 日期与时间有时区语义。date("2026-10-07") 与 date and time("2026-10-07T00:00:00") 是不同的类型,比较时会出错。
  • 空值(null)的传播。null > 100 的结果是 null 而不是 false,在条件里会被当作「不匹配」,这通常符合预期但要显式测试。

在 Camunda 里调用 DMN 时,输入的变量类型要与 DMN 定义一致,否则会出现「本地测试通过、线上类型不匹配」的问题。建议在 DMN 的单元测试里覆盖类型边界(字符串数字、空值、极值)。

6. 决策需求图与多级决策

一个复杂决策通常由多个子决策组成,它们之间有依赖关系。DMN 用决策需求图(DRG)表达这种依赖:

      [客户等级]        [申请金额]
           \              /
            \            /
         [基础风险评分]  [额度校验]
                   \      /
                    \    /
                  [最终审批路由]

每个方框是一个决策,箭头表示「输入依赖」。拆成多级决策的好处是每级都可以独立测试与复用:基础风险评分 可以在多个流程里复用,额度校验 可以单独调整阈值。

在 Camunda 里,一个 DRG 里的多个决策可以在同一个 DMN 文件里定义,用 informationRequirement 声明依赖,引擎会按依赖顺序求值。

<decision id="finalRoute" name="最终审批路由">
  <informationRequirement>
    <requiredDecision href="#riskLevel"/>
  </informationRequirement>
  <decisionTable hitPolicy="FIRST">...</decisionTable>
</decision>

实践建议:把决策拆到「每个决策表不超过 15 行」的粒度。超过这个规模时,通常意味着可以按维度拆成多个决策。

7. Drools 与 DRL

Drools 是 Java 生态最成熟的规则引擎,用 DRL 描述规则。它的核心优势是支持前向推理与复杂事件处理(CEP)。

// DRL 文件
rule "高额订单需要总监审批"
    when
        $o : Order(amount > 100000, status == "PENDING")
        not ApprovalRequest(orderId == $o.id, level == "DIRECTOR")
    then
        insert(new ApprovalRequest($o.getId(), "DIRECTOR"));
        modify($o) { setNeedsDirector(true) };
end

rule "黑名单客户直接拒绝"
    when
        $o : Order(customerId != null)
        $c : Customer(id == $o.customerId, blacklisted == true)
    then
        modify($o) { setStatus("REJECTED"); setReason("黑名单客户") };
end
KieServices ks = KieServices.Factory.get();
KieContainer kc = ks.getKieClasspathContainer();
KieSession session = kc.newKieSession("orderRules");

session.insert(order);
session.insert(customer);
session.fireAllRules();
session.dispose();

DRL 的 when 部分是模式匹配(类似 SQL 的 where),then 部分是动作。insert 会把新事实加入工作内存,可能触发其他规则,这就是前向推理。

DRL 的代价是「规则的执行顺序不可预测」(由引擎的议程与冲突解决策略决定),排查问题时需要开 agenda-group 与审计日志。这也是为什么多数业务规则场景用 DMN 就够。

8. 事实、会话与议程

Drools 的三个核心概念需要理解清楚才能用好:

  • 事实(Fact):插入工作内存的数据对象,规则对它们做模式匹配。
  • 会话(KieSession):工作内存的容器,有状态会话可跨请求保留事实,无状态会话每次执行后清空。
  • 议程(Agenda):待执行规则的队列,按优先级与冲突解决策略排序。
// 有状态会话:适合持续推理(比如实时风控)
KieSession session = kc.newKieSession("riskSession");
// 无状态会话:适合一次性求值(比如审批路由)
StatelessKieSession stateless = kc.newStatelessKieSession("routeSession");
stateless.execute(List.of(order, customer));

选择标准是「规则之间是否需要跨请求共享事实」。审批路由这类「一次输入、一次输出」的场景应该用无状态会话,避免内存泄漏与状态污染。有状态会话必须显式 dispose(),否则会泄漏。

9. 规则版本化与灰度

规则是业务逻辑的一部分,必须像代码一样版本化。三个层次:

  • 规则文件版本化:DMN/DRL 文件放进 Git,走 PR 评审。
  • 部署版本化:引擎部署时生成版本号,流程实例绑定启动时的版本。
  • 生效版本化:支持按时间、按流量、按客户灰度切换规则版本。
# 规则灰度配置
rules:
  risk-assessment:
    active: v12
    canary:
      version: v13
      percent: 5
      match: "customer.tier == 'PLATINUM'"
    rollback: v11

灰度对规则引擎尤其重要,因为规则变更的影响往往是「静默的」:不会报错,只是结论变了。一个把「额度 50 万」误写成「额度 5 万」的规则,会让大量申请被错误拒绝,且没有任何异常日志。所以规则上线必须有「对比运行」(影子模式):新旧规则同时计算,记录差异,人工确认后再切换。

10. 规则的可测试性

决策表最大的优势是「天然可测试」。每条规则行都是一个测试用例:

@ParameterizedTest
@CsvSource({
    "PLATINUM, 50000,  LOW,    false",
    "PLATINUM, 200000, MEDIUM, true",
    "GOLD,     200000, MEDIUM, true",
    "SILVER,   600000, HIGH,   true",
})
void riskLevelTest(String tier, double amount, String expectedLevel,
                   boolean expectedManual) {
    Map<String, Object> result = dmnEngine.evaluate("riskLevel",
        Map.of("customer", Map.of("tier", tier), "amount", amount));
    assertThat(result.get("level")).isEqualTo(expectedLevel);
    assertThat(result.get("manual")).isEqualTo(expectedManual);
}

除了逐行测试,还要测三类边界:规则的覆盖完整性(是否存在没有规则覆盖的输入组合)、规则的互斥性(用 UNIQUE 策略时检查是否有重叠)、规则的单调性(比如「金额越大风险等级不应越低」)。

第三类测试需要业务方参与定义不变量。这些不变量是规则的「业务契约」,比逐行测试更能发现设计错误。

11. 与工作流引擎的集成

规则引擎与工作流引擎的集成有三种模式:

  • 流程中调用规则:BPMN 的 BusinessRuleTask 调用 DMN,把结果写入流程变量,用于后续网关判断。
  • 规则驱动流程:规则的输出直接是「下一步该走哪个节点」,流程由规则表驱动。
  • 规则独立于流程:规则作为服务被调用,流程与规则完全解耦。
<bpmn:businessRuleTask id="riskDecision" name="风险评估"
    camunda:decisionRef="riskLevel"
    camunda:resultVariable="riskResult"
    camunda:mapDecisionResult="singleResult"/>

第二种模式(规则驱动流程)在审批流里很常见:一个「路由规则表」决定「这个申请该走哪几级审批」。它的好处是流程定义保持简单(一个多实例审批节点),复杂度收敛到规则表里。坏处是流程的可视化变得没有意义(图上只有一个节点,实际路径由规则决定)。

建议:网关判断用第一种模式(规则只提供结论,流程决定路径),审批人路由用第二种模式(规则提供列表,流程用多实例展开)。这样既保持了流程的可读性,又把易变的规则抽了出去。

12. 与策略即代码的边界

「策略即代码」(Policy as Code)与规则引擎有重叠但侧重不同:

维度规则引擎策略即代码
目的业务决策合规与治理约束
典型工具DMN、DroolsOPA/Rego、Kyverno、Sentinel
触发点业务流程内部准入控制(K8s、CI、API 网关)
失败语义返回结论拒绝操作
变更主体业务方平台与安全团队

选择原则:业务决策用规则引擎,基础设施与平台的约束用策略即代码。两者的技术可以互相借鉴(比如 OPA 也可以做业务决策),但组织归属不同,混用会导致「谁负责改规则」的争议。策略即代码的实践见 策略即代码与治理 。

一个实际的分工例子:API 网关用 OPA 做「这个租户能不能访问这个接口」的准入判断,业务流程里用 DMN 做「这个订单该不该人工审核」的业务决策。

13. 评分卡与风险模型

评分卡是规则引擎最常见的应用形态:多个因子加权求和得到分数,再按分数分档。

因子            权重    取值规则                    得分
客户等级        30      PLATINUM=100, GOLD=70       按等级线性
历史逾期次数    25      0 次=100, 1 次=50, >=2=0
申请金额        20      < 5万=100, 5-20万=60, >20万=20
渠道风险        15      自有=100, 合作方=70
设备指纹        10      可信=100, 未知=50

总分 = Σ(权重 × 得分),然后按总分分档:>= 80 自动通过,60 到 80 人工复核,< 60 拒绝。

-- 评分卡规则的持久化
CREATE TABLE scorecard_factor (
  id          BIGINT AUTO_INCREMENT PRIMARY KEY,
  card_code   VARCHAR(64) NOT NULL,
  factor_code VARCHAR(64) NOT NULL,
  weight      DECIMAL(5,2) NOT NULL,
  value_expr  TEXT        NOT NULL,   -- FEEL 表达式
  version     INT         NOT NULL,
  UNIQUE KEY uk_card_factor_ver (card_code, factor_code, version)
);

把评分卡做成数据表的好处是「调权重不需要改代码」,业务方可以在管理后台调整并预览效果。代价是需要一套「权重调整的审批与回归」机制,否则权重会被随意调整而无人知晓。

14. 规则的冲突与优先级

规则冲突有三种表现:

  • 重叠:两条规则同时匹配,结论不同。用 FIRST 时结果取决于顺序。
  • 覆盖:一条规则永远无法匹配(被前面的规则遮蔽)。这是静默的 bug。
  • 循环:Drools 里规则 A 触发 B、B 又触发 A,导致无限循环。
// Drools 的冲突解决:显式指定优先级
rule "高优先级规则"
    salience 100
    when ... then ... end

salience 是 Drools 的优先级属性,数值越大越先执行。但依赖 salience 是脆弱的:新增规则时需要重新审视所有优先级。更好的做法是让规则尽量互斥(通过条件设计),把优先级作为最后手段。

检测覆盖与重叠的实用方法是「规则覆盖率分析」:对输入空间做采样或穷举,统计每条规则被命中的次数。命中次数为 0 的规则要么是冗余的,要么是被遮蔽的,都应该被审查。

15. 规则的性能优化

规则引擎的性能瓶颈通常在「模式匹配」而不是「规则数量」。优化手段:

  • 用索引:Drools 的 RETE 网络会对事实建立索引,但要求事实对象的 equals/hashCode 正确实现。
  • 减少事实数量:只插入规则需要的事实,不要插入整个上下文。
  • 用无状态会话:避免跨请求的事实累积。
  • 缓存决策结果:对相同的输入(同样的客户、同样的金额)缓存结论,避免重复计算。
@Cacheable(value = "riskDecision", key = "#customerId + ':' + #amount")
public String evaluateRisk(String customerId, BigDecimal amount) {
    return dmnEngine.evaluate("riskLevel", buildInput(customerId, amount));
}

缓存要注意「规则版本变化时失效」:缓存 key 里应该包含规则版本,或者规则发布时主动清空缓存。忘记这一点会导致「规则改了但结论没变」的诡异问题。

16. 规则的可观测

规则引擎的观测重点是「可解释性」:每次决策都要能回答「哪条规则命中、输入是什么、输出是什么」。

public DecisionResult evaluate(String decisionRef, Map<String, Object> input) {
    DecisionResult result = engine.evaluate(decisionRef, input);
    // 记录决策日志,包含命中的规则 id
    decisionLog.record(DecisionLog.builder()
        .decisionRef(decisionRef)
        .input(input)
        .output(result.getOutputs())
        .matchedRules(result.getMatchedRuleIds())
        .ruleVersion(result.getVersion())
        .durationMs(result.getDurationMs())
        .build());
    return result;
}

三个关键指标:决策耗时(P99)、规则命中分布(哪些规则最常命中)、规则版本分布(有多少请求还在用老版本)。第二个指标能发现「某条规则从未命中」的异常,第三个能确认灰度发布是否生效。

决策日志的存储量可能很大(每次决策一条),要评估保留期与采样策略。合规场景通常要求全量保留一段时间(比如 1 年),之后可降采样。

17. 落地路线图

  • 第 1 周:把现有代码里的 if-else 规则清单化(列成表格),与业务方确认哪些规则易变。
  • 第 2 周:只把「易变且业务方关注」的规则抽成 DMN 决策表,用 FIRST 命中策略。
  • 第 3 周:为决策表写逐行测试与边界测试,接入 CI。
  • 第 4 周:加入决策日志与灰度机制(影子模式对比新旧规则)。

第一步的清单化不要跳过。它本身就是一次「规则审计」,常常能发现互相矛盾或从未生效的规则。很多团队在清单化阶段就解决了问题,不需要引入引擎。

18. 权衡取舍

选择收益代价
决策表(DMN)业务方可读,逐行可测表达力有限,不支持推理
DRL(Drools)支持前向推理与 CEP顺序不直观,调试复杂
表达式(CEL)轻量、性能好只适合单条判断
抽成独立模块无新组件,规则集中业务方仍不能直接改
FIRST 命中策略符合直觉,易于理解规则顺序变成隐式契约
UNIQUE 命中策略强制规则互斥重叠时报错,维护成本高
规则存数据库可在线调整需要审批与回归机制
规则存文件可版本化、走 PR调整需要发版
结果缓存性能提升明显需要处理版本失效

19. 常见坑清单

  1. 把 20 行 if-else 搬进规则引擎,收益为零但多了一个组件要运维。
  2. DMN 输入列类型定义错误(数字写成字符串),区间比较退化成字典序比较。
  3. 用 UNIQUE 策略但规则实际有重叠,运行时直接报错导致流程中断。
  4. 用 FIRST 策略但不做回归测试,插入新规则改变了已有行为。
  5. 规则里写死具体的人或组织(审批人姓名),人员变动后规则失效。
  6. 规则变更不做灰度,一次误改导致大量申请被错误拒绝且无告警。
  7. 规则结果缓存不带版本号,规则更新后缓存未失效,结论不一致。
  8. Drools 有状态会话忘记 dispose(),内存持续增长直到 OOM。
  9. 规则之间存在循环触发(A 触发 B,B 触发 A),引擎陷入无限循环。
  10. 规则从未命中(被前置规则遮蔽),无人发现直到业务投诉。
  11. 决策日志全量落库但没评估量级,半年后表爆掉。
  12. 把基础设施准入控制(K8s 准入、网关鉴权)也塞进业务规则引擎,职责混乱。

20. 小结

规则引擎的价值是「把易变的业务判断从代码里抽出来,让业务方能读、能改、能测」。它只在规则确实易变且由业务方驱动时才值得引入,否则一个结构清晰的配置类就够了。选择表达形式时,决策表覆盖 80% 场景,DRL 只在需要前向推理时使用。

落地时的三条底线:规则必须版本化(走 PR 与回归测试)、规则上线必须能灰度(影子模式对比)、每次决策必须可解释(记录命中规则与输入输出)。这三条做到了,规则引擎的长期维护成本就可控。

下一步建议读 策略即代码与治理 ,把「业务决策」与「平台约束」的边界划清;如果规则是为审批流提供路由,则应该结合 人工任务与审批流表单 一起看,理解规则输出如何驱动多实例审批节点。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「工作流引擎」更多文章

  1. 工作流成本优化
  2. 执行器与资源隔离
  3. 调度、回填与补数