本文对 Rete 算法的执行架构、网络拓扑、编译与匹配过程进行说明,并附带 Python 代码模拟。
1. 规则引擎执行架构(Match-Select-Act)
首先要理解 事实 (Fact) 与 规则 (Rule) 是如何交汇的。
graph LR
subgraph 业务端
Fact[Facts 事实对象]
Rules[Rules 规则描述]
end
subgraph 规则引擎运行时核心
WM[(工作内存<br/>Working Memory)]
PM[(规则内存<br/>Production Memory)]
Matcher{Rete 模式匹配器<br/>Pattern Matcher}
Agenda[议程控制<br/>Agenda]
end
Fact -->|1. Insert 断言插入| WM
Rules -->|2. 加载编译入库| PM
WM -->|事实进入| Matcher
PM -->|构建网络| Matcher
Matcher -->|3. 条件匹配成功的激活项| Agenda
Agenda -->|4. 按优先级解决冲突并执行| Action[触发操作逻辑 / RHS]
Action -.->|5. 可能会产生或修改事实| WM
执行分三个阶段:
- 匹配 (Match):Fact 插入工作内存,通过 Rete 网络与规则条件建立绑定。
- 选择 (Select):匹配成功的结果放入 Agenda,根据优先级决定执行顺序。
- 执行 (Act):触发动作,可能引起新的事实变更,从而触发新一轮匹配。
2. Rete 网络结构
当规则包含多个条件时,Rete 会构建多层网络,分为两部分:
- Alpha Network:对单个事实做属性过滤
- Beta Network:将多个 Alpha 节点的结果做 Join 连接
graph TD
subgraph AlphaLayer [Alpha层 单条件过滤]
Root[RootNode<br/>数据总入口]
OTN[ObjectType Node<br/>对象类型分发]
Root --> OTN
OTN --> Alpha1[Alpha 1: 地区=北京]
OTN --> Alpha2[Alpha 2: 车龄<10年]
OTN --> Alpha3[Alpha 3: 车价>1万元]
end
subgraph BetaLayer [Beta层 条件组合 / Join]
Beta1((Beta Node 1<br/>地区 + 车龄))
Alpha1 -->|左输入| Beta1
Alpha2 -->|右输入| Beta1
Beta2((Beta Node 2<br/>前置结果 + 车价))
Beta1 -->|左输入| Beta2
Alpha3 -->|右输入| Beta2
end
subgraph ActionLayer [执行区]
Terminal[Terminal Node<br/>条件满足,执行规则]
Beta2 --> Terminal
end
style Alpha1 fill:#e6f7ff,stroke:#1890ff
style Alpha2 fill:#e6f7ff,stroke:#1890ff
style Alpha3 fill:#e6f7ff,stroke:#1890ff
style Beta1 fill:#f9f0ff,stroke:#722ed1,shape:circle
style Beta2 fill:#f9f0ff,stroke:#722ed1,shape:circle
style Terminal fill:#fffbe6,stroke:#faad14
- 增量匹配:Beta1 会缓存”地区+车龄”的组合结果。后续新增”车价”事实时,只需与 Beta1 的缓存做 Join,不必从头遍历。
3. 为什么要分 Alpha 和 Beta 两层
如果不做拆分,每来一条新事实就要把所有规则的所有条件重新算一遍,复杂度是 O(规则数 × 条件数)。
拆成两层后:
Alpha 层做独立过滤:每个条件单独检查一个属性,结果缓存在 Alpha 内存中。
- 新事实进来,只触发属性匹配的 Alpha 节点,其他节点不动
- 多条规则如果共用同一个条件(比如都要检查
age < 10),共享同一个 Alpha 节点,只算一次
Beta 层做组合连接:把多个 Alpha 的结果按 ID 做 Join,中间结果缓存在 Beta 内存中。
- 已经组合好的部分结果不需要重算。比如”地区+车龄”已经 Join 好了,后面”车价”事实到了,直接拿缓存做第二次 Join
- 多条规则如果前几个条件相同,共享同一条 Beta 路径,只在分歧点才分叉
类比数据库:Alpha 相当于 WHERE 过滤,Beta 相当于 JOIN 连接,Alpha/Beta 内存相当于缓存的中间结果。不分层就等于每次全表扫描加嵌套循环 Join;分层后相当于建了索引并缓存中间结果,只对增量部分做计算。
4. 编译与匹配推演
在 Rete 算法中,事实 (Fact) 用”三元组”表示:(标识符 ^ 属性 值),简写为 (FactID, Attribute, Value)。
以车辆投保规则为例:
事实集(进入工作内存):
w1: (car1, area, "北京")w2: (car1, age, 9)w3: (car1, price, 15000)
规则条件(LHS / Pattern):
c1: 匹配地区(?, area, "北京")c2: 匹配车龄(?, age, <10)c3: 匹配车价(?, price, >10000)
第一阶段:创建 Rete 网络(编译期)
引擎在事实产生前,读取规则库生成 Rete 网络:
- RootNode:创建全局唯一的入口节点,所有事实从这里进入。
- Alpha 1:取条件
c1,生成AlphaNode1检查attr == area && value == "北京",挂在 Root 下,附带 Alpha 内存。 - Alpha 2:取条件
c2,生成AlphaNode2检查age < 10,同样挂在 Root 下。 - Beta 1:创建
BetaNode1,左输入接AlphaNode1,右输入接AlphaNode2。 - Alpha 3 + Beta 2:取条件
c3,生成AlphaNode3检查price > 10000;创建BetaNode2,左输入接BetaNode1的输出,右输入接AlphaNode3。 - Terminal:将规则动作封装为
TerminalNode,接在BetaNode2之后。
第二阶段:匹配过程(运行时)
网络构建完成后,每条新事实进入网络,依次经过 Alpha 过滤(Select)和 Beta 连接(Join):
- 插入
w1: (car1, area, "北京")w1从 RootNode 广播到所有 Alpha 节点。AlphaNode1匹配成功(area == "北京"),将w1存入 Alpha 内存,并通知下游BetaNode1。AlphaNode2、AlphaNode3属性不匹配,丢弃。
- 插入
w2: (car1, age, 9)AlphaNode2匹配成功(9 < 10),存入内存,通知BetaNode1。- 首次 Join:
BetaNode1检查左侧(AlphaNode1内存中有w1)和右侧(AlphaNode2内存中有w2),两者 ID 均为car1,Join 成功,生成组合(w1, w2)存入 Beta 内存,传递给BetaNode2。
- 插入
w3: (car1, price, 15000)AlphaNode3匹配成功(15000 > 10000),存入内存,通知BetaNode2。- 二次 Join:
BetaNode2左侧已有来自BetaNode1的(w1, w2),右侧新到w3,ID 一致,Join 成功,生成完整绑定集(w1, w2, w3)。
- 触发动作
(w1, w2, w3)到达TerminalNode,所有条件满足,执行规则动作。
5. Python 代码模拟
下面将 Root → Alpha → Beta(层1) → Beta(层2) → Terminal 的网络用 Python 实现,观察数据逐层传递与 Join 组合的过程。
from typing import Any, Callable, List, Tuple, Union
# ---- 数据结构 ----
class Fact:
"""三元组事实: (标识符, 属性, 值)"""
def __init__(self, id: str, attr: str, value: Any) -> None:
self.id: str = id
self.attr: str = attr
self.value: Any = value
def __repr__(self) -> str:
return f"({self.id}, {self.attr}, {self.value})"
# Token 是在网络中流转的绑定集,可以是单个 Fact 或 Fact 元组
Token = Union[Fact, Tuple[Fact, ...]]
def token_id(t: Token) -> str:
"""从 Token 中提取标识符,用于 Join 时的 ID 一致性校验"""
return t.id if isinstance(t, Fact) else t[0].id
def token_to_tuple(t: Token) -> Tuple[Fact, ...]:
return (t,) if isinstance(t, Fact) else t
# ---- 节点定义 ----
class ReteNode:
def __init__(self, name: str) -> None:
self.name: str = name
self.children: List['ReteNode'] = []
self.memory: List[Token] = []
def pass_to_children(self, token: Token) -> None:
print(f" [放行] \033[92m{self.name}\033[0m")
for child in self.children:
child.receive(token)
def receive(self, token: Token) -> None:
pass
# Alpha 节点:单条件过滤
class AlphaNode(ReteNode):
def __init__(self, name: str, attr: str, condition: Callable[[Any], bool]) -> None:
super().__init__(name)
self.attr: str = attr
self.condition: Callable[[Any], bool] = condition
def receive(self, token: Token) -> None:
if isinstance(token, Fact) and token.attr == self.attr and self.condition(token.value):
self.memory.append(token)
self.pass_to_children(token)
# Beta 节点:Join 连接(按标识符做等值连接)
class BetaNode(ReteNode):
def __init__(self, name: str, left_node: ReteNode, right_node: ReteNode) -> None:
super().__init__(name)
self.left: ReteNode = left_node
self.right: ReteNode = right_node
left_node.children.append(self)
right_node.children.append(self)
def receive(self, token: Token) -> None:
"""当左或右有新数据到达时,与对侧内存做 Join"""
incoming_id = token_id(token)
# 在左右两侧中寻找 ID 匹配的已有缓存
left_matches = [m for m in self.left.memory if token_id(m) == incoming_id]
right_matches = [m for m in self.right.memory if token_id(m) == incoming_id]
if left_matches and right_matches:
# 将左侧绑定集与右侧绑定集合并为更大的元组
combined = token_to_tuple(left_matches[0]) + token_to_tuple(right_matches[0])
self.memory.append(combined)
self.pass_to_children(combined)
# 终端节点:触发规则动作
class TerminalNode(ReteNode):
def __init__(self, name: str, action_func: Callable[[Tuple[Fact, ...]], None]) -> None:
super().__init__(name)
self.action = action_func
def receive(self, token: Token) -> None:
facts = token_to_tuple(token)
print(f"\n=> 🎯 【规则激活】{self.name}")
print(f" 匹配绑定集: {facts}")
self.action(facts)
# ============ 构建 Rete 网络 ============
root = ReteNode("RootNode")
# [Layer 1] Alpha 节点
alpha_area = AlphaNode("Alpha[地区=北京]", 'area', lambda v: v == '北京')
alpha_age = AlphaNode("Alpha[车龄<10年]", 'age', lambda v: v < 10)
alpha_price = AlphaNode("Alpha[车价>1万]", 'price', lambda v: v > 10000)
root.children.extend([alpha_area, alpha_age, alpha_price])
# [Layer 2] Beta1: Join 地区 + 车龄
beta_1 = BetaNode("Beta1[地区+车龄]", alpha_area, alpha_age)
# [Layer 3] Beta2: Join Beta1 结果 + 车价
beta_2 = BetaNode("Beta2[合规+车价]", beta_1, alpha_price)
# Terminal: 执行动作
terminal = TerminalNode(
"Terminal[推荐高价值投保套餐]",
lambda facts: print(f" ===> 执行动作:为 {facts[0].id} 推荐北京高价值投保套餐")
)
beta_2.children.append(terminal)
# ============ 运行测试 ============
print("1. 录入事实:地区...")
root.pass_to_children(Fact('car1', 'area', '北京'))
# Alpha[地区] 匹配,存入内存;Beta1 右侧为空,阻断
print("\n2. 录入事实:车龄...")
root.pass_to_children(Fact('car1', 'age', 9))
# Alpha[车龄] 匹配;Beta1 左右均有 car1 的数据,Join 成功 → 组合 (w1, w2)
# Beta2 右侧 alpha_price 为空,阻断
print("\n3. 录入事实:车价...")
root.pass_to_children(Fact('car1', 'price', 15000))
# Alpha[车价] 匹配;Beta2 左侧有 (w1,w2),右侧有 w3,ID 一致 → Join 成功
# 完整绑定集 (w1, w2, w3) 到达 Terminal,触发动作
6. 优缺点
优点
- 规则间共享节点:不同规则如果有相同的条件模式,会共享 Alpha/Beta 节点。某个节点被 N 条规则共用,该节点上的匹配效率提升 N 倍。
- 增量匹配:Alpha 内存和 Beta 内存缓存了中间结果。事实集变化不大时,大部分节点状态不需要重算,只处理增量部分。
- 匹配速度与规则数量无关:事实在网络中传播时,不满足条件就被拦截,不会继续向下。规则再多,单条事实经过的路径长度是固定的。
缺点
- 事实删除开销大:删除事实时,除了要做和添加相同的计算外,还需要在内存中查找并清除相关的缓存记录。
- 内存占用可能指数级增长:Beta 内存存储的中间组合结果,会随着规则条件数和事实数量增长而膨胀。规则和事实很多时,可能耗尽内存。
实践建议
| 建议 | 原因 |
|---|---|
| 容易变化的规则放后面 | 减少规则变动导致的网络重建 |
| 约束性强的条件放前面 | 尽早过滤,减少进入 Beta 层的数据量 |
| 内存中只存指针,建索引 | 缓解 Beta 内存膨胀问题 |
一句话总结:Rete 用空间换时间——通过缓存中间结果加速匹配,代价是内存开销大。适合事实集相对稳定、规则频繁执行的场景。
相关链接
PREVIOUS用 Node.js 实现 Windows RDP 自动登录