JSON在LLM场景下的Token成本问题
1.1 JSON的Token开销
JSON设计于2001年,初衷是解决服务间的数据交换。二十年后,当LLM成为技术栈的核心组件时,JSON的局限性开始显现。
在LLM场景中,Token是计价单位。JSON的大量引号、逗号、嵌套结构直接推高了API成本。同样的数据量,JSON格式要比实际内容多消耗60-70%的Token。
举个例子。一个包含10条用户记录的查询结果:
JSON格式(87 Tokens):
json
{
"users": [
{"id": 1, "name": "Alice", "email": "alice@example.com", "active": true},
{"id": 2, "name": "Bob", "email": "bob@example.com", "active": false}
]
}
那些引号、逗号、大括号,每一个都在烧钱。当你的RAG系统需要一次性塞进20个文档片段时,上下文窗口有一半被JSON的格式符号占用了。
1.2 ISON:表结构式的数据格式
要降低Token消耗,需要一种更紧凑、同时让LLM容易理解的格式。
ISON(Interchange Simple Object Notation)正是基于这个需求设计的。
它的核心思路是:采用LLM训练数据中常见的表结构和关系模式。SQL查询结果、CSV文件、数据库表------这些格式在预训练语料中出现了数十亿次,LLM对它们的理解成本极低。
同样的数据,ISON格式(34 Tokens):
ison
table.users
id:int name:string email active:bool
1 Alice alice@example.com true
2 Bob bob@example.com false
列名只定义一次、没有引号逗号、空格分隔值。同样是10条记录,Token消耗从87降到34,省了60%以上。
ISON实战模板
2.1 场景一:RAG检索结果
RAG检索出来的文档片段,用ISON传给LLM,上下文窗口能多塞一倍内容。
模板代码:
python
from ison_py import parse, to_json
import json
def rag_results_to_ison(chunks):
"""将RAG检索结果转换为ISON格式"""
ison_template = """table.chunks
chunk_id source content relevance_score
"""
for i, chunk in enumerate(chunks, 1):
# 清理内容中的换行和引号,避免破坏格式
content = chunk['content'].replace('\n', ' ').replace('"', '')[:500]
ison_template += f"{i} {chunk['source']} \"{content}\" {chunk['score']}\n"
return ison_template
# 使用示例
chunks = [
{"source": "doc1.pdf", "content": "用户登录流程包括...", "score": 0.95},
{"source": "doc2.pdf", "content": "认证模块使用JWT...", "score": 0.87},
]
ison_data = rag_results_to_ison(chunks)
# 直接塞进prompt
prompt = f"基于以下检索结果回答问题:\n\n{ison_data}\n\n问题:..."
效果对比:
| 格式 | Token数 | 可塞文档数 |
|---|---|---|
| JSON | ~800 | 5个 |
| ISON | ~320 | 12个 |
上下文窗口利用率直接翻倍。
2.2 场景二:多Agent数据传递
多个Agent协作时,中间数据用ISON传递,能显著降低Agent间的通信成本。
模板代码:
python
def agent_context_to_ison(agent_id, context_data, references=None):
"""多Agent上下文传递模板"""
ison = f"""object.agent_context
agent_id task status
{agent_id} {context_data['task']} {context_data['status']}
"""
if context_data.get('results'):
ison += """table.results
item_id value confidence
"""
for r in context_data['results']:
ison += f"{r['id']} {r['value']} {r['confidence']}\n"
if references:
ison += """table.references
ref_id ref_type target_id
"""
for ref in references:
ison += f"{ref['id']} {ref['type']} :{ref['target']}\n"
return ison
# 使用示例
context = {
"task": "data_analysis",
"status": "completed",
"results": [
{"id": 1, "value": 150, "confidence": 0.92},
{"id": 2, "value": 230, "confidence": 0.88},
]
}
refs = [
{"id": "r1", "type": "DEPENDS_ON", "target": "agent_001"},
{"id": "r2", "type": "PRODUCES", "target": "agent_003"},
]
ison_context = agent_context_to_ison("agent_002", context, refs)
2.3 场景三:数据库查询结果直接导出
查询完数据库直接输出ISON,比JSON省一半Token。
python
import sqlite3
def query_to_ison(db_path, query, table_name="results"):
"""SQL查询结果直接转ISON"""
conn = sqlite3.connect(db_path)
cursor = conn.cursor()
cursor.execute(query)
# 获取列名
columns = [desc[0] for desc in cursor.description]
rows = cursor.fetchall()
# 生成ISON
ison = f"table.{table_name}\n"
ison += " ".join(columns) + "\n"
for row in rows:
# 处理字符串中的空格,需要引号包裹
values = []
for v in row:
if isinstance(v, str) and (" " in v or v in ["true", "false", "null"]):
values.append(f'"{v}"')
else:
values.append(str(v))
ison += " ".join(values) + "\n"
conn.close()
return ison
# 使用示例
ison_output = query_to_ison("data.db", "SELECT id, name, status FROM users LIMIT 10")
几条使用技巧
3.1 字符串引号的省略规则
不是所有字符串都需要引号,但以下几种情况必须加:
| 情况 | 示例 | 处理 |
|---|---|---|
| 包含空格 | New York |
"New York" |
| 保留字 | true, false, null |
"true", "null" |
| 看起来像数字 | 12345(实际是ID) |
"12345" |
| 以冒号开头 | :tag(不是引用) |
":tag" |
省Token技巧 :字段名用下划线代替空格,比如first_name比"first name"省2个Token(咱就说能省一点是一点)。
3.2 关系引用的三种写法
ISON支持跨表引用,但写法不同用途不同:
ison
table.teams
id name
10 Engineering
20 Marketing
table.employees
id name team
101 Mahesh :10 # 简单引用
102 John :team:10 # 带命名空间
103 Alice :MEMBER_OF:10 # 带关系类型
:10- 简单引用,最省Token:team:10- 命名空间引用,防止ID冲突:MEMBER_OF:10- 关系类型引用,适合知识图谱场景
建议:普通场景用简单引用,复杂关系用关系类型。
3.3 用引用代替深度嵌套
ISON推荐用引用代替深度嵌套。比如订单和用户信息:
不推荐(嵌套):
ison
object.order
id customer.name customer.address.city total
1001 Alice "New York" 125.50
推荐(引用):
ison
table.customers
id name city
1 Alice "New York"
table.orders
id customer_id total
1001 :1 125.50
嵌套字段虽然支持,但可读性和可维护性不如引用方式。
3.4 ISON与ISONL的选择
ISONL(ISON Lines)是ISON的流式格式,每行包含一条完整记录,用管道符分隔字段定义和值。它相当于JSONL之于JSON的关系,适合日志、事件流等需要逐行追加数据的场景。
| 格式 | 适用场景 | 特点 |
|---|---|---|
| ISON | 配置文件、静态数据 | 多行结构清晰 |
| ISONL | 日志、事件流、微调数据 | 每行自包含,适合追加 |
ISONL示例(一行一条记录):
isonl
table.events|ts type user|10:30:00 click :1
table.events|ts type user|10:31:00 view :2
数据对比与适用场景
4.1 实际Token成本对比
基于官方20个数据集的基准测试(GPT-4o tokenizer):
| 格式 | Token总数 | 相比JSON | 解析准确率 |
|---|---|---|---|
| JSON | 12,668 | baseline | 84.7% |
| ISON | 3,550 | -72% | 88.3% |
不仅Token省70%+,LLM解析准确率还更高。
4.2 什么时候用ISON,什么时候用JSON
| 场景 | 推荐格式 | 理由 |
|---|---|---|
| LLM prompt上下文 | ISON | 省Token,LLM理解更好 |
| Agent间通信 | ISON | 降低传输成本 |
| RAG检索结果 | ISON | 上下文窗口利用率高 |
| 服务间API | JSON | 通用性更好 |
| 前端数据交互 | JSON | 浏览器原生支持 |
| 长期存储 | JSON | 生态更成熟 |
核心原则:ISON用在LLM边界,JSON用在系统内部。
4.3 快速上手
如果你用Python,一行命令安装:
bash
pip install ison-py
然后就可以用上面的模板代码直接开始了。
ISON目前支持6种语言:Python、JavaScript、TypeScript、Rust、Go、C++。
Java暂无官方库,以下是我实现的一个简化版,满足基础的解析和生成需求:
ruby
https://github.com/yuboon/ai-examples/tree/main/004-jison/IsonUtils.java
java
public class IsonUtils {
/**
* 解析ISON表格格式为List<Map>
*/
public static List<Map<String, Object>> parseTable(String isonText) {
List<Map<String, Object>> results = new ArrayList<>();
String[] lines = isonText.trim().split("\n");
// 第一行是表名,跳过
// 第二行是字段定义
String[] headers = lines[1].split(" ");
// 后续行是数据
for (int i = 2; i < lines.length; i++) {
String[] values = lines[i].split(" ");
Map<String, Object> row = new LinkedHashMap<>();
for (int j = 0; j < headers.length; j++) {
String header = headers[j].split(":")[0]; // 去掉类型注解
row.put(header, parseValue(values[j]));
}
results.add(row);
}
return results;
}
/**
* 生成ISON表格格式字符串
*/
public static String generateTable(String tableName,
List<String> headers,
List<List<Object>> rows) {
StringBuilder sb = new StringBuilder();
sb.append("table.").append(tableName).append("\n");
sb.append(String.join(" ", headers)).append("\n");
for (List<Object> row : rows) {
List<String> formatted = new ArrayList<>();
for (Object val : row) {
formatted.add(formatValue(val));
}
sb.append(String.join(" ", formatted)).append("\n");
}
return sb.toString();
}
private static Object parseValue(String val) {
if (val.equals("true")) return true;
if (val.equals("false")) return false;
if (val.equals("null")) return null;
if (val.startsWith(":")) return val; // 引用
try {
return Integer.parseInt(val);
} catch (NumberFormatException e) {
try {
return Double.parseDouble(val);
} catch (NumberFormatException e2) {
return val.replace("\"", ""); // 去掉引号
}
}
}
private static String formatValue(Object val) {
if (val == null) return "null";
if (val instanceof Boolean) return val.toString();
if (val instanceof Number) return val.toString();
String str = val.toString();
// 包含空格或是保留字时加引号
if (str.contains(" ") || str.contains("\t") ||
str.equals("true") || str.equals("false") || str.equals("null")) {
return "\"" + str + "\"";
}
return str;
}
}
// 使用示例:解析
String ison = """
table.users
id:int name:string email
1 Alice alice@example.com
2 Bob bob@example.com
""";
List<Map<String, Object>> users = IsonUtils.parseTable(ison);
// 使用示例:生成
List<String> headers = Arrays.asList("id:int", "name:string", "email");
List<List<Object>> rows = Arrays.asList(
Arrays.asList(1, "Alice", "alice@example.com"),
Arrays.asList(2, "Bob", "bob@example.com")
);
String isonOutput = IsonUtils.generateTable("users", headers, rows);
Token成本在LLM应用里是刚性支出,同样的效果,用ISON能省60-70%的传输Token,相当于把上下文窗口扩大2倍。如果你的应用有频繁的数据传输给LLM,值得一试。