Skip to content

Rule 与 LLM 的边界 ​

标签
AI/agent/规则与模型边界
字数
3633 字
阅读时间
14 分钟

一个系统里同时用规则引擎和 LLM 时,边界画在哪里决定了它是否可维护。判据是问题的性质,不是技术的新旧。

问题性质承担方理由
确定性、可枚举、有明确判据Rule可解释、可审计、成本固定、结果可复现
开放语义、需要理解意图LLM规则无法穷举,写了也维护不住
高风险且无人可兜底Human错了代价不可逆

对应的失效信号:规则承接开放语义,会退化成不断加白名单的补丁堆;LLM 承接确定性判断,会引入不可复现的结果和无法审计的决策链。

确定性约束优于概率性嘱托 ​

这一点和 Harness Engineering 的控制层是同一件事。让模型「遵守规范」是概率性合规,接一个违反规范就阻断 PR 的 Linter 是结构性约束。

在规则引擎里同理:靠提示词让模型「优先校验字段完整性」,和把字段校验写死在决策树的第一个分支,可靠性不在一个量级。前者的通过率是统计量,后者是确定事件。

边界画在哪里由「问题的性质」决定,不是技术的新旧。

   问题性质                        承担方        失效信号
   ──────────────────────────────────────────────────────────────
   确定性、可枚举、有明确判据        Rule          ——
     └─ 可解释、可审计、成本固定、结果可复现
   开放语义、需要理解意图            LLM           ——
     └─ 规则无法穷举,写了也维护不住
   高风险且无人可兜底                Human         ——
     └─ 错了代价不可逆

   两类画错的后果
     规则承接开放语义    ──▶ 退化成不断加白名单的补丁堆
     LLM 承接确定性判断  ──▶ 引入不可复现的结果和无法审计的决策链

   一句话:让模型「遵守规范」是概率性合规,
   接一个违反规范就阻断 PR 的 Linter 是结构性约束。
   在规则引擎里同理:靠提示词让模型「优先校验字段完整性」,
   和把字段校验写死在决策树的第一个分支,可靠性不在一个量级 ——
   前者的通过率是统计量,后者是确定事件。

语义泛化:关键词白名单的边界 ​

具体场景:系统目前只认 Button Label 为「继续」「下一步」的主流程按钮,之后出现「继续填写」「确认并继续」「去下一步」「Continue」「Proceed」怎么办。

关键词/白名单方案:维护 ["继续", "下一步", "确认", ...]。稳定、可解释、成本低。代价是泛化能力差,每次文案更新都要加规则,而且一旦有新文案没被覆盖,系统静默走兜底。

语义模型方案:把 Button Label、Button Role、Button Region、Page Context 一起交给模型判断「是否属于主流程继续行为」。

需要注意的写法错误是 label contains "继续" → 点击。这类判断把语义问题降级成了字符串匹配,等于两边都没做好。

实际可用的分法是分层:规则处理确定性部分(按钮是否 enabled、是否在 Help 区域、是否有多个候选),语义判断处理标签含义,两者结论冲突时走 置信度分层与 Human-in-the-loop 里的降级路径。

规则引擎怎么执行:三阶段与冲突集 ​

要谈规则优先级,先得知道规则引擎内部是什么模型。成熟实现(Drools / BizTalk / Grule 等)都是匹配 → 冲突解决 → 动作的三阶段循环:

阶段做什么
匹配用规则条件里的谓词去匹配工作内存中的事实。为效率,模式匹配在全部规则上做,且跨规则共享的条件只匹配一次
冲突解决匹配到的候选规则进入冲突集(Conflict Set),按预定策略决定下一个执行哪条
动作执行被选中规则的动作。动作可以断言新事实,于是循环继续 —— 这叫正向推理(forward chaining)

冲突集里只有一条规则时不存在冲突,直接执行;有多条才需要冲突解决策略。没有任何规则匹配时,循环停止。

