得物社区搜推公式融合调参框架-加乘树3.0实战

0 阅读16分钟

一、背景简介

近年来,搜索/推荐/广告系统在粗排(Pre-ranking)与精排(Ranking)阶段的模型训练中,呈现出一个明确的趋势:从单目标优化转向多目标建模 + 多目标融合。模型目标多、融合公式复杂,给工程维护、算法迭代效率都带来了挑战。

为了明文化直白展示公式全景、方便决策调参方向,直接配公式、线上自动算(既支持精排预估目标融合、也支持业务条件boost)。我们设计并落地了加乘树调参框架。从1.0优化至3.0,我们提供了:一个调参框架(Java版、同时引擎基建同学落地了C++版)能支持不同算法环节“公式即配即用”,一个打通AB实验的一站式产品化平台,支持一站式“辅助配置->调试->开实验->变更管控”。

带来收益:无论是粗排还是精排,“训多目标、融公式” 已成为工业界标准范式。在得物社区搜索、推荐的模型迭代实践中,我们也确实走“模型多目标训练 + 融合公式调参”范式,2025在社区推荐、社区搜索落地了几十次LR(社区推荐内外流精排、粗排,社区搜索精排)、近百次加乘树推全。

二、即配即用:算法爆发的催化剂,工程稳定的绊脚石?

在算法领域,“即配即用”的工程框架多次成为推动算法快速迭代甚至“爆发式增长”的关键基础设施。面对粗、精排“多目标建模 + 多目标融合”这一建模范式,社区算法和工程提出了如下基建目标:

即配即用提人效: 实时调整配置、线上就能自动生效数学逻辑,使算法工程师从过去几天才能完成一次调参,转变为一天内可进行多次迭代,从而将精力集中在模型和融合公式本身。

全量配置+增量配置范式: 实验只配要改的几行,降低配错风险。全量配置不动,形成天然降级能力。

DSL可解释性强: 粗、精排的融合公式配置量大,数学变换复杂,容易配错。我们提供的DSL让算法同学直接写数学公式/逻辑表达式。明文公式形成策略全景,方便算法同学决策调参方向。

编译校验与降级体系筑牢稳定性防线: 即配即用+数学公式DSL的需求,给工程稳定性带来极大挑战。我们采用“编译语法校验 + 自动用全量配置降级 + 手动切换编译/解释模式”三位一体保障稳定性。

三、可信赖底座:让复杂公式配置既灵活又可靠

全量配置+增量配置范式

传统的KV、JSON 或 YAML等配置格式在面对上百行数学公式时已显乏力:一方面配置体量大、人工修改易出错且缺乏容错机制;另一方面可读性差,难以维护和审查。

我们采用“全量配置+增量配置”的设计,天然解决了使用门槛&自动降级问题:

  • 只配增量,让使用更轻松、出错更可控: 全量配置锁定为只读,确保基线稳定;算法同学只需声明需要新增或修改的增量配置(upsert)。系统在运行时将增量动态合并到全量配置中,生成最终生效的实验配置——既简化了操作;又避免了误改全局参数的风险。
  • 增量可试,基线兜底: 增量配置有误,自动回退至基线,形成天然降级机制。

给一个社区搜索主搜精排的样例:

DSL接近数学公式/逻辑表达式明文

社区搜索、社区推荐的精排融合公式,服务了“多目标融合+业务boost调权”,语义包含:数学变换、逻辑判断、自定义UDF。当算法写下一串sin(log(max(UDF(x), y))),框架能否接住?框架必须托底,正确校验与执行,杜绝“配错即崩”。

从加乘树1.0到3.0,公式解析统一选用 ANTLR。相比手搓“逆波兰表达式”或“Flex & Bison”,它基于AST校验更可靠,且Java开发门槛低。实际加乘树的配置结构里,公式按KV配置(Key 为结果名,Value 为表达式),支持跨行引用——前序公式的输出可作为后序公式的输入,形成可串联的计算链,直至得出最终结果。

  • 公式链转DAG: 在加乘树3.0中,有相互依赖关系的多行公式,被框架解析成DAG。每个item都通过这套DAG计算融合分,1个item可能有多个融合分、每棵DAG的根结点对应1个融合分。
  • AST驱动逐行校验: 每行公式都依托编译原理,校验&解析为抽象语法树(AST)。结构化的AST可支撑后续可靠计算。
  • 加乘树3.0把DAG和AST直接翻译成代码: 框架将公式链直接翻译成可执行代码,用字节码技术加载到JVM中。每个item直接计算即可。

