規則引擎Drools在貸后催收業務中的應用
在日常業務開發工作中我們經常會遇到一些根據業務規則做決策的場景。為了讓開發人員從大量的規則代碼的開發維護中釋放出來,把規則的維護和生成交由業務人員,為了達到這種目的通常我們會使用規則引擎來幫助我們實現。
本篇文章主要介紹了規則引擎的概念以及Kie和Drools的關系,重點講解了Drools中規則文件編寫以及匹配算法Rete原理。文章的最后為大家展示了規則引擎在催收系統中是如何使用的,主要解決的問題等。
一、業務背景
1.1 催收業務介紹
消費貸作為vivo錢包中的重要業務板塊當出現逾期的案件需要處理時,我們會將案件統計收集后導入到催收系統中,在催收系統中定義了一系列的規則來幫助業務方根據客戶的逾期程度、風險合規評估、操作成本及收益回報最大原則制定催收策略。例如“分案規則” 會根據規則將不同類型的案件分配到不同的隊列,再通過隊列分配給各個催收崗位和催收員,最終由催收員去進行催收。下面我會結合具體場景進行詳細介紹。
1.2 規則引擎介紹
1.2.1 問題的引入
案例:根據上述分案規則我們列舉了如下的規則集:
代碼實現:將以上規則集用代碼實現
業務變化:
- 條件字段和結果字段可能會增長而且變動頻繁。
- 上面列舉的規則集只是一類規則,實際上在我們系統中還有很多其他種類的規則集。
- 規則最好由業務人員維護,可以隨時修改,不需要開發人員介入,更不希望重啟應用。
問題產生:
可以看出如果規則很多或者比較復雜的場景需要在代碼中寫很多這樣if else的代碼,而且不容易維護一旦新增條件或者規則有變更則需要改動很多代碼。
此時我們需要引入規則引擎來幫助我們將規則從代碼中分離出去,讓開發人員從規則的代碼邏輯中解放出來,把規則的維護和設置交由業務人員去管理。
1.2.2 什么是規則引擎
規則引擎由推理引擎發展而來,是一種嵌入在應用程序中的組件, 實現了將業務決策從應用程序代碼中分離出來,并使用預定義的語義模塊編寫業務決策。
通過接收數據輸入解釋業務規則,最終根據業務規則做出業務決策。常用的規則引擎有:Drools,easyRules等等。本篇我們主要來介紹Drools。
二、Drools
2.1 整體介紹
2.1.1 KIE介紹
在介紹Drools之前我們不得不提到一個概念KIE,KIE(Knowledge Is Everything)是一個綜合性項目,將一些相關技術整合到一起,同時也是各個技術的核心,這里面就包含了今天要講到的Drools。
技術組成:
- Drools是一個業務規則管理系統,具有基于前向鏈和后向鏈推理的規則引擎,允許快速可靠地評估業務規則和復雜的事件處理。
- jBPM是一個靈活的業務流程管理套件,允許通過描述實現這些目標所需執行的步驟來為您的業務目標建模。
- OptaPlanner是一個約束求解器,可優化員工排班、車輛路線、任務分配和云優化等用例。
- UberFire是一個基于 Eclipse 的富客戶端平臺web框架。
2.1.2 Drools介紹
Drools 的基本功能是將傳入的數據或事實與規則的條件進行匹配,并確定是否以及如何執行規則。
Drools的優勢:基于Java編寫易于學習和掌握,可以通過決策表動態生成規則腳本對業務人員十分友好。
Drools 使用以下基本組件:
- rule(規則):用戶定義的業務規則,所有規則必須至少包含觸發規則的條件和規則規定的操作。
- Facts(事實):輸入或更改到 Drools 引擎中的數據,Drools 引擎匹配規則條件以執行適用規則。
- production memory(生產內存):用于存放規則的內存。
- working memory(工作內存):用于存放事實的內存。
- Pattern matcher(匹配器):將規則庫中的所有規則與工作內存中的fact對象進行模式匹配,匹配成功后放入議程中
- Agenda(議程):存放匹配器匹配成功后激活的規則以準備執行。
當用戶在 Drools 中添加或更新規則相關信息時,該信息會以一個或多個事實的形式插入 Drools 引擎的工作內存中。Drools 引擎將這些事實與存儲在生產內存中的規則條件進行模式匹配。
當滿足規則條件時,Drools 引擎會激活并在議程中注冊規則,然后Drools 引擎會按照優先級進行排序并準備執行。
2.2 規則(rule)
2.2.1 規則文件解析
DRL(Drools 規則語言)是在drl文本文件中定義的業務規則。主要包含:package,import,function,global,query,rule end等,同時Drools也支持Excel文件格式。
function
規則文件中的方法和我們平時代碼中定義的方法類似,提升規則邏輯的復用。
使用案例:
query
DRL 文件中的查詢是在 Drools 引擎的工作內存中搜索與 DRL 文件中的規則相關的事實。在 DRL 文件中添加查詢定義,然后在應用程序代碼中獲取匹配結果。查詢搜索一組定義的條件,不需要when或then規范。
查詢名稱對于 KIE 庫是全局的,因此在項目中的所有其他規則查詢中必須是唯一的。返回查詢結果ksession.getQueryResults("name"),其中"name"是查詢名稱。
使用案例:
規則:
全局變量global
通過 KIE 會話配置在 Drools 引擎的工作內存中設置全局值,在 DRL 文件中的規則上方聲明全局變量,然后在規則的操作 ( then) 部分中使用它。
使用案例:
規則屬性
模式匹配
當事實被插入到工作內存中后,規則引擎會把事實和規則庫里的模式進行匹配,對于匹配成功的規則再由 Agenda 執行推理算法中規則的(then)部分。
- when
規則的“when”部分也稱為規則的左側 (LHS)包含執行操作必須滿足的條件。如果該when部分為空,則默認為true。如果規則條件有多個可以使用(and,or),默認連詞是and。如銀行要求貸款申請人年滿21歲,那么規則的when條件是Applicant(age < 21)
- then
規則的“then”部分也稱為規則的右側(RHS)包含在滿足規則的條件部分時要執行的操作。如銀行要求貸款申請人年滿 21 歲(Applicant( age < 21 ))。不滿足則拒絕貸款setApproved(false)
內置方法
Drools主要通過insert、update方法對工作內存中的fact數據進行操作,來達到控制規則引擎的目的。
操作完成之后規則引擎會重新匹配規則,原來沒有匹配成功的規則在我們修改完數據之后有可能就匹配成功了。
注意:這些方法會導致重新匹配,有可能會導致死循環問題,在編寫中最好設置屬性no-loop或者lock-on-active屬性來規避。
(1)insert:
作用:向工作內存中插入fact數據,并讓相關規則重新匹配
(2)update:
作用:修改工作內存中fact數據,并讓相關規則重新匹配
比較操作符
2.3 工程引入
2.3.1 配置文件的引入
需要有一個配置文件告訴代碼規則文件drl在哪里,在drools中這個文件就是kmodule.xml,放置到resources/META-INF目錄下。
說明:kmodule是6.0 之后引入的一種新的配置和約定方法來構建 KIE 庫,而不是使用之前的程序化構建器方法。
- Kmodule 中可以包含一個到多個 kbase,分別對應 drl 的規則文件。
- Kbase是所有應用程序知識定義的存儲庫,包含了若干的規則、流程、方法等。需要一個唯一的name,可以取任意字符串。
KBase的default屬性表示當前KBase是不是默認的,如果是默認的則不用名稱就可以查找到該 KBase,但每個 module 最多只能有一個默認 KBase。
KBase下面可以有一個或多個 ksession,ksession 的 name 屬性必須設置,且必須唯一。 - packages為drl文件所在resource目錄下的路徑,多個包用逗號分隔,通常drl規則文件會放在工程中的resource目錄下。
2.3.2 代碼中的使用
KieServices:可以訪問所有 Kie 構建和運行時的接口,通過它來獲取的各種對象(例如:KieContainer)來完成規則構建、管理和執行等操作。
KieContainer:KieContainer是一個KModule的容器,提供了獲取KBase的方法和創建KSession的方法。其中獲取KSession的方法內部依舊通過KBase來創建KSession。
KieSession:KieSession是一個到規則引擎的對話連接,通過它就可以跟規則引擎通訊,并且發起執行規則的操作。例如:通過kSession.insert方法來將事實(Fact)插入到引擎中,也就是Working Memory中,然后通過kSession.fireAllRules方法來通知規則引擎執行規則。
說明:以上案例是使用的Kie的API(6.x之后的版本)
2.4 模式匹配算法-RETE
Rete算法由Charles Forgy博士發明,并在1978-79年的博士論文中記錄。Rete算法可以分為兩部分:規則編譯和運行時執行。
編譯算法描述了如何處理生產內存中的規則以生成有效的決策網絡。在非技術術語中,決策網絡用于在數據通過網絡傳播時對其進行過濾。
網絡頂部的節點會有很多匹配,隨著網絡向下延伸匹配會越來越少,在網絡的最底部是終端節點。
關于RETE算法官方給出的說明比較抽象,這里我們結合具體案例進行說明。
2.4.1 案例說明
假設有以下事實對象:
A(a1=1,a2="A")
A(a1=2,a2="A2")
B(b1=1,b2="B")
B(b1=1,b2="B2")
B(b1=2,b2="B3")
C(c1=1,c2="B")
現有規則:
Bete網絡:
2.4.2 節點說明
1.Root Node:根節點是所有對象進入網絡的地方
2.one-input-node(單輸入節點)
- 【ObjectTypeNode】:對象類型節點是根節點的后繼節點,用來判斷類型是否一致
- 【AlphaNode】:用于判斷文本條件,例如(name == "cheddar",strength == "strong")
- 【LeftInputAdapterNode】:將對象作為輸入并傳播單個對象。
3.two-input-node(雙輸入節點)
- 【BetaNode】:用于比較兩個對象,兩個對象可能是相同或不同的類型。上述案例中用到的join node就是betaNode的一種類型。join node 用于連接左右輸入,左部輸入的是事實對象列表,右部輸入一個事實對象,在Join節點按照對象類型或對象字段進行比對。BetaNodes 也有內存。左邊的輸入稱為 Beta Memory,它會記住所有傳入的對象列表。右邊的輸入稱為 Alpha Memory,它會記住所有傳入的事實對象。
4.TerminalNode:
表示一條規則已匹配其所有條件,帶有“或”條件的規則會為每個可能的邏輯分支生成子規則,因此一個規則可以有多個終端節點。
2.4.3 RETE網絡構建流程
- 創建虛擬根節點
- 取出一個規則,例如 "Rete"
- 取出一個模式例如a1==1(模式:就是指when語句的條件,這里when條件可能是有幾個更小的條件組成的大條件。模式就是指的不能再繼續分割下去的最小的原子條件),檢查參數類型(ObjectTypeNode),
如果是新類型則加入一個類型節點; - 檢查模式的條件約束:對于單類型約束a1==1,檢查對應的alphaNode是否已存在,如果不存在將該約束作為一個alphaNode加入鏈的后繼節點;
若為多類型約束a1==b1,則創建相應的betaNode,其左輸入為LeftInputAdapterNode,右輸入為當前鏈的alphaNode; - 重復4,直到該模式的所有約束處理完畢;
- 重復3-5,直到所有的模式處理完畢,創建TerminalNode,每個模式鏈的末尾連到TerminalNode;
- 將(Then)部分封裝成輸出節點。
2.4.4 運行時執行
- 從工作內存中取一工作存儲區元素WME(Working Memory Element,簡稱WME)放入根節點進行匹配。WME是為事實建立的元素,是用于和非根結點代表的模式進行匹配的元素。
- 遍歷每個alphaNode和ObjectTypeNode,如果約束條件與該WME一致,則將該WME存在該alphaNode的匹配內存中,并向其后繼節點傳播。
- 對每個betaNode進行匹配,將左內存中的對象列表與右內存中的對象按照節點約束進行匹配,符合條件則將該事實對象與左部對象列表合并,并傳遞到下一節點。
- 和3都完成之后事實對象列表進入到TerminalNode。對應的規則被觸活,將規則注冊進議程(Agenda)。
- 對Agenda里的規則按照優先級執行。
2.4.5 共享模式
以下是模式共享的案例,兩個規則共享第一個模式Cheese( $cheddar : name == "cheddar" )
網絡圖:(左邊的類型為Cheese,右邊類型為Person)
2.4.6 小結
rete算法本質上是通過共享規則節點和緩存匹配結果,獲得性能提升。
【狀態保存】:事實集合中的每次變化,其匹配后的狀態都被保存到alphaMemory和betaMemory中。在下一次事實集合發生變化時(絕大多數的結果都不需要變化)通過從內存中取值,避免了大量的重復計算。
Rete算法主要是為那些事實集合變化不大的系統設計的,當每次事實集合的變化非常劇烈時,rete的狀態保存算法效果并不理想。
【節點共享】:例如上面的案例不同規則之間含有相同的模式,可以共享同一個節點。
【hash索引】:每次將 AlphaNode 添加到 ObjectTypeNode 后繼節點時,它都會將文字值作為鍵添加到 HashMap,并將 AlphaNode 作為值。當一個新實例進入 ObjectType 節點時,它不會傳播到每個 AlphaNode,而是可以從HashMap 中檢索正確的 AlphaNode,從而避免不必要的文字檢查。
存在問題:
- 存在狀態重復保存的問題,匹配過多個模式的事實要同時保存在這些模式的節點緩存中,將占用較多空間并影響匹配效率。
- 不適合頻繁變化的數據與規則(數據變化引起節點保存的臨時事實頻繁變化,這將讓rete失去增量匹配的優勢;數據的變化使得對規則網絡的種種優化方法如索引、條件排序等失去效果)。
- rete算法使用了alphaMemory和betaMemory存儲已計算的中間結果, 以犧牲空間換取時間, 從而加快系統的速度。然而當處理海量數據與規則時,beta內存根據規則的條件與事實的數目而成指數級增長, 所以當規則與事實很多時,會耗盡系統資源。
在Drools早期版本中使用的匹配算法是Rete,從6.x開始引入了phreak算法來解決Rete帶來的問題。
關于phreak算法可以看官方介紹:
??https://docs.drools.org/6.5.0.Final/drools-docs/html/ch05.html#PHREAK??
三、催收業務中的應用
3.1 問題解決
文章開頭問題引出的例子中可以通過編寫drl規則腳本實現,每次規則的變更只需要修改drl文件即可。
產生一個新的問題:
雖然通過編寫drl可以解決規則維護的問題,但是讓業務人員去編寫這樣一套規則腳本顯然是有難度的,那么在催收系統中是怎么做的呢,我們繼續往下看。
3.2 規則的設計
3.2.1 決策表設計
催收系統自研了一套決策表的解決方案,將drl中的條件和結果語句抽象成結構化數據進行存儲并在前端做了可視化頁面提供給業務人員進行編輯不需要編寫規則腳本。例如新增規則:
將逾期天數大于a天小于b天且逾期總金額小于等于c的案件分配到A隊列中。
表中的每一行都對應一個rule,業務人員可以根據規則情況進行修改和添加,同時也可以根據條件定義對決策表進行拓展。
決策表的主要構成:
- 規則條件定義
定義了一些規則中用到的條件,例如:逾期天數,逾期金額等。 - 規則結果定義
定義了一些規則中的結果,例如:分配到哪些隊列中,在隊列中停留時間等。 - 條件字段
在編輯一條規則時,需要用到的條件字段(從條件定義列表中選?。?/li> - 比較操作符與值
比較操作符包括:< 、<=、>、>=、==、!=,暫時不支持contain,member Of,match等
條件值目前包含數字和字符。條件字段+比較操作符+值,就構成了一個條件語句。 - 結果
滿足條件后最終得到的結果也就是結果定義中的字段值。
3.2.2 規則生成
催收系統提供了可視化頁面配置來動態生成腳本的功能(業務人員根據條件定義和結果定義來編輯決策表進而制定相應規則)。
核心流程:
1.根據規則類型解析相應的事實對象映射文件,并封裝成條件實體entitys與結果實體resultDefs,文件內容如下圖:
事實對象映射xml
2.根據規則類型查詢規則集完整數據
3.將規則集數據與xml解析后的對象進行整合,拼裝成一個drl腳本
4.將拼裝好的腳本保存到數據庫規則集表中
3.2.3 規則執行
核心流程:
- 根據規則類型從規則集表中查詢drl腳本
- 將腳步添加至KnowledgeBuilder中構建知識庫
- 獲取知識庫InternalKnowledgeBase(在新版本中對應 Kmodule中的Kbase)
- 通過InternalKnowledgeBase創建KieSession會話鏈接
- 創建AgendaFilter來制定執行某一個或某一些規則
- 調用insert方法將事實對象fact插入工作內存
- 調用fireAllRules方法執行規則
- 最后調用dispose關閉連接
四、總結
本文主要由催收系統中的一個案例引出規則引擎Drools,然后詳細介紹了Drools的概念與用法以及模式匹配的原理Rete算法。最后結合催收系統給大家講解了Drools在催收系統中是如何使用的。
通過規則引擎的引入讓開發人員不再需要參與到規則的開發與維護中來,極大節約了開發成本。通過自研的催收系統可視化決策表,讓業務人員可以在系統中靈活配置維護規則而不需要每次編寫復雜的規則腳本,解決了業務人員的痛點。系統本質上還是執行的規則腳本,我們這里是把腳本的生成做了優化處理,先通過可視化頁面錄入規則以結構化的數據進行存儲,再將其與規則定義進行整合拼裝,最終由系統自動生成規則腳本。
當前催收系統中的規則引擎仍然存在著一些問題,例如:
- 催收系統通過動態生成腳本的方式適合比較簡單的規則邏輯,如果想實現較為復雜的規則,需要寫很多復雜的代碼,維護成本比較高。
- 催收系統雖然使用的drools7.x版本,但是使用的方式依然使用的是5.x的程序化構建器方法(Knowledge API)
- 催收系統目前規則固定頁面上只能編輯無法新增規則,只能通過初始化數據庫表的方式新增規則。
后續我們會隨著版本的迭代不斷升級優化,感謝閱讀。
參考文檔: