3. DSL概述
在进入解析器与执行引擎的实现之前,必须先回答一个更基础的问题:我们到底要设计一门什么样的语言?很多风控DSL项目失败,并非因为解析算法写得不好,而是因为语言定位从一开始就模糊——既想当配置格式,又想当编程语言,最后两边都不好用。本节从DSL的分类讲起,逐步收敛到风控场景下的常见形态与设计目标,为后续的语法设计与执行模型建立判断标准。
一、核心原理:DSL的分类维度
领域特定语言(Domain-Specific Language)是相对于通用编程语言(GPL)而言的。它的核心特征不是“语法简单”,而是表达范围被刻意收窄:只描述某个领域内的概念与规则,放弃通用计算能力以换取可读性、可验证性和可治理性。理解DSL,通常从三个维度切入。
- 按外部形态分:文本型DSL(如SQL、正则表达式)与图形/表格型DSL(如决策表、流程图)。文本型便于版本管理与diff,图形型便于业务人员直接编辑。
- 按执行方式分:解释型(边解析边执行,灵活但性能一般)与编译型(先转为AST或字节码再执行,性能好但实现复杂)。
- 按宿主关系分:独立DSL(有完整语法与工具链)与嵌入式DSL(寄生在宿主语言中,如Java的链式API、Kotlin DSL)。
还有一个常被忽略的维度是声明式与命令式。声明式DSL描述“什么是合法/命中”,命令式DSL描述“按什么步骤做”。风控规则绝大多数属于声明式,因为规则的本质是条件判断,而非流程编排。
二、方法流程:风控DSL的常见形态
风控领域的DSL并非只有一种写法,实际工程中常见以下四种形态,它们往往在同一平台中共存,服务于不同角色。
- 表达式型:最基础的一类,形如
amount > 5000 && user.level == "NEW"。它通常作为其他形态的“叶子节点”,负责单个条件的求值。 - 规则集/决策表型:用表格或列表组织多条规则,每行是一个条件组合与对应动作,适合批量维护、便于业务人员审阅。
- 决策树/流程图型:用节点与连线表达分支逻辑,适合多级判断、需要可视化回溯的场景。
- 脚本型:嵌入一段受限脚本(如Lua、Aviator、Groovy沙箱),表达能力最强,但治理成本也最高。
从工程落地角度看,一个成熟的风控平台通常采用“表达式 + 规则集”作为主干,把脚本型作为逃生通道并严格限制。原因很直接:表达式与规则集可以被静态分析、可以枚举命中路径、可以做冲突检测,而任意脚本几乎无法做这些治理。
下面是一段典型的规则集DSL配置示例,采用YAML承载,条件部分复用表达式语法:
ruleSet: payment_risk_v2
version: 20240601
rules:
- id: R001
name: 大额新客拦截
when: "amount > 5000 && user.level == 'NEW'"
then: { action: REJECT, reason: "NEW_USER_HIGH_AMOUNT" }
priority: 100
- id: R002
name: 异地登录加验
when: "geo.city != user.lastCity && device.trusted == false"
then: { action: CHALLENGE, channel: "SMS" }
priority: 80
- id: R003
name: 默认放行
when: "true"
then: { action: PASS }
priority: 0
可以看到,规则集DSL把“条件表达式”和“动作声明”分离,条件部分只做布尔求值,动作部分只做结果声明。这种分离是后续实现优先级仲裁、命中日志、灰度发布的基础。
三、案例场景:从需求到DSL形态的选择
假设业务提出三类需求,我们分别看应选择哪种形态。
| 业务需求 | 推荐形态 | 理由 |
|---|---|---|
| 单笔金额超过阈值则拦截 | 表达式型 | 单条件、无分支,表达式最直接 |
| 按用户等级、金额区间、设备可信度组合出几十条策略 | 规则集/决策表型 | 规则数量多、需批量审阅与冲突检测 |
| 根据历史行为做多级评分与降级 | 决策树型 | 分支层级深,可视化回溯价值高 |
| 临时接入一个第三方风控模型打分 | 脚本型(受限) | 逻辑不确定、需快速试错,但须沙箱隔离 |
这个映射关系说明:DSL形态的选择应由需求的复杂度与治理要求共同决定,而不是由技术偏好决定。把几十条规则硬塞进一个脚本,短期省事,长期无法维护;把单条件判断做成决策树,则是过度设计。
四、设计目标与常见误区
风控DSL的设计目标可以归纳为四条,它们之间存在张力,需要权衡。
- 可读性:业务人员能读懂规则含义,减少沟通成本。
- 可治理性:支持静态校验、冲突检测、命中路径枚举、版本对比。
- 可执行性:求值性能满足线上毫秒级要求,且行为确定。
- 可演进性:新增字段、新增动作时不必推翻已有语法。
1. 把DSL做成通用语言——引入循环、递归、自定义函数,最终变成一门没人能治理的“小Java”。
2. 忽略空值与类型语义——
null > 5000 在不同实现下结果不同,导致线上规则行为漂移。3. 没有版本与灰度概念——规则一改全量生效,出问题无法快速回滚。
复盘一个真实教训:某团队最初用Groovy脚本承载全部风控规则,上线半年后规则文件超过两千行,无人敢改,也无法回答“某笔订单为什么被拒”。后来重构为“表达式 + 规则集”DSL,命中日志可以精确到具体规则ID与条件子表达式,治理成本大幅下降。这个案例说明,DSL的价值不在于表达能力强,而在于表达边界清晰。