TL;DR: 当你的主要消费者是LLM时,XML和JSON因在每个元素中重复结构而浪费token。一个紧凑的定位格式能够将使用量减少50%。结果发现这种想法早已有了名字:TOON(面向Token的对象标记法)。同样的选择压力——有限的token和重复的键值——导向了同样的解决方案。
Anthropic几乎所有东西都用XML。他们的_system prompts_被包裹在<instructions>标签中,示例在<example>标签中,工具列在<function>标签中。如果你在用Claude,就会发现自己到处都是标签。
于是当我让Claude为一个专为LLM设计的CLI输出格式时,显而易见的诱惑是:XML。既然模型以XML形式接收信息,CLI不给它返回XML难道不是更顺理成章吗?
但实际上,这并非最佳选择。
问题:强迫症般的重复
想象一下,你的CLI正在列出一个项目跟踪器中的任务。假设你有50个任务。在XML中,每个任务会是这样的:
<issue>
<id>PROD-587</id>
<state>Backlog</state>
<labels>backend</labels>
<title>从NAS备份中导入会话</title>
<age_days>14</age_days>
</issue>
---
看起来很美观,也很自描述。每个字段都有名字,一个XML解析器可以准确识别出每个字段的含义。
现在,把它乘以50个任务。`<issue>`、`<id>`、`<state>`、`<labels>`、`<title>`、`<age_days>`这些标签将被**重复50次**。它们没有提供任何新的信息,仅仅在浪费空间。大约每个任务需要70个token,列出50个任务需要约3,500个token。
JSON稍微好一点(去掉了闭合标签),但仍重复键值:
```json
{
"id": "PROD-587",
"state": "Backlog",
"labels": ["backend"],
"title": "从NAS备份中导入会话",
"age_days": 14
}
每行都重复"id":、"state":等键值。每个任务约需50个token,总计2,500个token。
如果我们去掉这些重复会怎样?
PROD-587 [Backlog] backend — 从NAS备份中导入会话 (14d)
25个token。没有键值、没有标签、没有大括号或引号。50个任务只需1,250个token。
请注意这一点:相比JSON减少了一半,比XML减少了三分之二。目的却完全相同。
“但LLM需要结构”
这时很多人会怀疑:“那LLM怎么知道每个字段代表什么呢?”
这是一个合理的问题,而我的回答是:就像你知道的一样。
看看这行内容:
PROD-587 [Backlog] backend — 从NAS备份中导入会话 (14d)
你需要别人告诉你PROD-587是ID吗?需要解释[Backlog]是状态吗?需要说明长横杠后的内容是标题吗?不需要。你通过位置和视觉格式就可以推断出来。
LLM做的事情完全一样。它们是一种识别文本模式的机器。一个一致的、定位明确的格式——ID放在第一、状态放在括号里、标签列在标题之前——对它们而言是直观的。不需要<state>Backlog</state>这种标签说明“Backlog”是个状态。
关键在于区分两种看似相似但完全不同的操作:
阅读是LLM的工作。它有上下文,理解语义,可以推断结构。就像一个人在浏览报告时不需要每个词都用标签标注——位置和惯例已经足够。
解析是程序的工作。它没有上下文,不理解语义,需要显式的分隔符来提取字段。jq '.state'需要"state"这样的键值因为它不知道什么是状态。
XML和JSON是为“解析”而设计的。它们是机器之间交流的格式,它们不“理解”内容。而LLM是“阅读”的。显式结构对它们来说是冗余的,这种冗余却消耗了额外的token。
格式选择:什么时候用什么
我并不是说XML和JSON不好。但它们并不适合这一情境。简单来说,用表格展示:
| 格式 | 每个任务的token数 | 自描述性 | 最适合 |
|---|---|---|---|
| XML | ~70 | 完全 | SOAP API、配置文件、有schema的文档 |
| JSON | ~50 | 完全 | REST API、服务间数据交换 |
| JSONL | ~50 | 完全 | 脚本、jq工具、数据流水线 |
| 定位式格式 | ~25 | 否 | LLM、人类、紧凑信息面板 |
规则简单:如果消费者能够理解上下文(人类、LLM),就可以省去显式结构。如果消费者无法理解上下文(脚本、解析器),才需要明确的键值。
所以我的CLI有两种模式:默认使用紧凑的定位格式(适用于LLM和终端中查看的用户),另提供--json模式,便于jq或脚本做进一步处理。
转折:被TOON抢先了
接下来是好戏。
在我改用Claude提交的格式几周后,有人问我:“你看过TOON吗?”
TOON——Token-Oriented Object Notation,是一种于2025年11月推出的格式。它的核心理念是:一种采用JSON数据模型,但专门为LLM优化的紧凑文本编码格式,用于减少token成本。
它看起来是这样的:
issues[3]{id,state,labels,title,age_days}:
PROD-587,Backlog,backend,从NAS备份中导入会话,14
PROD-612,Todo,tokamak,修复认证令牌刷新,2
PROD-501,Done,frontend,迁移数据库至新架构,30
请注意。包含字段信息的头部形式({id,state,labels,title,age_days})及数据数量标记([3])。然后是一行行按顺序排列的逗号分隔数据。没有重复的键值,没有标签,不需要为字符串加引号。
这几乎和Claude为我的CLI提议的方式一模一样——只不过TOON用显式的头部描述schema,而我的CLI直接用视觉惯例(比如状态用括号,标题以长横杠分隔)。
趋同进化
在生物学中有个很有趣的概念:趋同进化。没有亲缘关系的物种,由于面对同样的选择压力,进化出类似的特征,例如章鱼和人眼睛的结构相似,但它们完全独立演化。相同的压力——生存需要视力——带来了相同的解决方案。
TOON和我的CLI格式就是软件设计中的趋同进化。选择压力是一样的:token稀缺、键值重复、消费者能够通过位置理解数据。趋同的解决方案就是:消除重复,利用顺序编码。
TOON与我的CLI格式的区别在于头部信息。而这一点很重要。
如果没有头部,那么只有当LLM已经了解每个字段的语义时(例如通过system prompt或模式显而易见),格式才有意义。对于初次接触输出的用户,可能不知如何解析。而TOON解决了这个问题:头部一次性描述了所有字段,后续数据不再需要重复。
这就是隐式协议和显式协议的区别。而在工程领域,显式协议通常更占上风。
是否该迁移到TOON?
我曾认真想这个问题。最终答案是:要看你的数据消费者是谁。
如果你的CLI输出总是给同一个代理,并且有固定的CLAUDE.md文档描述格式,那么定位格式能很好地工作。它更精简(省去头部信息),上下文也早已预设清楚。
如果你的CLI输出有不同代理消费,或者提供给不了解上下文的人,TOON会更好。一条额外的头部信息换得了自描述能力,这点投资完全值得。
就我目前的情况来看,消费者始终是同一个agent,相关格式在CLAUDE.md中有详尽描述。所以我打算继续使用我的紧凑型定位格式。但是,如果我要让CLI公开发布,面向不特定用户,我会毫不犹豫地迁移到TOON。
所以,我学到了什么
总结三点:
1. 不要为解析器设计,如果你的消费者不是解析器。 XML和JSON非常适合需要明确键值的机器间通信。但LLMs不是这种机器,应该面向它们的阅读模式设计,而非传统解析逻辑。
2. 重复是敌人。 在50个元素的列表中,重复50次的键值是无意义的“垃圾”。就像在购物清单上每个名字前都写“名称:”,读到第二个后你的大脑就会忽略这些重复——但它们仍然占用空间。
3. 如果你的解决方案与现存的某种形式一致,说明你的方向很可能是对的。 我不是说我们应该总是重造轮子。只是说如果你的设计某种程度上自然地表现出同样的形式,这是个不错的信号。
所以下次设计LLM消费工具的输出时,在一窝蜂扑向serde_json::to_string()之前,请先问自己一个问题:我的消费者是需要解析它,还是仅仅阅读它?
如果答案是“阅读”,那么每个重复的键值都是浪费token——而token,就像酒吧里的钱,一不留神就花光了。