规则引擎是如何诞生的

你是否陷入了整天对着一堆规则判定改来改去,虽然每个信用评分卡和额度模型看起来都很像,但是总是有不同点,一堆密密麻麻的逻辑判定,别说开发了,测试看了挠头。作为一个开发,你想把它规范化,通用化,这就是规则引擎的动机了。

先从拦截规则说起,虽然条目很多,但它简单好写也好测,举个例子如下:

A类拦截规则判定(命中任意一条都拒绝):
1、年销售额低于30万不要
2、近一年存在偷税漏税情况不要

这种简单明了的规则,相信大家分分钟就能写出来,比如这样:

def lanjie_a(year_sales, tax_evasion_count):
	if year_sales < 300000:
		return True
	if tax_evasion_count > 0:
		return True
	return False

完美写出,大家信心满满,豪气道,这种拦截我可以写十个。求仁得仁,组合拦截来了。

B类拦截规则判定(命中两条及以上都拒绝)
1、法院轻度纠纷事件大于2起
2、年收入连续三年下降
3、存在工资拖欠情况

小小变种还是难不倒聪明的开发,一个计数分分钟解决,参考如下:

def lanjie_b(a, b, c):
	counter = 0
	if a > 2:
		counter += 1
	if b:
		counter += 1
	if c:
		counter += 1
	return couner >= 2

正当开发想给自己的机智打call的时候,新规则又来了,那就是A和B一起判定,后面C、D同上。求锤得锤,开发仰天长啸:怪不得网上都说代码就是粘贴复制,原来就是这个意思。熟练的按下Ctrl-C和Ctrl-V,一堆长长的代码就这么诞生了。奋战N个回合,终于在手酸腰痛眼花的情况下搞定,七分无奈一分嘲讽,还有两分隐藏心底的自豪。等下次换个行业再来类似的拦截,一堆CV猛如虎,两分自豪慢慢消失,变成茫然与不甘。

深吸一口气,放松下自己,放弃代码,我们来看看一条规则到底是什么东西?当某个变量满足一定阈值就会给出一个判定,然后这一个判定作为变量又进入了下一轮条件判定,直到规则结束。是不是一下子有了点思路,再抽象下可以变成这样:

特征 (年销售额) 运算符(小于) 阈值(300000) --->  触发结果(拒绝)

这里产生几个概念:特征feature、运算符operator、阈值value,这三要素构成条件表达式condition,加上触发结果decision,组成了规则rule的基础元素,他们之间关系如下:

image

先对条件表达式进行建模,假设只有拒绝与通过两种决策,那么模型可以这样

class Condition:
    __slots__ = [feature", "operator", "value"]

    def __init__(self, feature, operator, value):
        self.feature = feature
        self.operator = operator
        self.value = value

我们可以很轻松的将条件转化为上述的模型,那么如何得到结果呢?我们以常见的数字类型来举例,Python比较简单,参考如下:

class NumberCondition(Object):

    @property
    def ops(self):
        return [">", "<", ">=", "<=", "==", "!=", "in"]

    def __call__(self, context: dict, c: Condition):
       # TODO check
        val = context[c.feature]
        if not isinstance(val, (int, float)):
            raise TypeError(f"Feature {c.feature} not (int, float)")

        expr = f'{val} {c.operator} {c.value}'
        return eval(expr, {}, context)

    def __repr__(self):
        return 'number_condition'

条件表达式解决了,但是我们一般都是一堆条件在一起组合,那又该如何建模呢?

class Rule:
    """规则中所有条件都会执行"""
    __slots__ = ['conditions', 'logic', 'decision', 'depends']

    def __init__(self, conditions, logic, decision, depends):
        self.conditions = conditions
        self.logic = logic
        self.decision = decision
        self.depends = depends

单个条件判定只有是否两种结果,因此不需要logic,而比如拦截规则,可能属于A类规则,只要有一个就行;也可能是B类,最少2条才行,因此不同规则的逻辑是不同的。不同规则拦截可能输出提示也不同,因此我们再额外引入个 decision,用来承载描述,而 depends 则是我们包含的条件需要的特征有哪些,参考引擎如下:

class RuleExpression(object):

    def __init__(self):
		# 不同类型表达式实现不一样,我们整个工厂模型打个包,统一入口
        self.condition_expr = ConditionExpression()
		# 逻辑引擎需要根据需求来完善,比如上述案例,需要实现任意一条,和最少两条的逻辑判定
        self.logic_expr = LogicExpression()

    def __call__(self, context: dict, r: Rule):
        for feature in r.depends:
            if feature not in context:
                raise KeyError(f'Rule {r.name} depends {feature} not in context {context}')

        results = list(self.condition_expr(context, c) for c in r.conditions)
        logic_results = self.logic_expr(results, r.logic)
        retrun r.decision if logic_results and r.decision is not None else logic_results

规则之间很容易组合操作,那么我们参考规则与条件的关系,把规则集也建模出来。这里需要注意,嵌套两层就差不多了,无限嵌套看似灵活,实在大坑,可以通过完善条件表达式,以及合理的设计来实现降维,保证整体效果。

经过一阵不分日夜的努力,引擎底座研发出来了,那么我们如何提供服务呢?是嵌入到代码里,还是提供服务,如果提供服务,那么规则的更新又该如何实现呢?一阵头脑风暴,DSL该登场了。

DSL是什么?全称是Domain Specific Language,领域特定语言。举个例子SQL就是数据库领域的交互语言,它定义了一套标准语法,各大数据库厂商(如mysql、oracle)对其进行解析实现,任何人都可通过编写SQL实现与数据库的交互。类似还有正则表达式、HTML&CSS等均形成了自己的语法标准。

回想我们的引擎真实的操作对象是我们的模型,那么只要能将输入转化成模型,任何格式我们都可以用来定义我们的规则引擎。因此我们可以针对不同的场景设计不同的DSL,比如API更新,我们可以采用JSON,参考如下:

{
    'name': 'my_card',
    'rules': [
        {
            'name': 'A类拦截',
            'conditions': [
                ['number_condition', 'year_sales', '<', 300000],
                ['number_condition', 'tax_evasion_count', '>', 0],
            ],
            'logic': 'any',
            'decision': '该公司命中A类拦截,予以拒绝'
        }
    ]
}

这样整个规则变成了可配置,在设计个产品出来,一个规则引擎产品就这么诞生了。

posted @ 2023-09-23 13:28  last_coding  阅读(91)  评论(0编辑  收藏  举报