QLExpress4:AI时代下规则引擎的重构与革新之路

QLExpress4重构升级,在AI时代实现规则引擎性能、可观测性与AI友好性飞跃。

原文标题:AI时代,我们为何重写规则引擎?—— QLExpress4 重构之路

原文作者:阿里云开发者

冷月清谈:

QLExpress4对沉淀多年的规则引擎进行了全面重构,以适应AI时代的需求。面对“AI时代为何还要规则引擎”的疑问,项目重申了对开源社区的承诺,并指出开发者对规则引擎的需求不减反增。重构源于旧代码积累的数百个难以修复的问题,以及解析技术和虚拟机技术的飞速发展。

新版QLExpress4在多方面实现了大幅提升。
* **性能方面**:编译效率提升约10倍,执行效率提升约1倍。这得益于将自研语法分析器更换为Antlr4,并优化了虚拟机指令集,显著降低了内存和执行开销。
* **可观测性方面**:引入了表达式追踪功能,能够记录表达式计算过程中的中间结果,形成追踪树。这一特性在淘天集团的规则归因聚类场景中发挥了巨大价值,帮助业务人员分析规则命中或未命中的具体原因。
* **AI友好性方面**:
* 表达式追踪树可以帮助大模型更好地调试和诊断规则。
* 原生支持JSON语法,允许在脚本中便捷地定义和构造列表、映射乃至复杂的Java对象,结合约束解码技术,极大地提升了AI生成和理解规则表达式的效率和准确性。钉钉应用此特性实现了模型动态映射。

此外,QLExpress4还对开发体验进行了优化,提供token级别的错误提示。文档方面,创新性地采用Asciidoctor(adoc)格式,并直接引用单元测试代码片段,确保文档代码的实时有效性,同时通过GitHub Action解决了GitHub对adoc特定语法渲染不兼容的问题。

怜星夜思:

1、文章提到“都AI时代了,怎么还在研究规则引擎?”,但QLExpress的Star数反而增加了。大家觉得,在AI大模型日益强大的背景下,规则引擎的未来定位会发生什么变化?它是会被AI取代,还是会以某种形式与AI结合得更紧密?
2、QLExpress4的表达式追踪功能很强大,能记录中间结果,并指出能帮助AI进行调试和诊断。除了文章里提到的规则归因分析,大家还能想到哪些场景,是这种“可解释性”强的规则引擎和AI结合会带来突破的?比如在风控、财务审批这些领域?
3、QLExpress4通过adoc引用单元测试代码来维护文档,还用GitHub Action解决了GitHub渲染问题,这挺有意思的。大家在自己的开源项目或者团队内部项目里,遇到过文档难以维护的痛点吗?有哪些好的实践或者工具可以推荐的?(或者对QLExpress4这种方式有什么看法?)

原文内容

阿里妹导读


AI时代下,规则引擎的需求反而更旺盛。QLExpress4 通过全面重构,在性能、可观测性和AI友好性上大幅提升。

“都AI时代了,你们这些人怎么还在研究规则引擎?” 无聊时朋友调侃道。

QLExpress 是 Java 嵌入式脚本引擎,可以很方便地集成到 Java 应用中,能够用类 Java 的语法编写并执行表达式。因为轻量易用,灵活性强,而被广泛应用于业务规则场景。

为什么在 AI 时代还要不断迭代规则引擎?一方面是对开源社区的承诺不能随便中止。QLExpress4 是 QLExpress 开源以来最大的一次重构,几乎重写了全部的代码。我(前钉钉,现爱橙科技),小A(前供应链中台,现淘天)和小B(前阿里妈妈)从三年前就开始做这件事情了。

另一方面开发者对规则引擎的需求不仅没有因为AI时代的到来而降低,反而变得更加旺盛。从我 2022 年开始担任 Maintainer 以来,star 数量翻了一倍还多。显示出该项目在开源社区中的持续关注和认可。

和很多项目的 star 数爆发式增长不同,QLExpress 一直是靠着用户的口碑传播稳步增长,最近一年随着 QLExpress4 的更新才稍有加速。

既然需求依旧旺盛,就没有理由放弃。

QLExpress 原本的代码历史过于悠久,诞生之后在社区的缝缝补补中度过了近十年的时光。这期间,解析技术和虚拟机技术都已经发生了翻天覆地的变化,项目本身也积累了三百多个难以修复的 issue。

我们已经不想再在原来的代码上进行修改,于是在一次喝咖啡的时候就决定将代码全部重写掉。

