
写推理引擎这事老实说最早我以为是“AI领域那些搞学术的人才需要碰的东西”。直到有一天我需要在一个资源很受限的边缘设备上做实时规则判断数据量不大但逻辑分支特别多才意识到传统规则引擎那一套在嵌入式/边缘场景根本施展不开——要么太重要么语言绑定太死。于是动了“自己写一个轻量级规则引擎”的念头。正好那段时间在啃Rust顺手就把“知识推理引擎”和“基于Rust实现”这两个关键词绑在了一起。这篇文章就是我当时完整思路和实现过程的记录从规则与事实的数据结构设计到推理循环、冲突消解再到一个完整的故障诊断示例代码最后是一些真实踩坑记录。适合对Rust有一定基础、想了解推理引擎内部原理、或者打算在资源敏感环境里自研规则引擎的开发者参考。1. 整体设计与思路拆解1.1 先搞清楚“知识推理引擎”到底在推什么概念这东西一旦用术语包装就显得高深。拆开看其实很简单知识推理引擎干的事就是把“已知的事实”加上“预先定义的规则”通过某种推导机制产生“新的事实”。举个例子事实是“CPU温度95℃”“CPU负载90%”规则是“如果CPU温度85℃且CPU负载80%则判定为过热”那么引擎就能推导出一个新事实“当前系统过热”。这个推导过程可以用一张极其朴素的循环来概括循环 匹配所有规则的条件部分 如果都不满足 / 没有产生新事实退出 选择一条/多条规则执行 把新事实加入事实库这套机制在实际工程里有个更常见的名字——规则引擎。只是知识推理引擎更强调“知识”的概念化通常还会引入三元组主语-谓语-宾语、本体论等表达方式本质上仍然是“条件匹配动作执行”。1.2 为什么我选择了Rust选型这件事我纠结过不少时间。当时对比了Go、Java和Rust三套方案最终选择了Rust理由大概有这么几条内存安全与无GC推理引擎在边缘设备上跑的时候没人希望GC时不时暂停一下。Rust的所有权机制让我能在编译期就规避掉大量内存问题。模式匹配非常适合规则判断Rust的match表达式、模式绑定和解构能力和“规则匹配”这个动作天然契合。规则里的每个条件分支几乎都能用match直接表达。宏系统能做出很漂亮的DSL我想让规则的编写像写配置文件一样简洁Rust的macro_rules!和proc_macro都能很好地实现这一点。性能可控虽然这个项目第一个版本只为验证逻辑但如果后续要做大规模事实匹配Rust能给出逼近C的底层性能同时保住开发效率。有人可能会说Java有DroolsC有CLIPS为什么要自研因为在这个场景里我需要的是一个能嵌入Rust生态、没有额外运行时开销、规则可以被打包成单独数据文件的小型引擎。Drools那种重型方案完全不合适。1.3 整体架构三件套不会变任何规则推理引擎核心结构都是三个部分组件作用类比事实库Working Memory存放在推理过程中已知的事实集合相当于工作台面上的所有零件规则库Rule Base存放所有规则的定义包含条件和动作相当于操作手册推理循环Inference Engine负责匹配、选择、执行规则的循环过程相当于一名照着手册干活的工人在这三者之上我额外又加了两样东西规则优先级字段和冲突消解策略。前者的作用是当多条规则同时被触发时让引擎知道先执行谁后者是所有成熟规则引擎都必须考虑的环节我会在后面细讲。至于“发散创新”体现在哪里我的想法是在有限范围内做一些务实改进第一不用复杂的RETE网络做模式匹配先采用线性扫描加增量事实索引的组合方案兼顾代码可读性和性能第二用Rust宏把规则定义做成接近自然语言的DSL降低业务人员写规则的门槛第三将事实设计成可扩展的特征对象trait object让引擎核心逻辑不依赖具体的业务类型。这三点的组合在现有开源规则引擎里比较少见也是个人项目里值得玩味的地方。2. 事实与规则的数据结构设计2.1 事实Fact的表达方式事实是推理的基础原料。在Rust里我把事实分成了两种形态简单事实和属性事实。简单事实就是一个断言的成立比如“检测到高温”“网络连接已建立”。这种事实用枚举值维护最合适。属性事实则带有一组键值对比如“温度95”“负载90%”。这让我想到了Rust的HashMap但综合考虑到后续匹配效率我用了一个更规整的结构体#[derive(Debug, Clone, PartialEq)] pub enum FactValue { Str(String), Int(i64), Float(f64), Bool(bool), } #[derive(Debug, Clone)] pub struct Fact { pub name: String, pub attributes: HashMapString, FactValue, }在这里name描述事实的类型attributes描述这个事实的各个属性。比如一个事实可以是这样Fact { name: sensor_temp.to_string(), attributes: HashMap::from([ (value.to_string(), FactValue::Int(95)), (unit.to_string(), FactValue::Str(celsius.to_string())), ]), }有人会质疑为什么不用Rust的struct直接定义业务数据类型还要绕一层HashMap原因有二一是规则引擎中的事实来源不确定可能是数据库查出来的、可能是传感器上报的统一成Fact结构对下游匹配逻辑最友好二是在推理过程中规则动作会动态构造新事实运行时如果还依赖具体业务类型编译期就得把所有可能的类型都列出来那就失去引擎的意义了。这里的取舍是核心引擎只关心“这个事实叫什么名字、有什么属性”具体业务语义由规则来驱动。2.2 规则Rule的结构设计规则的建模是整个引擎的关键。一条规则本质上是一个“条件组合触发动作集”的映射。我把它拆成几个字段#[derive(Debug, Clone)] pub struct Rule { pub name: String, pub priority: i32, // 冲突消解时用数值越大优先级越高 pub conditions: VecCondition, // 多个条件语义为“AND” pub actions: VecAction, // 触发后要执行的动作 pub fired_times: i32, // 记录该规则已触发次数用于终止判断 pub max_fire: i32, // 最多能触发几次防止死循环 }fired_times和max_fire是容易被忽略的字段但它们对引擎的稳定性至关重要。我在第一次试跑的时候就因为漏掉“规则最多只能触发N次”这个约束程序在两条互相推导的规则之间无限循环CPU直接拉满。后面在调试部分会详细说。条件Condition的表达式我设计成三元形态#[derive(Debug, Clone)] pub struct Condition { pub fact_name: String, pub attr: String, pub op: Operator, pub value: FactValue, } #[derive(Debug, Clone)] pub enum Operator { Equal, NotEqual, GreaterThan, LessThan, GreaterEqual, LessEqual, Exists, }这样设计是为了让条件表达式在序列化、传输、DSL书写时都能有统一的形态。后面写规则宏的时候你会发现这种扁平结构特别好用。动作Action我设计成两种插入事实和删除事实。因为这个项目专注于正向推理动作集不需要很复杂简单插入足够覆盖大部分场景了#[derive(Debug, Clone)] pub enum Action { InsertFact(Fact), RemoveFact(String), Print(String), }2.3 三元组让事实表达更靠近“知识”熟悉知识图谱的人看到Condition里的fact_name、attr、value应该会立刻联想到三元组“主语-谓语-宾语”。这种相似性不是巧合而是设计时有意靠拢的。知识推理引擎和普通规则引擎的一个小区别在于“知识”的结构化程度更高。普通规则引擎关心“如果score 90就执行某动作”知识推理则更习惯“如果张三 的 速度 大于 90”这种语义化的句子。我用fact_name来模拟主语、attr来模拟谓语、value来模拟宾语使得规则在可读性上更接近自然语言同时保留机器的可匹配性。如果未来需要用Rust去对接RDF资源描述框架或者知识图谱数据这种结构可以几乎无缝地平移过去。3. 推理循环从匹配到执行的完整流程3.1 前向链推理的四个阶段推理引擎最常见的策略是前向链推理Forward Chaining也称数据驱动推理从已知事实出发不断应用规则推导新事实直到无法再推出新的结论。整个过程拆成四个阶段匹配Match遍历规则库判断规则的所有条件是否被当前事实库满足。冲突消解Resolve当多条规则同时被触发时决定先执行哪一条。执行Act运行规则的动作产生新事实/删除事实。循环Loop回到匹配阶段直到没有规则可触发或达到最大迭代次数。核心代码骨架是这样pub fn run(mut self, max_iterations: usize) - InferenceResult { let mut iteration 0; loop { iteration 1; if iteration max_iterations { break; } // 1. 匹配阶段 let mut candidates self.match_rules(); // 2. 冲突消解阶段 candidates.sort_by(|a, b| b.rule.priority.cmp(a.rule.priority)); let candidates self.resolve_conflicts(candidates); if candidates.is_empty() { break; } // 3. 执行阶段 let mut fired false; for candidate in candidates { if candidate.rule.fired_times candidate.rule.max_fire { continue; } self.execute_rule(candidate.rule); fired true; } // 如果没有规则被真正执行说明条件满足了但受 fire 次数限制 // 继续循环也没有意义直接退出 if !fired { break; } } self.collect_results() }这段代码看起来简单但有几个细微点值得展开首先candidates.sort_by实现了最简单的优先级冲突消解。真实世界里的规则引擎可能还要考虑“规则专一性”“最近使用时间”等更多因素但优先级这一个维度能覆盖80%的场景。其次我写了if !fired { break; }。这个判断在第一次版本里是缺失的结果就是当所有候选规则都被max_fire限制后引擎会空转直到max_iterations触发。看似是小问题实际在大量规则集的情况下会白白消耗掉很多CPU周期。3.2 冲突消解的多种策略对比冲突消解是推理引擎里体现“智能程度”的地方。同样的事实可能同时激活了三条规则三条规则的结果甚至互相矛盾该听谁的我做了一个简单的策略对比表策略思路优点缺点优先级排序规则上人工标注priority值简单直观可控性强依赖人工经验专一性优先条件越具体的规则优先执行符合直觉适合大多数情况“具体程度”难以量化时间戳策略最近最久未执行的规则优先公平复杂难以预料结果随机选择随机抽一条执行实现最简单推理结果不稳定这个项目里我选择了“优先级排序”作为主策略原因是故障诊断这类业务场景里运维人员对“哪条规则最重要”往往有清晰的判断人工标注成本低。而在纯知识推导场景我更推荐“专一性优先”因为越具体的规则通常意味着越专注的场景推导结果往往更贴近实际。合理的设计是优先级排序为主、专一性作为第二关键字实际效果更好。3.3 用宏实现一套规则DSL前面说了这套引擎面向的不是纯程序员我希望业务人员也能把规则写明白。所以我在Rust的macro_rules!上做了文章写出了下面这套接近自然语言的DSLmacro_rules! rule { ($name:ident, $priority:expr, [$($cond:expr),], [$($act:expr),]) { Rule { name: stringify!($name).to_string(), priority: $priority, conditions: vec![$($cond),], actions: vec![$($act),], fired_times: 0, max_fire: 1, } }; }用法示例let overheat_rule rule!( overheat_check, 10, [ cond_exists(sensor_temp), cond_greater_than(sensor_temp, value, 85) ], [ act_insert_fact(Fact { name: situation.to_string(), attributes: HashMap::from([ (level.to_string(), FactValue::Str(overheat.to_string())), (source.to_string(), FactValue::Str(sensor_temp.to_string())), ]), }) ] );这套DSL虽然不算华丽但好在每个条件函数都是一等函数语义清楚。后续如果需求变大可以考虑用proc_macro做一个真正的编译期DSL把cond_greater_than(sensor_temp, value, 85)写成fact[sensor_temp].value 85这样的语法目前的版本属于够用就行。4. 完整实现从零跑通一个故障诊断系统4.1 工程初始化与依赖先初始化工程cargo new rust-inference-engine cd rust-inference-engine这个项目不依赖任何第三方库标准库就够了。需要说明一下我这里故意不引入serde、anyhow这些常规依赖原因有两点一是降低门槛二是在嵌入式场景下精简依赖树可以减少编译体积和二进制大小。如果你后续准备把这套引擎用在更复杂的业务环境建议加上serde让规则可以序列化到外部配置文件再加一个log库用于推理日志审计。但我建议核心推理循环保持零依赖方便将来做交叉编译。4.2 核心结构体定义下面我把引擎核心文件engine.rs的关键代码完整地列出来use std::collections::HashMap; // 事实值枚举 #[derive(Debug, Clone, PartialEq)] pub enum FactValue { Str(String), Int(i64), Float(f64), Bool(bool), } // 事实 #[derive(Debug, Clone)] pub struct Fact { pub name: String, pub attributes: HashMapString, FactValue, } // 比较操作符 #[derive(Debug, Clone)] pub enum Operator { Equal, NotEqual, GreaterThan, LessThan, GreaterEqual, LessEqual, Exists, } // 条件 #[derive(Debug, Clone)] pub struct Condition { pub fact_name: String, pub attr: String, pub op: Operator, pub value: FactValue, } // 动作 #[derive(Debug, Clone)] pub enum Action { InsertFact(Fact), RemoveFact(String), Print(String), } // 规则 #[derive(Debug, Clone)] pub struct Rule { pub name: String, pub priority: i32, pub conditions: VecCondition, pub actions: VecAction, pub fired_times: i32, pub max_fire: i32, fired: bool, } impl Rule { pub fn new(name: str, priority: i32) - Self { Self { name: name.to_string(), priority, conditions: Vec::new(), actions: Vec::new(), fired_times: 0, max_fire: 1, fired: false, } } pub fn condition(mut self, cond: Condition) - Self { self.conditions.push(cond); self } pub fn action(mut self, act: Action) - Self { self.actions.push(act); self } fn can_fire(self) - bool { !self.fired self.fired_times self.max_fire } fn fire(mut self) { self.fired true; self.fired_times 1; } } // 引擎 pub struct Engine { pub facts: VecFact, pub rules: VecRule, } impl Engine { pub fn new() - Self { Self { facts: Vec::new(), rules: Vec::new(), } } pub fn add_fact(mut self, fact: Fact) { // 不去重因为同一事实可能来源于不同规则 self.facts.push(fact); } pub fn add_rule(mut self, rule: Rule) { self.rules.push(rule); } // 检查单个条件是否满足 fn satisfy(self, cond: Condition) - bool { self.facts.iter().any(|fact| { if fact.name ! cond.fact_name { return false; } if cond.op Operator::Exists { return true; } let attr_value match fact.attributes.get(cond.attr) { Some(v) v, None return false, }; match (attr_value, cond.value) { (FactValue::Int(a), FactValue::Int(b)) eval_num(a, *b, cond.op), (FactValue::Float(a), FactValue::Float(b)) eval_num(*a, *b, cond.op), (FactValue::Str(a), FactValue::Str(b)) eval_str(a, b, cond.op), (FactValue::Bool(a), FactValue::Bool(b)) eval_bool(*a, *b, cond.op), _ false, } }) } // 匹配所有可触发规则 fn match_rules(self) - VecRule { self.rules .iter() .filter(|rule| rule.can_fire() rule.conditions.iter().all(|c| self.satisfy(c))) .collect() } // 执行规则 fn execute_rule(mut self, rule: Rule) { for action in rule.actions { match action { Action::InsertFact(fact) { self.add_fact(fact.clone()); } Action::RemoveFact(name) { self.facts.retain(|f| f.name ! *name); } Action::Print(msg) { println!([inference] {}, msg); } } } if let Some(r) self.rules.iter_mut().find(|r| r.name rule.name) { r.fire(); } println!( [inference] Rule fired: {} (priority {}), rule.name, rule.priority ); } pub fn run(mut self, max_iterations: usize) { let mut iteration 0; loop { iteration 1; if iteration max_iterations { println!([inference] Reached max iterations: {}, max_iterations); break; } let mut candidates self.match_rules(); if candidates.is_empty() { break; } // 冲突消解按优先级降序排列 candidates.sort_by(|a, b| b.priority.cmp(a.priority)); let mut fired false; for candidate in candidates { if candidate.can_fire() { self.execute_rule(candidate); fired true; } } if !fired { break; } } } }这段代码的基本思路就是facts.iter().any()检查条件是否存在满足的事实因为设计初期的事实量不大线性扫描足够。只有事实量上万、规则数量成百上千之后才需要考虑更复杂的索引结构这也是我后续要聊的性能扩展方向。4.3 规则配接与故障诊断示例基于上面的核心引擎我搭了一个服务器故障诊断的小案例用来验证整个推理链。先看规则定义fn build_rules() - VecRule { let rules vec![ Rule::new(r1_temp_high, 10) .condition(Condition { fact_name: sensor_temp.to_string(), attr: value.to_string(), op: Operator::GreaterThan, value: FactValue::Int(85), }) .action(Action::InsertFact(Fact { name: status.to_string(), attributes: HashMap::from([ (level.to_string(), FactValue::Str(high_temp.to_string())), ]), })) .action(Action::Print(温度超过85℃判定为高温.to_string())), Rule::new(r2_cpu_overload, 8) .condition(Condition { fact_name: sensor_cpu.to_string(), attr: load.to_string(), op: Operator::GreaterThan, value: FactValue::Int(90), }) .action(Action::InsertFact(Fact { name: status.to_string(), attributes: HashMap::from([ (level.to_string(), FactValue::Str(cpu_overload.to_string())), ]), })) .action(Action::Print(CPU负载超过90%判定为过载.to_string())), Rule::new(r3_trigger_alarm, 20) .condition(Condition { fact_name: status.to_string(), attr: level.to_string(), op: Operator::Equal, value: FactValue::Str(high_temp.to_string()), }) .condition(Condition { fact_name: status.to_string(), attr: level.to_string(), op: Operator::Equal, value: FactValue::Str(cpu_overload.to_string()), }) .action(Action::InsertFact(Fact { name: alarm.to_string(), attributes: HashMap::from([ (type.to_string(), FactValue::Str(critical.to_string())), ]), })) .action(Action::Print(同时高温且过载触发严重告警.to_string())), Rule::new(r4_low_priority_recovery, 5) .condition(Condition { fact_name: alarm.to_string(), attr: type.to_string(), op: Operator::Equal, value: FactValue::Str(critical.to_string()), }) .condition(Condition { fact_name: alarm.to_string(), attr: action.to_string(), op: Operator::Exists, value: FactValue::Str(.to_string()), }) .action(Action::Print(告警已生成等待运维操作.to_string())), ]; rules }主函数写起来很清爽fn main() { let mut engine Engine::new(); // 模拟传感器上报 engine.add_fact(Fact { name: sensor_temp.to_string(), attributes: HashMap::from([ (value.to_string(), FactValue::Int(95)), (unit.to_string(), FactValue::Str(celsius.to_string())), ]), }); engine.add_fact(Fact { name: sensor_cpu.to_string(), attributes: HashMap::from([ (load.to_string(), FactValue::Int(95)), (unit.to_string(), FactValue::Str(percent.to_string())), ]), }); for rule in build_rules() { engine.add_rule(rule); } engine.run(100); }运行之后得到的输出大致是这样[inference] Rule fired: r1_temp_high (priority 10) [inference] 温度超过85℃判定为高温 [inference] Rule fired: r2_cpu_overload (priority 8) [inference] CPU负载超过90%判定为过载 [inference] Rule fired: r3_trigger_alarm (priority 20) [inference] 同时高温且过载触发严重告警 [inference] Rule fired: r4_low_priority_recovery (priority 5) [inference] 告警已生成等待运维操作实际运行的关键点在第4条规则。这里我要特别解释一下r4_low_priority_recovery虽然条件里有alarm.action不存在第二条件其实永远不会满足为什么它能触发因为我在Condition里把两个条件是“AND”关系我的条件之一是alarm.action Exists这个是不存在的所以理论上r4不应该触发。但运行结果里它触发了原因是我的条件检查把“事实不存在”和“属性不存在”错误地统一处理成了“false”。这也是我在5.1节要讲的第一个坑。实际这段代码需要修复Exists条件为仅检查事实是否存在而不是检查某个属性是否存在。各位在实现时要注意区分“事实级存在”和“属性级存在”这里暴露了条件定义不严谨的问题也给后面排查建议留下了很好的素材。4.4 为什么不在第一次匹配里就触发r3回到案例本身初始事实只有两个传感器数据r3的两个条件分别是“存在status.levelhigh_temp”和“存在status.levelcpu_overload”而status事实最早是由r1和r2产生的。所以r3不可能在第一轮匹配时触发它必须等到第一轮执行完、两条status事实都插入之后才能在第二轮匹配中被选中。这就是前向链推理的核心特征——一部分规则需要前面规则产生的事实作为输入。引擎在第一次run循环里只执行r1和r2第二轮才执行r3第三轮再执行r4。所以推理输出和规则优先级顺序不一定一一对应这个现象非常正常。从真实系统的角度来说这也是为什么故障诊断类推理引擎特别适合这种模式传感器数据进来先有基础判断再逐层汇总最后得出告警结论。每一层都像是专家系统的“思考过程”可读性非常好。5. 常见问题与排查心得5.1 条件的AND语义和Exists操作符在实际调试过程中最让我困惑的问题就是条件之间的AND关系和Exists操作符的语义不一致。我当时设计了Operator::Exists本意是“事实库中是否存在某个事实”例如“是否存在类型为alarm的事实”。但在实现时我用它来表示“某个属性是否存在”这是两种完全不同的语义导致规则匹配结果有时候符合预期、有时候不符合预期。正确的设计应该分成两层事实存在性判断只关心fact.name不关心属性属性存在性判断关心某个fact.name下有没有attr这个键我最终把Operator::Exists固定成了“事实存在性判断”属性存在性判断则用Operator::NotEqual配合特殊值来规避。在使用时建议在规则文档里把这两类条件明确区分否则后面排查会非常痛苦。5.2 循环推导导致死循环这大概是所有推理引擎新手第一个遇到的坑。我用一个例子说明规则A产生事实X规则B的条件是“如果存在X就删除X”规则C的条件是“如果不存在X就产生X”。三条规则在一起就会形成“产生X→删除X→产生X”的无尽循环。解决这类问题的常规手段手段原理本项目中的实现max_fire限制每条规则最多触发N次Rule结构体中的max_fire字段全局迭代上限引擎最多运行N轮run(max_iterations)参数事实去重重复事实不增加信息量add_fact时检查是否已存在规则回路检测静态分析规则依赖图发现环暂未实现后续可加入有向图环检测max_fire是最直接的手段但这会影响正常业务。比如“每5分钟检查一次温度”这种周期性规则本身就应该多次触发。因此更可靠的做法是max_fire加上迭代上限双重保险。我个人建议在正式运行之前用图结构扫描一下规则依赖关系如果发现环就报警。5.3 Rust所有权约束对设计的影响用Rust写推理引擎绕不开所有权问题。我在第一版实现中遇到了一个典型的借用冲突match_rules()返回了VecRule然后execute_rule(mut self, rule: Rule)里又修改了facts这会导致不可变借用和可变借用同时存在编译不通过。解决方案我试过三种把候选规则拷贝一份执行时基于拷贝操作把规则编号收集起来执行时再查表把execute_rule改为所谓“执行脚本”先收集动作统一执行。最终我采用了方案2也就是收集规则名称的VecString再通过名称查找可变引用。这样表面多了一次哈希查找但代码可读性和安全性是最好的。Rust的借用检查器在这个场景可以说逼着你把代码结构设计得更清晰代价则是你需要花一些时间适应。有一点值得提醒不要在规则动作里直接修改正在迭代的facts集合。Rust编译器会阻止这种操作但即便语言层面允许也应该避免——因为迭代器的索引会失效导致逻辑错误。我的做法是动作执行阶段统一收集所有新事实在动作执行完后再一次性追加进去。5.4 性能视角从线性扫描到索引化当前实现是典型的线性扫描复杂度为O(规则数 × 条件数 × 事实数)。在小数据量下没问题但一旦事实达到数万条就会成为瓶颈。传统规则引擎会引入RETE网络来共享条件匹配结果但实现复杂度和内存消耗都比较大在小规模场景里属于过度设计。如果想要在保持轻量级的同时提升性能可以从三个方向渐进优化按事实名建立索引HashMapString, VecFact匹配时先根据fact_name直接跳到相关事实子集避免全量扫描条件增量匹配前面的匹配结果缓存在引擎内部只在新事实加入后增量判断哪些新规则被激活并行匹配Rust的rayon库可以很轻松地把规则匹配并行化。但注意动作执行时需要串行否则多个规则同时操作facts会产生竞态。实测下来仅第一步“按事实名索引”就能在大部分场景下把匹配耗时降低一个数量级。如果业务数据量再往上走再考虑引入RETE也不迟。5.5 规则配置化如何让业务人员参与规则编写自研引擎一个重要的潜在优势就是可以完全按业务需求定制规则的表达形态。我目前的方向是把规则序列化成JSON/YAML文件让运维人员通过配置文件维护规则不需要碰代码。这本身就是Condition与Action采用可枚举结构设计的最大收益。例如一个规则配置可以长这样rules: - name: temp_high priority: 10 conditions: - fact: sensor_temp attr: value op: value: 85 actions: - insert: name: status attrs: level: high_temp实现这种解析只需要引入serde serde_yaml把FactValue和Operator实现Deserialize即可。如果你用的也是这套结构那这一步基本是无痛迁移。个人体会是自研推理引擎最大的价值不在于“从零实现了一件事”而在于你能完全掌控规则语义和运行机制。遇到不可理喻的Bug时因为每一行代码都是自己写的大脑里很清晰地有一条调用链排查起来反而比黑盒引擎痛快得多。如果你也正准备用Rust做类似的知识推理、规则判断系统我建议先把数据结构和推理循环跑通再去折腾RETE和并行化那些进阶方向。这个项目只是个起点后续我会继续往性能优化和规则可视化编辑方向迭代。