规则引擎

7,540

概念

规则引擎其实不是新兴起的东西了,这个概念的提出其实可以追溯到上世纪,规则引擎起源于基于规则的专家系统,专家系统这东西属于人工智能的范畴了,模仿人类的推理方式的推理引擎,是将逻辑规则应用于知识库以推断新信息的系统的一个组成部分。

先来看一下这个场景:

之前做会员场景的时候会遇到这种需求,根据用户购买的商品价格和品类,赠送不同的积分。

商品价格赠送积分
<100赠送20
<500赠送80
>500赠送200

如果我们写代码实现的话

const addPointsByOrderPrice = (order)=>{

if(order.price > 500 ){

add(order.account_id, 200);

} else if (order.price >100){

add(order.account_id, 80);

} else {

add(order.account_id, 20);

}

}

过了几天,运营同学说,这个效果不太好,我们再改下规则,变成100-200元的赠送20,200-500元赠送90,500-800赠送100.....如此类推一直到1w。

遇到这样的需求也是没办法了,总不能写n个if else。那就会想着,我把这个规则写配置中心或者数据库,不就ok了吗?

然后就有了以下的代码

let rules = await db.select("select * from rules");

const addPointsByOrderPrice = (order)=>{

for(let rule of rules){

if(order.price > rule.price){

add(order.account_id, rule.point);

break;

}

}

}

然后后面产品又脑洞大开了,觉得这个条件不够精细化,还要根据商品的标签属性,用户标签来判断。如果是新用户就乘二,如果商品是xx活动的特卖商品,积分就加多20。

这样下去正常人非得被产品需求搞死了。可以看出这里活动规则和代码已经耦合在一起了,即使用上配置中心,很多时候还是需要开发同学写硬编码去开发。

这时候就需要有个声明式语言,能把这个规则和玩法描述清楚。规则引擎主要完成的就是将业务规则从代码中分离出来。

在规则引擎中,利用规则语言将规则定义为if-then的形式,if中定义了规则的条件,then中定义了规则的结果。规则引擎会基于数据对这些规则进行计算,找出匹配的规则。这样,当规则需要修改时,无需进行代码级的修改,只需要修改对应的规则,可以有效减少代码的开发量和维护量。

目前最著名的开源规则引擎是Drools。

RETE算法

这是Drools里用到的一个核心算法。Rete算法是一种前向规则快速匹配算法,是一个用于产生式系统的高效模式匹配算法,其匹配速度与规则数目无关。解决的是两个问题:

  1. 大量重复的condition匹配效率的问题
  2. 规则冲突规范的问题

通过这个算法我们可以了解规则引擎的核心思想。

首先解释几个关于规则引擎的概念:

Fact(事实):对象之间和对象属性之间的关系

Rule(规则):由条件和结论构成的推理语句,一般表示为if…Then。一个规则的if部分称为LHS(left-hand-side),then部分称为RHS(right hand side)。

Module(模式):就是指IF语句的条件。这里IF条件可能是有几个更小的条件组成的大条件。模式就是指的不能在继续分割下去的最小的原子条件。

DSL(领域语言):领域专家只需要业务,而不需要关注技术

假设系统中有N条规则,平均每个规则的条件部分有P个模式,在某个时点有M个事实需要处理。则规则匹配要做的事情就是: 对每一个规则r,判断当前的事实o是否满足LHS(r)=True,如果满足,则将规则r的实例r(o),即规则+满足该规则的事实,加到冲突集中等待处理。 通常采取如下过程:

  • 从N条规则中取出一条r;
  • 从M个事实中取出P个事实的一个组合c;
  • 用c测试LHS(r),如果LHS(r(c))=True,将RHS(r(c))加入队列中;
  • 如果M个事实还存在其他的组合c,goto 3;
  • 取出下一条规则r,goto 2;

实际的问题可能更复杂,在规则的执行过程中可能会改变RHS的数据,从而使得已经匹配的规则实例失效或者产生新的满足规则的匹配,形成一种“动态”的匹配链。

目前常见的模式匹配算法包括Rete、Treat、Leaps,HAL,Matchbox等。

假设一个场景,物流分货物。货物的属性有重量,种类,收货地,是否加急。

