1 关于规则引擎
基于知识库和规则的专家系统是早期最主流的人工智能,不同于现在流行的基于统计、机器学习的智能算法,基于规则的算法相对来说更加直观和易于理解,毕竟如果简单理解的话,就是定义好了If-Than结构,从而让不同的输入得到相应的输出。但从现实角度出发,开发基于规则的业务系统很多时候是必须的,因为很多业务场景就是规定好的处理逻辑,比如一款游戏里,使用某个道具后给角色添加哪种效果,就是要用预先定义好的If-Than逻辑。
正如所有软件工程或设计模式教程里提到的,随着业务逻辑的扩张和更新,通过代码定义判断逻辑将变的越来越复杂并难以维护,因此规则引擎应运而生,其核心思想就是把规则(rule)和数据(fact)分离,我们自身的系统只维护数据的域(Domain),业务逻辑的判断、验证和运行交给规则引擎处理,当然我们还是要编写和维护规则文件的,不过因为规则已经独立于系统,这个工作就可以交给非程序开发者了,并且是在线和动态的。
在游戏应用、金融、航班系统等领域,软件系统面对的主要问题之一就是复杂多变业务规则和庞大并具有实时性的输入数据,因此这些领域规则引擎的应用也更加普遍。
2 关于Drools
2.1 简介
Drools引擎可以说是目前最有名的一个开源规则引擎,按照官网的解释,整个Drools是一个Business Rules Management System (BRMS),Drools引擎是其中的核心组件,基于原生JAVA编写,遵循Apache 协议并且加入Maven中心仓储。
另一方面,Drools又隶属于KIE (Knowledge Is Everything)这个大项目,按照官方教程的说明,KIE包括了多个相互依赖的子项目用以提供业务自动化管理的解决方案,例如OptaPlanner就是把Drools作为了重要的一个子组件(OptaPlanner也是我后面计划记录的)。
2.2 Drools的组件和结构
首先要了解构成Drools引擎处理模式的几个基本成分:
- Rule:业务规则,一个rule必须有一个触发它的condition,以及触发后对应的action,其实就是If-Than的意思
- Fact:就是输入到Drools引擎进行rule匹配的数据,fact不仅仅只是输入引擎的原始数据,也包括某些rule触发后生成的中间fact
- Production memory:Drools引擎内部会将所有的rule信息放在这个空间内
- Working memory: Drools引擎内部会将所有的fact放在这个空间内
- Agenda:在Drools引擎进行执行操作前,会将激活的rule在这个空间进行注册和存储, 正如该单词的字面意思,引擎会把激活的rule按照优先级生成运行序列表,按照顺序依次执行rule
对照上面这张图,Drools引擎大致是这样工作的:引擎启动后,首先进行编译工作,就是把我们定义好的rule加载到Production memory中,我们也可以把这个过程称为"构建知识库";当我们的业务系统生成新的数据并传入到引擎,引擎把这些数据作为fact存储进Working memory;接着引擎开始"模式匹配(pattern matching)"工作,即对Working memory中的fact,匹配rule的condition项,如果匹配了,那么把这个rule加入到Agenda里面;最后是真正的执行操作,引擎会对Agenda里的rule按照优先级进行序列安排,然后执行rule定义好的action操作
2.3 Rete算法解释
2.3.1 关于Rete算法
虽然上面关于Drools引擎处理过程的分析并不是十分繁琐,但是对于初次接触的人来说还是很困惑的,我在看教程的时候就很疑惑,为什么处理If-than逻辑需要处理地这么复杂,尤其是Agenda这个组件,很难理解它的意义。通过查阅官方文档里对算法部分的介绍,其实Drools引擎之所以这样处理,主要是由它的算法决定的。Drools引擎核心的规则算法叫Phreak,这是以Rete算法为基础扩展出的算法。事实上,目前几乎所有规则引擎的核心算法都是以Rete算法及其衍生算法为基础的。因此,如果我们想对Drools理解和使用得更自如,还是有必要先去了解一下Rete算法的,当然没有必要研究地太透彻,主要目的是要了解这个算法的目的、意义和大致流程。通过了解Rete算法我们也可以认识到,基于规则的专家系统并没有想象的简单,它也需要严谨和复杂的数学理论做支持的。
下面这段关于Rete算法的通俗解释我直接参考自这篇文章How the Rete Algorithm Works,该作者就认识Rete算法的发明者,整片文章可谓酣畅淋漓。
我们以一个常旅客计划(航空里程计算)作为例子。航空公司对于乘坐频繁的商务人士有奖励里程服务,具体的奖励里程计算需要遵循非常多的规则,例如下面列出的一些:
- If 去年或今年的奖励里程数 >25,000,than 用户账户评定为白银
- if 去年或今年的奖励里程数 >100,000,than 用户账户评定为黄金
- if 航班里程<500英里,than 奖励500
- if 航班里程>=500, than 奖励实际的航班里程数
- if 如果是商务舱, than 额外奖励本次航班50%的里程
- if 该用户是黄金并且该航班不属于合作公司, than 额外奖励本次航班100%的里程
- if 该用户是白银并且该航班不属于合作公司, than 额外奖励本次航班20%的里程
- if 该用户加入了本月3次航班奖励5000英里计划,并且本月达到了3次航班, than 奖励5000英里
我们可以想象一下,如果我们简单地通过手写代码实现上述业务逻辑,会遇到一个很显著的问题:如何安排这些rule的执行次序? 因为实际上很多rule之间是有关联性的, 比如某次航班后用户奖励了里程后达到了黄金评级,那么相应地又要触发对黄金用户的奖励措施,这样庞大又有关联性的rule库使得手写业务逻辑很难保证和维护正确性和完备性。除此之外,里程计算系统实际处理的不是只有这一个用户,往往是需要同时处理大批量的用户账户的,这使得手写业务逻辑也需要面临性能问题。
2.3.2 Rete算法过程描述
我们用Rete算法的思想来处理这个问题(当然只是说明算法的思想,其具体过程不会很严谨)。首先要做的工作是根据rule库,构建Rete网络。Rete网络Rete算法的核心,它有多个结点构成,每个结点关联了一系列满足相应condition的对象。我们先构造由两个Alpha结点构成的判别树,每个Alpha结点对应了一种类型(域),在这个例子里,有两个对象类型Account(用户账户)和Flight(航班)。
各个rule所定义的condition也作为结点加入到对应的Alpha结点或父结点下,从而扩充判决树。如下所示:
-
Account:
- 评级是白银
- 评级是黄金
- 去年或今年的奖励里程数>25k
- 去年或今年的奖励里程数>100k
- 加入了本月3次航班奖励5000英里计划
- 本月达到了3次航班
-
Flight
- 里程数>=500
- 里程数<500
- 所属公司不是合作公司
- 所属公司是合作公司
- 是商务舱
最后我们需要利用condition分支生成action,这里可能就需要不同的分支进行链接。
这样我们就构建好Rete网络了,如果你有新的rule加入,只需要在原网络基础上增加结点。
接着就对输入的fact进行评价(Evaluate)。Evaluate过程就是把输入的数据在Rete网络里过一遍,从而识别出那些满足condition的rule(也就是激活过程)。我们输入这样一个fact: Joe乘坐非合作公司的经济舱航班从A飞到B,这趟航班有2,419英里,并且他的账户上已经有150k的奖励里程。我们将这个fact输入到Rete网络的根节点,在Account的Alpha结点对应的分支下,因为Joe账户上150k的奖励里程,满足了">100k"的condition,所以"if 去年或今年的奖励里程数 >100,000,than 用户账户评定为黄金"这条rule被激活。在这个例子我们还假设这个规则系统不存储用户评级系统状态并且需要每次运行时重新计算,这样我们就不去激活和用户评级相关的rule。同样的,在Flight这个分支内,激活了"if 航班里程>=500, than 奖励实际的航班里程数"这条rule。"not partner"这个condition虽然满足,但是因为用户评级目前属于未知因此图上有链接的rule尚未激活,如下图所示,激活的路径用a标注。
现在我们可以认为存放激活rule的agenda内有两个rule,真实情况下,agenda内的rule的执行顺序需要根据它们的优先级来判定,这个例子里我们假设右边的这个rule(航班里程>=500)先执行,即奖励了2419的里程。
第一条rule执行后,fact发生了改变,这个变化需要进行传播,也就是再次过一下Rete网络来激活某些rule。原fact的Flight部分没有发生变化,其分支不需要进行重新
Evaluate; Account分支则需要重新Evaluate,当然这个例子里这次重新Evaluate没有引起变化。经过这次fact传播,agenda内仍然剩下一个"if 去年或今年的奖励里程数 >100,000,than 用户账户评定为黄金"的rule,那么继续执行这个rule的action,即将用户评级设为黄金。Account信息再次更新,这次传播将导致"if 该用户是黄金并且该航班不属于合作公司, than 额外奖励本次航班100%的里程"的rule生效,因此最终Joe的奖励里程为4839。如下图所示,激活的路径用b标注。
上面就是Rete算法的一个通俗解释。Rete算法充分运用了推理技术,达到了大规模业务规则下构建评估(Evaluate)和保证状态序列正确性(ordering the statements properly)的快速高效。算法内的这些构建网络、Evaluate、Agenda等过程也正是上文提到的Drools引擎组件和工作模式的由来。
3 写一个Drools的hello world
最后我们写一个应用Drools引擎的helloworld程序来熟悉其开发。我们就假设这样一个业务场景:一个ID证件颁发部门要对公民进行证件颁发,如果这个人的年龄在18岁以下,颁发Child证件,如果他的年龄大于等于18岁,则颁发Adult证件。
3.1 新建工程
打开Intellij Idea, 新建一个Maven工程,选择quickstart的archetype,等待项目完全生成。
在pom.xml里添加drools的依赖,等待相关jar包获取完成。
...other dependencies
org.kie
kie-api
7.32.0.Final
org.drools
drools-core
7.32.0.Final
org.drools
drools-compiler
7.32.0.Final
org.kie
kie-ci
7.32.0.Final
添加resources文件夹, 里面再新建META-INF和rules两个子文件,META-INF是Drools引擎规定的默认结构,存放Drools知识库和会话配置;rules文件夹存放规则文件,这个不是必须的,你可以起个其他的名字,或者干脆不需要这个子文件夹,直接把规则文件放resources下。
3.2 创建域
接着我们定义这个场景需要用到的class。这个业务场景只需要定义一个Person类,有name和age属性
package drools.samples.Domain;
public class Person {
private String name;
private Integer age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
}
3.3 写一个规则文件
在rules子文件下新建一个sample1.drl文件,在里面定义Drools规则。
package drools.samples.rules.sample1
import drools.samples.Domain.Person
rule "Give Child ID card"
when
person: Person(age<18)
then
System.out.println("Give "+person.getName() + " a Child ID card");
end
rule "Give Adult ID card"
when
person: Person(age>=18)
then
System.out.println("Give "+person.getName() + " a Adult ID card");
end
简单描述下:首先需要定义这个规则所属的package,这里我们把它归在"drools.samples.rules.sample1"; 然后用import局导入我们定义的Person类,这样规则引擎才能获取域信息;接着定义了两个rule,每个rule里的when子句就是condition, 我们在里面用person:Person(age<18)这种语法来对fact进行匹配,而then子句的内容就是condition对应的action,这里就简单的打印对应的通知信息。
3.4 配置文件
我们还需要在META-INF下添加一个名为kmodule.xml的配置文件,这是必须的步骤,不然Drools引擎无法知道怎么编译rule库。
kbase结点用于配置规则库或知识库的信息,这里我们指定了一个指向drools.samples.rules.sample1的名为rules1的kbase结点,意思就是这个rules1结点将使用我们在sample1.drl里定义好的规则(sample1.drl里定义的package名就是drools.samples.rules.sample1)来构建知识库;kbase结点下又配置了一个名为sample1的ksession结点,ksession代表了运行时的执行会话,可以在一个kbase下指定多个不同参数的session
3.5 主程序
最后我们写主程序:
public static void main( String[] args )
{
KieServices ks=KieServices.Factory.get();
KieContainer kieContainer=ks.getKieClasspathContainer();
KieSession kieSession=kieContainer.newKieSession("sample1");//就是kmodule.xml里定义的那个ksession
Person person=new Person();
person.setName("Joe");
person.setAge(17);
kieSession.insert(person);//把这个fact传入
kieSession.fireAllRules();//开始规则检验
}
执行结果