一条容易违反的性质:算法永不抢占当前正在执行的规则——当前被触发规则的所有动作必须全部执行完,才重新进入匹配阶段。也就是说动作中途不会被打断,这也意味着单条规则的动作应该是幂等且可重入的。

规则引擎内部是「匹配 → 冲突解决 → 动作」的三阶段循环。

   ┌─ ① 匹配 ─────────────────────────────────────────────┐
   │ 用规则条件里的谓词去匹配工作内存中的事实                  │
   │ 为效率:模式匹配在全部规则上做,                         │
   │        且跨规则共享的条件只匹配一次                      │
   └────────────────────────┬─────────────────────────────┘
                            ▼
   ┌─ ② 冲突解决 ─────────────────────────────────────────┐
   │ 匹配到的候选规则进入冲突集(Conflict Set)              │
   │ 按预定策略决定下一个执行哪条                            │
   │ └─ 只有一条规则时不存在冲突,直接执行                    │
   │    有多条才需要冲突解决策略                             │
   └────────────────────────┬─────────────────────────────┘
                            ▼
   ┌─ ③ 动作 ─────────────────────────────────────────────┐
   │ 执行被选中规则的动作                                   │
   │ └─ 动作可以断言新事实,于是循环继续                     │
   │    这叫正向推理(forward chaining)                    │
   └────────────────────────┬─────────────────────────────┘
                            │
                            └──▶ 回到 ①(没有任何规则匹配时循环停止)

一条容易违反的性质:算法永不抢占当前正在执行的规则
   当前被触发规则的所有动作必须全部执行完,才重新进入匹配阶段。
   动作中途不会被打断 —— 这也意味着单条规则的动作应该是幂等且可重入的。

规则冲突的四种解决策略 ​

两条以上规则同时命中、且结论不同时,靠这四种之一决定谁赢:

策略规则适用
特异性(Specificity)条件更具体的规则优先。具体性可粗略定义为「前置条件数量最多」捕获例外与特殊情况——它会在触发更通用的(默认)规则之前先命中
优先级(Salience / Priority)给规则一个数值优先级,高的先跑需要人工显式控制时。Drools 的默认是 Salience + LIFO
拒绝优先(Deny Takes Precedence)只要有任何一条匹配规则说拒绝,动作就被拒绝,不管其他规则允不允许安全关键系统里最常见。理由很硬:保证单条拒绝规则不会被别处新增的允许规则覆盖掉
规则顺序单条策略内:先匹配的赢还是后匹配的赢要显式定义,别靠默认

「拒绝优先」这条值得单独记:它的价值不在严格,在可增量演进——新增一条允许规则不会意外打开一个原本关闭的口子。规则集在长期迭代里最怕的就是这种「改 A 出错在 B」。

部署前跑冲突分析 ​

自动工具能识别「匹配的动作集重叠、但效果不同」的规则对。 提前解决歧义,而不是在事故里发现它。

冲突解决是设计决策,不是事后补丁。 选定策略、写进文档、一致地执行。

一条来自 Drools 文档的实践原则 ​

一般原则是:不要指望规则按任何特定顺序触发,写规则时不该操心「流程」。当确实需要流程时,才用 agenda groups / rule flow groups / activation groups 这类机制显式表达。

后半句是关键——「需要流程」可以做,只是不能靠命中顺序碰运气。

两个实现层面的坑 ​

一、规则集必须有循环次数上限。 Grule 的做法是:重复评估与执行的次数超过实例化时指定的上限,引擎直接终止并返回错误。没有这个上限,正向推理可以无限循环下去——这和 Agent Loop 的 max_steps 是同一类护栏。

二、不能假设规则执行顺序与添加顺序一致。 Grule 里优先级相同的多条规则,引擎选「找到的第一条」,而底层 Go map 不保证输入顺序。所以「我按顺序加的,应该按顺序跑」这个假设在实现层就不成立。未指定优先级的规则默认 salience 为 0,这也是为什么显式优先级很重要——默认值相同意味着顺序未定义。

决策树的分支顺序 ​

