把 JSON 换成 TOON:在与 LLM 交换结构化数据时省下 30–50% token

把 JSON 换成 TOON:在与大模型交换结构化数据时节省 30–50% token,更贴近 LLM 的“表格式”理解方式,并让推理更稳定。本文带你理解 TOON 的语法、适用边界与工程落地。

阅读时长: 6 分钟
共 2980字
作者: eimoon.com

我发现很多人在给 LLM 喂 JSON 时都栽了跟头。太贵。太慢。我遇到的典型场景是,要把一大坨对象数组塞进提示里,键名重复、标点满天飞,token 数量飙升得让人肉疼。因为 JSON 的一堆引号、逗号、花括号和重复键名都会被分词成大量 token,而在对话式模型里 token 就是钱、就是速度、也是上下文窗口的生命线,这个开销往往比你想象的大得多。我换成 TOON 之后,常规数据能省 30–50% token,模型读起来更像看表,这才是关键。

为什么我开始用 TOON

TOON,全称 Token-Oriented Object Notation,本质是为模型看数据而生的结构化表示。它把标点减到最低,把重复键名折叠成“表头”,让模型像扫表一样扫列。更干净。我在批处理、RAG 候选聚合、Agent 传参与函数调用的批量输入里反复验证过,结构噪音会吞掉大量宝贵的上下文窗口,而 TOON 用列式头+多行数据的写法把噪音剥离掉,让同样的信息以更短的 token 进入模型、以更稳定的结构从模型出来,同时它还能和 JSON 双向往返、支持嵌套并已有 JS/TS 与 Python 的实现。我不打算在服务间通信里替换 JSON,定位不同。互补就好。

TOON 语法一眼过

目标很明确:在和 LLM 交换结构化数据时,把 token 压低但不丢结构。直觉也简单:少引号、少括号、少重复键,把对象数组写成“表头+行”的形状。很直接。在相对规则的对象列表上,我测到 30–50% 的节省幅度,当然实际节省和字段数量、嵌套深度、值分布强相关,越规整越省得多。

先看一个对比。

JSON:

{
  "users": [
    { "id": 1, "name": "Alice", "role": "admin" },
    { "id": 2, "name": "Bob", "role": "user" }
  ]
}

TOON:

users[2]{id,name,role}:
  1,Alice,admin
  2,Bob,user

我一般这样读它:声明行 users[2]{id,name,role}: 说清楚了有两行数据和三列字段,下面每行就是一条记录,没有引号也没有冒号,信息密度高且语义不变。更省心。这不是“更简版的 JSON”,而是把噪音剔掉、让模型专注于列的序列化方式,尤其在让模型复述或按列推理时会更稳定。

为了对照,我把几类高频 JSON 都写成了 TOON,你一眼就能对上。看例子。后面会用到这些模式,尤其是对象数组和嵌套。

  1. 简单对象

JSON:

{ "name": "Alice", "age": 30, "city": "Bengaluru" }

TOON:

name: Alice
age: 30
city: Bengaluru
  1. 值数组

JSON:

{ "colors": ["red", "green", "blue"] }

TOON:

colors[3]: red,green,blue
  1. 对象数组

JSON:

{
  "users": [
    { "id": 1, "name": "Alice" },
    { "id": 2, "name": "Bob" }
  ]
}

TOON:

users[2]{id,name}:
  1,Alice
  2,Bob
  1. 嵌套对象

JSON:

{
  "user": {
    "id": 1,
    "name": "Alice",
    "profile": { "age": 30, "city": "Bengaluru" }
  }
}

TOON:

user:
  id: 1
  name: Alice
  profile:
    age: 30
    city: Bengaluru
  1. 带嵌套字段的对象数组

JSON:

{
  "teams": [
    {
      "name": "Team Alpha",
      "members": [
        { "id": 1, "name": "Alice" },
        { "id": 2, "name": "Bob" }
      ]
    }
  ]
}

TOON:

teams[1]:
  - name: Team Alpha
    members[2]{id,name}:
      1,Alice
      2,Bob

这些例子里,缩进表达嵌套,风格有点像 YAML,但 TOON 更紧凑,也强调 JSON 的可往返。列头+数据行的写法,和 LLM 的“按列扫表”习惯非常契合。就这味儿。要点是你可以嵌套使用声明语法,比如 members[2]{id,name}: 跟在对象字段后,编码器会正确处理并在解码时还原成数组对象,不需要你手搓任何转义字符。

在工程里落地:JS/TS 和 Python

TOON 不是为了手写。别手写。工程里我一律用官方库做编码和解码,这样转义、类型、空值这些细节都由库兜底,接口更稳也更可维护,而且 encode/decode 的往返才有保障。

有官方 NPM 包可用:

npm install @toon-format/toon
# 或 yarn add / pnpm add

在 JavaScript/TypeScript 里,encode 会把普通对象或 JSON 生成 TOON 字符串,格式就是上面看到的“声明+数据行”。很顺手。下面是一个完整例子,包含一个用户数组。

JSON -> TOON:

import { encode } from "@toon-format/toon";

const data = {
  users: [
    { id: 1, name: "Alice", role: "admin" },
    { id: 2, name: "Bob", role: "user" },
  ],
};

const toonString = encode(data);
console.log(toonString);
/*
users[2]{id,name,role}:
  1,Alice,admin
  2,Bob,user
*/