QLExpress4 除了单纯技术升级外,也对性能和体验进行了大幅优化。无论是碳基还是硅基生物,用起来都会更加顺手。

性能上,编译性能在不同场景下会有大约10倍的提升,执行性能会有大约1倍的提升(具体场景测试见下文)。

体验上,无论编译或者运行时的错误都能达到 token 级别的提示。语法上除了兼容Java外,还原生支持JSON语法,以及便捷的字符串处理操作,通过 “表达式追踪” 特性大幅度提升了表达式执行过程的可观测性,方便人或者AI对规则进行调试和诊断。

新版特色场景

QLExpress4 正式版(最新版本4.0.3)新特性发布以来,已经在集团(包括淘天,钉钉,爱橙等等)和社区得到广泛的应用。

这里列举一些规模较大的场景供大家参考。更多的场景还有待大家发掘。

规则归因聚类-淘天集团

在业务人员完成规则脚本的配置后,很难对其线上执行情况进行感知。比如电商的促销规则,要求用户满足规则 isVip && 未登录10天以上。到底有多少线上用户是被 vip 条件拦截,又有多少用户是因为登录条件被拦截?这还是只是仅仅两个条件的简单规则,实际线上情况则更加复杂。

线上规则执行情况的追踪,不仅仅可以帮助业务人员了解线上的实际情况,排查和修复问题。其沉淀的数据也非常有价值,可以用于后续的规则优化和业务决策。

淘天集团利用 QLExpress4 新版本的表达式追踪能力,对物流时效规则计算结果的原因进行分析聚合,产出的报表对于业务分析有很大价值:

归因分析的原理在于利用 QLExpress4 的表达式追踪能力,获得表达式在计算过程中每个中间结果的值, 据此判断表达式最终运行结果产生的原因。

具体使用方法参考文档:表达式计算追踪[1]

模型动态映射-钉钉

QLExpress4 原生支持 JSON 语法,可以快捷定义复杂的数据结构。

JSON 数组代表列表(List),而 JSON 对象代表映射(Map),也可以直接定义复杂对象。

产品上可以基于该特性实现 JSON 映射规则。让用户可以便捷地定义从一个模型向另一个模型的映射关系。以下是钉钉连接平台基于该能力实现的模型映射产品图:

具体使用方法参考文档:方便语法元素[2]

AI友好(体验提升)

这一章的标题本来想叫 “体验提升” 的,但考虑到目前越来越多的规则表达式都是由 AI生成,而非人类编写时,就更名了 “AI友好”。毕竟对人友好的东西,AI用起来也会更加顺手。

QLExpress4 新增了表达式追踪功能,能够在返回计算结果的同时,返回一颗表达式追踪树。表达式追踪树的结构类似语法树,不同之处在于,它会在每个节点上记录本次执行的中间结果。

比如对于表达式 !true || myTest(a, 1),表达式追踪树的结构大概如下(语法树每个节点的左侧为符号,右侧为计算结果):

        || true
       /      \
    ! false  myTest true
    /        /   \
truetrue  a 10  11

具体使用见文档:表达式计算追踪[1]

利用中间运算结果,大模型可以更好地对表达式进行调试和诊断。我们提供了 qlexpress-mcp [3]的示例,让大模型结合 mcp,可以帮助业务人员更好地对规则运行结果进行分析。比如下面的优惠券规则未命中的原因分析:用户近三天有过登录,且购物车非空,导致未命中规则。

最后,QLExpress4原生支持 JSON 语法,除了可以用 JSON 构造简单的列表和映射,还可以直接构造复杂的 Java 对象。

以下代码可以在脚本中构造一个 List<Map> 类型:

list = [
  {
    "name": "Li",
    "age": 10
  },
  {
    "name": "Wang",
    "age": 15
  }
]

assert(list[0].age==[10,15])

也可以直接构造复杂的 Java 对象。

假设如下 Java 类:

publicclassMyHome {

    private String sofa;

    private String chair;

    // 嵌套对象
    private MyDesk myDesk;

    private String bed;
}

可以直接在脚本中用如下代码构造:

myHome = {
  '@class': 'com.alibaba.qlexpress4.inport.MyHome',
  'sofa': 'a-sofa',
  'chair': 'b-chair',
  'myDesk': {
    'book1': 'Then Moon and Sixpence',
    '@class': 'com.alibaba.qlexpress4.inport.MyDesk'
  }
}
assert(myHome.sofa=='a-sofa')

