可视化规则引擎决策池:规则复用、顺序执行与工程落地
简介:面向规则引擎学习者与业务系统开发者的实操型文档,聚焦“决策池”这一规则复用机制:与按条件分支的决策树不同,决策池会按引用顺序依次执行池内全部复用规则,适合集中管理高频规则并保证执行次序的业务场景。内容以 Visual Rules Solution 旗正规则引擎为操作环境,围绕规则包创建、变量定义、规则编写展开,并延伸至规则包引入决策池、全局变量共享与规则实现,最后借助批量测试、测试用例和测试结果核对各规则的执行状态与输出。整份资料为 1 个 docx 文件,压缩包约 1.19MB,图文步骤与界面截图配合,便于对照软件逐项操作;已有 458 人学习,对初学者可建立规则复用思路,对开发者可作为配置与验证的参考手册。
1. 规则引擎决策池:一次配置、多处复用的规则池到底解决什么问题
上个季度接手一个理赔计算的老系统,最烦的不是逻辑复杂,是同一段「按证件号截取地区码」的规则在七个规则包里各写了一遍,改一次口径要动七处,漏一处就是线上事故。后来改成可视化配置器里的决策池:把这段逻辑抽成独立规则包,其他包只做引用,口径只改一处,剩下六处跟着生效。这篇拆的就是这个机制——一款可视化规则引擎配置器中的决策池,它跟决策树最大的区别在于,决策池里所有被引用的规则都会按引用顺序依次全部执行,不做条件挑拣。适合正在做业务规则配置化、手里有一堆重复规则要收敛的后端和配置人员看。
2. 规则包骨架:工程、规则包、变量与 rpk/rsc 三层对象模型
2.1 从工程到规则包:对象树长什么样
配置器里的组织方式和代码工程几乎一一对应:工程(Project)对应一个业务域,规则包(Rule Package)对应模块,规则对应方法,决策池对应一个「只能顺序执行的方法调用链」,批量测试对应单元测试。原文的示例是在「功能解析」这个工程下建两个包:基本工程2负责放被复用的原子规则,决策池负责引用它们。落到磁盘上大致是这么一棵树:
功能解析/ # 工程:一个业务域的顶层容器 ├── 基本工程2/ # 复用规则包:原子规则都放这里 │ ├── 变量/ # 传入数据 a、b、c,类型均为 string │ ├── 外部调用/ # 从左截取字符、取字符串长度 │ ├── 规则/b取值 │ ├── 规则/c取值 │ ├── 基本工程2.rpk # 设计期源文件,配置器可再打开 │ └── 基本工程2.rsc # 编译产物,运行引擎实际加载的东西 └── 决策池/ ├── 变量/ # 同样要定义 a、b、c ├── 决策池/决策池1 # 复合节点,内部是一个有序引用列表 ├── 批量测试/批量测试1 ├── 决策池.rpk └── 决策池.rsc看清楚这棵树,后面所有问题都好定位:变量和外部调用是包级作用域,规则是包内成员,决策池节点是包内的一个复合成员,批量测试挂在包上而不是挂在单条规则上。初学者最容易把「工程」当作用户隔离边界,其实它只是分组,真正参与编译和运行的最小单位是规则包。
2.2 变量定义:三个 string 变量为什么都挂在「传入数据」下
基本工程2的对象库里要建三个 string 变量 a、b、c,原文明确说都是在「传入数据」里添加的。这跟引擎的变量分域有关:多数可视化规则引擎会把变量分成两类——「传入数据」跟随会话进出,调用方塞进来什么、执行完就能取到什么;「局部变量」只在一次执行内可见,执行完就丢。
| 变量名 | 类型 | 所属域 | 方向 | 用途 |
|---|---|---|---|---|
| a | string | 传入数据 | 入参 | 外部送进来的原始串,比如BJ1101 |
| b | string | 传入数据 | 出参 | 由规则b取值写入的地区码 |
| c | string | 传入数据 | 出参 | 由规则c取值写入的二次加工结果 |
三个都放「传入数据」,是为了让批量测试和外部 API 调用用同一套路径:测试时在用例里直接给 a 填值、给 b 和 c 填期望值;线上调用时用一个 Map 塞 a,执行完从结果 Map 里按名字取 b 和 c。我一般会要求团队把「对外暴露的输出变量」全部放传入数据域,中间量一律用下划线前缀(_tmpLen这类)关在局部域里,避免内部值从结果里漏出去。
类型统一给 string 是这类入门配置的常见做法,代价是数值比较要显式转换。比如你后面想判断「长度大于 4」,不能在表达式里直接拿字符串比大小,得先转数值,否则"10" < "9"会按字典序判成真。
2.3 外部调用:把「从左截取字符」挂进规则包对象库
变量建好之后,规则表达式里想用字符串函数,不能直接写——引擎的内置公式库要先注册到规则包的对象库里,相当于 Java 里给类加 import。原文的做法是打开规则包对象库的「外部调用」,从「字符截取」分类里挑「从左截取字符」,再补一个方法,确认后对象库的「外部调用」节点下就多了两个方法,规则里才能调它们。
这一步的实际含义,等价于给规则包挂了一个静态工具类:
// 规则表达式里写的是 b = left(a, 2),引擎编译后等价于调用内置函数库 public final class StringFuncs { // 从左截取:从下标 0 开始取 len 个字符 // len <= 0 返回空串;len 超过原串长度时返回原串(不补位、不报错) public static String left(String src, int len) { if (src == null || len <= 0) return ""; return src.length() <= len ? src : src.substring(0, len); } // 第二个方法一般配「取字符串长度」或「从右截取」,看你要拼什么 public static int length(String src) { return src == null ? 0 : src.length(); } }两个参数要留意:src传的是规则变量 a,len是字面量 2,改成 3 就取三位。截取单位是字符不是字节,北京1101取两位得北京,取一位得北,不会出现把一个汉字劈成半个的乱码。如果业务口径是按字节截,这两个方法就不够用,得自己在函数库里加一个按字节处理的版本,别硬套。
外部调用是包级作用域这件事,是这个机制里最容易翻车的地方,第 5 章会专门展开。
2.4 保存与编译:rpk 和 rsc 分别是什么
规则写完点「全部保存」,消息栏会提示保存成功并生成 rsc。这两个文件的角色必须分清,否则调试能耗掉一下午:
- rpk:规则包源文件,通常是 XML 或 JSON 结构,里面装着变量定义、规则节点、外部调用注册、决策池的引用列表、批量测试用例。它是设计期产物,配置器双击就能重新打开编辑。
- rsc:编译产物,把 rpk 里的可视化配置翻译成引擎运行时能直接加载的结构。运行侧加载的是它,不是 rpk。
一次「保存全部」实际走了两步:先落盘 rpk,再编译出 rsc。真正决定线上行为的,是最后这一步有没有发生。我在项目里见过太多次「改了配置、点了保存、测试环境没变」的现场,追到最后全是 rsc 没重新生成,引擎加载的还是几小时前那份。
3. 复用规则编写:b 取值与 c 取值的赋值链路怎么落地
3.1 b 取值:一条赋值语句的四个要素
在基本工程2上右键新增规则,命名b取值,进规则编辑区给变量 b 赋值。可视化配置里看着是拖控件、点选变量,落到执行层面就是一条赋值语句,四个要素必须齐:目标变量、表达式、类型、空值策略。
// 规则 b取值 的等价执行逻辑 // 目标变量:b(string,传入数据域) // 表达式:left(a, 2) String b = StringFuncs.left(a, 2); // 代入实际数据 // a = "BJ1101" -> b = "BJ" // a = "SH02" -> b = "SH" // a = "" -> b = "" (空串原样返回,不抛异常) // a = null -> b = "" (看函数库实现,有的版本返回 null)参数怎么改:2是硬编码的截取长度,如果业务上不同渠道长度不同,别在这里写死,改成读另一个变量(比如len,同样放传入数据域,由调用方传),表达式变成left(a, len)。类型这块,b 声明成 string 就不会有隐式转换问题;如果误声明成数值型,BJ这种结果会直接赋值失败或者变成 0,测试用例立刻能暴露。
空值策略值得单独说一句。a 传空串时返回空串还是返回 null,取决于函数库实现,很多版本两者行为不一致。我的习惯是:在b取值里明确加一条前置判断,a 为空就直接给 b 赋空串并跳过截取,这样下游c取值拿到的永远是确定的字符串,不用到处判 null。
3.2 c 取值:依赖 b 的二次加工
c取值是同一规则包里再加的一条规则,逻辑建立在 b 之上。原文只说了「在规则中添加规则逻辑」,具体表达式没给,按这个示例的数据流,最合理的写法是拿 b 再拼上原串长度做校验位:
// 规则 c取值 的等价执行逻辑 // 依赖 b 已经算完 String c = b + "_" + StringFuncs.length(a); // 代入实际数据 // a = "BJ1101" -> b = "BJ" -> c = "BJ_6" // a = "SH02" -> b = "SH" -> c = "SH_4"这里用到了第二个外部方法length,也正好解释了为什么对象库里要挂两个方法而不是一个——一条规则里可以调多个内置函数,缺一个就编译不过或者运行时报「方法未定义」。
关键点在依赖关系:c 的表达式里出现了 b。如果 b 还没赋值,c 算出来的就是_6(b 是空串)或者null_6(b 是 null)。这个错误在单包测试时不一定暴露,因为你可能先手动跑了一遍 b;但一进决策池按顺序执行,问题立刻现形。
3.3 依赖顺序:规则名不等于执行顺序
很多人下意识觉得规则包内的规则会按名字排序执行,b取值在c取值前面是「碰巧对」。实际上多数配置器是按规则在包内的排列顺序(也就是你创建的先后、或者规则列表里的上下位置)来触发,字母序靠不住。血泪经验:直接在c取值已经存在的情况下回头新建b取值,新规则很可能排到列表末尾,单包跑一遍就翻车。
稳妥做法有两条,任选其一:
- 建规则的顺序 = 依赖顺序,先建
b取值再建c取值,建完在规则列表里确认一眼上下位置。 - 不依赖包内隐式顺序,把 b 的计算直接内联到
c取值的第一行,c = left(a,2) + "_" + length(a),牺牲一点复用性换确定性。规则超过五条、依赖超过两层的时候,我更倾向这条。
3.4 编译产物核对:rsc 时间戳是最便宜的验证手段
规则写完、保存全部之后,消息栏会提示 rpk 保存、rsc 生成。想确认编译真的发生,不用打开配置器,直接看文件时间:
# 只盯这两个文件的修改时间,编译有没有真的发生一眼就看出来 ls -l --time-style=full-iso 基本工程2.rpk 基本工程2.rsc # 期望结果:rsc 的时间 >= rpk 的时间 # 如果 rsc 时间明显更早,说明只保存了没编译,线上跑的还是旧逻辑这个习惯救过我很多次。规则包多了以后,你根本记不住上一次点的是「保存」还是「保存全部」,而这两者的差别在生产环境就是「新逻辑生效」和「一切照旧」。顺手把 rpk 和 rsc 一起提交到版本库,出问题能直接 diff 两版 rpk,看清楚是谁改了哪条表达式。
4. 决策池搭建:拖入顺序就是执行顺序
4.1 新建决策池规则包并补齐同名变量
基本工程2编译通过之后,在同一个工程下新建决策池规则包,然后再定义一遍a、b、c 三个 string 变量,同样放传入数据域。这一步新手最容易省,觉得「引用别人包的规则,变量应该自动继承」,结果一执行就发现拿不到值。
原因在于:决策池包是一个独立的执行单元,引擎为它开自己的会话,会话里只有它自己声明的变量。它调基本工程2的规则时,是把同名变量按名字传进去、执行完再按名字取回来。所以决策池包里的 a、b、c 不是冗余,是它对外的输入输出契约。两个包的变量名、类型必须严格一致,差一个大小写都映射不上。
4.2 从「所有规则」拖入:引用列表就是一个有序数组
决策池的添加方式和普通规则不一样:不是「新建规则」,而是在包里添加一个「决策池」节点,然后在它的属性栏「所有规则」里找到基本工程2,展开,把b取值和c取值依次拖进决策池。拖完节点里就多了两条引用,顺序就是拖进去的先后。
配置器里看到的是一个树形节点,落到底层存的就是一个有序数组:
{ "nodeType": "decisionPool", "name": "决策池1", "mode": "sequentialAll", "refs": [ { "pkg": "基本工程2", "rule": "b取值", "order": 1 }, { "pkg": "基本工程2", "rule": "c取值", "order": 2 } ] }mode: sequentialAll是决策池的语义核心——不做条件挑拣,全部执行;order就是拖入顺序,也是执行顺序。这个结构带来两个直接结论:
- 想调整执行顺序,不用改任何规则逻辑,在决策池节点里上下拖动引用即可。这是决策池比决策树灵活的地方。
- 同一条规则可以被拖进来两次,它就会执行两次。有时候这是特性(比如两轮校验),更多时候是误操作,看到输出重复两遍的时候先回来数一下 refs 长度。
4.3 决策池 vs 决策树 vs 决策表:三种复用单元的分工
同一个引擎里三种复用单元经常被混用,选错了后面全是补丁。按原文的定义,决策树能设复用规则的执行条件和规则之间的条件,决策池则是全执行、只保证顺序。
| 维度 | 决策池 | 决策树 | 决策表 |
|---|---|---|---|
| 复用什么 | 整条规则 | 规则 + 执行条件 | 行级的条件组合 |
| 执行方式 | 全部执行,按引用顺序 | 按条件选分支 | 命中行才执行 |
| 能否设执行条件 | 不做条件挑拣 | 可以,规则间条件也能设 | 行本身即条件 |
| 顺序敏感度 | 极高,顺序错就错 | 中,取决于分支 | 低,各行独立 |
| 典型场景 | 公共赋值链、前置校验、口径统一 | 多分支决策、审批流走向 | 保费/折扣这类多维组合 |
选型的一句话判断:结果只跟输入有关、且每条都要跑,用决策池;结果取决于走哪个分支,用决策树;结果是多个维度的组合命中,用决策表。原文这个示例属于第一种,截取 + 拼接,两条都得跑,天然适合决策池。
4.4 变量映射:同名传参是默认,不同名要显式配
决策池引用外部规则时,变量怎么传?常见做法是按名字自动映射:决策池会话里的 a 传给被引用规则包的 a,被引用包算出的 b 再回写到决策池会话的 b。同名同类型,零配置就能跑通,这也是原文示例能一路走通的原因。
一旦名字不完全一致,就得在决策池节点的属性里显式配输入输出映射。踩过的坑是:基本工程2里把 b 改名成regionCode,决策池那边没跟着改映射,跑出来 b 全是空——不报错,只是结果不对,这种「静默失败」最难查。所以改名这种操作,我一般要求同一次提交里把引用方一起改完,改完立刻跑一遍批量测试。
5. 批量测试与常见问题排查:用例、期望值和五类翻车现场
5.1 批量测试挂载与用例录入
规则写完不代表对,原文的做法是给规则包添加「批量测试」,在批量测试下添加若干测试用例,每条用例填输入和期望结果,保存并编译后执行,系统逐条比对期望值与实际输出,全一致才算通过。
操作路径是:选中规则包 → 添加批量测试 → 在该节点下添加测试用例 → 填 a 的输入值、b 和 c 的期望值。三条用例足够覆盖这个示例:
| 用例 | 输入 a | 期望 b | 期望 c | 覆盖点 |
|---|---|---|---|---|
| case_1 | BJ1101 | BJ | BJ_6 | 常规路径 |
| case_2 | SH02 | SH | SH_4 | 短串,长度不足 4 |
| case_3 | 空串 | 空串 | _0或空串 | 边界,取决于空值策略 |
case_3 这种边界用例价值最高。它逼你在动手前就想清楚「a 为空时 b 该是什么」,而不是等线上报错再回头补判断。
5.2 期望值比对:测试通过的唯一标准是什么
用例录完,点「全部保存」,让 rpk 落盘并生成 rsc,然后执行批量测试。结果页会列出每条用例的期望值和实际输出值,一致标通过,不一致标失败。
比对逻辑要注意两点。第一,string 是严格相等比对,期望值里手滑多敲一个空格,"BJ"和"BJ "就不相等,测试红一片,但规则其实是对的。第二,比对发生在编译之后,你改了规则没编译,跑的还是旧 rsc,可能出现「测试全绿但线上错」的幻觉。
这两点合起来给一个操作纪律:改规则 → 保存全部 → 核对 rsc 时间 → 跑批量测试,四步固定顺序,不要跳。
5.3 排查一:只保存没编译,改了等于没改
现象:改了b取值的截取长度,从 2 改到 3,批量测试跑出来还是旧结果;或者消息栏只有「保存成功」没有「编译成功」。
原因:rpk 更新了,rsc 还是上一版,引擎加载的一直是 rsc。配置器里「保存」和「保存全部」在部分版本上行为不同,前者只落 rpk。
解决:统一走「保存全部」,并在执行测试或发布前核对 rsc 时间戳,rsc早于rpk就说明没编译。团队里把这个检查写进发布清单,比靠记忆靠谱。
5.4 排查二:c 被拖到 b 前面,输出变成_6
现象:单包测试通过,一进决策池,c 的结果变成_6或者null_6,b 单独看是对的。
原因:决策池按引用顺序执行,c 在前面时 b 还没赋值,b + "_" + length(a)里的 b 是空串或 null。
解决:在决策池节点里把b取值拖到c取值上面;或者在c取值里做防御,先判 b 是否为空,为空就自己算一遍left(a,2)。前者治本,后者治标,赶工期时先治标也行,但要在规则注释里写清楚。
5.5 排查三:外部调用没在两个包里都注册
现象:基本工程2单独测全绿;把规则拖进决策池,执行报「方法未定义」或者「找不到函数 left」。
原因:外部调用是包级作用域,基本工程2的对象库里注册了left,决策池的对象库里没有。规则在决策池会话里执行时,查的是决策池自己的函数表。
解决:两个包的对象库都注册同名方法;包多了以后更好的做法是建一个「基础函数包」,把公共函数集中注册,其余包统一引用它,改一处全局生效。
5.6 排查四:用例只填输入不填期望
现象:批量测试一片通过,看着很舒服,上线第二天就出问题。
原因:期望值留空时,比对逻辑要么跳过该字段,要么把空值当成「不比」,测试等于只验证了「没抛异常」。
解决:每条用例的每个输出变量都必须填期望值,哪怕填「空串」也要显式写出来。加一条硬规矩:用例数少于三条、或没有边界用例的规则包,不允许发布。
5.7 排查五:同名变量撞车,中间值漏到输出里
现象:决策池返回的结果 Map 里多出了几个你没打算暴露的字段,或者数字被当成字符串拼进去,6变成了"6"。
原因:决策池包自己也定义了 a、b、c,和复用包同名变量在映射时按名字对齐;只要你在决策池里加了个同名中间变量,它就会跟着进结果集。另外所有变量都是 string 类型,数值参与拼接会自动转字符串。
解决:中间量统一加前缀_tmp,对外只暴露 a、b、c 三个;需要数值运算的地方,在表达式里显式转数值再比,别指望引擎隐式转换。
6. 二次开发:把决策池接进自己的服务并做可回滚的灰度
配置器里跑通只是第一步,真要落地得接到 Java 服务里。我的做法是包一层很薄的 Runner,把引擎句柄藏起来,对外只暴露「传 a、拿 b 和 c」这一个方法:
public class DecisionPoolRunner { // 引擎加载 rsc 后拿到的知识库句柄,随应用启动初始化一次,别每次 new private final KnowledgeBase kb; public DecisionPoolRunner(KnowledgeBase kb) { this.kb = kb; } public PoolResult run(String inputA) { Map<String, Object> in = new HashMap<>(); in.put("a", inputA); // 只传入口变量,其余留空由规则算 // 执行指定规则包,返回值是该包传入数据域变量的快照 Map<String, Object> out = kb.execute("决策池", in); PoolResult r = new PoolResult(); r.setB((String) out.get("b")); // 按变量名取,注意空值强转 r.setC((String) out.get("c")); return r; } }类名和execute的签名按你手上版本的实际包名替换,别照抄。三个关键点:知识库句柄单例复用,规则包名要和配置器里完全一致,取结果时对 null 做兜底。
改造清单我都按这张表过一遍:
| 改造点 | 做法 | 验证手段 |
|---|---|---|
| 版本管理 | rpk 进 Git,rsc 要么一起提交要么由构建流水线编出来 | 每次发布前 diff rpk,看引用列表有没有变 |
| 执行埋点 | 在引擎监听器里按 refs 顺序打每条被引用规则的耗时和输出 | 单次调用日志能看到b取值 12ms / c取值 3ms |
| 灰度比对 | 老代码和新 rsc 并行跑,输出不一致只告警不阻断 | 采样一周,diff 率归零再切流量 |
| 用例沉淀 | 把线上采样输入导成批量测试用例,随包一起提交 | 每次改完决策池先跑一遍批量测试 |
灰度这块值得多花两句。规则配置化和写代码最大的差别是:代码 diff 看得见,配置改动在 rpk 里也看得见,但业务方永远说不清「这个口径改了会影响哪些下游」。所以并行跑 + 输出 diff 是最省事的后悔药,比对不一致的样本直接变成新的测试用例,一轮下来覆盖度自己就上去了。
从那以后我每次动决策池里的引用顺序,都强制先把批量测试跑一遍,再去看一眼 rsc 和 rpk 的时间戳,两个都对上才敢提发布单。这套动作看着笨,但确实让我少加了很多班。希望帮到你。
本文还有配套的精品资源,点击获取