为什么我的CLI输出不是XML(以及我如何无意中重塑了TOON)

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),就可以省去显式结构。如果消费者无法理解上下文(脚本、解析器),才需要明确的键值。 ...

2026年3月26日 · Fernando