Laya 决策模型使用指南:从零跑通第一个决策模型
业务系统里常常出现一种尴尬情况:代码已经写得很完整,规则却散落在几十个 if...else 中。产品经理每次调整策略,开发就要改代码、重新发布,而运营人员想配置一条新规则,只能提交需求排队。Laya 决策模型解决的就是这一类问题:把业务决策从代码中抽离出来,用一套可配置、可解释、可追溯的模型来描述“在什么条件下,得出什么结论”。它不是某一个特定框架的专属名词,而是一类规则编排和决策管理思路的统称,在实际项目中经常以决策表、决策树、规则集或评分卡的形式落地。
这篇文章的目标是帮助你从零理解 Laya 决策模型的基本概念,并跑通一个最小可用的决策流程。你会看到决策模型如何定义输入、如何组织规则、如何计算输出,以及当结果不符合预期时应该从哪一层开始排查。内容适用于刚开始接触决策引擎、规则引擎,或者准备把复杂业务规则从代码中迁移到配置化模型的开发者。
1. 先搞清楚决策模型到底在做什么
1.1 从一段硬编码规则说起
大多数项目的第一步都是从硬编码规则开始的。例如一个简单的风控判断:当用户余额低于 100 时,拒绝提现;当用户是白名单用户时,免除手续费。用 Java 写出来大致是这样:
java复制public String decideWithdraw(double balance, boolean whitelist) {
if (balance < 100) {
return "REJECT";
}
if (whitelist) {
return "EXEMPT_FEE";
}
return "APPROVE";
}
这段代码在业务量小的时候没有问题,逻辑直接、调试方便。但规则增加到几十条甚至上百条时,问题就出现了:规则顺序稍一调整,结果可能完全不同;业务人员想看一眼现有规则,只能去看代码;每次调整都要走完整发布流程。
决策模型做的事情,就是把“判断逻辑”和“业务代码”分开。业务代码只负责传参数、调接口、拿结果,而“用什么规则、按什么顺序、输出什么结论”由模型决定。这个模型可以存储在数据库里、JSON 文件里、Excel 表格里,或者专门的规则配置中心里。
1.2 Laya 决策模型的通俗理解
可以把决策模型理解成一张“判断地图”。输入一组事实,例如用户年龄、余额、订单金额、会员等级,模型根据预定义的规则一步步走到某个终点,终点就是决策结果。整个过程有点像快递分拣:包裹根据地址标签被不断分流,最终进入正确的运输线。
Laya 决策模型不是某个单一的软件产品名称,更接近一类按规则决策的方法论和实践组合。不同团队落地时,可能选用 Drools、Easy Rules、Aviator,也可能自己写一套 JSON 规则解析器。无论底层用什么实现,核心要素都一样:条件、动作、优先级、输入输出定义和规则编排方式。
1.3 决策模型带来哪些实际收益
用配置化的决策模型代替硬编码,最直接的收益是规则变更成本下降。运营人员或者策略人员修改一条线上规则,不再需要等待开发排期,只需要在配置界面或配置文件中修改对应条件,模型层重新加载即可生效。
其次是可解释性。硬编码规则里的逻辑只有开发者清楚,而决策模型可以导出成结构化描述,例如“如果 userLevel == VIP && orderAmount >= 500,则 discountRate = 0.8”。这种描述业务人员能看懂,审计人员也能追溯某条决策基于什么规则产生。
第三个收益是规则收敛。散落在多个服务里的判断逻辑集中到一个模型后,重复规则减少,互相矛盾的概率下降。排查问题时也只需要看一个规则源,而不是在多个项目的代码仓库里搜索条件关键字。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 环境准备:选型前先确认这些基础条件
2.1 决策模型的三种落地形态
在动手写代码之前,先确定你的项目适合哪种形态。决策模型不是非此即彼的技术选型,它取决于规则数量、变更频率、团队技术栈和运维能力。
| 形态 | 适用场景 | 变更方式 | 典型实现 |
|---|---|---|---|
| 代码内嵌规则集 | 规则少、变更频率低、业务简单 | 改代码发版 | 枚举、策略模式、简单工厂 |
| 配置文件驱动 | 规则中等、需要业务调整但不希望频繁发版 | 修改 YAML/JSON 后重启服务 | JSON 规则解析、自定义 DSL |
| 独立决策引擎/规则引擎 | 规则多、变更频繁、需要版本管理和可视化 | 动态加载、远程配置中心推送 | Drools、Easy Rules、自研引擎 |
对于刚接触 Laya 决策模型的人来说,不建议跳过第二阶段直接上重型引擎。先从配置化驱动开始,理解条件、结果、优先级怎么表示,再逐步引入引擎能力。
2.2 技术栈和依赖准备
本文示例采用 Java + Spring Boot + JSON 规则文件的方式演示,因为这套组合最常见,也最容易迁移到其他语言。实际项目中,无论采用什么语言,核心依赖无非三类:
- JSON 解析库,例如 Jackson、Gson、Fastjson,用于读取规则定义。
- 表达式求值库,用于解析条件表达式,例如 Aviator、QLExpress、SpEL。
- 规则加载器,可以是简单的文件读取工具,也可以对接配置中心。
如果你用的是 Python,可以对应使用 json 模块和 simpleeval 之类的表达式库;如果是 Go,可以使用 gjson 和自写条件判断函数。决策模型的思路是跨语言的。
2.3 学习环境的目录结构
建议按下面的目录结构组织演示项目,这样规则文件和代码分离,便于管理:
text复制laya-demo
├── pom.xml
└── src
├── main
│ ├── java
│ │ └── com/example/laya
│ │ ├── LayaDemoApplication.java
│ │ ├── model
│ │ │ ├── DecisionInput.java
│ │ │ ├── DecisionOutput.java
│ │ │ └── RuleDefinition.java
│ │ ├── parser
│ │ │ └── JsonRuleParser.java
│ │ ├── engine
│ │ │ └── DecisionEngine.java
│ │ └── service
│ │ └── DiscountDecisionService.java
│ └── resources
│ └── rules
│ └── discount-rules.json
└── test
└── java
└── com/example/laya
└── DecisionEngineTest.java
这里没有把规则写入数据库,是为了先跑通最小闭环。生产环境建议把规则文件迁移到配置中心或数据库,加载逻辑不变。
3. 核心概念先对齐:条件、动作、优先级、输入输出
3.1 条件(Condition)
条件就是决策的“入口判断”。一条规则是否被触发,取决于所有条件是否同时满足。条件通常由左值、操作符、右值组成,例如 orderAmount >= 500。在 JSON 规则定义中,一个条件可以这样表示:
json复制{
"field": "orderAmount",
"operator": ">=",
"value": 500
}
这里 field 表示输入参数的字段名,operator 是比较运算符,value 是阈值。需要注意:不同表达式库支持的运算符不完全一样,比如相等判断在 Aviator 里用 ==,在 JSON 配置里经常写成 eq 或 =。设计规则时统一规定运算符的字符串写法,解析层做转换。
3.2 动作(Action)
动作是规则命中后要做的事。在决策模型中,动作可以是返回一个结果值、设置一个变量、调用一个外部接口,也可以是组合动作。最小模型里,动作通常简化为给输出对象赋值。例如:
json复制{
"type": "assign",
"target": "discountRate",
"value": 0.8
}
动作可以是一条,也可以是多条。多条动作按顺序执行,前一条动作的结果可以被后一条动作引用。
3.3 优先级(Priority)
规则不是无条件从上到下执行的。很多业务判断有顺序依赖,例如“先判断黑名单,再判断白名单”,因为黑名单的拦截优先级更高。JSON 规则中可以用 priority 字段控制执行顺序,数值越小越先执行。
json复制{
"priority": 10,
"condition": {...},
"action": {...}
}
在实际设计时,不要用文件顺序代替显式的优先级。文件顺序容易在多人编辑时产生混乱,显式优先级则让决策顺序一目了然。
3.4 输入输出定义(Input/Output)
决策模型必须明确输入输出结构,否则规则里的字段名就失去依据。建议先定义输入输出对象,再写规则。例如输入可以包括用户会员等级、订单金额、用户余额,输出可以包括折扣率、是否允许操作、审核结果等。
java复制public class DecisionInput {
private String userLevel;
private double orderAmount;
private double userBalance;
// getter/setter 省略
}
java复制public class DecisionOutput {
private double discountRate;
private String decisionResult;
// getter/setter 省略
}
字段名必须和规则配置中的 field 完全一致,否则解析时找不到值,会产生空指针或默认值问题。
4. 写一个最小可运行案例:折扣决策模型
4.1 业务场景描述
设计一个电商折扣决策场景,输入条件包括:
- 用户会员等级:NORMAL、SILVER、GOLD。
- 订单金额:整数或浮点数。
- 用户当前余额:用于风控附加判断。
输出结果包括:
- 折扣率
discountRate。 - 决策结果
decisionResult,包括APPROVE、REJECT。
规则需求如下:
- 如果会员等级是 GOLD,且订单金额大于等于 500,则折扣率 0.8,决策结果 APPROVE。
- 如果会员等级是 SILVER,且订单金额大于等于 300,则折扣率 0.9,决策结果 APPROVE。
- 如果会员等级是 NORMAL,直接折扣率 1.0,决策结果 APPROVE。
- 如果用户余额低于订单金额的 20%,则决策结果改为 REJECT,无论会员等级。
这四条规则体现了两个典型特征:命中多条规则时如何合并结果,以及风控规则如何覆盖正常规则。
4.2 规则文件定义
将规则写入 src/main/resources/rules/discount-rules.json:
json复制{
"rules": [
{
"id": "rule_vip_gold",
"name": "黄金会员大额订单折扣",
"priority": 20,
"conditions": [
{ "field": "userLevel", "operator": "eq", "value": "GOLD" },
{ "field": "orderAmount", "operator": ">=", "value": 500 }
],
"actions": [
{ "type": "assign", "target": "discountRate", "value": 0.8 },
{ "type": "assign", "target": "decisionResult", "value": "APPROVE" }
]
},
{
"id": "rule_vip_silver",
"name": "白银会员中额订单折扣",
"priority": 20,
"conditions": [
{ "field": "userLevel", "operator": "eq", "value": "SILVER" },
{ "field": "orderAmount", "operator": ">=", "value": 300 }
],
"actions": [
{ "type": "assign", "target": "discountRate", "value": 0.9 },
{ "type": "assign", "target": "decisionResult", "value": "APPROVE" }
]
},
{
"id": "rule_normal",
"name": "普通会员默认折扣",
"priority": 100,
"conditions": [
{ "field": "userLevel", "operator": "eq", "value": "NORMAL" }
],
"actions": [
{ "type": "assign", "target": "discountRate", "value": 1.0 },
{ "type": "assign", "target": "decisionResult", "value": "APPROVE" }
]
},
{
"id": "rule_balance_check",
"name": "余额不足风控拦截",
"priority": 10,
"conditions": [
{ "field": "userBalance", "operator": "lt", "value": "orderAmount * 0.2" }
],
"actions": [
{ "type": "assign", "target": "decisionResult", "value": "REJECT" }
]
}
]
}
注意 rule_balance_check 的优先级是 10,比折扣规则更靠前。这意味着系统会先执行风控判断,把结果设为 REJECT;之后再命中折扣规则,又把决策结果改回 APPROVE。这里暴露了一个重要问题:动作的执行方式决定结果是否会被覆盖。后面 4.4 节会专门处理。
4.3 解析器和模型类
先定义规则模型类,把 JSON 映射到 Java 对象。
java复制public class RuleDefinition {
private String id;
private String name;
private int priority;
private List<Condition> conditions;
private List<Action> actions;
// getter/setter 省略
}
java复制public class Condition {
private String field;
private String operator;
private String value;
// getter/setter 省略
}
java复制public class Action {
private String type;
private String target;
private String value;
// getter/setter 省略
}
使用 Jackson 解析 JSON 文件:
java复制public class JsonRuleParser {
public List<RuleDefinition> parse(InputStream jsonStream) throws IOException {
ObjectMapper mapper = new ObjectMapper();
JsonNode root = mapper.readTree(jsonStream);
JsonNode rulesNode = root.get("rules");
return mapper.readValue(
rulesNode.toString(),
new TypeReference<List<RuleDefinition>>() {}
);
}
}
这里有一个坑:Jackson 默认要求 JSON 字段名与 Java 属性名一致。RuleDefinition 里的 priority、conditions、actions 都是小写开头,和 JSON 保持一致就没有问题。如果字段名不一致,要么改 JSON,要么在 getter 上使用 @JsonProperty。
4.4 执行引擎:规则如何按优先级执行
执行引擎的职责是接收输入、加载规则、按优先级执行、返回输出。核心逻辑为:先按优先级排序规则,逐条判断条件,命中后执行动作。
java复制public class DecisionEngine {
private final List<RuleDefinition> rules;
public DecisionEngine(List<RuleDefinition> rules) {
this.rules = rules.stream()
.sorted(Comparator.comparingInt(RuleDefinition::getPriority))
.collect(Collectors.toList());
}
public DecisionOutput execute(DecisionInput input) {
DecisionOutput output = new DecisionOutput();
// 设置默认值
output.setDiscountRate(1.0);
output.setDecisionResult("APPROVE");
for (RuleDefinition rule : rules) {
if (match(rule, input)) {
applyActions(rule, input, output);
}
}
return output;
}
private boolean match(RuleDefinition rule, DecisionInput input) {
for (Condition condition : rule.getConditions()) {
Object leftValue = resolveValue(condition.getField(), input);
Object rightValue = resolveValue(condition.getValue(), input);
if (!compare(leftValue, condition.getOperator(), rightValue)) {
return false;
}
}
return true;
}
}
这里的关键设计是:所有规则都会按优先级执行,命中规则的 action 会被执行,而后执行的动作可能覆盖先执行的动作。这个机制叫“叠加覆盖”,它允许低优先级规则设置默认值,高优先级规则在后续覆盖,也可以反过来。具体业务中要明确采用哪种模式,并在规则文档里写清楚。
对于余额不足的风控规则,如果希望在折扣计算完成后再覆盖,应该把 rule_balance_check 的优先级设置成大于所有折扣规则的数值,例如 200。之前的配置为 10,实际执行时会导致风控结果被后续规则覆盖,这是一个经典的设计错误。本文演示时特意保留了错误配置,以便展示排查路径。
4.5 跑通案例:测试代码与日志输出
编写测试代码验证不同输入下的决策结果。
java复制public class DecisionEngineTest {
@Test
public void testDecisionEngine() throws Exception {
JsonRuleParser parser = new JsonRuleParser();
InputStream stream = getClass().getResourceAsStream("/rules/discount-rules.json");
DecisionEngine engine = new DecisionEngine(parser.parse(stream));
DecisionInput goldInput = new DecisionInput();
goldInput.setUserLevel("GOLD");
goldInput.setOrderAmount(600);
goldInput.setUserBalance(200);
DecisionOutput output = engine.execute(goldInput);
System.out.println("discountRate=" + output.getDiscountRate());
System.out.println("decisionResult=" + output.getDecisionResult());
DecisionInput rejectedInput = new DecisionInput();
rejectedInput.setUserLevel("GOLD");
rejectedInput.setOrderAmount(600);
rejectedInput.setUserBalance(50);
DecisionOutput rejectedOutput = engine.execute(rejectedInput);
System.out.println("rejected discountRate=" + rejectedOutput.getDiscountRate());
System.out.println("rejected decisionResult=" + rejectedOutput.getDecisionResult());
}
}
第一组输入 GOLD / 600 / 200,余额与订单金额比例为 33%,大于 20%,风控规则不触发,预期输出 discountRate=0.8、decisionResult=APPROVE。
第二组输入 GOLD / 600 / 50,余额与订单金额比例约 8.3%,小于 20%,风控规则触发。但因为当前配置中风控优先级为 10,折扣规则优先级为 20,先执行风控把结果设为 REJECT,随后折扣规则又把结果改回 APPROVE。这个输出明显不符合业务预期。这就是决策模型中最常见的坑:规则执行顺序没有结合叠加覆盖语义设计。
5. 关键参数和设计取舍:不要让规则变成黑盒
5.1 优先级、全量命中与首个命中
决策模型引擎通常有两种执行模式:全量命中执行和首个命中停止。两种模式的使用场景不同。
| 执行模式 | 行为 | 适用场景 |
|---|---|---|
| 全量命中 | 所有条件命中的规则都执行,允许后续覆盖前序结果 | 同时计算折扣、积分、风控等多个相互独立维度 |
| 首个命中停止 | 第一条命中的规则执行后直接返回,不再判断后续规则 | 黑名单拦截、优惠券互斥、会员等级归属判断 |
如果使用全量命中模式,必须明确动作的覆盖语义:是“后执行覆盖先执行”,还是“先执行后不再覆盖”。如果使用首个命中模式,规则排序非常重要,需要把最严格、优先级最高的规则放在前面。
实际项目里不要混合使用两种模式,否则规则编写者很难预测结果。推荐的做法是:引擎支持配置 mode 字段,但一个规则集合只使用一种模式。示例中为了控制篇幅,采用了全量命中模式,把风控规则放在最后覆盖,即优先级大于 200。
5.2 取值解析:字段引用与直接值
条件 userBalance < orderAmount * 0.2 中,orderAmount * 0.2 不是一个直接值,而是引用字段并做计算。解析层需要区分“字面量”和“表达式”。常见的做法是:以数字开头判断为字面量,包含字段名或运算符时判断为表达式,或者使用明显的前缀标记,比如 $orderAmount * 0.2。
推荐显式标记表达式字段,避免歧义。例如把规则里的 value 写成 "$orderAmount * 0.2",解析器看到 $ 就知道这是一个表达式。字面量直接转换类型,表达式则交给 Aviator 或 SpEL 求值。
5.3 动作类型扩展
实际决策模型的动作绝对不止 assign 一种。常见类型还包括:
log:记录规则命中日志。call:调用外部接口。setFlag:设置一个内部标记,给后续规则引用。return:直接结束决策流程并返回结果。increment:给数值变量增加指定值。
动作类型越丰富,引擎的表达能力越强,但配置复杂度也越高。建议第一版先实现 assign 和 return,跑通后再扩展 call。外部调用介入决策流程后,需要额外处理超时、重试和异常兜底,不建议在核心决策链路上同步调用慢接口。
5.4 表达式求值的安全问题
规则来源于配置文件或数据库后,表达式安全就变成重点。如果直接使用 Java 反射解析规则里的字段和方法,恶意规则可能触发危险操作。无论表达式库是什么,生产环境都要做以下限制:
- 只允许访问白名单内的字段。
- 禁止在表达式中调用任意 Java 方法。
- 设置表达式执行超时时间。
- 限制表达式复杂度,例如最大深度、最大节点数。
正确示例:
java复制Expression compiledExpression = ExpressionEngine.compile("orderAmount * 0.2");
double threshold = compiledExpression.evaluate(input);
不要这样做:
java复制Object result = new ScriptEngineManager().getEngineByName("JavaScript")
.eval("importClass(java.lang.Runtime); ...");
后者等于把服务器权限直接暴露给规则配置人员。
6. 运行验证:结果正确不一定说明模型正确
6.1 从日志和输出两方面验证
决策模型上线前,至少要做三类验证:
- 功能验证:典型场景输入是否得到预期结果。
- 边界验证:空值、零值、负数、超大数、重复规则是否异常。
- 回归验证:修改规则后,旧用例是否全部保持正确。
日志方面,建议记录每次执行的规则 ID 和命中结果。参考输出格式:
text复制decisionId=7f3a2b, input={"userLevel":"GOLD","orderAmount":600,"userBalance":50}
ruleHit=rule_balance_check, actionResult=REJECT
ruleHit=rule_vip_gold, actionResult=APPROVE
finalResult=APPROVE
这种日志让“某条结果由哪几条规则叠加产生”一目了然。如果线上决策结果和预期不一致,直接查日志中命中的规则列表即可。
6.2 重新修正规则并验证
按照第 5 节的建议,把风控规则优先级改为 200,让它最终覆盖决策结果。修改后的 JSON 片段如下:
json复制{
"id": "rule_balance_check",
"name": "余额不足风控拦截",
"priority": 200,
"conditions": [
{ "field": "userBalance", "operator": "lt", "value": "$orderAmount * 0.2" }
],
"actions": [
{ "type": "assign", "target": "decisionResult", "value": "REJECT" }
]
}
再次运行第二组测试用例,预期输出变为 discountRate=0.8、decisionResult=REJECT。这里也要注意:折扣率被计算出来了,但最终结果被风控拒掉。业务上完全合理,因为折扣率可以先算出来展示给用户,提交时才触发风控拦截。
6.3 规则命中结果的判定标准
以下判定标准可以用于验收决策模型:
| 检查项 | 通过标准 |
|---|---|
| 正常场景 | 普通、白银、黄金会员分别得到正确折扣 |
| 边界场景 | 订单金额刚好等于阈值时按大于等于规则处理 |
| 风控场景 | 余额不足时最终结果为 REJECT |
| 未命中场景 | 空输入或未知会员等级不抛异常,返回默认结果 |
| 规则变更 | 修改 JSON 后无需改代码,重载规则即生效 |
未命中场景通常容易被忽略。如果输入数据缺失 userLevel,比较操作可能直接空指针。解析层应该对缺失字段返回默认值或直接视条件不成立。否则线上一条脏数据就能让决策接口报错。
7. 常见坑:至少避开这五个错误设计
7.1 规则默认值设置太晚,导致风控被覆盖
错误现象是风控规则明明命中,最终结果还是 APPROVE。原因如前面分析,全量命中且后执行覆盖先执行,低优先级风控被高优先级折扣规则覆盖。解决方式是把不可被覆盖的规则优先级调到最高,或者在动作里加 return 类型,命中后立即终止。
7.2 类型不一致导致阈值判断错误
规则值全部按字符串解析时,orderAmount >= 500 可能变成字符串比较。字符串比较按字典序进行,"500" 和 "600" 比较可能得到错误结果,尤其当数值位数不同时,例如 "100" 小于 "99"。
解决方式是解析条件时做类型推断:value 是纯数字就转成数字类型,是带引号的字符串就按字符串处理。测试时要显式验证数值比较和字符串比较两条路径。
7.3 用文件顺序代替优先级
多人协作时,不同人往 JSON 文件不同位置插入规则,顺序一下就乱了。必须显式设计 priority,并尽可能在加载时按 priority 排序,而不是按照数组顺序执行。生产环境还应该在规则校验阶段检查优先级是否重复,重复时给出告警。
7.4 规则条件使用硬编码魔法值
条件 userLevel == "VIP" 本身没有问题,但状态值的枚举散落在规则各处时,改动一个状态名要搜索多个 JSON。建议把枚举值收敛到常量定义或字典中,规则文件里引用字典 key。例如 userLevel 直接使用 SILVER、GOLD 这类可读值,避免使用 "1"、"2" 这类无意义数字。
7.5 没有规则版本管理,线上回滚困难
规则文件一旦存储在服务器本地,就面临版本管理问题。上线前必须确认规则有版本号或 Git 标签。配置中心方案里,每次变更都要生成新版本号,回滚时指定旧版本生效。没有版本管理的规则模型,本质上只是把 if...else 换了个存放位置。
8. 生产环境落地:从最小案例到稳定服务
8.1 规则加载与热更新
最小案例使用启动时一次性加载 JSON。生产环境要支持规则动态更新,常见做法有三种:
- 本地文件监听:监听规则文件变化,自动重新加载。
- 配置中心推送:接入 Apollo、Nacos 或自研配置中心,监听配置变更事件。
- 数据库版本管理:规则存入数据库,定期拉取版本号,有变化才刷新内存。
推荐生产环境使用配置中心或数据库方案。本地文件监听在单机项目里够用,但在多实例部署时需要额外处理不一致问题。热更新加载时要注意原子性:先加载新规则,校验通过后再替换内存中的引用,避免加载了一半的规则被线上请求使用。
8.2 规则校验和测试体系
建立三种校验层次:
- 格式校验:JSON 是否能解析、字段是否完整、操作符是否合法。
- 单元测试:对单个规则做白盒测试,验证条件判断是否正确。
- 集成测试:把规则文件加载进完整引擎,跑业务场景用例。
测试用例建议固化到代码仓库,每次变更规则都执行一遍。规则改坏了比代码改坏了更隐蔽,因为问题往往只在特定组合输入下出现。
8.3 监控指标设计
决策模型上线后,至少监控以下几类指标:
| 指标 | 含义 | 告警建议 |
|---|---|---|
| 决策执行耗时 | 单次决策从输入到输出的时间 | 超过 100ms 告警 |
| 规则命中分布 | 各规则命中次数占比 | 某条规则命中率为 0 时告警 |
| 异常次数 | 解析异常、空指针、超时次数 | 任何异常都告警 |
| 规则变更频率 | 单位时间内规则版本变化次数 | 过高时提醒检查变更流程 |
决策引擎本身通常不是性能瓶颈,真正的风险在于表达式求值、外部接口调用和不合理的全量扫描。如果规则量达到千条级别,建议使用规则索引、决策树或 RETE 算法优化。这是后续扩展方向,不是第一版必须做的事情。
8.4 与业务代码的边界
决策模型不是万能药。不是所有逻辑都适合迁移进规则文件。如果一个判断只在一个地方使用、一年不变、没有业务人员参与调整,继续写在代码里反而更清晰。适合迁入决策模型的是那些“变化频繁、多条件组合、需要业务共同维护”的逻辑。
推荐在项目里定义一个清晰的接口边界,例如 DecisionEngine.execute(DecisionInput)。上游业务只依赖这个接口,不关心内部规则如何实现。模型内部怎么拆规则、怎么改优先级,完全不影响业务代码,这是决策模式迁移到配置化的核心价值。
9. 可复用的实施清单
开发一个新决策模型时,按照下面的清单逐项确认,能减少大部分低级问题。
前置设计阶段:
- 确定输入输出字段及类型,用 Java 对象或 JSON Schema 固化。
- 确认执行模式:全量命中还是首个命中。
- 定义动作覆盖语义:后覆盖先,还是保持不变。
- 设计优先级规则,明确哪些规则不可被覆盖。
- 确定规则存储位置:文件、数据库还是配置中心。
规则编写阶段:
- 条件字段名与输入对象属性一致。
- 运算符统一表达,例如
eq、ge、lt。 - 数值类型不要写成字符串比较。
- 显式设置优先级,不依赖数组顺序。
- 表达式引用字段时使用标记或规范格式。
测试验证阶段:
- 正常场景用例至少覆盖每个规则的一条路径。
- 边界场景覆盖阈值相等值。
- 异常场景覆盖空输入、未知枚举、类型不匹配。
- 回归用例在每次规则变更后全部执行。
- 日志确认
ruleHit列表,而非只看最终结果。
上线运维阶段:
- 规则文件有 Git 版本标签或配置中心版本号。
- 支持回滚到上一个版本。
- 配置热更新具备原子替换能力。
- 监控决策耗时、命中分布和异常次数。
- 表达式库开启白名单、超时和安全限制。
生产变更流程至少要有“提交规则 -> 自动校验 -> 测试环境跑批 -> 灰度环境验证 -> 生产发布”五步。灰度环境中可以先让 5% 流量进入新规则,对比决策结果分布和异常率,稳定后再全量放开。
10. 下一步可以往哪里扩展
Laya 决策模型的最小案例跑通后,可以沿三个方向继续深入。
第一个方向是决策表和决策树的图形化表示。把 JSON 规则的可读性再提升一层,让业务人员直接在页面上配置“条件行、结果列”。底层仍然编译成规则定义,但配置和维护的体验完全不同。
第二个方向是把规则从 Java 中完全解耦,实现跨团队协作。规则文件不再由开发写,而是由业务策略人员在配套工具里维护。此时需要提供规则校验、发布审批、版本对比、灰度生效等能力,已经接近商业化决策引擎的范畴。
第三个方向是引入机器学习辅助决策。有些场景下规则无法覆盖所有特征组合,模型可以根据历史数据计算风险分或转化率,再与规则引擎配合。决策模型负责可控、可解释的硬规则,模型负责复杂概率预估。两者结合时要注意:模型的输出作为决策输入字段传入,而不是直接替代规则层。
无论选择哪个方向,核心能力仍然是“把决策逻辑结构化、可视化、可排查”。先跑通一个最小决策模型,把规则加载、条件匹配、动作执行、日志追踪做扎实,后面的扩展都建立在这些基础之上。实际项目里,最值得学习的不是复杂引擎的高级特性,而是把几条简单规则组织得稳定、可靠、可回滚的工程能力。