反过来,decode 可以把 TOON 字符串还原为普通对象结构,你可以像处理 JSON 一样继续下游逻辑。够用。我在线上也用它接稳了不少模型输出。

TOON -> JSON:

import { decode } from "@toon-format/toon";

const toonString = `
users[2]{id,name,role}:
  1,Alice,admin
  2,Bob,user
`;

const jsonObject = decode(toonString);
console.log(jsonObject);
/*
{
  "users": [
    { "id": 1, "name": "Alice", "role": "admin" },
    { "id": 2, "name": "Bob", "role": "user" }
  ]
}
*/

Python 也有可用实现,安装包名是 python-toon,API 形式和 JS 版本对应。简单直接。下面是最小可用的编码与解码示例。

安装:

pip install python-toon

编码:

from toon import encode

channel = {"name": "tapaScript", "age": 2, "type": "education"}
toon_output = encode(channel)
print(toon_output)
# name: tapaScript
# age: 2
# type: education

解码:

from toon import decode

toon_string = """
name: tapaScript
age: 2
type: education
"""

python_struct = decode(toon_string)
print(python_struct)
# {"name": "tapaScript", "age": 2, "type": "education"}

我的实践建议很简单:不要自己拼字符串,统一走 encode/decode;传给 LLM 前先 encode,拿到结果再 decode 回来;遇到大字段时记得提醒自己,TOON 省的是结构,不是内容。记住这点。这样团队协作时每个人都能用相同的约定读写数据,排查问题也更容易。

把 TOON 串进 LLM:提示、输出与流式

压缩提示上下文是我用 TOON 的第一目的。数据库或服务拉回来的对象数组,先转 JSON,再 encode 成 TOON,最后丢进模型上下文,RAG 的候选集、Agent 的记忆与工具参数、函数调用的批量输入都能这么干。就是省。这个套路在上下文窗口吃紧时尤其稳,因为你用更少的 token 承载了同样的信息密度,还给模型更清晰的表头提示。

我也常让模型直接输出 TOON。给出固定的列头,模型更容易对齐字段,而且输出更短,后端一把 decode 就能继续走业务流。很稳。提示片段像这样,你可以按需替换字段集合。

请仅以 TOON 返回结果,字段为 {id,name,role},不要包含额外说明。
users[]{id,name,role}:

在流式和无服务器场景里,这种节流尤其有用。延迟更低、吞吐更好,账单压力也小一点。别小看这点。在大规模并发或函数级计费的场景里,少 30% 的 token 经常就是能不能开二倍并发的分水岭,这件事在跑周更作业和长链路代理系统时都让我肉眼可见地松了口气。

边界、差异与踩坑

我也会克制使用 TOON。深度嵌套、多层引用、形状经常变化的对象,我宁愿保留 JSON 的显式边界和成熟的 Schema 校验;非 AI 的服务内通信、对外 API、需要 JSON Schema/OpenAPI 的生态集成,一律继续用 JSON。别逞强。这些地方 TOON 的列式优势变小,维护成本反而会上升,而且当一堆字段缺省或顺序漂移时,你还得追加约定去兜底,这就划不来了。

如果要和 JSON、YAML、CSV 做个直觉对照,我是这么想的:JSON 通用且健壮,但在 LLM 里标点和重复键名的开销高;YAML 人写友好,却容易歧义和解析不一致,不适合作为模型输出的硬格式;CSV 极致紧凑,却只适合扁平表格,遇到嵌套就束手无策。很清楚。TOON 处在中间地带,既能用列式拿到紧凑度,又能表示嵌套并支持往返,目标就是为 LLM 的读写做优化。

踩坑清单我踩过几遍。字段缺省怎么办、是空值还是报错,要在编码和解码侧保持一致;数值、布尔、null 与字符串的区分交给库,不要手写转义,逗号、冒号、换行这些细节自己拼必翻车;金额等高精度数值仍按业务约束用字符串或定点数处理,不跟序列化纠缠;大文本字段放进 TOON 仍然照样占 token,省的是结构不是内容,这点别误会;嵌套很深或结构经常变形的场景直接留在 JSON;安全与校验还是你自己的事,要做长度限制、内容过滤和鉴权。都要记住。为了工程落地,我一般把策略定成:外部/业务 API 用 JSON,进入模型前转 TOON,出模型后再转回。

如果问 TOON 的根本价值,我给三个动机:把结构噪音拿掉,让模型盯着“列”和“行”;在同等信息下减少 token,从而兼顾成本与延迟;把输出定成固定列头,解码就更稳定,流水线编排更省心。够直接。把这三件事做到位,你就会得到一个能稳定往返、易于测试、可扩展的结构交换层,它不取代 JSON,却在 AI 工作流里让 JSON 不再背锅,且在多人协作与自动化评测中显著降低失败率。

未来呢?我看到三个方向:更紧凑的结构化微调数据、在多代理/工作流引擎/MCP 里的低开销传递、在 Serverless AI API 里的成本与吞吐优化。还在早期。我也没完全搞懂模型是不是天然更喜欢列式表示,但从现在的效果看,把 JSON 换成 TOON 起码是一条立竿见影的工程化路径,尤其当你已被 token 花费与窗口限制逼到墙角时它会显得格外顺手。

就这些。还有更好的方案吗?

关于

关注我获取更多资讯

公众号
📢 公众号
个人号
💬 个人号
使用 Hugo 构建
主题 StackJimmy 设计