依靠约束解码技术(Constrained Decoding),大模型已经可以稳定地输出正确的 JSON 对象,原生支持JSON语法的语言对模型和人类可读性而言都更加友好。

具体的使用方法参考文档:方便语法元素[2]

性能优化

在多种场景下进行性能基准测试表明,关闭编译缓存时,QLExpress4能比3有接近10倍性能提升;开启编译缓存,也有一倍性能提升。

以下为不同场景的性能对比,详细性能测试见文档 QLExpress性能测试[4]

场景
关注点
对比图
长脚本
编译性能
简单计算(编译缓存)
计算性能
斐波那契数列
递归性能

结合淘天时效产品在线上的测试结果,脚本平均RT在 100us 以内:

场域
总量
成功率
RT
相关截图
商品详情
400w/s
6个9
<40us
下单渲染
36.4w/s
5个9

<100us


交易创单
16.3w/s

QLExpress 脚本的执行链路由编译和执行两部分组成。用户输入一段脚本文本后,会先通过语法分析将脚本解析成自定义的指令集,之后通过虚拟机执行指令集,得到执行结果。可以通过缓存指令集,跳过编译部分,提升脚本的执行效率。

QLExpress4对编译和执行两个链路都进行了充分的优化。

在编译链路上,我们将原本自研的自动语法分析器更换成了生态和性能更好的 Antlr4。Antlr4基于ALL算法实现,能够自动将探测过的路径缓存成 DFA 状态机,缓存到表中,下次碰到相同前缀就直接查表。第二次再走过这个路径时,性能就逼近手写状态机。缺点就是第一次走解析路径时性能会较差,不过我们在类初始化时会对常见路径进行一次初始化,保证第一次运行时,性能也是可接受的。

执行路径上,一方面优化了超时检测的逻辑:旧版本虚拟机会在指令执行的主循环中,每条执行前获取一次系统时间,并进行检测。在指令很多的情况下(即使是简单表达式也会产生大量的指令),反复获取系统时间就会造成大量的性能损耗。

另一方面,老版本采用的类汇编指令集虽然精简通用,但是却会导致指令冗余复杂,一个简单的表达式就会产生几十条指令,给内存和执行性能都造成巨大压力。新版本将常见场景包装成单独的复杂指令集,一条指令就能代替原来数条指令执行的内容,大大降低内存压力,提升执行效率。

测试用例与文档工程

QLExpress 作为一个基础二方库项目,单元测试是一个重要且有效的测试手段。所以我们十分重视新版的单元测试覆盖。

在源码的 src/test/resources/testsuite 目录存放了我们的测试套件,按特性划分目录,给每个特性都准备了最经典的几个测试用例,确保场景覆盖率 100%:

举例:array 目录存放的是数组相关的用例,array_index_out_of_bound.ql文件中都是数组下标溢出场景的相关用例。

场景覆盖的同时,最终代码行覆盖率也能达到 77%。

开源项目常见的另一个问题,就是文档保鲜的问题:随着项目不断迭代,如何才能保证文档中的代码都是能正确执行的?

有句俗话单元测试就是最好的文档。我觉得没必要将单元测试和文档分开,最好的方式就是从单元测试中直接引用部分代码进入文档。

在变更功能时,我们一定也会改单元测试,文档也会同步更新,不需要单独维护。

在合并分支或者发布版本之前,肯定也会有代码门禁执行单元测试,这样就能确保文档中代码示例都是有效的。

因此我们没有使用 markdown 作为项目文档,而是使用 adoc(官网链接[5])文档。

adoc 最强悍的能力就是在仓库文档中可以对另一个文件的一部分进行引用。

在单元测试 Express4RunnerTest 中,用 // tag::firstQl[] 和 // end::firstQl[]圈出一个代码片段:

// import 语句省略...

/**
 * Author: DQinYuan
 */
publicclassExpress4RunnerTest {
    // 省略…

    @Test
    publicvoiddocQuickStartTest(){
        // tag::firstQl
        Express4Runner express4Runner = new Express4Runner(InitOptions.DEFAULT_OPTIONS);
        Map<String, Object> context = new HashMap<>();
        context.put(“a”, 1);
        context.put(“b”, 2);
        context.put(“c”, 3);
        Object result = express4Runner.execute(“a + b * c”, context, QLOptions.DEFAULT_OPTIONS);
        assertEquals(7, result);
        // end::firstQl
    }

    // 省略…
}