按照规则引擎的三大概念来划分:

StockFact:该对象存放货物的基本信息。

SelectStockRule: 筛选规则


IF:

Weight < 10kg

Category != 易燃物

收货地 != 新疆

THEN

交给xx航班空运

Rete算法划分

image.png Agenda:一旦一个业务对象匹配了一个规则,会形成该规则和该业务对象的一个议程。即StockFact要把该货物放在空运的事件。

Execution-Engine:业务对象匹配上一个规则后,业务对象执行规则结果的执行器。即将StockFact该货物放在空运中事件的执行器

Rete 算法可以被分为两个部分:规则编译和规则执行 。当 Rete 算法进行事实的断言时,包含三个阶段:匹配、选择和执行,称做 match-select-act cycle。本质上是利用空间换换时间,会消耗较多内存。

AlphaNode:例如。Weight < 10kg 这就是一个 alpha node 节点,当一条规则有多条字面条件,这些字面条件被链接到一起。

BetaNode:用来对2个对象进行对比、检查。比如这两个条件的组合

Weight < 10kg

&&

Category != 易燃物

约定BetaNode的2个输入称为左边(Join Node)和右边。左边通常是一个a list of objects,右边(NotNode)通常是 a single object。每个Bate节点都有自己的终端节点等组成。BetaNode 具有记忆功能。左边的输入被称为 Beta Memory,会记住所有到达过的语义。右边的输入成为 Alpha Memory

一个规则引擎最重要的是Patten Matcher。我们经常用的正则表达式就是一种模式匹配。

Patten Matcher流程展示 image.png 1.顺序为ABCDE。ABD为Alpha节点,CD为Beta节点。

2.从WM中拿出Fact,首先先匹配A,A如果条件符合,把Fact的引用存到A的Alpha Cache。A没有左引用的Beta节点,退出。

3.然后匹配B,B如果条件符合,把Fact的引用存到B的Alpha Cache。然后B有左引用的Beta节点,就是C。

4.C节点作为Beta节点,有左引用的节点A,然后找下Alpha Cache有没有A节点的引用。如果有,那就存到C节点对应的Beta节点。退出

5.到D节点的匹配,如果符合条件,把Fact的引用存到D的Alpha Cache。同样找到D的左引用Beta节点E,E的左引用为C。如果C的Beta Cache是否有C的引用,如果有证明所有条件符合,E的引用存在BetaCache中

6.该Fact匹配了这些规则,形成了一个议程,放入冲突区并执行结果。

那如果有多个规则被触发呢?

我们定义了多个规则,每个规则都有不同的显着性,当一条消息被断言时,它们将按照显着性(从高到低)的顺序触发。

PS:显着性是一个可以在规则上指定的选项,赋予它优先级并允许开发人员对激活的冲突解决方案进行一些控制。

Nodejs开源组件-Nools

上github链接noolsjs/nools。首先要说明的是,Nools这个项目已经停止维护一段时间了,甚至谷歌都没有搜出什么有用的中文文档,都是有段历史的的外文文档,但是官方文档齐全。因此这里不建议大规模使用在生产环境,可能会误伤。

虽然不太能用,但是用他来理解规则引擎的概念也是比较好的,毕竟Nools也是使用Rete算法实现的。

var nools = require("nools");

var flow = nools.compile("./Demo2.nools"),

Param = flow.getDefined("Param");

session = flow.getSession(new Param(11))

.on('assert', function (result) {

console.log(result);//回调返回assert的内容,输出 result2

})

.on('modify', function (result) {

console.log(result, result.param1);//回调返回修改的值,输出{param1:21} 21(无参数note)

})

.match().then(function () {

console.log('end');//不管是否有规则满足都执行,输出end

});

可以看到这里有几个状态,assert,modify。

根据官方文档描述的是

  • assert (fact) - emitted when facts are asserted
  • retract (fact) - emitted when facts are retracted
  • modify (fact) - emitted when facts are modified
  • fire (name, rule) - emitted when an activation is fired.

使用这几个状态,我们可以轻易的在nools文件里控制网络的流向。

假设目前有nools文件如下

define Param { //作为规则的输入消息
param1 : 0,
note :'test for nothing',
constructor : function(p1){
    this.param1 = p1;
}

}

