关注「索引目录」公众号,获取更多干货。
如果你正在构建基于人工智能的功能,你很可能需要向语言模型发送大量的 JSON 数据。而如果你发送了大量的 JSON 数据,那么你消耗令牌的速度可能比你预期的要快得多。
在Kollabe,我们使用人工智能生成回顾会议和站会的摘要、行动项和建议。当数十名团队成员每天提交更新时,JSON 数据量会迅速增长。我们需要在不丢失信息的情况下缩小数据规模的方法。
目前有一些更新的正式解决方案,例如TOON(面向标记的对象表示法),它可以将 LLM 的 JSON 压缩高达 40%。它是一个完整的规范,包含基准测试和 SDK。如果您想要一种标准化的方法,值得了解一下。
但有时您需要掌控一切。您不希望增加额外的依赖项。您希望确切地了解发送给模型的内容,并根据您的具体用例进行调整。以下是我们用来减少令牌使用量而不增加复杂性的简单技巧。
1. 将长 ID 替换为短 ID
UUID无处不在。它们对数据库来说很棒,但对令牌效率来说却很糟糕。
// This UUID is 4-5 tokens
"550e8400-e29b-41d4-a716-446655440000"
// This is 1 token
"u-1"
当你在数百个站立会议记录中引用同一个用户时,这些额外的令牌会迅速累积起来。
解决方案:在处理数据时构建一个简单的映射。遇到的第一个用户变成 1 u-1,第二个用户变成 2 u-2,依此类推。如果再次遇到相同的 UUID,则重复使用已分配的短 ID。
// Before: UUIDs everywhere
{
odUserId: "550e8400-e29b-41d4-a716-446655440000",
odQuestionId: "7c9e6679-7425-40de-944b-e07fc1f90ae7",
odAnswerId: "f47ac10b-58cc-4372-a567-0e02b2c3d479"
}
// After: short, prefixed IDs
{
uid: "u-1",
qid: "q-1",
aid: "a-1"
}
关键在于,同一个 UUID 总是对应同一个短 ID。因此,当 LLMu-1在不同的答案中多次看到同一个 UUID 时,它就能理解这些条目属于同一个人。为不同的实体类型使用不同的前缀,以便模型能够区分用户 ID 和问题 ID。
2. 去掉格式
JSON.stringify还有第二个和第三个参数,大多数人都会忽略它们。第三个参数用于添加缩进:
// Pretty printed (wasteful)
JSON.stringify(data, null, 2);
// Minified (efficient)
JSON.stringify(data);
区别如下:
// Pretty: ~80 characters
{
"name": "Alice",
"role": "Engineer",
"team": "Platform"
}
// Minified: ~45 characters
{"name":"Alice","role":"Engineer","team":"Platform"}
如果是小物件,无所谓。但如果是成千上万个站立式会议记录呢?那些空白加起来就不少了。反正LLM(立法程序管理员)也不在意格式。
3. 使用更短的键名
仔细想想,这似乎显而易见。对比一下:
// Verbose
type StandupEntry = {
odUserId: string;
userName: string;
yesterdayUpdate: string;
todayPlan: string;
blockerDescription: string;
};
// Concise
type StandupEntry = {
odUid: string;
name: string;
yesterday: string;
today: string;
blocker: string;
};
当有数百条记录时,较短的键可以节省实际的令牌。只需确保键足够易读,以便语言学习模型 (LLM) 能够理解上下文即可。
我们遵循以下几条规则:
-
删除冗余词语:如果它明显是用户对象, userId则变为id -
使用常用缩写: desc而不是description -
措辞要清晰明确: y比如“昨天”这个说法太隐晦了,但yest效果不错。
4. 删除空值和空字符串
不要发送不存在的数据:
function removeEmpty<T extends object>(obj: T): Partial<T> {
return Object.fromEntries(
Object.entries(obj).filter(([_, v]) => {
if (v === null || v === undefined) return false;
if (v === "") return false;
if (Array.isArray(v) && v.length === 0) return false;
return true;
})
) as Partial<T>;
}
// Before
{
"name": "Alice",
"blocker": null,
"tags": [],
"notes": ""
}
// After
{
"name": "Alice"
}
如果没有人举报阻塞问题,为什么要告诉LLM呢?
5. 尽可能扁平化嵌套结构
有时嵌套只是组织上的冗余:
// Before
{
"user": {
"profile": {
"name": "Alice",
"team": "Platform"
}
},
"update": "Finished feature"
}
// After
{
"name": "Alice",
"team": "Platform",
"update": "Finished feature"
}
第二个版本用更少的结构标记传达了相同的信息。显然,如果层级结构本身就具有意义,就不应该将其扁平化,但通常情况下并非如此。
6. 使用数组代替重复对象
如果您有一系列相似项目,请考虑是否需要每个项目的完整对象结构:
// Before: 3 objects with repeated keys
{
"entries": [
{ "name": "Alice", "status": "done" },
{ "name": "Bob", "status": "blocked" },
{ "name": "Carol", "status": "done" }
]
}
// After: header row + data rows
{
"cols": ["name", "status"],
"rows": [
["Alice", "done"],
["Bob", "blocked"],
["Carol", "done"]
]
}
这样虽然牺牲了一些可读性,但提高了效率。对于大型数据集来说,这是值得的。
7. 删除不必要的元数据
人工智能处理通常不需要时间戳、审计字段和内部 ID:
// Before: full database record
{
odAnswerId: "f47ac10b-58cc-4372-a567-0e02b2c3d479",
odUserId: "550e8400-e29b-41d4-a716-446655440000",
text: "Great sprint!",
createdAt: "2024-01-15T10:30:00.000Z",
updatedAt: "2024-01-15T10:30:00.000Z",
isDeleted: false,
version: 1
}
// After: just what the LLM needs
{
uid: "u-1",
text: "Great sprint!"
}
问问自己:模型真的需要这个字段才能生成有用的响应吗?如果不需要,就把它删掉。
8. 高效地表示布尔值
对于布尔标志,请考虑当其值为 false 时是否还需要该字段:
// Before
{ "name": "Alice", "isAdmin": false, "isActive": true, "isVerified": false }
// After: only include truthy flags
{ "name": "Alice", "active": true }
// Or use a flags array for multiple true values
{ "name": "Alice", "flags": ["active", "verified"] }
如果大多数用户都不是管理员,则不要isAdmin: false在每个记录中包含该信息。
综合起来
以下是 Kollabe 生成的一份真实回顾总结中的前后对比图:
优化前:
{
"retrospectiveData": {
"questions": [
{
"odQuestionId": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"questionText": "What went well this sprint?",
"questionType": "positive"
},
{
"odQuestionId": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
"questionText": "What could be improved?",
"questionType": "negative"
}
],
"answers": [
{
"odAnswerId": "1b9d6bcd-bbfd-4b2d-9b5d-ab8dfbbd4bed",
"odQuestionId": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"odUserId": "550e8400-e29b-41d4-a716-446655440000",
"userName": "Alice Chen",
"answerText": "Team collaboration was excellent during the release",
"createdAt": "2024-01-15T10:30:00.000Z",
"voteCount": 3
},
{
"odAnswerId": "6ec0bd7f-11c0-43da-975e-2a8ad9ebae0b",
"odQuestionId": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"odUserId": "6ba7b810-9dad-11d1-80b4-00c04fd430c8",
"userName": "Bob Smith",
"answerText": "CI/CD pipeline improvements saved us hours",
"createdAt": "2024-01-15T10:32:00.000Z",
"voteCount": 5
},
{
"odAnswerId": "3f333df6-90a4-4fda-8dd3-9485d27cee36",
"odQuestionId": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
"odUserId": "550e8400-e29b-41d4-a716-446655440000",
"userName": "Alice Chen",
"answerText": "Documentation was often outdated",
"createdAt": "2024-01-15T10:35:00.000Z",
"voteCount": null
}
]
}
}
优化后:
{"qs":[{"id":"q-1","text":"What went well this sprint?","type":"positive"},{"id":"q-2","text":"What could be improved?","type":"negative"}],"ans":[{"id":"a-1","qid":"q-1","uid":"u-1","name":"Alice Chen","text":"Team collaboration was excellent during the release","votes":3},{"id":"a-2","qid":"q-1","uid":"u-2","name":"Bob Smith","text":"CI/CD pipeline improvements saved us hours","votes":5},{"id":"a-3","qid":"q-2","uid":"u-1","name":"Alice Chen","text":"Documentation was often outdated"}]}
发生了哪些变化:
-
UUID 已替换为短 ID ( q-1,,a-1)u-1 -
长键名缩写( odQuestionId到qid,answerText到text) -
已移除包装对象( retrospectiveData) -
已删除空值(无 voteCount: null) -
已移除时间戳(生成摘要时不需要) -
无空格格式
优化后的版本体积缩小了约 50%。对于一个拥有 50 名成员、数百条回复的团队来说,这能显著降低令牌成本并加快推理速度。
何时优化
并非所有 JSON 数据都需要这种处理。如果您发送的是一个小型配置对象或单个用户查询,那么优化带来的额外开销就得不偿失了。
但是,当你构建需要处理大量结构化数据的功能时,比如我们在 Kollabe 开发的回顾和站会总结功能,这些技巧就显得尤为重要。它们易于实现,无需外部依赖,并且能立即带来成效。
掌控数据管道也至关重要。编写自己的优化层,就能完全了解其运行机制。您可以调整短 ID 前缀,决定删除哪些字段,并随着数据的变化调整策略。一切都无需担心,没有黑箱操作。
最棒的是什么?LLM 可以完美处理优化后的 JSON 数据。它们不需要漂亮的格式或冗长的键名就能理解你的数据。它们只需要信息本身。
关注「索引目录」公众号,获取更多干货。

