跳到主要内容

使用 Toon 格式节省 Token 消耗

摘要

本文介绍了如何使用 Toon 格式来减少与大模型交互时的 Token 消耗。

完整的代码在 GitHub JavaAIDev/toon-spring-ai.

在 AI 应用的开发中,大模型的输入和输出 token 数量一直是需要关注的问题。现在的模型服务基本上都按照输入和输出 token 的数量来收费,应用使用的 token 数量直接决定了运营的成本。虽然现在模型服务的 token 价格在快速下降中,但是只要应用还在运行中,token 所带来的成本就一直存在。如果输入的 token 数量过大,不仅可能遇到模型的上下文窗口的上限,还可能由于提示中的内容过多,反而影响大模型的输出质量。基于上述的原因,节省输入和输出的 token 就成为了一个现实的问题。

结构化数据的格式​

如果要在输入和输出中包含结构化数据,通常有几种格式可以选择,JSON,CSV 和 XML。

JSON 是目前使用最广的一种格式,这一方面得益于 JSON 格式的流行,有大量的工具和第三方库可供使用。另外一方面是由于 JSON schema 的大量使用,也促进了 JSON 的流行。MCP 协议中工具的输入格式,使用的也是 JSON schema。当然了,在不同的地方,所支持的JSON schema 的语法不尽相同。

JSON 的语法存在一定的冗余,尤其是在表示数组时。数组中包含的元素的类型通常是相同的,具有相似的结构。每个元素在表示时,它们的字段都会被重复。

对于表格型的数据,CSV 是最有效的格式,所增加的额外开销最少,因为只需要在第一行添加字段的名称即可。

JSON 格式对于表示嵌套层次复杂的数据,有其独特的优势。这是CSV无法表达的。

在现实的开发中,实际的数据介于完全的嵌套结构,与表格型之间。数据中内嵌的表格越多,使用 JSON 格式所产生的额外开销就越大,因为重复的字段名称越多。

Toon​

Toon,作为一种新的数据格式,尝试解决 JSON 格式开销过大的问题。Toon 是 Token-Oriented Object Notion 的缩写,含义是面向 token 的对象表示。Toon 是 JSON 数据的一种编码方式。Toon 格式的出发点是在与大模型进行交互时,优化 token 的消耗。应用发送请求给大模型时,把数据使用 Toon 编码。要求大模型使用 Toon 格式进行输出,再把 Toon 转换成对象或 JSON 格式。

Toon 在格式上,可以看成是 YAML 和 CSV 的组合。使用类似 YAML 的格式表示嵌套的对象结构。对于数组,使用类似 CSV 的格式,字段的名称只会出现一次。还会包含数组的元素个数,方便解析。

Toon 使用与 JSON 相同的数据模型,包括基本类型,对象和数组。基本类型包括字符串,数字,布尔类型和 null,对象是名值对,数组是值的序列。简单的对象使用 key: value 的格式。内嵌的对象使用缩进来表示内嵌的层次。对于数组,数组的长度,以 [] 的方式添加到字段的名称之后。如果包含的是基本类型,这些值编码之后作为单个值。如果包含的是结构相同的表格型数据,则以 {} 包含字段的名称,数据以 CSV 的格式编码。如果包含的是结构不同的数据,则以类似 YAML 的格式,使用 - 来分隔多个元素。

下面是 Toon 格式的示例,从中可以看到基本类型,对象和数组的表示方式。

Toon 格式的示例
context:
task: Our favorite hikes together
location: Boulder
season: spring_2025
friends[3]: ana,luis,sam
hikes[3]{id,name,distanceKm,elevationGain,companion,wasSunny}:
1,Blue Lake Trail,7.5,320,ana,true
2,Ridge Overlook,9.2,540,luis,false
3,Wildflower Loop,5.1,180,sam,true

与大模型交互时使用 Toon​

在与大模型交互时,当需要在请求中包含结构化数据时,使用 Toon 编码之后,包含在提示中。

如果希望大模型产生结构化的输出,可以在提示中描述 Toon 的格式,并要求大模型给出 Toon 格式的输出,再使用类库解析即可。

不过,不建议使用 Toon 作为大模型的输出格式,这主要是因为大模型并不了解 Toon 格式,需要在提示中进行描述。这个描述可能是不够准确的。大模型对 JSON 格式的支持很好,不需要在提示中描述。大模型对 JSON schema 的支持,也方便了生成满足 schema 要求的数据。从结构化输出的角度出发,使用 JSON 是最好的选择,很多模型服务已经原生提供了对输出的 schema 的支持,并不需要在提示中包含。这就进一步提升了结构化输出的质量。

Spring AI 集成​

在应用中,使用 Toon 提供的类库就可以提供 Toon 格式的编码和解码。Java 也有同样的类库。下面通过一个示例来说明。示例使用的是 Spring AI 与模型交互。

使用 Toon 时添加相关的 Maven 依赖。使用的入口类是 JToon,encode 和 decode 方法分别进行编码和解码。

Toon 的 Maven 依赖
<dependency>
<groupId>dev.toonformat</groupId>
<artifactId>jtoon</artifactId>
<version>1.0.6</version>
</dependency>

为了展示 Toon 的格式,这里创建了一个 User 对象,User 类的声明如下所示,其中包含了 Address 这个内嵌的对象。

User
package com.javaaidev.toon.model;

import java.util.List;

public record User(String id,
String name,
String email,
String mobilePhone,
List<Address> addresses) {

public enum AddressType {
HOME,
OFFICE,
OTHER,
}

public record Address(
String id,
AddressType addressType,
String countryOrRegion,
String provinceOrState,
String city,
String addressLine,
String zipCode) {

}
}

代码中生成了 3个 User 对象。包含了这3个对象的数组,分别使用 JSON 和 Toon 进行编码。

Toon 和 JSON 编码
package com.javaaidev.toon;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.javaaidev.toon.model.Users;
import dev.toonformat.jtoon.JToon;

public class ToonUtils {

private static final ObjectMapper objectMapper = new ObjectMapper().enable(
SerializationFeature.INDENT_OUTPUT);

public static void main(String[] args) throws JsonProcessingException {
var json = objectMapper.writeValueAsString(Users.USERS);
var toon = JToon.encode(Users.USERS);
System.out.println("""
JSON:
%s

TOON:
%s
""".formatted(json, toon));
}
}

从两者的结果可以看到,Toon 比 JSON 节省了 45% 的 token。

Toon token savings

在使用 Spring AI 与大模型交互时,可以使用下面这样的提示模板。对 Toon 格式进行了简单的描述。这就足够了,大模型可以自行对内容进行解析。

Spring AI 中使用 Toon
package com.javaaidev.toon.controller;

import com.javaaidev.toon.model.Users;
import dev.toonformat.jtoon.JToon;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class ToonController {

private final ChatClient client;

public ToonController(ChatClient.Builder builder) {
this.client = builder.build();
}

@PostMapping("/request")
public ChatOutput request(@RequestBody ChatInput input) {
var output = client.prompt().user("""
%s

Data is in TOON format (2-space indent, arrays show length and fields).
```toon
%s
```
""".formatted(input.input(), JToon.encode(Users.USERS)))
.call()
.content();
return new ChatOutput(output);
}

public record ChatInput(String input) {

}

public record ChatOutput(String output) {

}
}

在大模型的实际处理中,把查询和数据发送给模型之后,模型可以正确的完成解析。

Test API using Toon

总结一下,使用 Toon 格式对发送给大模型的数据进行编码,可以减少 token 的消耗。