编译校验与降级体系筑牢稳定性防线

即配即用给算法同学迭代提效带来便利,同时给工程维稳带来挑战。尤其加乘树面临的配置是可自由组合、千变万化的数学公式时,绝对不能出现“配错即崩”的情况。我们做了如下一整套安全设计:

  • 编译原理强校验: 如何应对无限组合的公式配置?加乘树选择了编译原理强校验,用了ANTLR框架,把公式校验&解析成严谨的可访问结构(AST)。
  • DAG强校验公式链: 加乘树3.0初始化阶段自动解析公式链间的依赖关系,一边将公式链解析成DAG、一边强校验。能通过校验、最终编排成DAG的公式,才会进入实际计算;不能通过校验的危险配置(漏配公式、公式配错)都会在初始化阶段就被拦截,不会进入实际计算。
  • 自动降级范式: 加乘树设计了一套自动降级范式,方便“前置拦截错误、事中有效托底、后置发出告警”。一旦有错误的实验开流量,加乘树初始化阶段就会校验出错误,当次请求忽略AB实验配置、直接用全量配置计算,并及时发出“实验配置有误”的告警。
  • 串行重算托底: 如果有“编译原理校验”、“DAG校验”没有校验出的意外怎么办?如果框架仅仅是高峰期计算超时失败了怎么办?加乘树最后一层安全托底是“用全量配置串行重算”。无论如何保证线上效果。

四、核心攻坚:加乘树3.0升级编译执行

加乘树2.0在社区搜索落地后,“每次请求3000个item、线程并发拆的多”的情况,暴露出加乘树耗CPU、耗线程的弱点。C++版加乘树替换了计算引擎,没有采用antlr visitor解释执行数学运算的方式,而是用exprtk框架、收获了更高的性能。

受C++版加乘树的启发,我们计划替换Java版加乘树的计算引擎,降CPU消耗、降执行平响。加乘树3.0变成“直接将配置翻译成代码,字节码加载,直接计算”的编译执行形态。

极致性能:配置直译硬代码,零中间损耗 + 最优 JIT

Antlr翻译&Javassist加载,直接“公式翻译成可执行代码”: 包括多行公式的依赖关系、数学计算&UDF调用,直接拉平成硬代码。硬代码执行效率最高,没有map缓存、递归调用栈等损耗。

多行公式传递中间结果,map换POJO: 每个item维护自己的缓存map,高并发put/resize,造成明显的CPU消耗、youngGC压力。本次会初始化时决策缓存POJO,避免resize、且读写更高效。

核心Javassist管理类借鉴Dubbo写法: Dubbo的ClassGenerator写法,对内存管理考虑比较完善。本次借鉴ClassGenerator,把动态生成代码收入唯一管理单例类。

性能收益

晚高峰模块平响、CPU火焰图消耗和内存分配火焰图消耗均显著降低。

典型踩坑

字节码加载不容忍语法糖:

动态生成的字节码必须严格遵循JVM 范,平时习惯手写的Java法糖是不容忍的。例如,Float a = (float) b; 在源码中合法,但若b是Double类型,该语句涉及拆箱 + 窄化转换 + 装箱,而字节码层面需显式插入doubleValue() → (float) cast → Float.valueOf() 等指令。若直接按表面类型生成字节码,将触发VerifyError。

OOM在多处需要关注:

Javassist使用不当容易OOM:Javassist 在生成和操作字节码时(如通过 CtClass),因为其缓存机制,需要开发者主动管理资源释放。每次parse字节码的CtClass要及时释放,否则高频生成字节码容易触发OOM。这一点上,加乘树参照了Dubbo的ClassGenerator写法,创建、销毁内聚在同一个类里,即用即释放。