回到具体场景——决策树的分支顺序本身是设计决策,不能靠命中顺序碰运气。正确的顺序是:

决策树的分支顺序本身是设计决策,不能靠命中顺序碰运气

   ① 字段完整性校验        ← 适用条件最宽:任何页面都要先校验字段
        │
        ▼
   ② 业务字段一致性校验
        │
        ▼
   ③ 页面 / Button 判断
        │
        ▼
   ④ 执行动作

   为什么字段校验必须在最前(用「特异性」解释)
      字段校验的适用条件比按钮判断更宽 —— 把它放在前面意味着
      无论后面命中什么分支,它都已经生效过。
   └─ 错误写法:看到按钮文案是「继续」就直接点击。
      字段缺失的情况下,按钮存在不代表可以继续执行 ——
      正确输出应该是「缺少必要字段,需要补充字段」。
      这里考的是规则优先级,而不是规则本身写没写对。

   同类的还有 last_result
      它的取值(例如 timeout)也应该在较靠前的位置判断,
      因为它会推翻后面所有分支的结论 ——
      超时之后不能直接继续,要先重新查询状态。

错误写法是看到按钮文案是「继续」就直接点击。字段缺失的情况下,按钮存在不代表可以继续执行——正确输出应该是「缺少必要字段,需要补充字段」。这里考的是规则优先级,而不是规则本身写没写对。

这条顺序的逻辑可以用上面的「特异性」解释:字段校验的适用条件比按钮判断更宽(任何页面都要先校验字段),把它放在前面意味着无论后面命中什么分支,它都已经生效过。

同类的还有 last_result:它的取值(例如 timeout)应该在决策树较靠前的位置判断,因为它会推翻后面所有分支的结论——超时之后不能直接继续,要先重新查询状态。

三种失败方向 ​

未知输入的处理方式是这套设计的核心问题。先分清三种失败方向:

方向失败时做什么什么时候用
Fail Safe停在安全侧 —— 拒绝执行、暂停、交给人动作有副作用且不可逆(本系统就是这一类)
Fail Open放行 —— 让流程继续只在放行的代价确定小于阻断的代价时,例如某些可用性优先的读路径
Fail Fast立刻报错并停止 —— 让问题尽早暴露开发期、配置加载期;错误继续跑只会掩盖问题

本系统的合理答案是 Fail Safe:

本系统的合理答案是 Fail Safe

   Known Case
        │
        ▼
   ┌─────────┐        ┌──────────────┐
   │  Rules  │ ─────▶ │     执行      │
   └─────────┘        └──────────────┘

   Unknown Case
        │
        ▼
   ┌─────────┐        ┌──────────────────────────┐
   │  Stop   │ ─────▶ │ Ask User / Human Review  │
   └─────────┘        └──────────────────────────┘

   └─ 规则没命中时走兜底、暂停自动操作、询问用户,代价是一次人工介入;
      猜错的代价是「执行了一个不该执行的动作」——
      这两个代价不对称,所以选择是明确的。

三种失败方向的判据可以写成一句话
   把「失败时代价更大的是误做还是不做」想清楚,方向就定了
   ├─ 误做的代价不可逆  ──▶ Fail Safe(停在安全侧:拒绝执行、暂停、交给人)
   ├─ 不做的代价不可逆  ──▶ Fail Open(放行,如错过告警)
   └─ 开发期 / 配置加载期 ──▶ Fail Fast(立刻报错并停止,继续跑只会掩盖问题)

不是随便选一个看起来最像的分支执行。 规则没命中时走兜底、暂停自动操作、询问用户,代价是一次人工介入;猜错的代价是执行了一个不该执行的动作——这两个代价不对称,所以选择是明确的。

判据可以写成一句话:把「失败时代价更大的是误做还是不做」想清楚,方向就定了。误做的代价不可逆 → Fail Safe;不做的代价不可逆(如错过告警)→ Fail Open。

相关 ​

参考 ​

贡献者 ​

文件历史 ​