然后在文档 README-source.adoc 中就可以 firstQl 这个 tag 引用代码片段:

=== 第一个 QLExpress 程序

[source,java,indent=0]

include::./src/test/java/com/alibaba/qlexpress4/Express4RunnerTest.java[tag=firstQl]

include::./src/test/java/com/alibaba/qlexpress4/Express4RunnerTest.java[tag=firstQl] 用于引用 Express4RunnerTest 文件中被 firstQl tag 包围的代码片段,其他的部分,等价于 Markdown 下面的写法:

### 第一个 QLExpress 程序

这个 adoc 文档在渲染后,就会用单测中真实的代码片段替换掉 include 所占的位置,如下:

缺点就是 adoc 的语法和 Markdown 相差还挺大的,对以前用 Markdown 写文档的程序员有一定的熟悉成本。但是现在有 AI 啊,我们可以先用 Markdown 把文档写好,交给 Kimi 把它翻译成 Markdown。我对 adoc 的古怪语法也不是很熟悉,并且项目以前的文档也都是 Markdown 写,都是 AI 帮我翻译的。

另外还有一个坑,就是 Github 根本不支持 adoc 的 include 语法的渲染(参考[6])。不过好在参考文档中也给了解决方案:

  • 源码中用 README-source.adoc 编写文档;

  • 使用 Git Action 监听 README-source.adoc 文件的变化。如果有变动,则使用 asciidoctor 提供的命令行工具先预处理一下 include 语法,将引用的内容都先引用进来。再将预处理的后的内容更新到 README.adoc 中,这样 README.adoc 就都是 Github 支持的语法了,可以直接在 Github 页面上渲染;

Github Action 的参考配置如下(QLExpress中的配置文件[7]):

name: Reduce Adoc
on:
  push:
    paths:
      - README-source.adoc
    branches: ['**']
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout Repository
        uses: actions/checkout@v3
      - name: Install Asciidoctor Reducer
        run: sudo gem install asciidoctor-reducer
      - name: Reduce README
        # to preserve preprocessor conditionals, add the --preserve-conditionals option
        run: asciidoctor-reducer --preserve-conditionals -o README.adoc README-source.adoc
      - name: Commit and Push README
        uses: EndBug/add-and-commit@v9
        with:
          add: README.adoc

添加这个配置后,你会发现很多额外的 Commit,就是 Git Action 在预处理 README-source.adoc 后,对 README.adoc 发起的提交:

附录:

QLExpress4开源地址:https://github.com/alibaba/QLExpress

