草庐IT

风控决策引擎——决策流构建实战

是咕咕鸡 2023-03-28 原文

引言

本篇主要聚焦介绍风控决策引擎中决策树编排能力的构建。决策引擎是风控的大脑,而决策树的编排能力和体验是构建大脑的手段,如何构建高效丝滑稳定可靠的决策树编排能力,是对风控决策引擎的一大挑战,本篇文章和大家分享一下过往构建心得。

背景

任何系统在初期构建肯定不是往“一步到位”的方向去构建的,只是架构设计者尽量向后期可扩展可维护的方向去搭建。好的底层设计,不怕产品后期疯狂迭代,且改动调整方便。糟糕的“填鸭式”代码,可能在当时为了尽快实现了功能,最终也会逐步发展成“屎山”,维护成本越来越高,要么跑路,要么只能另起炉灶。

MVP 小步迭代 1.0

此阶段目标:最小化可行产品(MVP);小布迭代,快速上线;一人分饰多角色。

风控部门成立初期,人员少,缺少 UED 和 前端,毕竟风控本身对视觉设计和前端不是刚需,主要是后端研发和策略运营对抗黑产即可。此时为了能尽快上线决策树功能,研发人员本着小步快跑的思想,直接在代码层资源目录 resource 下放置决策树静态配置文件(具体实现在下文分解),每次更改都需要发版。本身引擎的构建也是不完善的,需要添加的功能很多,一周发个几版也是家常便饭的事,此阶段大家也是能接受的。

“由静转动”2.0

此阶段目标:无需发版,生产快速变更;稳定性相关考虑。

随着部门队伍的逐步壮大,以及研发流程的规范,风控策略运营人员对于决策编排的响应时效可视化能力需求越来越迫切,对于研发需要发版才能部署新的决策能力现状不满,黑产是高效的,但是研发发版又是需要编排和时间的,大家都要发版,且集中在一个发版周期,策略周一提出的修改,待到周三和大家的需求一起上,此时黑产早撸完跑路了。同时发版是有一定的风险的,出错了需要立即回滚,此时又延误了策略上线的时间。

基于上述,我们考虑到是时候开放生产环境直接可视化的编排决策树能力了,但是我们没有前端的同学,找别的部门借可能又不熟悉决策引擎这一套流程规范,沟通成本还高。那折中了一个方案:将静态配置文件挪到 DB 存储中去,且配置以文本字符的形式展现在前端即可,不需要复杂的前端设计,只需要简单的表单文本框填充即可满足研发修改决策流的诉求。这样让原本静态的配置“动”起来,直接在生产可配置,大大提高了生产部署的效率

可视化决策流编排 3.0

此阶段目标:高效、稳定、智能的可视化决策树编排能力产品构建

接入风控的业务线越来越多,研发人员忙于风险场景对应的变量开发迭代,此时还需要分出一部分精力负责修改决策树。2.0 版本的决策树对运营来说就是一段字符串,不是一棵树,策略运营是没办法修改,也不敢修改,出错的风险太大。考虑到整个风控的体量和模式已经非常稳定了,也有一定的时间去考虑将决策编排做成一个可视化的产品交付策略人员使用了,毕竟决策树的调整本身也是策略的职责之一,需要将此沉淀为一个高可用的产品

我们参照了业内 BPMN 工作流的前端样式设计规范,摘取了在风控决策树种需要用到的元素,构建了自己的决策引擎智能编排能力,可视化的拖拽节点,可完全交付给策略人员自行配置使用。

设计实现

技术选型

决策树,实际上就是一个变种 DAG(有向无环图),图中的节点在业务层面有不同的属性及功能。

那么如何存储这个 DAG 结构呢?用二维数组存储,是不能满足节点属性及边属性要求的,一是边界没法定义,可能这棵树很大,二是假设属性由关联表来实现,就会很割裂,没法直观看得到。

其实图可以用链表表示,链表的存储结构第一反应就是 JSON 或者 XML 来表示。可以想象, 如果用 JSON 来表示的话,层级嵌套关系会非常繁琐,毕竟 JSON 是用来序列化数据用的,展示方面,还是 XML 添加属性更为方便直观。

数据结构

举例简易决策树如下

