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,就像酒吧里的钱,一不留神就花光了。