跳到正文
Databricks Blog·· 3 小时前AI 评分47

为智能体而构建:Spark Declarative Pipelines 的论证

Built for Agents: The Case for Spark Declarative Pipelines

AI 导读

Databricks 上超过 70% 的 SDP 管道已由 AI 智能体创建,其基准测试显示声明式管道比过程式 PySpark 更安全、更省、更精简。在 22 个任务、264 次构建的对比中,声明式管道因运行前校验将计算量降低约 34 倍(17 对 596 executor-seconds),代码中位数 64 行、约为过程式 131 行的一半。

正文 · AI 翻译

AI 智能体现在编写越来越多的生产数据管道(Databricks 上超过 70% 的 SDP 管道现在由智能体创建),这提高了两个方面的风险:(1) 智能体的每次尝试都会消耗计算资源和时间,因此迭代需要廉价且高效地运行;(2) 代码审查成为关键关卡,因此代码需要简洁、可读且正确。Apache Spark® 提供两种编程模型来编写管道:过程式和声明式,它们在编写内容、Spark 在运行前能检查的内容以及错误的代价方面有所不同。我们用同一个智能体在相同任务上对两者进行了基准测试,声明式模型更安全、更廉价、更精简。

过程式管道是告诉 Spark 如何逐步构建每个数据集的代码。要知道它是否有效,唯一的方法就是运行它。

声明式管道描述你想要的数据集,让 Spark 决定如何构建它们。这类似于你向 LLM 发出提示,由它来弄清楚如何完成(通过使用正确的技能和工具)。用代码最能说明一个简单的拼写错误会如何失败:

过程式声明式
bronze = spark.read.parquet(RAW)
bronze.write.saveAsTable("bronze")
silver = 
bronze.withColumnRenamed("revenue", 
"revenues")  # typo
silver.write.saveAsTable("silver")
gold = 
spark.read.table("silver").groupBy("revenue")
.sum("revenue")
gold.write.saveAsTable("gold")  
# runtime error
@dp.materialized_view(name="gold")
def gold():
    return 
spark.read.table("silver").groupBy("revenue")
.sum("revenue")

1. 声明式管道可以在运行前进行验证

Bug 有两种类型。结构性 Bug 可以通过阅读管道发现:未解析的列引用、尚不存在的表、不允许的配置更改。静默 Bug 只在运行结束且结果错误时才出现在输出中。

声明式管道可以在运行前检查结构性 Bug,类似于 Rust 等类型化语言中的数据流可以在编译时检查。由于声明式管道是描述而非运行,Spark 可以通过阅读它来回答三类问题:

  • 每个列引用都会根据其读取的数据集的 schema 进行解析,因此不存在的列会被拒绝,而不是在运行时失败。
  • 数据集之间的每个依赖关系都会与图中的生产者匹配,因此对没有任何声明产出的表的引用会在管道启动前被捕获。
  • 图会检查完整性和顺序,因此由引擎决定何时运行什么,而不是信任模型碰巧写出的顺序。

2. 声明式管道将样板任务卸载给 Spark

在过程式 PySpark 中,AI 编码智能体编写数据操作,例如过滤无效行和连接表。它还编写代码以正确的顺序运行管道的各个步骤并保存其结果。这些支持性代码在多个管道中重复了大量相同的样板代码。

在声明式管道中,AI 智能体定义每个数据集以及生成它的查询。Spark 处理执行顺序并保存结果。由于 Spark 处理这些样板代码,智能体编写的代码更少,团队需要审查和维护的内容也更少。

我们测试了什么

我们测试了 22 个数据工程任务。每个任务看起来像一张真实的工单:它描述了一个问题,并指定了管道必须生成的表、列名和数据类型。智能体可以选择任何实现,只要输出符合这些要求。

对于每个任务,我们生成了 12 个不同的输入数据集,其中包含生产数据中常见的各种杂乱值。智能体为每个数据集编写了一条新流水线,一条使用过程式 PySpark,另一条使用 Spark 声明式流水线。这样每种方法产生 264 次构建,总计 528 次:22 个任务 × 12 个数据集 × 2 种方法。所有构建均使用 Opus 4.8。

我们运行了每条流水线,并对照所需表检查其输出。自动评分器并不知道结果是由哪种方法产生的。声明式流水线首先经过 Spark 的试运行。如果某次构建失败,智能体可以修改并重试,直到达到固定上限。

你可以从公开仓库自行重新运行整个过程:环境设置、提示词、数据集、评分器,以及每个数字背后的结果。

结果

两种方式几乎完成了所有任务:过程式完成了 264 个中的 255 个,声明式完成了 264 个中的 258 个,因此无论哪种方式都能让你得到一条可运行的流水线。不同之处在于围绕它的一切:错误何时暴露、修复需要多少代码,以及输出的一致性如何。下面我们介绍声明式流水线的三个优势,随后是两点注意事项。

1. 运行前验证使声明式流水线耗时减少约 34 倍

流水线的错误会在执行前、运行期间或根本不暴露。过程式流水线在执行前无法拦截任何错误,因为它们没有执行前阶段。要发现结构性错误,Spark 必须在集群上运行流水线,直到到达出错步骤。