如上决策树用 XML 数据结构表示如下:

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<flow id="test01" desc="建议决策流">

  <!-- 开始节点 -->
  <start id="start">

    <!-- 链接到下一节点 -->
    <link to="black01"/>
  </start>

  <!-- 名单节点 -->
  <nameList id="black01" desc="黑名单">

    <!-- 名单属性:名单类型:黑/白/灰;领域类型;适用范围 -->
    <field key="type" val="black"/>
    <field key="domain" val="10001,10002"/>
    <field key="scope" val="deviceHash,phone,uid"/>
    <link to="split01"/>
  </nameList>

  <!-- 分流节点 -->
  <split id="split01" desc="是否为微信渠道">

    <!-- 条件分支 -->
    <condition order="0" desc="是" expr="system == 'wechat'" to="strategy01"/>
    <condition order="10" desc="否" to="strategy02"/>
  </split>

  <!-- 策略节点 -->
  <strategy id="strategy01" desc="微信专属策略">

    <!-- 关联专属策略元数据 -->
    <field key="strategyGuid" val="25F7C71A5F834F24A12C478CEE4CB9EB"/>
    <link to="end"/>
  </strategy>
  <strategy id="strategy02" desc="非微信渠道策略">
    <field key="strategyGuid" val="0FC8A95A4D6A4F169C77950BB4A98D80"/>
    <link to="end"/>
  </strategy>

  <!-- 结束节点 -->
  <end id="end" desc="结束"/>
</flow>

上述数据结构非常直观的表示了当前需要绘制的决策树数据结构,相较于 JSON 的数据表现形式,XML 更灵活,扩展更方便,在横向和深度上可以有较好的平衡

决策流解析

XML 是很成熟的技术实现了,市面上有很多解析 XML 的开源实现,如上数据结构我使用 common-digester解析,POM 中引入如下依赖即可:

<!-- https://mvnrepository.com/artifact/commons-digester/commons-digester -->
<dependency>
    <groupId>commons-digester</groupId>
    <artifactId>commons-digester</artifactId>
    <version>1.8.1</version>
</dependency>

实体关系如下:

XML 数据解析如下:

@Data
public class FlowEntity {
    private String id;
    private String desc;

    private INode startNode;

    private Map<String, INode> nodeMap = new HashMap<>();
}
Digester digester = new Digester();

// parse flow node
digester.addObjectCreate("flow", FlowEntity.class);
digester.addSetProperties("flow");

// parse start node
digester.addObjectCreate("flow/start", StartNode.class);
digester.addSetProperties("flow/start");

// 在 FlowEntity 实现 addNode 方法,将当前节点录入
digester.addSetNext("flow/start", "addNode");
digester.addObjectCreate("flow/start/link", LinkBranch.class);
digester.addSetProperties("flow/start/link");

// 在 StartNode 实现 addLink 方法,将当前边录入
digester.addSetNext("flow/start/link", "addLink");

// parse split node
digester.addObjectCreate("flow/split", SplitNode.class);
digester.addSetProperties("flow/split");
digester.addSetNext("flow/split", "addNode");
digester.addObjectCreate("flow/split/condition", ConditionBranch.class);
digester.addSetProperties("flow/split/condition");

// 在 SplitNode 实现 addCondition 方法,将当前条件录入
digester.addSetNext("flow/split/condition", "addCondition");

// 省略...

InputStream inputStream = new ByteArrayInputStream(xmlResource.getBytes());
return (FlowEntity) digester.parse(inputStream);

其中 addNode 逻辑为将所有节点都存储在一个 nodeMap 结构内,并且如果当前节点是开始节点,则赋值到
startNode节点。

当 XML 解析完后,此时关联关系还没有建立,轮询每个节点后将节点与节点之间联系起来,并且校验节点是够存在,确保能关联成一个树。

public void assembleToNode(Map<String, INode> nodeMap) {
    if (Objects.isNull(nodeMap)) {
        return;
    }

    if (!nodeMap.containsKey(this.to)) {
        throw new RuntimeException(String.format("%s to: %s can't find node from nodeMap", this.desc, this.to));
    }

    this.toNode = nodeMap.get(this.to);
}

决策流执行

决策的执行只需要从 startNode 执行开始,递归执行,直到找到唯一的出口弹出即可。注意,策略接口是有输出决策结果的,如果是拒绝的话,此时可以直接中断流程执行,返回结果即可。

@Override
public void execute(FlowContext context) {

    // 出口
    if (this instanceof EndNode) {
        return;
    }

    // 递归执行
    this.execute(context);
}

其中,SplitNode节点执行需要计算条件表达式,只要满足一个条件,即可确定往下走的节点,子类覆盖实现如下:

注:条件表达式我之前单独发了一篇文章,感兴趣的话欢迎关注,可在我的历史文章归档中查找,此处就不在展开说明了。

@Override
public void execute(FlowContext context) {

    Validate.notEmpty(condition, "node id: {} desc: {} [condition] is empty", this.getId(), this.getDesc());

    // 主动判断
    Optional<ConditionBranch> target = condition.stream().filter(c -> c.evaluate(context)).findFirst();

    // TODO: 考虑返回默认兜底分支节点
    if (!target.isPresent()) {
        throw new RuntimeException("node id: {} ConditionBranch expr execute find nothing, please check your expr condition");
    }

    target.get().getToNode().execute(context);
}

StrategyNode 节点执行原理和 SplitNode 一致,只需要子类覆写实现方法,去执行相应的规则引擎,获取到决策结果,即可判断走向,此处就不在列出。

如上设计好了决策树的存储结构,再配合前端同学构建的基于 BPMN 流图的样式配合,定制风控需要的节点信息和表达,即可随时构建一棵理想的树(此处一句话带过,但在丝滑编排和辅助校验上,前端同学付出了很多,当然这不是本篇文章的重点)。

总结

本文分享了决策引擎中决策流图的思考及构建过程,从最小可用产品上线支撑业务发展到沉淀出可视化编排能力的工作区。当然,本文仅仅展示了通用决策流的思考构建过程,显示业务中还是会遇到各种挑战,比如对性能的要求对成本的控制等等,挑战非常多,我将在后续一一分享出来,欢迎关注。

往期精彩

欢迎关注公众号:咕咕鸡技术专栏
个人技术博客:https://jifuwei.github.io/