动态生成ClassLoader/Class/Instance要能GC:Instance能GC,ClassLoader/Class能GC吗?答案是能,只有从ClassLoader -> Class -> Instance全链路都GC Root不可达了,这一串才能GC。所以用Spring的ClassLoader这类常驻ClassLoader加载动态生成类是不行的,必须用即用即弃的自定义ClassLoader,并注意全链路的强引用问题。

我们实际验证了动态生成的类确实能被GC掉。

多重护航:防止非法Java字节码引发线上问题

ASM + Javassist双重检验: 翻译生成的代码,经Javassist生成字节码后,除Javassist .toClass()的自检验,我们还让字节码过了ASM的字节码静态校验(会运行类似JVM的类型推断验证,确保每条指令执行前后,局部变量表和操作数栈的状态是类型安全的)。

沙箱加载: 我们将加乘树管理平台封装成了一个沙箱,算法同学调试公式点击“校验”,平台会用同一套SDK模拟线上全套加载流程:“AST强校验 -> DAG强校验 -> 真实翻译代码 -> Javassist & ASM 双校验 -> 反射调用构造器创建实例”,一整套无误后才往线上推配置。

线上异步加载,任何问题自动降级: “可执行代码(执行器)初始化”读写分离,新配置上线是异步刷新,刷新错误只会造成线上流量过来找不到执行器,自动降级走全量配置(并发出告警),不影响效果。

可回退解释执行: 加乘树2.0、1.0的解释执行能力十分稳定、只是性能略差,3.0可以一键回退解释执行。

加乘树管理平台:一站式配置、调试与实验平台

面向算法同学: 做了一套一站式“辅助配置->校验->实时调试->开实验->变更管控”的使用体验,告别繁琐配置、体感更丝滑。

面向系统稳定: 加乘树管理平台把自己封装成了一个沙箱,如上一个模块所述,一切风险都拦截在沙箱爆炸。

五、稳扎稳打:从1.0到3.0的演进

加乘树1.0: 支持配公式、框架直接算公式,支持UDF,解释执行。加乘树2.0: 少量性能优化,抽象成SDK。加乘树3.0: 升级为编译执行,外观简化为只需要配公式、框架自动解析DAG。

加乘树1.0和2.0都是用的解释执行,antlr visitor遍历AST做“数学/逻辑/if判断”运算。加乘树3.0升级成了编译执行,多行公式解析DAG、每行公式用antlr解析AST时,直接翻译成Java执行代码,用字节码技术把执行代码加载进JVM直接执行。同时加乘树3.0也支持降级至解释执行。

加乘树1.0

解决:落地即配即用公式,解决手搓硬代码迭代效率低、代码腐化导致生效逻辑不清晰的问题。缺陷:费线程&CPU。

加乘树1.0于2025年1月在社区推荐外流精排落地,配法(使用外观)、降级机制是后续迭代不变的:

  • 配法:1): “全量配置+叠实验改动”的配置机制 2)配置总共分 consts(输入物料)、paramBranch(条件分支替换参数)、formulas(公式)、root(融合结果字段名)。
  • 降级机制:1): 初始化阶段就检测公式配错、漏配公式等,一旦检出就自动降级走全量配置、并发出告警 2)少量运行时才能发现的问题,串行重算、降级算全量配置。

当时是从手搓硬代码做公式融合,无DIFF迁移过来,解决了如下2个迭代痛点:

  • 迭代效率: 除调参是可配,调公式形态、调生效条件等都需要开发&上线。
  • 逻辑黑盒: boost、融合公式迭代复杂之后,生效逻辑变得黑盒,不容易分析调参方向。

加乘树1.0的实现要点

纯item维度(请求维度的公式也会每个item重复计算)。consts->paramBranch->formulas串行计算。antlr解析单行公式成AST,框架递归解析树依赖,antlr visitor解释执行。

为什么用antlr

DSL语法校验: 我们需要一种配置设计,能尽可能简洁地表征模型融合公式(支持逻辑判断/复杂数学变换/UDF)——接近Java语法&数学公式的DSL(当时有对标字节的配置外观)。我们需要准确校验DSL配置正确、并正确解析DSL配置——在antlr、手搓逆波兰表达式、flex&bison里,选了用antlr校验、解析DSL(用AST校验原理可靠,Java上手难度低)。

