Java 中的 TOON 格式簡介
1. 概述
JSON 是系統間交換結構化資料的標準格式。但是,當我們向大型語言模型 (LLM) 發送 JSON 資料時,相當一部分令牌預算都用於語法,包括花括號、方括號、引號和重複的鍵名,而不是實際資料。
TOON (以標記為導向的物件表示法)是一種緊湊、易於閱讀的格式,它使用較少的標記來編碼與 JSON 相同的資料模型。它用縮排取代了 JSON 中大量的標點符號語法,並且對於統一集合,採用表格佈局,字段名稱只需聲明一次,值則逐行顯示。
在本教程中,我們將調查可用的 Java TOON 庫,使用json-io提供 TOON 序列化和反序列化的工作範例,比較不同格式之間的標記計數,並討論 TOON 在哪些方面最有價值。
2. 什麼是 TOON?
TOON 對與 JSON 相同的基本類型、物件和陣列進行編碼。差別在於語法:
- 物件使用
key: value並用換行符和縮排分隔,而不是使用大括號。 - 陣列使用
[N]:長度前綴列表,而不是方括號。 - 表格陣列在標題行中聲明一次欄位名,然後在類似 CSV 的行中列出值。
- 引用的使用非常少——字串只有在包含結構字元時才需要引用。
以下是相同資料的 JSON 和 TOON 格式:
JSON:
[
{"name": "Alice", "age": 28, "department": "Engineering"},
{"name": "Bob", "age": 34, "department": "Marketing"},
{"name": "Charlie", "age": 22, "department": "Sales"}
]
卡通(表):
[3]{name,age,department}:
Alice,28,Engineering
Bob,34,Marketing
Charlie,22,Sales
表格格式在表頭中列出name 、 age和department各一次。後續每一行僅包含對應的值。隨著行數的增加,由於 JSON 會在每個物件中重複使用相同的鍵名,因此可以節省更多空間。
對於單一對象,TOON 使用類似 YAML 的鍵值佈局:
name: Alice
age: 28
department: Engineering
完整的規格位於toonformat.dev 。
3. TOON 的 Java 函式庫
TOON 生態系包含超過 25 種語言的實作。目前,Java 平台有兩個可用的程式庫:
| 圖書館 | Maven座標 | Java 版本 | 地位 | 筆記 |
|---|---|---|---|---|
| JToon | dev.toonformat:jtoon:1.0.8 |
Java 17+ | Beta | 官方社群實現。支持 Jackson 註解。注重規範相容性。 |
| json-io | com.cedarsoftware:json-io:4.98.0 |
Java 8+ | 穩定的 | 成熟的 JSON 函式庫,支援 TOON 讀/寫。支持 Jackson 註解。只有一個運行時相依性( java-util )。 |
這兩個庫都支援使用表格數組進行 TOON 編碼和解碼。 JToon 是官方的 Java 社群實現,專為 TOON 而構建,並整合了 Jackson。 json json-io是一個成熟的 JSON 序列化函式庫,除了 JSON 之外,還加入了 TOON 作為額外的輸出格式。
我們可以在 Maven Central 上找到最新版本的json-io 。本教程中的範例將使用json-io ,因為它支援 Java 8 到 24,從而使盡可能多的專案都能遵循本教程:
<dependency>
<groupId>com.cedarsoftware</groupId>
<artifactId>json-io</artifactId>
<version>4.98.0</version>
</dependency>
4. 將 Java 物件寫入 TOON
4.1. 單一對象
我們可以使用JsonIo.toToon()將任何 Java 物件轉換為 TOON 物件:
Person person = new Person("Alice", 28, "Engineering");
String toon = JsonIo.toToon(person, null);
這將產生:
name: Alice
age: 28
department: Engineering
第二個參數接受一個WriteOptions實例來控制輸出。傳遞null將使用預設值。
4.2. 集合:表格格式
表格格式是 TOON 的最大優勢所在。當我們序列化一個共享相同欄位的物件清單時, json-io會自動使用緊湊的每行每個物件的佈局:
List<Employee> employees = Arrays.asList(
new Employee("Alice Johnson", 28, "Engineering", 95000),
new Employee("Bob Smith", 34, "Marketing", 78000),
new Employee("Charlie Brown", 22, "Engineering", 72000)
);
String toon = JsonIo.toToon(employees, null);
輸出:
[3]{name,age,department,salary}:
Alice Johnson,28,Engineering,95000
Bob Smith,34,Marketing,78000
Charlie Brown,22,Engineering,72000
[3]表示數組長度。 {name,age,department,salary}列標題定義了各列的名稱。每行僅包含以逗號分隔的值。鍵名name 、 age 、 department和salary只出現一次,而不是三次。
如果我們喜歡展開的清單格式(每行一個鍵值對),我們可以使用建構器模式啟用prettyPrint :
WriteOptions options = new WriteOptionsBuilder().prettyPrint(true).build();
String toon = JsonIo.toToon(employees, options);
這將產生詳細形式:
[3]:
- name: Alice Johnson
age: 28
department: Engineering
salary: 95000
- name: Bob Smith
age: 34
department: Marketing
salary: 78000
- name: Charlie Brown
age: 22
department: Engineering
salary: 72000
預設的表格佈局是 LLM 互動的首選,因為它能最大限度地減少令牌數量。展開的prettyPrint表單會在每個物件上重複每個鍵名,因此它使用的令牌數量與 JSON 大致相同,節省的令牌數量也就消失了。
4.3. 嵌套結構
TOON 透過縮排自然地處理嵌套。這裡,一家公司包含儲存在LinkedHashMap中的部門,每個部門又包含一個成員清單:
Map<String, Object> eng = new LinkedHashMap<>();
eng.put("name", "Engineering");
eng.put("members", Arrays.asList(
new Person("Alice", 28, "Engineering"),
new Person("Bob", 34, "Engineering"),
new Person("Charlie", 22, "Engineering")));
Map<String, Object> company = new LinkedHashMap<>();
company.put("name", "Acme Corp");
company.put("founded", 2010);
company.put("departments", Arrays.asList(eng, marketing, sales)); // marketing and sales built similarly
String toon = JsonIo.toToon(company, null);
輸出:
name: Acme Corp
founded: 2010
departments[3]:
- name: Engineering
members[3]{name,age,department}:
Alice,28,Engineering
Bob,34,Engineering
Charlie,22,Engineering
- name: Marketing
members[3]{name,age,department}:
Eve,29,Marketing
Frank,45,Marketing
Grace,27,Marketing
- name: Sales
members[3]{name,age,department}:
Hank,38,Sales
Iris,33,Sales
Jack,41,Sales
請注意,內部members陣列會自動使用表格格式。外部departments數組使用展開格式,因為每個部門都包含嵌套的結構化數據,而內部members數組由於其字段均為標量值,因此符合表格格式的要求。
json-io在文件的每個層級都應用了這種優化:任何元素僅包含標量字段的數組都會自動以表格格式呈現,無論嵌套深度如何。在本例中,三個members陣列各自擁有獨立的列標題和緊湊的行。這種在單一文件中混合使用鍵值對和表格行的方式是 CSV 無法實現的。
5. 閱讀 TOON 返回 Java
解析 TOON 的過程與編寫 TOON 的過程類似。我們可以將其反序列化為類型化的 Java 物件:
String toon = "name: Alice\nage: 28\ndepartment: Engineering";
Person person = JsonIo.fromToon(toon, null).asClass(Person.class);
assertEquals("Alice", person.getName());
assertEquals(28, person.getAge());
assertEquals("Engineering", person.getDepartment());
對於通用集合類型,我們使用TypeHolder來解決類型擦除問題:
String toon = "[2]{name,age,department}:\n Alice,28,Engineering\n Bob,34,Marketing";
List<Person> people = JsonIo.fromToon(toon, null)
.asType(new TypeHolder<List<Person>>(){});
assertEquals(2, people.size());
assertEquals("Alice", people.get(0).getName());
assertEquals("Engineering", people.get(0).getDepartment());
當沒有可用的 Java 類別時(例如在中間件或日誌處理中),我們可以解析為 Map:
Map<String, Object> map = JsonIo.fromToonToMaps(toon).asClass(Map.class);
6. 代幣效率:衡量差異
TOON 的目標是在向語言學習模型 (LLM) 發送結構化資料時減少詞元消耗。我們不比較字元數(因為不同格式的語法密度存在根本差異,比較字元數會產生誤導),而是使用與 GPT-5 相同的詞元器來測量實際的位元組對編碼 (BPE) 詞元數。
我們使用jtokkit ,它是 OpenAI 分詞器的 Java 實現,編碼方式為o200k_base :
EncodingRegistry registry = Encodings.newDefaultEncodingRegistry();
Encoding encoding = registry.getEncoding(EncodingType.O200K_BASE);
int jsonTokens = encoding.countTokens(jsonString);
int toonTokens = encoding.countTokens(toonString);
以下是針對不同資料集大小,序列化Employee物件(姓名、年齡、部門、薪水)統一清單的結果:
| 數據集 | JSON 令牌 | 卡通代幣 | 儲蓄 |
|---|---|---|---|
| 3 件 | 46 | 35 | 24% |
| 10件 | 206 | 121 | 41% |
| 20件 | 412 | 231 | 44% |
| 25 項(產品,5 個欄位) | 680 | 459 | 33% |
隨著集合規模的增大,節省的令牌數量也會增加,因為 JSON 會在每個物件中重複使用每個鍵名,而 TOON 只聲明一次。對於包含 20 個條目和 4 個欄位的集合,我們可以節省 44% 的令牌。
對於單一對象,由於不存在需要消除的關鍵重複項,因此節省的空間並不多(約 5%)。對於包含小型嵌入式陣列的巢狀結構,節省的空間也不多(約 3-5%),但內部陣列仍可從表格格式中受益。
6.1. TOON 與 CSV 相比有何不同?
問得好:如果我們發送的是表格數據,為什麼不直接使用 CSV 檔案呢?
對於純粹的平面數據,CSV 使用的標記數略少於 TOON:在我們的基準測試中,CSV 大約少 6%。額外的開銷來自於 TOON 的[N]{fields}:標題以及每行兩個空格的縮排。
但 CSV 無法表示嵌套資料。當我們嘗試將公司及其部門的結構編碼為 CSV 時,我們必須進行反規範化處理,在每一行中重複“Acme Corp”和“2010”,這比 TOON 和 JSON 都使用了more標記。
| 格式 | 平面資料(20 行) | 嵌套數據 |
|---|---|---|
| JSON | 413 個代幣 | 150個代幣 |
| 卡通 | 231 個代幣 | 141 個代幣 |
| CSV | 217 個代幣 | 167 個字元(非規範化) |
TOON 佔據了一個實用的中間位置:對於平面數據,其標記效率與 CSV 的 6% 以內,但它也能處理現實世界 API 產生的嵌套和混合結構。
在代理程式和工具呼叫工作流程中,結構化資料通常佔輸入令牌的大部分。將這部分資料減少 30-40% 意味著更低的 API 成本(提供者按令牌收費)和更有效率的上下文窗口,從而在不截斷資料的情況下,將更多資料放入相同的 128K 或 200K 窗口中。使用json-io Java 應用程式無需任何基礎架構變更即可從JsonIo.toJson()切換到JsonIo.toToon() 。
7. Spring AI 集成
對於使用 Spring AI 的項目, json-io提供了一個單獨的啟動器,可以在工具呼叫結果到達 LLM 之前自動將其轉換為 TOON 格式。我們只需新增一個依賴項:
<dependency>
<groupId>com.cedarsoftware</groupId>
<artifactId>json-io-spring-ai-toon</artifactId>
<version>4.98.0</version>
</dependency>
然後我們使用ToonToolCallResultConverter來註解任何工具方法:
@Tool(description = "Look up employees by department",
resultConverter = ToonToolCallResultConverter.class)
List<Employee> findByDepartment(String department) {
return employeeRepository.findByDepartment(department);
}
當 Spring AI 呼叫此工具並將結果傳回 LLM 時,轉換器會將員工清單序列化為 TOON 的表格格式,也就是我們在 4.2 節中看到的緊湊佈局。 LLM 接收的令牌較少,而工具的業務邏輯則沒有任何變更。
8. 何時使用卡通
TOON 並非 JSON 的通用替代品。以下是它的適用場景:
在以下情況下使用 TOON:
- 向LLM發送結構化資料(工具結果、RAG有效載荷、資料分析上下文)
- 處理同質物件集合(表格格式的優點所在)
- 令牌成本或上下文視窗壓力是一個需要考慮的問題。
- 資料結構兼具扁平化和嵌套化兩種類型。
以下情況請堅持使用 JSON:
- 在服務(REST API、訊息佇列)之間進行通訊時,每個消費者都期望使用 JSON 格式。
- 數據消費者並非法學碩士。
- 需要進行模式驗證或使用 JSON Schema 工具。
- 資料完全是扁平的,沒有嵌套結構。 CSV 格式可能效率更高。
TOON 使用與 JSON 相同的資料模型進行編碼,而json-io可以將任何物件圖寫入這兩種格式。 JSON 的優勢在於其生態系統:幾乎所有語言、框架和工具都原生支援 JSON,而像 JSON Schema 這樣的標準提供了 TOON 目前尚不具備的驗證基礎設施。在 REST API、訊息佇列和設定檔等需要這種生態系統的地方,JSON 是自然之選。而在令牌效率至關重要的地方,例如 LLM 工具結果、RAG 有效負載和代理工作流程,TOON 可以在不損失表達能力的前提下,大幅節省資源。
9. 結論
本文研究了目前可用的 Java TOON 函式庫:JToon 和json-io ,並使用json-io將 Java 物件序列化為 TOON 格式,然後使用 OpenAI 的 BPE 分詞器比較 TOON 和 JSON 的詞元數量節省情況,最後將 TOON 整合到 Spring AI 工具中呼叫。對於統一類型的物件集合,TOON 的表格格式比純 JSON 格式減少了 30% 到 44% 的詞元。它在處理扁平資料時效率接近 CSV,同時還能處理 CSV 無法表示的嵌套結構。
本文中的完整程式碼範例可在 GitHub 上找到。