重复字段名每个Token都在收费,改用紧凑格式可显著降低调用成本,文章提供了替代JSON的具体方案。
我在为同一个词付300倍的钱,却浑然不知
几周前,我注意到一个很蠢的问题。
我在向 LLM 发送一批记录,格式和往常一样是 JSON。盯着 payload 看的时候,我发现同一个字段名反复出现。每行一次。三百行数据意味着同一个词重复了三百次,而除了第一行之后,这个词并没有告诉模型任何它不知道的东西。
这个问题比我预想的更让我困扰。于是我开始寻找一个很基本的问题的答案:有没有更好的方式向 LLM 发送数据?还是说 JSON 之所以被广泛使用只是因为我们一直以来都在用它?
这个问题最终变成了一个名副其实的兔子洞。以下是我发现的内容。
为什么我开始在意这件事
说实话,我并不是醒来就在想 token 成本。我在构建一个向提示词发送结构化数据的 pipeline,一直看着 token 数随着数据量增长而攀升。那种感觉像是浪费。我只是还没给这种"感觉浪费"找到一个名字。
为什么 token 首先很关键
这里有个让我恍然大悟的部分。LLM 不是像我们一样阅读文本的。它们把一切都拆成 token,即单词或子词的粗糙片段,而每一个 token 都要花钱,并且占用上下文窗口。
现在想象一个包含几百行数据的 JSON 数组。每一行都重复着相同的字段名。"id"、"name"、"status",一遍又一遍,每次都完全相同。这些重复没有任何意义——模型从第一行就已经理解了数据的结构。
那么所有这些重复的文本实际上在做什么?什么都没做。它只是填充物。
削减这些填充物不会改变模型看到的数据。它只是去除了周围的噪声。同样的信息,更少的 token。填充物与实际信息的区别,就是这个兔子洞存在的全部原因。
三种格式,以及它们各自实际的使用场景
理解问题之后,我以为会有一个显而易见的"正确"格式来替代 JSON。
并没有。至少有三种,而每一种都在回答一个略有不同的问题。
JSON 之所以胜出是因为它的通用性。每种语言都能解析它,每个 API 都使用它,每个数据库驱动都序列化到它。这不会消失,说实话,它也不应该消失。
问题不在 JSON 本身。问题在于 JSON 进入 LLM 提示词时具体发生了什么。它自描述的结构,在每个对象上重复的 "key": 正是那种大规模下浪费金钱而对阅读它的模型毫无价值的冗余。
继续使用 JSON 的场景:API、数据库、配置文件。任何不直接进入提示词的地方。
一种 token 高效的表格格式
我研究的第一个替代方案是专门为扁平、表格数据构建的,基本上是电子表格或数据库查询结果的形状。
不再在每行重复字段名,而是只在表头声明一次字段,然后在下面只列出值:
items[4]{id,name,category,status}:
IT-001,Widget-A,tools,active
IT-002,Widget-B,parts,inactive
IT-003,Widget-C,tools,active
IT-004,Widget-D,parts,active
就这样。没有重复的键。每行没有重复的括号或引号。只有一个表头,只声明一次,然后是值的行。
我第一次看到它时差点忽略了一个小细节,后来发现这很重要。看到那个 [4] 了吗?这是声明的行数。如果 payload 在某个 pipeline、网络抖动、缓冲区限制、序列化步骤的 bug 等地方被截断了,模型可以将"被告知有 4 行"与"实际只收到了 3 行"进行对比。JSON 没有内置这种东西。没有任何字段说"期望这么多条目",所以被截断的 JSON 数组只是安静地看起来像一个更短的数组。格式本身不会升起任何旗帜。
最适合:产品目录、日志行、数据库导出、搜索结果,任何天然是表格的东西。
一种为嵌套结构构建的格式
第二个替代方案针对完全不同的问题形状:嵌套的、重复的结构。想想文件树、多步骤 agent 消息、API 响应中同一个对象出现在 payload 内五个不同地方。
不再每次出现时都完整复制重复的结构,这个格式定义它一次,然后通过 ID 引用它出现的每一次:
{
"items": {
"@id": "item-1",
"name": "Widget-A",
"children": [
{ "@ref": "item-1" }
]
}
}
与其每次递归时都粘贴完整对象,不如直接指回去。一次定义,多次引用。
最适合:多工具 agent 工作流、嵌套 API 响应、任何相同数据结构在自身内部出现多次的场景。
所以你应该用哪个?
这里是我花了一会儿才真正想清楚的部分。这不是三个互斥的选项,你挑"最好的"然后完事。它们解决的是不同的问题。
表格格式适用于扁平行。嵌套结构格式适用于重复的层次结构。JSON 适用于 prompt 之外的通用兼容性。用哪个正确取决于你数据的形状,而不是哪个格式的文档主页吹得更大声。
Schema 定义是完全独立的问题
这是我最初搞错的部分。我以为"如何高效发送数据"和"如何告诉模型发回什么"是同一个问题,有同一个答案。
数据格式掌管你发送给模型的内容。Schema 掌管你要求模型发回的内容。不同的问题,不同的答案。
对于后端验证,如 API 契约或数据库约束,冗长的 schema 格式确实是正确的工具。它详尽地写出 "type": "object", "properties": {...},而这种冗长正是它的全部意义。它是明确的,代码可以自动检查它。
但在系统提示词内部,同样的冗长就是死重量。每一个花在 schema 样板上的 token 都是一个没有花在实际指令上的 token。
我最终找到的解决方案:用更接近类型定义的方式来描述输出形状。
interface Item {
id: string;
name: string;
status: "active" | "inactive";
}
同样的结构保证。少得多的 token。LLM 在训练过程中见过大量这种语法,所以它们能可靠地遵循它,而不需要把 schema 写得很冗长。
我一直使用的经验法则:用于验证代码的冗长 schema 格式。用于生成提示词的轻量级类型风格定义。
好吧,但这些真的有效吗?
此时我有了一个可行的理论:在进入提示词之前切换格式,其他地方保持 JSON,在没有真正缺点的情况下节省大量 token。
我不想只是信任这个理论。关于 token 节省的说法到处都是,而且大多数来自销售这种格式的人。所以我进行了实际的比较,而不是听任何人的说法。
一个通用记录的扁平数据集,通过 JSON、表格格式和嵌套结构格式运行
一个模型,通过标准路由设置调用
每种格式的固定问题集,涵盖字段检索、聚合、过滤、结构感知,以及一个专门设计的完整性检查,多次运行
对照直接从数据计算的真实结果进行评分,而不是由另一个 LLM 评分
需要提前说明:这是一个模型、一个数据集、一次运行。我把下面的内容当作方向性信号,而不是对任何格式的定论。
我实际发现的内容
Token 和字节节省。两种替代格式的结果几乎完全相同,都将 token 使用量削减到 JSON 的约 45%,节省了约 55%。两种格式之一的文档声称比另一种有显著优势。这种优势在我的运行中没有出现。它们在 token 和字节两方面都只差一个百分点。
有一件事让我保持诚实:仅 gzip 压缩的 JSON alone 就实现了比两种格式都更大的字节节省。模型无法读取 gzip 压缩的文本,所以这不是一个真正竞争的选项,但它是很有用的参考。这意味着两种格式节省的很大一部分就是"移除 JSON 的标点和重复的键",而不是某种与 LLM 特别处理文本方式相关的东西。Token 级别的优势仍然是真实的。只是不完全是魔法。
准确性。这是我预期更精简的格式会有优势的地方,因为理论上它们给模型的是更干净的信号、更少的噪声需要解析。但这基本上没有发生。所有三种格式的准确性都很接近,没有一个明显的赢家。其中一种替代格式实际上略微落后于纯 JSON。
这比它最初看起来更重要。盲目削减 token 如果伴随着准确性损失就不是什么好事,这次运行没有排除这种可能性。
无论什么格式,有一种模式都会出现:要求模型在整个数据集上计数或过滤的问题得分都很差。模型似乎在列表足够长后进行估算而不是穷举计数,没有数据格式能解决这个问题。这是模型对长列表推理的限制,不是压缩能掩盖的东西。
延迟。响应时间在格式之间有一些变化,但从单次运行来看,没有一致的方向值得深究。
真正让我惊讶的部分
我进去时期望 token 节省数字成为头条。结果不是。
我故意通过删除最后几行来截断每个 payload,而不触碰声明的行数,然后问模型数据是否看起来完整。
每种格式都让模型说"这看起来不完整"。每一次。从简单的通过/失败来看,那是三方平局。
但模型为什么这么说,完全取决于格式。
对于两种替代格式,表头仍然声明原始行数,而实际存在的行数更少。模型可以指出那个精确的不匹配作为证据。一个真实的、可检查的事实,而不是猜测。
对于 JSON,没有任何计数字段可以检查。模型仍然说"看起来不完整",但这个答案背后的推理读起来更像是关于典型数据集大小的一种模糊直觉。一种听起来合理但实际上是答案打扮的猜测,而不是实际的检查。
如果我只看了原始的通过/失败数字,我会完全错过这个。所有三种格式得分相同。只有在区分"说是不完整的"和"说是不完整的并引用了实际的不匹配作为证据"之后,真正的差距才显现出来。两种替代格式在那个更严格的标准下都达到了完美分数。JSON 是零。
对我来说,这是整个调查中最有用的发现,而且它几乎与 token 计数无关。它是关于给模型一些结构性的东西来真正检查其工作,在任何 payload 可能在某处上游被静默截断的 pipeline 中。
如果你是用 LLM 构建的,这意味着什么
我从中得出的几点。
格式问题不是"哪个最好"。而是"我的数据是什么形状"。扁平且表格化指向一个方向。深度嵌套且重复指向另一个方向。这在 token 计数进入对话之前就决定了格式。
更少的 token 不自动意味着更好的答案。在这个运行中,一种替代格式在准确性上略微领先 JSON。另一种略微落后。
在真正推出这些之前,专门测试截断情况,而不只是测试 token 计数。"听起来正确"和"实际上是建立在可检查的东西上"之间的差距只有在我直接检查时才会显现。表面通过率在每种格式上看起来都一样。
JSON 不会消失,也不应该。它仍然是 API、数据库和配置文件的标准。这一切都是关于一个特定的时刻,就在 payload 进入提示词之前,而且只在那个时刻。
还有一个值得一提的小胜利:对于 schema 定义,在提示词内从冗长的验证风格 schema 转向轻量级类型风格定义几乎是无痛的。同样的保证,少得多的 token。
我真正学到的
我开始这件事是因为一个重复的字段名,它让我比应该的更烦恼。我最终得到的是一个真正不同的心理模型,关于如何思考向 LLM 发送数据。
token 节省是真实的,大约一半,并且在实际测试中得到了验证,而不是只是文档页面上的一个声明。那部分证实了我进去时的猜测。
我没有预料到的是,更有趣的问题原来是完全不同的东西:不是"这节省了多少 token",而是"这是否给了模型一些具体的东西来检查自己"。这个区别在大多数 token 优化的营销中几乎没有出现,而这正是我一直在回想的。
如果这篇文章有什么值得记住的:小 payload 是好的。模型能够实际验证的 payload 更好。这两者不自动是同一件事,而我花了一次真正的基准测试才看到其中的区别。