antlr visitor解释执行: 依靠AST解析计算是一种可靠的计算逻辑。我们需要稳定靠谱的计算引擎,因为算法同学大规模使用后、会出现大量千变万化的公式组合——依靠AST解析计算是一种可靠的计算逻辑。

类SIMD设计使性能可接受: antlr解析AST非常耗时,必须一次parse多次复用,不能在item维度重复parse。一般用antlr visitor做线上实时计算,性能是不可接受的。我们采用了一种类SIMD的代码写法,使落地性能可接受——类SIMD的设计,一次antlr visitor算一批item。最终落地的性能、没有因为antlr visitor拖过多后腿,性能比旧版硬代码融合公式还要好。

antlr语法定义文件

antlr visitor如何通过访问AST计算1行公式

加乘树2.0

解决:抽象成SDK;执行计划自动识别请求维度公式、便于序融合等逻辑写UDF。缺陷:受限于解释执行,仍然比较耗线程。

加乘树2.0于2025年9月在社区搜索落地。优化点如下:

  • 使用体验: 配置json结构简化,只需要配递归的一组公式即可(砍掉了consts、paramBranch)。if()的配法简化:旧版编译器设计的简单,将 “logic表达式”与“math表达式”分别放在2个编译器里,使用者不允许if里嵌套函数,加乘树2.0合并了编译器,if()里可以嵌套函数。支持“隐式item正排”。

  • 性能: 框架自动识别Req维度的公式,全局只计算1次。执行计划加缓存,砍掉“每次请求都重新build执行计划”,平响降低。
  • 横向扩展: Java版加乘树抽象为SDK,方便扩场景直接引用。

加乘树3.0

解决:升级为编译执行,性能大幅提升。

加乘树3.0于2026年1月在社区搜索落地。之前“核心攻坚”模块有提到,高并发&计算量大的情况下,暴露出加乘树耗CPU、耗线程的弱点(类SIMD设计虽然能让性能可接受,但毕竟antlr visitor计算方式需要升级)。

加乘树3.0替换了执行引擎。我们观察火焰图发现“按公式逻辑直接裸写的java代码”性能最高效,但是迭代效率最低。加乘树为了即配即用公式,性能却打了折扣。为了平衡“即配即用”的迭代效率问题和“性能”,我们“将配置公式直接翻译成可执行代码,用字节码技术加载到JVM中直接计算”,这让加乘树从解释执行升级为编译执行。

六、还能更好

多语言 & 模块化: 加乘树有Java版,同时有C++版,是引擎同学创新实现的另一个高性能版本。支持多种业务场景及模块(如粗排、精排),可灵活接入 Java 业务引擎或 C++ 高性能引擎。欢迎其他场景和模块接入。

稳定性 & 产品化: 重点打磨“加乘树管理平台沙箱拦截 -> 线上容错降级 -> 失败监控告警发现 -> 解释执行托底” 的有效性,定期演练降级、验证算法效果。增强“加乘树管理平台”DIFF能力,扩展展示“调试DAG”、“可DIFF动态生成的代码”,打通实时debug平台,可以“DAG展开看计算的中间结果”。

多层公式组成DAG(打磨中)

配置生成的可执行代码做DIFF(建设中)

打通模型调用自动化: 在加乘树这里打通精排模型调用,对精排模型的调用也高度抽象,一配即用、一配即可加入公式融合。

往期回顾

1.深入剖析Spark UI界面:参数与界面详解|得物技术

2.Sentinel Java客户端限流原理解析|得物技术

3.社区推荐重排技术:双阶段框架的实践与演进|得物技术

4.Flink ClickHouse Sink:生产级高可用写入方案|得物技术

5.服务拆分之旅:测试过程全揭秘|得物技术

文 /啊俊 风林 益嘉

关注得物技术,每周更新技术干货

要是觉得文章对你有帮助的话,欢迎评论转发点赞~

未经得物技术许可严禁转载,否则依法追究法律责任。