开源反馈交流钉钉群(群号:122730013264

[1]https://github.com/alibaba/QLExpress?tab=readme-ov-file-1

[2]https://github.com/alibaba/QLExpress?tab=readme-ov-file

[3]https://github.com/DQinYuan/qlexpress-mcp

[4]https://alidocs.dingtalk.com/i/nodes/2Amq4vjg89lYaKvGt2ok0YLOJ3kdP0wQ

[5]https://asciidoctor.org/

[6]https://martincarstenbach.com/2023/02/14/rendering-adoc-include-directives-properly-on-github/

[7]https://github.com/alibaba/QLExpress/blob/main/.github/workflows/reduce-adoc.yml

创意加速器:AI 绘画创作


本方案展示了如何利用自研的通义万相 AIGC 技术在 Web 服务中实现先进的图像生成。其中包括文本到图像、涂鸦转换、人像风格重塑以及人物写真创建等功能。这些能力可以加快艺术家和设计师的创作流程,提高创意效率。


点击阅读原文查看详情。


我们团队内部项目也经常有“文档保鲜”这个问题。一开始大家都激情满满写Markdown,后面功能一迭代,文档就跟不上。后来我们尝试过几个办法:一是强制Code Review时必须检查文档更新;二是使用一些像Swagger UI这样的工具直接从代码注释生成API文档,减少人工维护。但像QLExpress这种规则引擎场景,更多是业务规则的逻辑说明和示例,Swagger肯定不行。所以这种通过自动化测试来保障文档示例准确性的思路非常值得学习。我们现在也在探索用RAG结合内部文档,让AI协助查询和生成特定场景的文档内容,但准确性还有待提升。

哎呀,文档维护这事儿,简直是所有程序员的痛!项目开始前,文档写得巨详细,项目一跑起来,三天两头改需求,代码改了,文档谁还记得改?最后就是代码和文档完全是两个世界。QLExpress4这种直接把单元测试代码嵌入文档的方式,我觉得非常有借鉴意义。虽然adoc学习成本可能高一点,但如果能保证文档的准确性,这投入是值得的。我们现在就经常遇到看文档以为是这样,结果代码跑出来完全不是那么回事的情况,心累。这种实践真的太重要了,代码即文档,文档即代码。

关于“AI时代规则引擎的未来”这个问题,我个人觉得规则引擎非但不会被取代,反而会成为AI决策的“基石”之一,尤其是在需要确定性、可解释性和强业务约束的场景。想象一下,大模型用于生成初步的复杂策略或建议,但最终的落地执行和关键判断,仍然需要规则引擎来把关,确保符合法规、业务逻辑和风险控制。它可能从“写死”的规则,转变为**“AI生成+人工审核/微调+规则引擎执行验证”**的模式。AI生成更复杂的规则,规则引擎更高效地执行和审计,实现智能与严谨的并重。

哈哈,AI再牛,也得遵纪守法不是?就像公司的财务审批、风控流程,你让AI自己跑,那出问题了谁背锅?规则引擎的可解释性和确定性太重要了!大模型擅长“模糊”和“创造”,但规则引擎擅长“精确”和“约束”。我觉得未来可能会是AI负责“提议”规则,规则引擎负责“执行”和“校验”这些提议,甚至反过来,规则引擎发现异常后,触发AI进行更深层次的分析。这不就是AI和确定性逻辑的梦幻联动吗?毕竟,有了AI,规则可能生成得更快、更智能,但仍然需要一个靠谱的执行者。

大模型就像个学霸,啥都懂,但有时候也爱胡说八道。规则引擎就像是那个永远板着脸的教导主任,规定什么能做,什么不能做。所以,学霸再聪明,也得听教导主任的。QLExpress4不是重写规则引擎了吗?这说明AI现在还不够“听话”,或者说人类还没完全信任AI能独立处理所有决策。等它真能把规则逻辑写得比人还清晰、还没有bug的时候再说吧。短时间内,规则引擎只会越来越香,给AI套个“紧箍咒”。

我觉得QLExpress4这种“把单元测试当文档”的思路太赞了!毕竟代码不会骗人,单元测试更是代码最直接、最准确的“使用手册”。至于adoc的学习成本,文章里不是说了吗,“有AI啊,交给Kimi翻译就行了!” 这简直是把AI用到了点子上。未来的文档维护可能就是:你给我个需求和一堆测试用例,AI帮你生成代码和文档,然后测试用例帮你校验文档和代码的一致性。人类只负责审核和决策。所以,文档和代码的一致性问题,AI时代反而可能得到更好的解决,简直是懒人福音!

嘿,说到这种“可解释性”强的规则引擎和AI结合,我脑子里就浮现了一个画面:未来的程序员写完规则,AI自动跑一遍,然后一个红叉叉,旁边直接弹出来:“你这儿变量isVipfalse,导致整个true || false短路了!下次麻烦麻烦注意一下逻辑,AI很累的!” 简直是程序员的福音,再也不用手动debugger查半天究竟哪个条件没跑通了。尤其对于那些复杂的嵌套规则,人脑看了都费劲,让AI看懂并找出问题,这才是真AI!省了多少头发啊!

关于“表达式追踪”与AI结合能带来突破的场景,我想到的是智能合约审计和低代码平台。在区块链的智能合约中,规则的每次执行和状态变化都是透明的,如果能结合追踪能力,对合约的执行路径进行可视化和故障排除,将极大提高智能合约开发的安全性。对于低代码平台,用户通过拖拽配置生成业务规则,AI可以利用追踪反馈来优化用户配置的合理性,甚至主动纠正潜在的逻辑错误,让低代码平台的“智能”更深一层,降低业务人员理解成本,也提高了规则的健壮性。

QLExpress4这种通过adoc引用单元测试代码的方式确实很棒,完美解决了文档与代码脱节这个老大难问题。我们团队也尝试过类似思路,但用的是JSDoc/JavaDoc结合一些自动化工具生成API文档,并在CI/CD流程中强制执行文档生成和更新。对于更偏业务逻辑的文档,我们倾向于使用Confluence结合Mermaid图表来可视化流程,并定期进行评审。关键在于将文档作为代码的“第一优先级”资产,并融入开发流程。Markdown虽然普及,但对于这种需要嵌入实时代码的场景,其局限性也比较明显。