信任,但基准测试:我们如何让AI代理优化Elasticsearch

Lobsters Hottest 新闻

摘要

Elastic描述了使用一个带基准测试框架的AI代理来自动化Elasticsearch中的代码优化,从而带来经过验证的性能改进。

<p><a href="https://lobste.rs/s/iul1vv/trust_benchmark_how_we_let_ai_agent">评论</a></p>
查看原文
查看缓存全文

缓存时间: 2026/09/17 15:39

# AI代码优化:证明智能体的性能提升成效 来源:https://www.elastic.co/search-labs/blog/ai-code-optimization-elasticsearch-agent-harness Elasticsearch (https://elastic.co/elasticsearch)需执行多种类型的工作负载,包括持续的大规模索引构建以及实时搜索与分析。要在所有场景中都提供出色的性能,就需要既广泛覆盖各类情况,同时又深入代码层面,理解每种工作负载的优化机会。传统上,人类的注意力是这个过程的瓶颈;工程人员根本没有足够的时间在庞大且不断演进的代码库中,逐一审视每个热点代码路径以寻找低效之处。 然而,随着编码智能体的快速发展,性能优化已成为我们能够半自动化处理的任务。与许多软件工程挑战不同,代码优化提供了一个低成本且客观的验证手段。如果你要求AI模型使代码运行更快,最终会有一个确切的数字告诉你发生了什么,还有性能分析工具解释原因。这使得性能优化成为自动化的理想候选领域,前提是你确实能信任这些数据。 如果你只是简单地将编码智能体指向一个基准测试,通常会得到大量低信噪比的结果:提升幅度落在环境波动范围内,或者变化是由热节流而非更好的代码引起的。为了捕获真正有益于Elasticsearch用户的优化,我们必须弥合“理论上可验证”和“实践中已检验”之间的鸿沟。我们构建了一个高度可信的测量循环:一个测试框架 (https://en.wikipedia.org/wiki/Agent_harness),它假设智能体在大部分时间里会出错,但当它正确时能可靠地捕捉并证明这一点,然后引导它下一步该去哪里寻找。 一旦这套机制就位,结果自然显现。通过让这个测试框架在代码库中运行,我们已经开始在各个层面发现有意义的性能提升。在本文的第二部分,我们将深入探讨它迄今为止发现的一些案例,包括Elasticsearch查询语言 (ES|QL (https://www.elastic.co/docs/explore-analyze/query-filter/languages/esql)) 中的字符串转换低效问题、我们NEON向量点积实现的改进,以及我们当时使用的gzip库的升级机会。在第一部分,我们将审视我们做出的设计选择,以及它们如何与高效测试框架开发这一更广泛的主题相关联。 ## AI代码优化流程架构 任何软件工程问题的第一步都是识别正确的高层级组件。我们做出了一个对解决此问题非常有帮助的架构选择:将“识别机会所在”与“执行代码修改的循环”分离。智能体从真实工作负载开始,但仅用它来挖掘关于何处寻求性能改进的信息。在此阶段,它被指示要广泛考虑,评估一系列与性能相关的信号。一旦找到并分类了热点,智能体就会读取这些热点周围的代码上下文,以理解优化机会。我们使用一个单独的任务,将排好序的热点列表精简为可在几分钟内迭代测试的产物:一个能证明在真实运行环境下有效执行热点路径的微基准测试。最后,我们使用一个提议-验证循环来实际修改代码库,以提升基准测试上的性能。这个过程最终会交由验证环节评估对真实工作负载的影响。我们的CLI工具(`atune`)提供了这个流程所需的工具,其余部分则主要由一套特定任务的指令来自动完成。 为了便于理解,我们的高层架构如下所示。粉色方框代表人类,青色方框代表智能体。共有三种任务类型、一个骨架循环和一个裁判。 *一个探索任务对真实工作负载进行性能剖析,并生成排好序的优化机会;人类将其中一个提升为开发任务,该任务针对一个已批准的微基准测试进行迭代,并提交每个被接受的实验;在真实工作负载上运行一次验证,以在人类审查并开启拉取请求之前守护结果。当没有基准测试覆盖热点路径时,基准测试任务负责创建一个,人类批准后将其加入注册表。性能图谱为每个任务提供信息,并积累每个任务学到的知识。* ## 为何性能优化适合自主智能体 有三个特性使一项任务非常适合自主工作,明确它们很有价值,因为它们提供了一个评估自动化候选方案的清单。你需要: 1. 一个客观的判定,以便智能体可以依据其自身意见之外的标准进行衡量。 2. 一个密集的指导信号,使其知道接下来该查看何处,而不是靠猜测。 3. 一个有限的爆炸半径,使得出错的代价是可承受的。 性能优化提供了这三点。基准测试提供判定,性能分析器提供梯度方向,而一个被拒绝的补丁消耗的是你的挂钟时间,而不是正确性。该更改会被恢复,原因会被记录。生活继续。关于前两点,我们开始认为梯度比判定更重要。“变快了”是二元的,而一份性能分析报告暗示了下一步该尝试什么。智能体可以根据丰富的指导信号(越丰富越好)来生成下一个假设,而不是机械地遍历一个列表。 ## 智能体看到的信号 一个有用的思维模型是:CLI就是智能体的感觉器官。这改变了你设计每个命令的方式。你不是暴露一项功能,而是设计它返回关于当前任务问题的清晰简洁的答案,并在相关时提供模型可以推理的解释,而不是需要其解析和解释的原始数据。这些是智能体行动所依赖的信号,最终我们为测试框架制定了以下内容: **信号** | **它回答的问题** --- | --- 分面分解的宏观剖析 | 按查询类型,真实工作负载的时间在代码中的何处消耗? 同一捕获中的分配与锁采样 | 成本是CPU周期、垃圾回收还是竞争? 成本构成分类 | 这是范围内计算、其他产品代码、GC、JIT开销还是挂起的线程? 输入形状检测 | 工作负载实际向这段代码输入了什么? 统计判定 | 在当前测量的噪声基底下,此更改是否有帮助? 分配速率比较 | 新代码最终是否分配了更多内存? 解释性反汇编 | 为什么会发生那个结果? 端到端A/B测试防护与差异剖析归因 | 是否有任何东西看起来损坏了,是我们的原因吗? 环境检查 | 这台机器现在是否适合进行测量? 上游重复项搜索 | 是否已有人报告或修复过这个问题? 其中有四个信号值得深入探讨,因为在每种情况下,工具都编码了一个判断,否则智能体必须手动不断做出这个判断。 分面分解是最清晰的例子。混合的CPU份额会隐藏广度:如果你剖析一个混合查询工作负载,分组哈希映射插入和百分位数草图更新都可能显示为总运行时间的个位数百分比,且看起来差不多。但它们完全不可比,因为哈希映射插入几乎被每个聚合查询所支付,而草图更新仅在有人请求百分位数时才发生。因此,性能分析器将每个命名的查询分面作为独立竞赛运行,智能体记录的每个机会都包含一个广度字段(通用、广泛或狭窄),并按余量 × 可处理性 × 广度进行排序。一个通用的3%性能提升胜过一个狭窄的10%提升。将排序函数放入工具中,可以避免在每次运行时重新发现它。 成本构成分类的作用类似于路由器。它不是给智能体一个扁平的前N个帧名称列表,而是将每个采样到的调用栈归入“范围内计算”、“其他产品计算”、“GC”、“JIT和安全点开销”、“脱机等待”和“挂起线程”等桶中。每个桶意味着不同类型的调查。如果GC占比超过约15%,真正的目标是分配速率,而CPU顶部帧会主动误导你,因为它们显示的是对象被回收的位置,而不是被创建的位置。如果JIT和安全点开销超过约25%,你看到的是一个天花板,而非机会,因为任何范围内的代码更改都无法改变它。如果线程被挂起且核心利用率低,这是一个并发问题,CPU火焰图完全不是合适的工具。我们将这种映射写入了操作手册的表格中,这样模型(即使是便宜的模型)就能像经验丰富的工程师那样阅读性能剖析报告,而不是直接冲向顶部帧。然而,分类是形成假设的先验知识,而非证据的替代品,因此智能体在提议实验时仍需引用具体的帧。 解释性反汇编是我们最初未提供,但绝对值得拥有的工具。它有助于回答“为什么”,这是解开下一个假设的关键问题。火焰图告诉你时间花在哪里;它们很少告诉你为什么某个更改使情况变糟。因此,`atune asm`会短暂运行基准测试,指示JIT打印某个热点方法的汇编代码,捕获工作树差异的两侧,将每一侧缩减到最终的C2编译结果,标准化地址,然后进行差异对比。仅差异对比本身可能仍有4000行aarch64代码,因此在其之上有一个解释层:每个助记符的差异、净指令数、实际捕获的编译层级,以及一个向量化信号,该信号统计两侧的向量寄存器引用次数,并在它们被消除、减半或从AVX (https://en.wikipedia.org/wiki/Advanced_Vector_Extensions) 缩小到SSE (https://en.wikipedia.org/wiki/Streaming_SIMD_Extensions) 宽度时发出警告。然后,操作手册将助记符模式映射到原因: **差异中的模式** | **可能原因** --- | --- `b\.eq`/`b\.ne`增加,`csel`减少 | 新增了不可预测的分支 `str`/`ldr`与栈指针成簇出现 | 编译器寄存器耗尽 NEON加载被标量比较替代 | 向量路径退化 在一次实验中,智能体将两个SIMD (https://en.wikipedia.org/wiki/Single_instruction,_multiple_data) 掩码提取操作合并为一个,基准测试性能下降了26%。向量化警告在约10秒内解释了原因。没有这个工具,智能体就走进了死胡同,对机器没有工作模型;有了它,它就能修正模型并获得几个新想法。 第四个信号与其说是单一工具,不如说是一种习惯;即仪器进行自我检查。核心利用率通过两种独立方式得出:采样密度和进程采样,以便进行比较。如果未分类桶超过预算,则分类结果会被拒绝,因为一个无法解释自身样本的剖析结果不应被用于推理。反汇编捕获会在捕获的编译结果不是稳态版本时发出警告。上游重复项搜索仅限于只读命令,并且通过一个grep源代码的测试来强制执行此限制,因此未来的编辑无法悄悄地重新引入提交任何内容的功能。每一项的存在都是因为一个可能出错的自信工具比一个仅仅缺失的工具更糟。 有一处小设计值得作为良好返回实践的特例来强调。`atune compare`返回0表示性能提升,1表示无变化,2表示性能下降,3表示错误,循环依据此代码分支。这意味着无需解析,也无需对判定结果有任何歧义。此外,无需浪费token来解释文本描述。 如果你从我们的CLI设计中只学到一件事,那就是一个更广泛的设计原则。在一般场景下,有趣之处并非我们发现的用于理解性能的单个信号;而是CLI正在将资深性能工程师的判断捕获并封装到返回答案而非原始数据的工具中。从结构上强制执行关于智能体负责问题的良好判断,能够改善结果。正确的CLI与指令同样重要。此外,通过返回决策和摘要而非数据的工具,可以节省token。这意味着返回比较判定而非原始的JMH输出;在4000行反汇编差异之上提供一个分类摘要。 ## 从20秒探测到数小时的验证 构建一个你可以负担得起去咨询的验证器,与构建一个你可以信任的验证器,是两个不同的问题。这里涵盖了可负担性部分。或者,如果你喜欢格言:真实工作负载是真相所在,也是迭代走向死亡的地方。一次宏观剖析需要45到60分钟,而端到端验证运行需要数小时。但微基准测试只需几分钟。这就是为什么探索然后开发的拆分是有效的;你在真实工作负载上广泛探索一次,然后交由一个你可以在几分钟内迭代的微基准测试。 关键在于,只有当微基准测试在真实的操作环境中执行热点路径时——即具有正确的基数和正确的数据分布——交接才是合理的。如果你搞错了,你的快速循环就会快速地在错误方向上旋转。在我们最终确定交接程序之前,我们看到一些案例,智能体接受了一个其关键分布恰好有利于它的基准测试上的更改,只有端到端运行才发现了问题。 ### 从探索到开发的交接 两个阶段之间的交接是结构化的,而非非正式的。探索任务的主要交付物是一组机会记录,每条记录都包含开发任务被允许编辑的范围路径、一个余量估计、一个分类(常数因子、结构性、分配或并发),以及它将依据的基准测试(如果不存在,则为明确的“无基准测试覆盖”标志)。每条记录还携带一个窄测试模式,以保持正确性检查的低成本。测试模式字段的存在源于一个具体事件;一个探索任务省略了它,导致生成的开发任务在每次实验上都运行了非常沉重的测试套件。修复方法是更改上游产物,而不是在下游添加指令。这是我们反复使用的模式;尽早做出并记录决策,而不是每次都重新推导。 ### 在允许用于门控之前验证新基准测试 当真正缺少基准测试覆盖时,一个专门的基准测试任务会创建一个,该新基准测试必须通过有效性检查,然后才能被依赖。其中两项是机械的:基准测试热点自耗时中,有多少比例来自实际出现在生产剖析中的帧,以及参数是否在我们测量的输入形状范围内。第三项是一个清单,需要智能体逐项确认。它存在是为了避免JIT获得一个比生产环境更简单的世界。 1. 输入必须被打乱顺序,而非固定或排序,这样分支预测器不会变得太好。 2. 结果必须被消费,否则死代码消除会删除你本意要测量的东西。 3. 输入不能是编译时常量,否则它们会被折叠掉。 4. 调用点必须看到大致的产品类型混合,因为单态调用点会被内联,而多态调用点则不会。 然后由人类批准将其加入一个哈希固定的注册表。在此之前,它是无效的,因为任务设置会拒绝任何引用未批准基准测试的任务。我们将此批准视为系统中最严格的门控,并且是精心设计的。

相似文章