在我们测量的构建中,过程式运行使用了 596 个执行器秒,而声明式运行仅用了 17 个。对声明式代码的试运行验证及早捕获了结构性错误,将计算量减少了 34 倍。

Procedural Pipelines

2. 声明式流水线代码量是过程式流水线的一半

在已完成的构建中,声明式流水线的规模约为一半,中位数为 64 行,而过程式流水线为 131 行。

过程式实现的变化也更大。对于同一项工作,有些使用 Spark 转换,而另一些则将数据加载到内存中并逐行处理。两者在测试数据上都通过了,但内存版本在数据增长时可能失败。在整个基准测试中,有 19 个过程式构建使用了这种捷径。声明式构建则没有一个这样做。

更短、更一致的代码意味着团队需要审查的内容更少,并且只需维护一种模式。

Declarative Pipeline Code

3. 在逻辑错误上,两者持平

以上就是两个优势。在运行测试时,我们还发现了两点注意事项。

首先,试运行只能捕获结构性错误。它无法捕获静默错误:即流水线运行正常却返回错误答案,这是更危险的一种。在这一点上,两者持平:它们的静默错误率相差约 15% 以内,差距太小,不构成真正的差异。声明式让结构性错误的捕获成本很低;它对逻辑错误毫无帮助,逻辑错误仍然需要测试和审查。

4. 声明式前期投入更多,以在后期节省更多

声明式方法在单点投入更多。智能体会在 dry run 上反复迭代,重试直到管道健全,因此它更多地把思考放在前期以 token 形式完成,而不是之后在集群上发现问题。token 是一次性的固定开销,在编写管道时支付;其数量不随数据规模或管道运行频率变化。浪费的集群算力则相反:它会在每次失败的运行中反复出现,并随数据增长,因此你处理的数据越多,一次糟糕构建的代价就越高。前期支付固定成本以避免一个不断增长的成本,只会随着管道和数据变大而显得更划算。

我们测量的一切,并排对比

完整的测量差异如下所示。

维度过程式 PySpark声明式(SDP)
智能体产出什么能运行的代码会被检查的描述
结构性错误在运行中途、部分写入之后浮现,或从不出现;没有一个在输出前被阻止在 dry-run 时被捕获;264 个中有 55 个在任何输出前被阻止
成本

集群时间:约高 34 倍(一次失败运行最多约 521 executor 秒)

LLM token:基线

集群时间:基线(一次失败运行约 0.5 executor 秒)

LLM token:约 2.3 倍(构建中位数约 $0.50 对 $0.26,Opus 4.8 标价)

代码规模基线

代码行数:约少 51%(中位数)

AST 规模:约小 44%

静默 bug(运行干净、答案错误)264 个中有 73 个(27.7%)264 个中有 85 个(32.2%)

结论

在生产规模下,声明式方法更胜一筹。它在 LLM token 上多花约 2.3 倍,作为交换,它从不在一个注定无法工作的管道上花费集群时间。在生产规模下,声明式方法通过以略高的 LLM token 成本换取对失败管道浪费集群时间的保证性预防,证明其优势。随着数据量扩展,这变得愈发划算,因为算力浪费按比例增长,而 token 成本保持不变。

如果你在评估用于管道工作的智能体,请像对待选择哪个智能体一样严肃对待智能体如何编写。更好的智能体犯更少的错误;声明式管道让错误变得廉价,并在它们造成任何代价之前捕获它们。

仓库记录了设计、数据和完整的来源。要开始使用 Spark Declarative Pipelines 配合你的智能体,请参阅官方编程指南,并在 PySpark 中使用 SDP skill。

常见问题

我必须信任 AI 智能体才能让它工作吗?

不,而且不必如此正是关键。声明式智能体只编写声明式管道代码——即它想要的表的描述——而 Spark 本身会在任何东西运行之前检查该描述。检查器是平台的一部分:你信任的是 Spark 对管道的评估,而不是模型。智能体从不自己运行代码或读取你的数据。

这只是关于 Spark Declarative Pipelines,还是适用范围更广?

它更广泛地适用于声明式方法:任何你能将“这是我想要的”与“现在去构建它”分开的地方,这样第一部分就可以被独立检查。你在 SQL 本身、基础设施即代码工具以及 schema-first API 中都能看到同样的形态。Spark Declarative Pipelines 是我们基准测试的那个,但推理适用于任何以这种方式构建的工具。

声明式代理使用的 token 数量是 2.3 倍。这难道不是致命缺陷吗?

在实际的流水线规模下并非如此。按 Opus 4.8 的标价计算,2.3 倍意味着每次中位数声明式构建的 token 成本约为 $0.50,而程序式构建为 $0.26。一次失败的程序式运行消耗了约 521 个执行器秒,而被拒绝的声明式运行仅花费约半秒——因此避免几次糟糕的运行就能抵消大量额外 token 成本,而且随着数据量增长,这一差距只会进一步扩大。此外,token 价格会随你使用的模型服务而波动,而计算成本则更稳定、更易于规划。

来源:Databricks Blog · databricks.com