ClickHouse 发布 Node.js RowBinary 库:支持 AI Agent 自动生成高性能解析器

 

本文字数:6176;估计阅读时间:16 分钟

作者:Peter Leonov

 

 

 

 

编者按: 本文译自 ClickHouse 原博客。 原文围绕「探索 AI Agent 与高性能二进制数据解析的深度集成」展开。该库通过将解析逻辑转化为可由 AI 编译的技能,平衡了开发灵活性与执行效率。这种“库即编译器”的思路为大模型时代的中间件开发提供了新参考。 

 

我们正式发布了 @clickhouse/rowbinary,这是一个专为 Node.js 设计的读写库,支持 ClickHouse 的 RowBinary、RowBinaryWithNames 及 RowBinaryWithNamesAndTypes 格式。该库支持标准引入并调用通用解析器。同时,它也作为一项 Agent 技能提供:只需让代码 Agent 读取内置的 SKILL.md 文件,Agent 就能根据具体的查询列类型生成专属解析器,无需再调用库函数。相比在循环中组合调用通用函数,这类自动生成的解析器运行速度提升了 1.5 到 3.4 倍。单次生成成本仅约 0.20 美元,且能有效规避大模型凭记忆编写二进制解码器时可能引入的隐性数据损坏风险。

 

 

开发背景

 

作为 ClickHouse 数据提取效率最高的格式之一,RowBinary 在 JavaScript 中的处理逻辑却极为繁琐。虽然这种网络传输格式本身并不复杂——采用小端序基础类型,利用 LEB128 变长整数表示动态宽度,且无单行额外开销——但难点在于读取端。每种叶子节点类型都有特定的读取模式,且 NullableArrayMapTuple 和 LowCardinality 等类型可以无限嵌套。此外,读取 DateTime64 需要处理精度缩放和可选时区,而 VariantDynamic 及 JSON 等自描述递归类型则要求每个值都根据类型标签重新调度解析逻辑。

受此影响,多数应用倾向于退而使用 JSON 格式。但这不仅会消耗额外的 CPU 资源,还会带来潜在风险:当 UInt64 数值超过 Number.MAX_SAFE_INTEGER 时,会被静默近似为 float64,除非采用先转字符串再重新解析这种低效方案。而坚持使用 RowBinary 的团队,通常会编写一套基于类型调度的通用解析器,即为每种类型分配函数并在运行时逐个单元格调度。这种方式虽易于维护,但性能瓶颈明显,因为每个单元格的处理都会产生调度开销,且 V8 引擎的内联优化往往会放弃处理此类超多态(megamorphic)调用点。在高并发场景下,真正需要的是针对当前查询进行单态化(monomorphized)处理的解析器:根据具体列按序内联读取操作,实现零调度开销。然而在实际开发中,手动为每个查询编写专属解析器显然并不现实。

这个包的出现填补了这一空白。它不仅为 ClickHouse 类型系统提供了经过严谨测试的读取原语,还通过内置技能引导编程 Agent 将这些原语组装成针对特定查询精细调优的解析器——这类高性能代码通常很难由人工手写完成。

 

 

核心架构与用法

 

该包由两层构成。

第一层是针对具体类型的读取原语库。每个叶子类型对应一个微型函数,并为 NullableArrayMapTuple 等类型代数提供可组合的包装器,涵盖全量缓冲和分块流两种模式。每个原语都遵循单态化设计(体积小、功能单一、无巨态分发),确保能被内联到特定查询的解析器中而不触发 V8 优化器失效。作为独立库,它非常实用:只需导入并调用 parseRowBinary(...) 即可获得准确结果。此外,它还提供了双向流式写入器,以及基于 @clickhouse/datatype-parser 的动态 RowBinaryWithNamesAndTypes 流水线。

第二层是 SKILL.md。它并非传统的 API 文档,而是向编程 Agent 传授一套组装策略,指导其将原语构建为针对特定查询列类型的定制解析器。库源码中的注释详细说明了代码块的设计初衷及可修改范围,包括缓冲区所有权、64 位整数的类型选择(BigInt 或 number)、Date 映射钩子、Decimal 精度处理、Array 物化策略以及定宽列的快速路径。如果说库是参考实现,那么注释就是设计原理,而 SKILL.md 则是代码生成指南。

安装方式:

npm i @clickhouse/rowbinary

该技能已在包的 agents.skills 字段中注册,支持技能扫描的 Agent 会自动识别。你也可以手动添加:

npx skills add ClickHouse/clickhouse-js --skill clickhouse-js-node-rowbinary

 

Agent 生成代码示例

以基准测试中的 orders 表结构为例:

id     UInt8
uid    UUID
price  Decimal64(2)
status Enum8('new' = 1, 'shipped' = 2, 'done' = 3)