有关风控决策引擎——决策流构建实战的更多相关文章

  1. ruby - 在没有 sass 引擎的情况下使用 sass 颜色函数 - 2

    我想在一个没有Sass引擎的类中使用Sass颜色函数。我已经在项目中使用了sassgem,所以我认为搭载会像以下一样简单:classRectangleincludeSass::Script::FunctionsdefcolorSass::Script::Color.new([0x82,0x39,0x06])enddefrender#hamlengineexecutedwithcontextofself#sothatwithintemlateicouldcall#%stop{offset:'0%',stop:{color:lighten(color)}}endend更新:参见上面的#re

  2. ruby-on-rails - Rails 中的推荐引擎 - 2

    我想为我的Rails网络应用程序提供推荐功能。特别是,我想向新注册的用户推荐他可能想要关注的其他用户。Rails中是否有用于此目的的引擎/gem?如果没有,我应该从哪里开始构建它?谢谢。 最佳答案 有Coletivogemhttps://github.com/diogenes/coletivo我试了一下。在MySQL上运行。Neo4jhttp://neo4j.org真的很容易实现一个“跟随谁”。事实上,大多数展示其能力的样本都涉及“跟随谁”。快速提示-只有在JRuby上运行时,Neo4j.rb才会很酷。如果不是-使用Neograph

  3. 世界前沿3D开发引擎HOOPS全面讲解——集3D数据读取、3D图形渲染、3D数据发布于一体的全新3D应用开发工具 - 2

    无论您是想搭建桌面端、WEB端或者移动端APP应用,HOOPSPlatform组件都可以为您提供弹性的3D集成架构,同时,由工业领域3D技术专家组成的HOOPS技术团队也能为您提供技术支持服务。如果您的客户期望有一种在多个平台(桌面/WEB/APP,而且某些客户端是“瘦”客户端)快速、方便地将数据接入到3D应用系统的解决方案,并且当访问数据时,在各个平台上的性能和用户体验保持一致,HOOPSPlatform将帮助您完成。利用HOOPSPlatform,您可以开发在任何环境下的3D基础应用架构。HOOPSPlatform可以帮您打造3D创新型产品,HOOPSSDK包含的技术有:快速且准确的CAD

  4. ruby - 在 Ruby 中构建长字符串的简洁方法 - 2

    在编写Ruby(客户端脚本)时,我看到了三种构建更长字符串的方法,包括行尾,所有这些对我来说“闻起来”有点难看。有没有更干净、更好的方法?变量递增。ifrender_quote?quote="NowthatthereistheTec-9,acrappyspraygunfromSouthMiami."quote+="ThisgunisadvertisedasthemostpopularguninAmericancrime.Doyoubelievethatshit?"quote+="Itactuallysaysthatinthelittlebookthatcomeswithit:themo

  5. 叮咚买菜基于 Apache Doris 统一 OLAP 引擎的应用实践 - 2

    导读:随着叮咚买菜业务的发展,不同的业务场景对数据分析提出了不同的需求,他们希望引入一款实时OLAP数据库,构建一个灵活的多维实时查询和分析的平台,统一数据的接入和查询方案,解决各业务线对数据高效实时查询和精细化运营的需求。经过调研选型,最终引入ApacheDoris作为最终的OLAP分析引擎,Doris作为核心的OLAP引擎支持复杂地分析操作、提供多维的数据视图,在叮咚买菜数十个业务场景中广泛应用。作者|叮咚买菜资深数据工程师韩青叮咚买菜创立于2017年5月,是一家专注美好食物的创业公司。叮咚买菜专注吃的事业,为满足更多人“想吃什么”而努力,通过美好食材的供应、美好滋味的开发以及美食品牌的孵

  6. UE4 源码阅读:从引擎启动到Receive Begin Play - 2

    一、引擎主循环UE版本:4.27一、引擎主循环的位置:Launch.cpp:GuardedMain函数二、、GuardedMain函数执行逻辑:1、EnginePreInit:加载大多数模块int32ErrorLevel=EnginePreInit(CmdLine);PreInit模块加载顺序:模块加载过程:(1)注册模块中定义的UObject,同时为每个类构造一个类默认对象(CDO,记录类的默认状态,作为模板用于子类实例创建)(2)调用模块的StartUpModule方法2、FEngineLoop::Init()1、检查Engine的配置文件找出使用了哪一个GameEngine类(UGame

  7. 微信小程序开发入门与实战(Behaviors使用) - 2

    @作者:SYFStrive @博客首页:HomePage📜:微信小程序📌:个人社区(欢迎大佬们加入)👉:社区链接🔗📌:觉得文章不错可以点点关注👉:专栏连接🔗💃:感谢支持,学累了可以先看小段由小胖给大家带来的街舞👉微信小程序(🔥)目录自定义组件-behaviors    1、什么是behaviors    2、behaviors的工作方式    3、创建behavior    4、导入并使用behavior    5、behavior中所有可用的节点    6、同名字段的覆盖和组合规则总结最后自定义组件-behaviors    1、什么是behaviorsbehaviors是小程序中,用于实现

  8. ruby - 使用 rbenv 和 ruby​​-build 构建 ruby​​ 失败,出现 undefined symbol : SSLv2_method - 2

    我正在尝试在配备ARMv7处理器的SynologyDS215j上安装ruby​​2.2.4或2.3.0。我用了optware-ng安装gcc、make、openssl、openssl-dev和zlib。我根据README中的说明安装了rbenv(版本1.0.0-19-g29b4da7)和ruby​​-build插件。.这些是随optware-ng安装的软件包及其版本binutils-2.25.1-1gcc-5.3.0-6gconv-modules-2.21-3glibc-opt-2.21-4libc-dev-2.21-1libgmp-6.0.0a-1libmpc-1.0.2-1libm

  9. ruby-on-rails - 如何构建复杂的 Rails 系统 - 2

    关闭。这个问题需要更多focused.它目前不接受答案。想改进这个问题吗?更新问题,使其只关注一个问题editingthispost.关闭8年前。Improvethisquestion我们有以下(以及更多)系统,我们将数据从一个应用推送/拉取到另一个:托管CRM(InsideSales.com)Asterisk电话系统(内部)横幅广告系统(openx,我们托管)潜在客户生成系统(自行开发)电子商务商店(spree,我们托管)工作板(本土)一些工作网站抓取+入站工作提要电子邮件传送系统(如Mailchimp,自主开发)事件管理系统(如eventbrite,自主开发)仪表板系统(大量图表和

  10. ruby-on-rails - lovdbyless VS 社区引擎……哪个最好? - 2

    随着ruby​​被引入为新的编程救世主,我想知道是否有人基于易用性、运行所需的资源、可用性和易定制性而有偏好。两者有更好的吗? 最佳答案 好吧,任何基于Rails的社交网络应用程序的比较都应该包括insoshi(http://portal.insoshi.com/)。话虽这么说,这三个都非常相似,区别在于实现细节。Lovd和Insoshi都是完整的Rails应用程序;它旨在供您将它们用作入门工具包,并使用您自己的自定义功能对其进行扩展。另一方面,CommunityEngine是一个Rails插件。这意味着您可以更轻松地向现有Rail

随机推荐