All news
ASCodeworksReleases

SYNX 3.7:SYNXL 为 AI 数据集带来带类型的记录流

by APERTURESyndicate · 2026年8月1日

SYNX 3.7:SYNXL 为 AI 数据集带来带类型的记录流

SYNX 3.7 发布了。它给语言添加了一个运算符,并在旁边带来一整套新的文件格式:SYNXL——面向数据集的记录流格式,专为人们通常硬塞给 JSONL 和 CSV、而它们又做不好的两件事而生。

简单说:字段只声明一次,然后以流的方式写入带类型的记录。嵌套和多行文本一并跟上,格式有问题的行会被报告出来,而不是悄悄消失。

JSONL 和 CSV 在数据集上为什么难受

随便打开一个 JSONL 训练集,看看你实际在为什么付费。每一条记录都重复着每一个键、每一个括号、每一个引号。在几十万条对话样本的语料里,光是字段名就占了你喂给模型的 token 中可观的一部分——而从第一行之后,它们不再携带任何信息。

多行文本更糟。一段提示词、一段代码、一条模型回答——全都被压成用 \n 转义的噪音,没人愿意读,每次 diff 都会被搅乱。

CSV 避开了重复,却几乎放弃了其他一切:没有类型、没有嵌套,还有每条 CSV 流水线迟早都会撞上的悬案——空单元格到底是空字符串,还是缺失值?往字段里加一个逗号,你就进入了各家方言各不相同的引号规则。

一屏看懂 SYNXL

!synxl 1
!fields id[type:int] ; score[type:float] ; messages[block]

1 ; 0.91
  messages
    - role system
      content You are a helpful assistant.
    - role user
      content |+
          def f(x):
              return x + 1

2 ; 0.74
  messages
    - role user
      content Привет

字段列表只声明一次。记录从第 0 列开始,标量字段之间用 ; 分隔,任何标记为 [block] 的字段都会在记录下方以完整的 SYNX 展开——列表、嵌套对象、多行文本。正因如此,对话形态的数据可以直接表达,而不必压平成一段转义字符串。

上面的例子可以投影成普通的 JSON:

[{"id":1,"messages":[{"content":"You are a helpful assistant.","role":"system"},
  {"content":"def f(x):\n    return x + 1","role":"user"}],"score":0.91}, …]

这个格式真正保证了什么

  • 类型待在该待的地方。 [type:int][required][enum:a|b|c] 写在头部,而不是放在旁边的 schema 文件里,也不需要逐行重新推断。

  • 空不等于没有。 空字段就是 null;空字符串写成 ""。CSV 的那种歧义从结构上就不存在。

  • 不会静默丢数据。 SYNX 会安静地跳过结构有误的部分,SYNXL 则会把它报出来——缺字段、多字段、类型转换失败、未知的块键,每一条都带着记录序号和行号。一个悄悄丢掉一列的数据集,比一个大声失败的数据集更糟。

  • 记录彼此独立。 记录边界只看一个字节就能判定,所以这个格式可以安全地追加、干净地分片、并行地解析。

  • 文件中途也能演进结构。 多了一列?写一行新的 !fields 然后继续追加。之前的记录依然有效,并保持原来的形状。

为放不进内存的文件而生

SYNX 对单个文档施加的 16 MiB 输入上限,在 SYNXL 里是按每条记录计算的——数据集动辄以 GB 计,而记录之间彼此独立,所以整文件上限并不能多保护什么。

流式读取器同时只在内存里保留一条记录。用 CLI 在一个 142 MB、含 240 万条记录的文件上实测,validateparsesplit 的峰值内存始终平稳,约 10 MB——而把同一个文件整个读进内存,代价至少等于它自身的大小。

3.7 的另一半:|+

语言本身只多了一样东西:|+,一个保留缩进的多行开启符。普通的 | 会把每一行续行的前导空白都裁掉,用在散文里没问题,用在任何空白有意义的地方都是破坏性的。|+ 会以第一行正文确定基准缩进,并保留其后的一切——于是代码、ASCII 图和提示词骨架都能原样留在一个值里面。

SYNX 3.6 仍然是冻结的互操作基线。3.7 只做加法:对于任何不使用 3.7 专属结构的文档,3.6 的解析器依然是合规的。

在代码里读取

SYNXL 目前有四个实现,背后的语义完全一致:

// Rust
let doc = synx_core::synxl::parse_lines(&text)?;
for record in &doc.records { /* … */ }

// TypeScript
import { parseSynxl, streamSynxlFile } from '@aperturesyndicate/synx-format'
for (const record of streamSynxlFile('dataset.synxl')) { /* … */ }

# Python
import synx_native as synx
for record in synx.synxl_stream_file("dataset.synxl"):
    train(record["messages"])

在命令行里也一样,并且双向转换都有:

synx synxl parse dataset.synxl --format ndjson
synx synxl validate dataset.synxl --constraints
synx synxl convert corpus.jsonl          # jsonl → synxl
synx synxl split dataset.synxl -n 500000 # shards that are valid on their own

获取渠道

3.7.1 已发布到 crates.ionpmPyPINuGet

SYNXL 本身用 Rust、TypeScript、Python 和 CLI 实现。C++、Dart、.NET、Go、Java 和 Swift 的原生解析器已支持 SYNX 3.7,包括 |+,但还读不了 .synxl——在那之前,用 CLI 转换,或者用这四个实现之一读取。

规范与一致性

SYNXL 有自己独立的版本轴——格式版本 1,与语言版本无关——因为 SYNXL 文档并不是 SYNX 文档:它的顶层是一串记录,而不是一个对象。

规范文本是 SYNXL-1-NORMATIVE.md,其背后的契约是一套 67 个一致性用例,全部独立于任何实现、直接从该文本推导而来。Rust 的执行器还会用它的三个读取器把每个通过的用例重读一遍,一旦结果不一致就判定失败。

完整文档和在线试验场:synx.aperturesyndicate.com

接下来

为其余解析器补上 SYNXL 支持,从 Go 和 .NET 开始。规范已经稳定,一致性测试也已写好,所以第二波是机械劳动而不是探索——这正是先写测试再写实现的意义。

Other stories

All stories
SYNX 3.7:SYNXL 为 AI 数据集带来带类型的记录流 | APERTURESyndicate