使用该库的公共 API 组合而成的解析器通常如下所示:

export const readOrderRow: Reader<OrderRow> = (s) => ({
  id: readUInt8(s),
  uid: formatUUID(readUUID(s)),
  price: readDecimal64(2)(s),   // closure rebuilt every row
  status: readInt8(s),
});

但在上述代码中,每个字段都会触发独立的边界检查,readDecimal64(2) 每行都会创建新闭包,且 formatUUID 需经 BigInt 处理。加载技能后,Agent 会识别出所有列均为定宽,转而生成如下优化代码:

export const readOrderRowFast: Reader<OrderRow> = (s) => {
  const { buf, view } = s;
  // Every column is fixed-width: 1 + 16 + 8 + 1 = 26 bytes.
  // One bounds check for the whole row, then read at constant offsets.
  const o = advance(s, 26);
  const id = buf[o]!;
  const uid = formatUUIDTable(buf.subarray(o + 1, o + 17)); // table, not BigInt
  const price: DecimalValue = [view.getBigInt64(o + 17, true), 2];
  const status = view.getInt8(o + 25);
  return { id, uid, price, status };
};

整行 26 字节数据仅需一次边界检查,读取逻辑基于固定偏移量,Decimal 精度被硬编码,并采用了查表式 UUID 格式化。最终输出与常规版本完全一致,但执行速度提升了 3.41 倍。

由于解析器本身是特化的,诸如列重命名、派生新字段或剔除冗余字段等转换逻辑可以零成本融入读取循环中,无需在底层库中增加复杂的配置项。

在采用该方案前,请了解以下特性:

• 生成的解析器是常规代码。 它由开发者提交,并遵循标准的评审、测试和基准评估流程。该技能生成的是可读源码,而非不可见的二进制文件。底层库提供了完备的测试用例,可直接用于验证业务方的读取器。

• 过程可审计。 技能由 Markdown 文件和注释详尽的 TypeScript 代码组成,全部包含在 npm 包中。

• 通用读取器依然可用。 你可以跳过 Agent,直接调用 parseRowBinary(...) 走稳定路径。该技能是性能增益项,而非强制替代。

• 适配高性能模型。 该方案最适合能读取完整上下文并遵循多步指令的模型。虽然小模型表现略逊,但在配合专注的子 Agent 后依然可行:例如 Haiku 的通过率可从 52% 提升至 86%。

 

 

演进之路

传统方案是使用编译器

Schema 驱动的二进制格式在生成单态化解析器时通常采用编译器模式。protocflatc 和 Cap'n Proto 均遵循此路径:接收 Schema,运行编译器,为目标语言生成专属代码。这种方法虽然有效,但编译器本身是一个庞大的工程,涉及 Schema 解析器、中间表示(IR)、多语言后端及复杂的配置矩阵。用户任何细微的定制需求(如更换 Decimal 库、自定义日期映射、指定 Int64 的表示类型、调整 Array 的实例化策略等),都必须设计为具体的参数开关,并承担相应的文档编写、版本维护和兼容性测试成本。

我们并未为 RowBinary 编写 JavaScript 编译器,而是将其拆解为 Agent 可在推理时重组的组件:带注释的基础原语和一份 Markdown 指南。这样一来,自定义配置不再是枯燥的参数列表,而是与理解代码库的模型进行对话。无论是将 Decimal128 映射到特定的 big-decimal 库,还是通过字符串驻留表实现 LowCardinality(String),Agent 都能通过阅读 decimal.ts 中的注释在几百个 token 内完成处理,无需我们专门开发配置项。

 

评估结果说明了什么

针对不同 Schema 的 5 万行数据,我们对比了三种路径:正确的 JSON 路径(服务端转字符串,客户端解析为 BigInt)、基于库 API 组合的通用 RowBinary 读取器,以及 Agent 生成的单态化解析器。硬件配置和软件版本见文末。

在 Apple M4 Max 上处理宽整数金融账本时,RowBinary 的速度是 JSON 的 3.3 倍(基准测试源码),在 CI 环境(4 核 AMD EPYC 7763)下为 2.5 倍(运行结果)。在高性能硬件上,这种差距更加明显,因为 RowBinary 侧重指针运算和连续内存读取,而 JSON 则涉及大量带分支的词法解析和内存分配。对于 IoT Schema,两台机器的性能提升均稳定在 2.1 倍左右。需要强调的是,这是与正确的 JSON 路径对比的结果。若使用原生的 JSONEachRow,差距看似会缩小到 1.8 倍,但它会静默地将超限的整数舍入为 float64。表面上解码成功,实则数值已经失真,且只有在比对本地计算总数与服务端数据时才会暴露。

相比通用组合式读取器,Agent 生成的解析器性能进一步提升了 1.5 到 3.4 倍:

Schema

数据形态

相比组合读取器的提速

金融账本

宽整数 (UInt128Int128)

1.55x

IoT 遥测

Float64

 / 整数

2.46x

订单

固定宽度,反范式化

3.41x

在所有数值密集型 Schema 中,性能提升均超过 1.5 倍。不过,RowBinary 并非万能。在字符串密集的日志 Schema 中,JSONCompactEachRow 的表现甚至优于优化后的 RowBinary 解析器,而该技能的指南也会建议在此类场景下避开 RowBinary。这证明了该技能具备识别适用边界的能力。

使用 Claude Sonnet 4.6 时,四个数值密集型 Schema 的平均生成成本如下:输入约 230k token(大部分由 prompt 缓存覆盖,单次调用独占约 28k),输出约 2.1k token。在命中缓存的情况下,每个解析器的生成成本仅为 0.20 美元,无缓存时上限为 0.72 美元。对于每次部署而言,这一开销完全可以接受。

评估还发现,该技能在提升速度之余,更显著增强了可靠性。RowBinary 将 UUID 存储为两个小端序 UInt64 半块,字节序与文本格式相反。在脱离文档和工具的情况下,Sonnet 4.6 在 5 次尝试中有 3 次弄错了字节序,且每次都会产生隐性错误:将 16 字节按原序转为十六进制,生成看似合规但内容错误的 UUID。而加载技能后,由于参考原语触手可及,Agent 每次生成的代码都能保证正确。

这种错误模式让我们意识到,其防范价值甚至超过了性能提升。ClickHouse 用户处理的数据量往往以十亿计。JSON 路径的精度舍入可能导致财务报表在数周后才暴露微小偏差;而 UUID 字节序错误则会暗中破坏 JOIN 逻辑。这类 bug 会让团队对 AI 生成的代码失去信心。该技能通过组合经过审计的官方原语,确保了生成代码的安全性。你在 PR 中审查的不再是不可控的 AI 逻辑,而是由我们维护的代码块拼装而成的产物。

 

开发该 skill 的成本

有观点认为,强大的 Agent 无需技能也能写出解析器。这确实可行,因为这项技能本身就是这样诞生的。通过提供 RowBinary 规范和源码,Claude Code 最终生成了可用的读取器,但这消耗了数百万 token,耗费了整天时间进行提示词微调、测试和性能优化,且每一步都需要人工介入。

这项技能是那一整天工作的“成果固化”。它沉淀了关于 LEB128 读取、Decimal 精度、整数类型选择及 V8 内联机制的经验,避免了模型每次从头摸索。这与编译器的成本分摊逻辑一致:我们投入人机协作成本将其转化为 Markdown 和带注释的代码,让所有下游用户只需花费 0.20 美元即可复用这些专家级经验。

 

它的定位

Agent Skills 标准自 2025 年推出以来,最常见的模式是随 npm 包提供 SKILL.md 以引导 Agent 正确调用 API。我们也发布了类似的 clickhouse-js-node-troubleshooting 项目,作为 Agent 的故障排查手册。

@clickhouse/rowbinary 采用了另一种模式:将代码库视为参考而非黑盒 API。Agent 不再仅仅是调用者,而是能够根据需求分叉(fork)并重组代码。这种模式要求我们改变编写代码的方式:供调用的代码需要清晰的说明,而供阅读的代码则需要详尽的注释和逻辑一致性,避免使用难以复现的奇技淫巧。

我们预见这种模式将日益普及。对于范围明确、模式驱动且性能敏感的场景,开发者现在多了一种比传统编译器更轻量、更灵活的选择。若你发现生成的代码存在问题,可以通过修改库中的注释进行修复;届时请 提交 Issue。

 

基准测试环境

本地环境:Apple M4 Max,Node v24.6.0,macOS 26.5.1,ClickHouse 26.1.1.200。CI 环境:AMD EPYC 7763 (4 vCPU),Node v24.17.0,ClickHouse 26.6.1.1193(测试工作流,运行记录)。源码版本:8c51d9a。运行 npm run bench 进行测试,每个 Schema 样本量为 5 万行。

 

关于我们

ClickHouse 是面向 AI 时代打造的高性能实时分析数据库,能够以极致性能处理海量数据分析任务。凭借高并发、低延迟和云原生架构,ClickHouse 广泛应用于可观测性、数据仓库、实时分析及 AI 数据基础设施等场景。我们致力于帮助企业在公有云平台上构建安全、弹性且高性价比的实时分析与 AI 数据平台,加速释放数据价值,推动智能化创新与数字化转型。目前,Trip.com、DiDi、Meta、Sony、Netflix、Deutsche Bank、Sierra、Cloudflare 等全球领先企业均在使用 ClickHouse 支撑其关键业务和数据分析平台。

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值