rule condition1 { //定义规则condition1

when { p:Param p.param1 <= 10;}

then { assert("result111");}

}

rule condition2 { //定义规则condition2

when { p:Param p.param1 > 10 && p.param1 <= 20;}

then {
//这些事件的触发都可以是多个
assert('result2'); //触发assert事件
//fire(p);
modify(p,function(){ //触发modify事件
p.param1 -= 10;
});
}

}

它定义了一个两个rule,一个是params <= 10, 一个是 params > 10 && params <=20;

这里不贴图了,直接看代码演示,从DSL语法来更近一步了解Rete网络。

场景设计

现在我们要设计一个抽奖规则可视化。

image.png

除了基础服务如抽奖服务,用户服务,还应该要考虑以下的问题:

  • 可视化规则编辑器
  • 规则自动化测试框架
  • 事实浏览器
  • mock服务
  • 规则管理和版本
  • 用户不敏感状态的缓存
  • 规则执行

如果要做到能用的话,这里仅需要关注AST和规则引擎即可。

image.png 第一部分是可视化规则编辑,虽然规则引擎把逻辑和业务代码分离开,但是DSL真的太不好阅读了。目标当然是方便配置,甚至可以不用开发介入就可以完成规则的配置。假设设计为只需要写这么一行简单易懂的话,就可以生成具体的DSL给规则引擎。

If

(老用户 为 真

和

活跃天数 大于 20)

或

新用户

Then

抽奖机会 + 1

这段话就很简单易懂。这里要引入Parse模块,把这段话转化成DSL。

其实就是语法解析,这里考虑使用Pegjs这个库实现。这里不做进一步的介绍了,有兴趣可以参考pegjs.org/online。更高阶的…

第二部分是规则引擎,那就可以自由选择nools或者json-rules-engine来实现。(json-rules-engine也提供了对应的开源编辑器,如果图方便可以直接用这个可视化编辑,虽然我个人觉得很难用)。

以上提供了一个规则生成器的思路,往细里分析的话会发现,以上我定义的都是静态规则,实际生产环境上一定会有动态规则的出现,这里就交给大家在实际业务中去思考了。

杂谈

业界里使用规则引擎的场景并不少,特别是现在中台概念提出一段时间后,很多复杂逻辑都整合到一个大型系统中。规则引擎的引入能减少很多代码复杂度和编码工作。

规则引擎Tob的标杆就要数IBM的iLog,跟中移动的合作公司的朋友了解过,广东移动上新的计费和套餐就是用这一套的,很多保险公司也是在用iLog。像淘宝美团电商类的优惠券规则,也都会用到规则引擎,不过一般都是自研的。银行的金融风控也会有规则引擎,甚至一些APP像抖音,BigoLive这种短视频APP里也会有规则引擎,模型去做识别,然后作为动态规则去加载对应的图形。

虽然网上查规则引擎,基本都是出来Drools,但是很多时候实际选型上根本不会选它,主要因为

  • Drools相关的组件太多,需要逐个研究才知道是否需要
  • Drools逻辑复杂,不了解原理,一旦出现问题排查难度高
  • Drools需要编写DSL(领域特殊语言)文件,学习成本高

所以像美团,淘宝系这些团队在规则引擎的选型上会更倾向于使用动态脚本引擎,像Groovy,aviator,easyRule这些,这些引擎DSL的写法跟java相似,组件生态跟java无缝贴合。

小规模的规则引擎,还是推荐自己写或者用轻量级的框架要来的快,毕竟很多时候都不会接触到上百万条规则的场景。如果是自己写的话可以考虑直接设计决策树,又能兼容可视化又方便遍历。js的组件可以参考一下CacheControl/json-rules-engine也是一个不错的选择。

遗留问题:

  • 如何选定合适组件
  • 规则可视化配置
  • 规则维护
  • 规则执行效率

引用文档

PEG.js 介绍与基础使用

nools

[转]规则匹配--Rete 算法原理及实现_我的代码管家---houwenbin的专栏-CSDN博客_rete

别再说你不懂规则引擎啦!

RETE算法初窥

字节跳动校/社招投递链接: job.toutiao.com/s/SYWV68P