Rust打造的可独立部署的决策规则引擎微服务
cmx-rulesengine 设计与功能方案
一个用 Rust 打造、可独立部署的决策规则引擎微服务。对标全球最先进的规则引擎(GoRules ZEN / Camunda DMN / Drools / IBM ODM),采用
cmx-flowengine/cmx-report的**「一芯多壳」架构范式,是 CMX 平台metaKind分类学的第五元:RULE(决策规则)**。
- 文档性质:源码级设计与功能方案(图文并茂,SVG 以 base64 内嵌)
- 对应源码:
cmx-rulesengine/(6 crate + 前端 6 页 + 5 存储表) - 技术栈:Rust · axum · tokio-postgres · Rhai 1.25 · 自研 FEEL 引擎
- 部署端口:独立微服务
:8094(或内嵌门户 / 反代壳三形态)
目录
- 产品定位与设计目标
- 总体架构:一芯多壳
- 领域模型:决策集一体三态
- 决策表:声明式内核主体
- FEEL 表达式引擎
- 决策图:JDM 拓扑编排
- 脚本能力:Rhai 过程式逃生舱
- 完整性分析:gap / overlap
- 可解释性:求值主流程与 trace
- 多租户:db-per-tenant
- 存储层:五张表
- API 全景
- 前端工作台
- 里程碑与质量保障
一、产品定位与设计目标
cmx-rulesengine 是 CMX 平台 metaKind 分类学继 DCT(字典)/ DOC(单据)/ FLX(流程表单)/ RPT(报表) 之后的第五元:RULE(决策规则)。它对齐当前 Rust 生态唯一生产级业务规则引擎 GoRules ZEN 的形态(决策图 + 决策表 + 表达式 + 逃生舱、纯 Rust、嵌入式微秒级),并在两处谋求超越。
1.1 三种规则范式的选型
业界规则引擎可归为三大范式,本引擎的取舍如下:
| 范式 | 代表 | 适用 | 本引擎取舍 |
|---|---|---|---|
| ① 产生式(Rete/PHREAK) | Drools、CLIPS、IBM ODM | 推理链 / 事实累积 / CEP / 专家系统 | 后置可选(少数场景) |
| ② 决策表(DMN 标准) | Camunda DMN、OpenL Tablets | 无状态「输入→决策」(定价/资格/合规/风控) | ✅ 内核主体 |
| ③ 决策图(JDM,JSON 化) | GoRules ZEN、DecisionRules | 编排多决策 + 可视化 + 嵌入式高性能 | ✅ 顶层编排 |
引擎以 ②决策表为内核主体、③决策图为顶层编排,绝大多数业务(信贷审批、风控、定价、资格、合规)是无状态的「输入事实 → 输出决策」,决策表 + 决策图足以覆盖;产生式推理作为后置可选能力。
1.2 相对 ZEN 的两处超越
- 表达式走 FEEL/DMN 标准 —— ZEN 用私有 business-first 方言(与 DMN 工具链不互通),本引擎自研 FEEL 引擎拿到「世界级」标准的可信度、可移植性、业务可读性。
- 补齐 ZEN 缺失的两块世界级能力:
- 决策表 gap / overlap 完整性分析(空隙与重叠检测)——ZEN 与多数引擎缺此;
- 决策 trace 记录失败归因(ZEN 的 trace 不显示「哪个节点导致失败、为什么」)。
1.3 设计纪律
- 永不 panic:一切错误(表达式错、脚本错、结构错)以
Result返回并落 trace 的failure,绝不穿透宿主。 - 确定性 / wasm 友好:核心引擎不引
Instant、不做 IO,求值可复现(脚本沙箱用操作数计数而非 wall-clock 超时)。 - 判定侧永远 FEEL:脚本能力只在输出/计算侧,判定侧(决策表输入列)永远是可静态分析的 FEEL,保 gap/overlap 完整性分析始终有效。
二、总体架构:一芯多壳
参照 cmx-flowengine(S0→S6)与 cmx-report 的架构范式:中立核零框架依赖,薄壳承载部署形态。整个引擎由 6 个 crate 分层组成,依赖严格自上而下(壳 → 核)。
2.1 六 crate 职责
| crate | 层次 | 职责 | 依赖 |
|---|---|---|---|
| cmx-rule-model | 中立领域核 | HitPolicy / DecisionDef / DecisionBody / DecisionTable / DecisionGraph / ScriptBody / TraceNode / CoverageReport | 仅 serde |
| cmx-rule-feel | 表达式 / 脚本 | 自研 FEEL 引擎(tokenizer+Pratt+eval+~25 内置)+ Constraint 区域推理 + Rhai 脚本沙箱 | model, rhai |
| cmx-rule-engine | 求值引擎 | evaluate_with(决策表/图/脚本分派)+ graph.rs(Kahn 拓扑)+ analyze.rs(gap/overlap) | feel, model |
| cmx-rule-store-pg | 持久化 | PgDecisionStore(实现 DecisionStore trait)+ 5 张表 DDL | model |
| cmx-rule-app | 中立应用核 | 26 条 REST 路由(rule_routes::<S> 泛型)+ handlers + tenancy + stats + 求值即落审计 | engine, feel, model, store, axum |
| cmx-rule-server | 部署壳 | 独立 bin,chassis 装配,监听 :8094 | app, axum |
依赖倒置纪律:cmx-rule-model 是中立核,只依赖 serde,不知道 HTTP、不知道 PostgreSQL、不知道 Rhai;求值引擎 cmx-rule-engine 只认领域模型与 FEEL/脚本内核,不认存储与 Web。这使得引擎可被内嵌、可独立部署、可多租户,无框架锁定。
2.2 三种部署形态
- 独立微服务:
cmx-rule-serverbin 监听:8094,自持 PostgreSQL 连接,RULE_AUTH_MODE/RULE_TENANCY控认证与租户。 - 门户内嵌:
cmx-rule-app的rule_routes::<S>泛型路由挂进门户,同进程共享装配。 - 门户反代壳:
cmx-rule-api(proxy-only)在门户侧反代到独立:8094,内嵌 ↔ 独立仅切urls.rules一处配置,字节级 parity。
三、领域模型:决策集一体三态
一个决策集(DecisionDef)是引擎的基本单元,拥有跨版本稳定的 key、展示 name、version 与决策体 body。决策体 DecisionBody 是 internally tagged(tag="kind",与 GoRules JDM 节点标注同构)的三态枚举:
#[serde(tag = "kind")]
pub enum DecisionBody {
#[serde(rename = "decisionTable")] DecisionTable(DecisionTable), // 决策表
#[serde(rename = "graph")] Graph(DecisionGraph), // 决策图
#[serde(rename = "script")] Script(ScriptBody), // 脚本决策
}
序列化时 body flatten 进顶层,即 {key, name, version, kind, ...}:kind:"decisionTable" 带 hitPolicy/inputs/outputs/rules,kind:"graph" 带 nodes/edges,kind:"script" 带 lang/script。
3.1 版本 · 发布 · 激活生命周期
| 阶段 | 表 | 说明 |
|---|---|---|
| 草稿 | cmx_rule_definition | 设计器保存,含结构校验 + 脚本语法预检 |
| 发布 | cmx_rule_release | 产不可变 release,version+1 |
| 激活 | cmx_rule_release.active | 求值按 key 装载 active 版本 |
求值有两条路径:POST /decisions/{key}/evaluate(按 key 装载激活版本)与 POST /evaluate(内联 definition 试算,不落库、先 validate)。
四、决策表:声明式内核主体
决策表是引擎的招牌能力:一张二维表,输入列 × 规则行 × 输出列 + 命中策略。判定侧(输入列)永远是 FEEL unary test,输出侧(输出列)是 FEEL 表达式 / 字面量 / =rhai: 脚本。
- 输入列(InputClause):绑定一个 FEEL 表达式(如
score、income * rate),单元格是 unary test(> 700、[600..700)、"north","south"、-通配)。 - 输出列(OutputClause):命名输出字段,单元格是 FEEL 表达式 / 字面量 /
=rhai:脚本。 - 规则行(DecisionRule):
inputEntries[]对齐输入列、outputEntries[]对齐输出列。 - 命中策略(HitPolicy):多行同时命中时如何裁决。
4.1 十一种命中策略
对齐 DMN 标准,分单命中 / 多命中 / 聚合三族,共 11 种:
| 族 | 代码 | 名称 | 语义 |
|---|---|---|---|
| 单命中 | U | Unique | 唯一——多命中即冲突报错(最严格,默认) |
A | Any | 任意——多命中须输出相同,否则报错 | |
P | Priority | 优先级——按输出值优先序取第一 | |
F | First | 首个——按规则行序取第一命中 | |
| 多命中 | C | Collect | 收集——所有命中输出成列表 |
R | RuleOrder | 规则序——按行序收集 | |
O | OutputOrder | 输出序——按输出优先序收集 | |
| 聚合 | C+ | CollectSum | 求和 |
C< | CollectMin | 最小 | |
C> | CollectMax | 最大 | |
C# | CollectCount | 计数 |
HitPolicy::is_multi() 判定 C/R/O 返回数组;is_aggregate() 判定 C+/C</C>/C# 返回单一聚合值。
五、FEEL 表达式引擎
因离线环境无法拉取 dsntk-rs,本引擎自研 FEEL(DMN 标准表达式语言)引擎:一条 tokenizer → Pratt 解析器 → 求值器 三段流水线,配 ~25 个内置函数。这是相对 ZEN 私有方言的关键超越——拿到 DMN 标准的可移植性与业务可读性。
5.1 三段流水线
- Tokenizer:词法扫描,识别数字、字符串、标识符、运算符、区间
[a..b]。 - Pratt Parser:按最小绑定力(binding power)解析,infix 左结合、
**右结合,构造 AST。 - Evaluator:递归求值 AST →
serde_json::Value,数值统一 f64(与脚本引擎归一一致)。
三个入口覆盖不同载体:
eval_unary_test(cell, value, ctx)—— 决策表输入格判定(> 700/[18..65)/contains(?, "vip"))。eval_output(src, ctx)—— 决策表输出格 / 图 expression 节点求值。eval_expression(src, ctx)—— 完整表达式(/feel/expression端点)。
5.2 内置函数库(~25,逐字对齐)
floor ceil round abs modulo sqrt min max sum avg upperCase lowerCase substring contains startsWith endsWith concat string number trim len sort append not coalesce。
5.3 区域推理(Constraint / NumRange)
FEEL 引擎额外提供 parse_constraint(cell),把单元格解析为区间 / 集合 / 通配的 Constraint,供决策表 gap/overlap 分析做区域相交推理(而非逐点求值)——见第八章。含 not() / != 的列判为「未知」,不误报。
六、决策图:JDM 拓扑编排
决策图(JDM,JSON Decision Model)是顶层编排能力:一张 JSON DAG,数据从 Input 左→右流至 Output,中间穿过多类节点。引擎按 Kahn 拓扑排序逐节点求值,前序节点输出 merge 回累积上下文,传递后继。
6.1 六类节点
节点 type | 作用 |
|---|---|
input | 输入事实入口 |
output | 决策输出出口 |
decisionTable | 内嵌决策表求值 |
expression | FEEL 表达式映射({key: expr} 若干) |
script | Rhai 脚本节点(SC1) |
decision | 引用另一决策集(子决策,可递归) |
6.2 拓扑求值与子决策路由
- Kahn 拓扑:拓扑序逐节点,读累积上下文 → 求值 → 输出 merge 回上下文 → 传递后继。成环在求值期报错并落 trace(后端不校验环,仅求值期检出)。
- 子决策路由:
decision节点按主决策维度(org / 字典)BFS 预载解析器(build_resolver)→ 递归求值,MAX_DEPTH防失控。每层子决策的逐节点 trace 并入父 trace,可解释性穿透。
七、脚本能力:Rhai 过程式逃生舱
当声明式的决策表 / 单表达式 FEEL 表达不动(阶梯累进、多步过程、带状态迭代)时,规则作者用一段受控、沙箱、可审计的 Rhai 脚本兜底。脚本能力分 SC0(接缝)+ SC1-SC4(四载体),全部复用同一求值内核,主流程零改动(均在既有 match 分派点加分支)。
7.1 四载体
| 载体 | 形态 | 实现接缝 |
|---|---|---|
| SC1 · 决策图 Script 节点 | graph 中 type:"script" 节点 | graph.rs 加 "script"=> 分支,累积上下文→对象 merge |
| SC2 · 决策表脚本单元格 | 输出格 =rhai: 前缀 | eval_output → eval_scripted 一行 seam 换 |
| SC3 · 脚本函数库 | cmx_rule_script_function 表 | 发布后注册进引擎,三载体皆可 name(args) 调用 |
| SC4 · 脚本决策 | kind:"script" 整决策 | DecisionBody::Script,可被图 decision 节点引用 |
唯一接缝是 eval_scripted(lang, src, ctx):feel/"" → 现有引擎(零回归)、rhai → 脚本内核,且 sniff =rhai: 前缀。
7.2 沙箱与安全
Rhai 沙箱内核(script.rs)每次求值新建引擎,脚本间不共享可变状态,四道闸门:
| 闸门 | 限额 | 防护 |
|---|---|---|
max_operations | 100,000 | 死循环 / CPU 耗尽 |
max_call_levels | 32 | 递归爆栈 |
max_string_size | 64 KiB | 字符串内存爆炸 |
max_array_size / max_map_size | 10,000 | 集合内存爆炸 |
- 确定性:用操作数计数而非 wall-clock 超时(不引
Instant,保 wasm 友好),求值可复现。 - Don’t-Panic:Rhai 官方保证脚本永不 panic 宿主,错误以
Result返回 → 落FeelError::Script(带行号)。 - 数值归一:Rhai 有独立 i64/f64(
1+1→整数2),递归归一为 f64(2.0),与 FEEL 算术一致,防Number(2) != Number(2.0)破坏。
7.3 架构约束:判定侧永远 FEEL
脚本只在输出 / 计算侧,决策表判定侧(inputEntries)永远 FEEL → gap/overlap 完整性分析对决策表判定始终有效,脚本黑盒天然跳过。脚本决策(SC4)为黑盒,analyze 显式返回「不参与完整性分析」(非静默假 complete)——诚实披露。
八、完整性分析:gap / overlap
这是相对 ZEN 的世界级超越能力:对决策表做区域推理(非逐点穷举),检出「空隙」(gap,某输入组合未被任何规则覆盖)与「重叠」(overlap,多规则覆盖同一区域,单命中策略下潜在冲突)。
8.1 两种检测机制
| 检测 | 方法 | 要点 |
|---|---|---|
| overlap 重叠 | Constraint::intersects 对每对规则逐列判相交 | 含 not()/!= 的列判「未知」→ 不误报;U/A/P 等单命中策略下重叠即潜在冲突 |
| gap 空隙 | 每列按边界切分 → 取代表点 → 笛卡尔积(里程表编码)遍历组合 → 逐组合过真实 any_rule_matches 查覆盖 | 用真实求值器验证,非近似;笛卡尔积上限保护,超出则显式告知 |
gap 检测的巧妙在于:不穷举无穷输入空间,而是按规则边界把每列切成有限段,每段取一个代表点,代表点的笛卡尔积就是「有区分意义的输入组合」的有限集,逐一过真实求值器即可确定覆盖性。
8.2 应用价值
设计器(designer.js / graph-designer.js)在 property 区实时展示 gap/overlap 徽标,规则作者保存前即可发现「score < 600 没有任何规则处理」这类完整性缺陷——这是金融/合规规则不可或缺的质量门。POST /decisions/{key}/analyze 端点消费 CoverageReport{gaps, overlaps}。
九、可解释性:求值主流程与 trace
每次求值都是无状态的(输入事实 → 输出决策),并产出逐节点 trace(含失败归因)与审计日志。这是相对 ZEN 的另一处超越——ZEN 的 trace 不显示「哪个节点导致失败、为什么」。
9.1 主流程
POST /evaluate或/decisions/{key}/evaluate—— 提交 input 事实(或内联 definition 试算)。- 装载定义 —— 按 key 取
cmx_rule_releaseactive 版本;内联走validate。 - build_resolver —— 决策图含
decision节点时 BFS 递归预取被引用决策。 - with_functions ——
load_published_functions→thread_localRAII 注入已发布脚本库(全程同步无 await,无跨请求泄漏)。 - evaluate_with —— 分派求值:决策表
row_matches/ 图 Kahn 拓扑 / 脚本 Rhai。 - 组装响应 + append_log —— 返回
{output, trace[], timingUs, failure}并落审计日志。
9.2 TraceNode 结构
每个节点产一个 TraceNode,构成完整的可解释链:
| 字段 | 含义 |
|---|---|
nodeId / nodeKind | 节点标识与类型(input/decisionTable/script/…) |
matchedRules[] | 命中的规则行号 |
output | 该节点输出快照 |
timingUs | 微秒级时延 |
failure? | 失败原因(脚本错带行号) |
失败归因不误标:脚本节点失败时 nodeKind 保持 "script"(而非被误标为 "graph"),审计中心(logs.js)与仿真台(simulator.js)据此下钻,红标定位失败节点。
十、多租户:db-per-tenant
多租户采用 db-per-tenant 物理隔离(R3,镜像 flow S2)。single 模式单库零回归;multi 模式按租户派生 rule_<tenant> 库并懒备。
- 派生(
tenancy.rs):single→RULE_DB_ID(默认rule_pg);multi→rule_<tenant>(小写),由RULE_TENANT_DB_URL_TEMPLATE的{tenant}占位派生。 - 懒备库:
ensure_current_ready按租户选 db_id + 懒建库建表。规则引擎无长驻运行态,故比流程 S2 更简单——只需「选库 + 懒备」,无 per-tenant 缓存引擎。 - 认证:
RULE_AUTH_MODE=off单租户default放行零回归;开启则验 JWT claim(HS256/RS256)+ scoped 租户上下文(task_local)。
十一、存储层:五张表
PgDecisionStore 实现 DecisionStore trait,5 张表覆盖草稿 ↔ 发布 ↔ 日志 ↔ 用例 ↔ 脚本库。无 RU(工作内存)表——决策是无状态的,不需要产生式引擎的事实工作内存。
| 表 | 职责 |
|---|---|
cmx_rule_definition | 决策集草稿(key PK · body JSONB · published 标记) |
cmx_rule_release | 不可变发布版本(key+version · active 激活位) |
cmx_rule_decision_log | 决策审计日志(input/output/trace/timing/failure) |
cmx_rule_test_case | 测试用例(决策集绑定 · 期望输出 · 覆盖率) |
cmx_rule_script_function | 脚本函数库(name PK · params · body · published) |
决策集稳定 key 贯穿五表:发布产不可变 release,求值装载 active 版本,每次求值落 log。tokio-postgres 并行连接池,日志表按 decision_key / created_at / failure 建索引支持审计下钻。
十二、API 全景
全部 26 条 REST 路由挂在 /api/rules/v1 前缀下(rule_routes::<S> 泛型,内嵌/独立同源),按功能分八组:
| 组 | 端点 |
|---|---|
| 定义 · 设计 | GET /definitions · POST /definitions/draft · POST /definitions/validate · GET,DELETE /definitions/{key} |
| 发布 · 版本 | POST /definitions/{key}/publish · GET /definitions/{key}/versions · POST /definitions/{key}/versions/{version}/activate |
| 求值 | POST /decisions/{key}/evaluate · POST /evaluate(内联试算) |
| 仿真 · 分析 | POST /decisions/{key}/simulate · POST /decisions/{key}/analyze(gap/overlap) |
| 测试用例 | GET,POST /decisions/{key}/tests · POST /decisions/{key}/tests/run · DELETE /decisions/{key}/tests/{id} |
| 审计日志 | GET /decisions/{key}/logs · GET /logs/{id} |
| FEEL | POST /feel/eval · POST /feel/expression · POST /feel/validate · GET /feel/functions |
| 脚本 · 监控 | GET,POST /functions · POST /functions/draft · GET,DELETE /functions/{name} · POST /functions/{name}/publish · POST /script/eval · GET /stats |
统一响应信封 {code, msg, data};code:0 成功,业务错 code:1 带可读 msg。
十三、前端工作台
前端是门户 native 四区工作台:explorer(决策集列表)/ content(编辑)/ property(属性/分析/trace)。共 6 个页面——3 个列表工作台 + 3 个设计器,均为 shadow DOM 自包含 ES 模块,适配 light/dark 双主题。
| 页面 | 角色 |
|---|---|
design-workbench | 决策设计工作台 · 总入口(决策集浏览 + 图态只读预览) |
designer | 决策表设计器 · 可编辑网格 + fx 向导 + 实时 gap/overlap |
graph-designer | 决策图设计器 · 手搓 SVG DAG 编辑(增删节点/连线防环/内嵌决策表) |
sim-workbench | 决策应用工作台 · facts 表单 → 求值 → 输出 + trace |
simulator | 决策仿真台 · 用例集批跑 + 通过率 + 覆盖率 |
logs | 决策审计中心 · 日志列表 + 全量 trace 归因下钻(失败红标) |
多实例 openWorkNode 开成 Tab;native 页经 /api/native-pages 供给,门户 F3 反代与独立 :8094 同源。设计器 / 仿真台对标报表的 designer / applier 双台形态。
十四、里程碑与质量保障
14.1 演进里程碑
| 里程碑 | 目标 | 状态 |
|---|---|---|
| R0 | 决策表内核 · 11 命中策略 · FEEL 子集 · trace · 4 表 | ✅ |
| R1 | 全 FEEL 表达式引擎(tokenizer+Pratt+eval+~25 内置) | ✅ |
| R2 / R3 | JDM 决策图拓扑编排 / db-per-tenant 多租户 | ✅ |
| F1–F5 | 前端:发布版本 · 决策表设计器 · 决策图设计器 · 仿真台 · 审计中心 | ✅ |
| SC0–SC4 | 脚本能力:Rhai 接缝 + 四载体 + 函数库 | ✅ |
14.2 质量保障
| 维度 | 断言数 | 结果 |
|---|---|---|
| 单元测试(feel + engine + model) | 63 | ✅ 全绿 |
脚本专项 API 测试(qa-script.sh) | 55 | ✅ 全绿 |
后端功能回归(qa-backend-1/2.sh) | 146 | ✅ 全绿 |
| clippy 静态分析 | — | ✅ 零告警 |
全部核心能力有真机黑盒(HTTP API 断言)+ 单元测试双重覆盖;脚本沙箱三闸门、数值 f64 归一、判定侧 FEEL 保护均经端到端验证。详见 docs/脚本能力测试报告-2026-08-19.md 与 docs/测试报告-2026-08-17.md。
更多推荐
所有评论(0)