內(nèi)容轉(zhuǎn)自微信公眾號植榕,技術(shù)時光幻馁,關(guān)注第一時間獲取最新文章
I.決策流介紹
在上一篇文章中脓钾,我們實現(xiàn)了一個完整的規(guī)則集解析過程售睹,實現(xiàn)了規(guī)則引擎。具體請參考上一篇文章:(一)規(guī)則引擎實現(xiàn)惭笑。
實際風(fēng)控需求通常不會只有一組規(guī)則侣姆,會對不同的規(guī)則、規(guī)則集進行編排沉噩,還會出現(xiàn)分支流程捺宗,子流程,形成一個更復(fù)雜風(fēng)控流程川蒙,我們叫決策流蚜厉,決策流的解析就是實現(xiàn)一個流程引擎。
流程引擎也叫工作流引擎畜眨,有很多開源實現(xiàn)昼牛,比較出名的是java的Activiti,jBPM5康聂。這里我們從業(yè)務(wù)需求分析贰健,抽象建模并自己實現(xiàn)流程解析過程。
II.決策流抽象建模
先看一個決策流長什么樣恬汁?
這是一個比較簡單的決策流伶椿,它由兩個規(guī)則集順序編排,并有起始和結(jié)束,是符合BPMN規(guī)范的脊另。
BPMN是什么导狡?Business Process Diagram(BPM)是指一個業(yè)務(wù)流程圖,“N”是Notation符號偎痛,BPMN業(yè)務(wù)流程建模符號旱捧,是由OMG組織維護的一套業(yè)務(wù)流程建模標(biāo)準(zhǔn)。這里我們關(guān)注和使用其中流對象(Flow)相關(guān)定義及元素踩麦。具體如下圖:
1. 簡單決策流實現(xiàn)
流對象中枚赡,選取事件Event中開始Start Event作為決策流的開始節(jié)點,結(jié)束End Event作為決策流的結(jié)束節(jié)點靖榕,一個規(guī)則集作為一個活動Activity标锄,后面決策樹、決策矩陣等決策節(jié)點也是活動Activity的一種類型茁计,流向Flow作為決策流的編排執(zhí)行順序料皇,由此一個簡單的決策流就用BPMN規(guī)范做了抽象。
數(shù)據(jù)結(jié)構(gòu)上星压,決策流節(jié)點使用單向線性鏈表践剂,每個節(jié)點持有下一節(jié)點指針。
代碼實現(xiàn)上娜膘,使用pipline架構(gòu)作為流程解析逊脯,前一個節(jié)點解析結(jié)果(out/sink)作為下一個節(jié)點的輸入(in/source),每個節(jié)點封裝解析算法(parse)竣贪,節(jié)點的執(zhí)行結(jié)果統(tǒng)一存儲在上下文中军洼,直到全部節(jié)點執(zhí)行完成或中斷退出。
用DSL表述演怎,將決策流抽象成Workflow結(jié)構(gòu)體匕争,由多個Node組成,具體yaml語法表達如下:
workflow:
- node:
node_name: start_1
category: start
next_node_name: ruleset_1
next_category: ruleset
- node:
node_name: ruleset_1
category: ruleset
next_node_name: ruleset_2
next_category: ruleset
- node:
node_name: ruleset_2
category: ruleset
next_node_name: end_1
next_category: end
- node:
node_name: end_1
category: end
next_node_name: ""
next_category: ""
Node結(jié)構(gòu)體如下:
type Node struct {
NodeName string `yaml:"node_name"`
Category string `yaml:"category"`
NextNodeName string `yaml:"next_node_name"`
NextCategory string `yaml:"next_category"`
}
規(guī)則集DSL抽象和上一篇一樣爷耀,將多個Ruleset組成Rulesets數(shù)組甘桑,用ruleset_name進行區(qū)分。
rulesets:
- ruleset_name: ruleset_1
ruleset_category: internal
rules:
#...
- ruleset_name: ruleset_2
ruleset_category: external
rules:
#...
此時完整DSL結(jié)構(gòu)體歹叮,包括Workflow代表Node節(jié)點數(shù)組跑杭,Rulesets代表規(guī)則集Ruleset數(shù)組。
type Dsl struct {
Workflow []Node `yaml:"workflow,flow"`
Rulesets []Ruleset `yaml:"rulesets,flow"`
}
決策流解析過程:
先找到start節(jié)點咆耿,執(zhí)行解析
循環(huán)節(jié)點下一指針德谅,執(zhí)行每個節(jié)點parse()直到下一指針為空或中斷退出
返回決策結(jié)果
//define result struct
type DslResult struct {
NextNodeName string
NextCategory string
Decision interface{}
Track []string
}
//dsl flow parse
func (dsl *Dsl) Parse() DslResult {
log.Println("dsl parse start...")
if len(dsl.Workflow) == 0 {
panic("dsl workflow is empty")
}
var result = new(DslResult)
//from start node
firstNode := dsl.FindStartNode()
dsl.gotoNextNode(firstNode.NodeName, firstNode.Category, result)
//loop parse node and go to next node
for !isBreakDecision(result.Decision) && result.NextNodeName != "" {
dsl.gotoNextNode(result.NextNodeName, result.NextCategory, result)
}
log.Println("dsl parse end.")
return result
}
//parse node and find next
func (dsl *Dsl) gotoNextNode(nodeName string, category string, result *DslResult) {
//find current node from workflow
node := dsl.FindNode(nodeName)
if node == "" {
return
}
result.Track = append(result.Track, nodeName)
//default
result.NextNodeName = node.NextNodeName
result.NextCategory = node.NextCategory
result.Decision = nil
//parse different category node
switch category {
case configs.START:
return
case configs.RULESET:
ruleset := dsl.FindRuleset(node.NodeName)
result.Decision = ruleset.parse()
case configs.END:
result.NextNodeName = ""
result.NextCategory = ""
}
}
2.增加決策流分支
分支流程需要增加網(wǎng)關(guān)Gateway節(jié)點,網(wǎng)關(guān)又分為并行網(wǎng)關(guān)萨螺、排它網(wǎng)關(guān)窄做、包容網(wǎng)關(guān)宅荤,而排它網(wǎng)關(guān)更符合風(fēng)控場景業(yè)務(wù)語義,即一個決策流只能走一個分支浸策。
決策流滿足條件1時走第一條分支,經(jīng)過規(guī)則集A惹盼,滿足條件2時走規(guī)則集B的分支庸汗。
2.1 數(shù)據(jù)結(jié)構(gòu)選擇
增加了分支流程,線性鏈表結(jié)構(gòu)是否仍適用手报?樹結(jié)構(gòu)蚯舱,圖結(jié)構(gòu),鏈表結(jié)構(gòu)如何選擇掩蛤?
首選考慮樹結(jié)構(gòu)枉昏,可能是N叉樹,也可能是無分支的線性結(jié)構(gòu)揍鸟,且是有向的兄裂,因此樹結(jié)構(gòu)并不合適。
進一步考慮有向圖結(jié)構(gòu)(如有向無環(huán)圖DAG)阳藻,圖的解析主要是進行深度或廣度遍歷晰奖,可以執(zhí)行每條分支流程,因此圖的遍歷更適合做并行網(wǎng)關(guān)腥泥,而決策流是排它網(wǎng)關(guān)語義匾南,同時有且只有一個分支滿足執(zhí)行條件,使用圖結(jié)構(gòu)并不合適蛔外,一次決策的執(zhí)行流程蛆楞,更像一個鏈?zhǔn)搅鞒蹋m合單向鏈表結(jié)構(gòu)夹厌。
2.2 條件網(wǎng)關(guān)實現(xiàn)
條件網(wǎng)關(guān)功能:滿足某個分支線上的條件即走該分支豹爹。
條件網(wǎng)關(guān)解析:循環(huán)執(zhí)行每個分支的條件表達式,并選擇結(jié)果為true的第一個分支尊流,決策結(jié)果即為決策流下一步要走的分支名帅戒。這里也注意,一般情況不允許配置兩個分支條件有重合崖技,可以同時滿足的情況逻住。
//conditinal gateway node
type Conditional struct {
ConditionalName string `yaml:"conditional_name"`
Depends []string `yaml:"depends"`
Branchs []Branch `yaml:"branchs,flow"`
}
//branch in conditional
type Branch struct {
BranchName string `yaml:"branch_name"`
Conditions []Condition `yaml:"conditions"`
Logic string `yaml:"logic"`
Decision string `yaml:"decision"`
}
//conditional gateway parse
func (conditional *Conditional) parse() string {
depends := internal.GetFeatures(conditional.Depends) //need to check
for _, branch := range conditional.Branchs { //loop all the branch
var conditionRs = make([]bool, 0)
for _, condition := range branch.Conditions {
if data, ok := depends[condition.Feature]; ok {
rs, _ := operator.Compare(condition.Operator, data, condition.Value)
conditionRs = append(conditionRs, rs)
} else { //get feature fail
continue //can modify according scene
}
}
logicRs, _ := operator.Boolean(conditionRs, branch.Logic)
if logicRs { //if true, choose the branch and break
return branch.Decision
} else {
continue
}
}
return "" //can't find any branch
}
現(xiàn)在把條件網(wǎng)關(guān)加入workflow,即構(gòu)造了完整DSL語法迎献。
workflow:
- node:
node_name: start_1
category: start
next_node_name: conditional_1
next_category: conditional
- node:
node_name: conditional_1
category: conditional
next_node_name: ""
next_category: ""
- node:
node_name: ruleset_1
node_category: ruleset
next_node_name: end_1
next_category: end
- node:
node_name: ruleset_2
node_category: ruleset
next_node_name: end_2
next_category: end
- node:
node_name: end_1
node_category: end
next_node_name: ""
next_category: ""
- node:
node_name: end_2
node_category: end
next_node_name: ""
next_category: ""
決策流執(zhí)行過程需要加上對條件網(wǎng)關(guān)conditional的執(zhí)行解析瞎访。
func (dsl *Dsl) gotoNextNode(nodeName string, category string, result *DslResult) {
//...
switch category {
//...
case configs.CONDITIONAL:
conditional := dsl.FindConditional(node.NodeName)
rs := conditional.parse()
if rs == nil { //not match any branch, error
result.NextNodeName = ""
log.Println(node.NodeName, "not match any branch")
} else {
result.NextNodeName = rs.(string)
result.NextCategory = dsl.FindNode(rs.(string)).Category
}
}
}
最后決策流執(zhí)行情況如下:
3. 分流網(wǎng)關(guān)實現(xiàn)
風(fēng)控工作中,需要不斷對規(guī)則策略和模型進行迭代優(yōu)化吁恍,新的規(guī)則模型效果需要經(jīng)過實驗證明扒秸,與原規(guī)則模型進行效果比對播演,選擇較優(yōu)的規(guī)則模型使用,這種叫冠軍/挑戰(zhàn)者試驗伴奥,原規(guī)則模型分支叫冠軍写烤,新增分支叫挑戰(zhàn)者,也有叫A/B Test拾徙。
這里使用分流網(wǎng)關(guān)來實現(xiàn)ABTest洲炊,決策流配置如下:
abtests:
- abtest:
abtest_name: abtest_1
branchs:
- branch:
branch_name: branch_1
percent: 45
decision: ruleset_1
- branch:
branch_name: branch_2
percent: 55
decision: ruleset_2
代碼實現(xiàn)上,45%的流量分給第一分支尼啡,55%流量分給第二分支暂衡,這里實現(xiàn)一個0-99隨機數(shù),獲取結(jié)果大于45崖瞭,走分支2狂巢,否則走分支1。
rand.Seed(time.Now().UnixNano())
rs := rand.Intn(99)
具體實現(xiàn)上仍有幾個問題需要考慮:
流量配比可能為小數(shù)书聚。
流量配比之和應(yīng)該為100唧领,可在發(fā)布時校驗。
隨機種子的設(shè)置雌续,如果希望隨機數(shù)與用戶id和實驗名相關(guān)疹吃,需要取其hash來設(shè)置種子seed。
實驗及分流可用redis/mysql固化西雀,下一次直接使用(有些業(yè)務(wù)場景需求)萨驶。
4. 復(fù)雜決策流實現(xiàn)
目前實現(xiàn)了開始、結(jié)束艇肴、規(guī)則集腔呜、條件網(wǎng)關(guān)、分流網(wǎng)關(guān)再悼,通過組合幾種節(jié)點核畴,即可實現(xiàn)了大多數(shù)風(fēng)控場景下的決策流配置。
III.問題與思考
****1. ****配置****決策流是否匯合****
第一種配置會省事一些冲九,第二種對分支表述會更清晰些谤草,如果針對更復(fù)雜的流,有匯合邏輯會更復(fù)雜且易出錯莺奸。
在實際執(zhí)行解析過程中丑孩,由于鏈?zhǔn)浇馕觯暗拇a可支持上述兩種方式都正常解析灭贷。Workflow配置一個end節(jié)點還是兩個end節(jié)點温学,之前是按兩個結(jié)束節(jié)點表述,這里可改成一個end節(jié)點:
workflow:
#...
- node:
node_name: ruleset_3
node_category: ruleset
next_node_name: end_1
next_category: end
- node:
node_name: ruleset_4
node_category: ruleset
next_node_name: end_1
next_category: end
- node:
node_name: end_1
node_category: end
next_node_name: ""
next_category: ""
2.決策流管理后臺
工作中甚疟,為了讓風(fēng)控分析師更方便的配置決策流仗岖,還需要配套一個可視化交互后臺逃延,所有節(jié)點以組件拖拉拽的方式提供服務(wù),這里分享一個開源前端庫轧拄。
通過該組件揽祥,可以生成一個XML格式文件,我們對其進行解析檩电,然后轉(zhuǎn)化為我們需要的Yaml格式DSL文件盔然。
3.獲取依賴特征數(shù)據(jù)
決策流執(zhí)行和規(guī)則執(zhí)行都離不開特征數(shù)據(jù)的支持,而特征數(shù)據(jù)何時加載會對解析效率有不同影響是嗜。一般有兩種方式:
決策流執(zhí)行前將數(shù)據(jù)全部獲取
邊解析決策流邊加載特征
一些商業(yè)決策引擎就需要先將所有數(shù)據(jù)特征加工好,統(tǒng)一推給決策引擎挺尾,決策引擎給出決策結(jié)果鹅搪。這種方式主要缺點:決策請求數(shù)據(jù)不同,可能執(zhí)行第一個規(guī)則(集)即命中拒絕中斷遭铺,剩余規(guī)則(集)依賴的特征就沒必要加載丽柿,如果這部分特征來自三方收費數(shù)據(jù)源,會導(dǎo)致數(shù)據(jù)成本浪費魂挂,所以按需加載更符合一般業(yè)務(wù)場景甫题。
實際風(fēng)控場景,會拆分內(nèi)部數(shù)據(jù)規(guī)則集和外部數(shù)據(jù)規(guī)則集涂召,決策先使用內(nèi)部數(shù)據(jù)規(guī)則集坠非,如命中拒絕即退出不再執(zhí)行外部數(shù)據(jù)規(guī)則集。更精細化控制果正,可將外部數(shù)據(jù)規(guī)則集中每條規(guī)則做優(yōu)先級排序炎码,規(guī)則集執(zhí)行策略按命中即退出模式,這樣可進一步控制外部數(shù)據(jù)成本秋泳。
4.關(guān)于決策流退出
決策流以pipline方式依次執(zhí)行,引起決策流退出的情況可能有如下幾種:
決策流執(zhí)行到end節(jié)點,正常結(jié)束退出亚情。
決策流執(zhí)行規(guī)則集結(jié)果為拒絕昙沦,根據(jù)業(yè)務(wù)語義中斷退出(isBreakDecision代碼進行控制),這里是隱式的進行了退出卓起,也有配置結(jié)束節(jié)點顯示退出的做法和敬,但每個規(guī)則集配置一個并不方便。
決策流執(zhí)行條件網(wǎng)關(guān)未匹配到任何分支戏阅,或決策流在獲取依賴特征時因系統(tǒng)問題導(dǎo)致失敗概龄,這時候觸發(fā)異常退出情況,需要進行監(jiān)控和報警處理饲握。
5.架構(gòu)部署
將決策引擎部署成一個web服務(wù)私杜,使用gin或net/http即可輕松搭建一個api服務(wù)蚕键,業(yè)務(wù)場景在需要風(fēng)控時調(diào)用決策引擎系統(tǒng),獲取決策結(jié)果數(shù)據(jù)衰粹。
決策引擎在啟動時加載DSL到內(nèi)存锣光,當(dāng)有變更發(fā)布時更新內(nèi)存DSL,關(guān)于如何熱發(fā)布后續(xù)文章會進一步分析铝耻。
數(shù)據(jù)返回一般可以包括:結(jié)果數(shù)據(jù)誊爹,拒絕還是通過;過程數(shù)據(jù)瓢捉,命中規(guī)則频丘、規(guī)則集情況,執(zhí)行路徑泡态,執(zhí)行分支搂漠,以及每個特征值情況,用于后期分析使用某弦。結(jié)果數(shù)據(jù)可落地mysql數(shù)據(jù)庫桐汤,較大的數(shù)據(jù)源數(shù)據(jù)可存儲hbase,或通過log靶壮、kafka等方式異步存儲怔毛。
決策引擎依賴的特征數(shù)據(jù)可從特征引擎獲取,特征引擎進行數(shù)據(jù)庫腾降、數(shù)據(jù)源的對接以及特征衍生加工拣度,有些公司會進一步拆分成(前置)數(shù)據(jù)平臺和特征加工平臺兩個系統(tǒng)。
具體架構(gòu)圖如下:
執(zhí)行引擎流程時序圖:
6.引入模型及模型引擎
使用規(guī)則策略做風(fēng)控螃壤,通常只能決策是否通過蜡娶,適用于反欺詐,而綜合評估用戶風(fēng)險等級映穗,對用戶進行信用評級窖张,通過風(fēng)險定價可進一步提升風(fēng)控能力。隨著大數(shù)據(jù)技術(shù)的發(fā)展蚁滋,對數(shù)據(jù)的深度挖掘并建立機器學(xué)習(xí)宿接、深度學(xué)習(xí)模型,成為更重要的風(fēng)控手段辕录。通過模型結(jié)果睦霎,可更精細化的進行風(fēng)險評估。下一篇走诞,我們將介紹模型相關(guān)開發(fā)和技術(shù)實現(xiàn)副女。
內(nèi)容轉(zhuǎn)自微信公眾號,技術(shù)時光 techyears蚣旱,關(guān)注第一時間獲取最新文章
文章相關(guān)代碼實現(xiàn):
https://github.com/skyhackvip/risk_engine