如果你正在建立基於人工智慧的功能,你很可能需要向語言模型發送大量的 JSON 資料。而如果你發送了大量的 JSON 資料,那麼你消耗令牌的速度可能比你預期的要快得多。
在Kollabe ,我們使用人工智慧產生回顧會議和站會的摘要、行動項和建議。當數十名團隊成員每天提交更新時,JSON 資料量會迅速成長。我們需要在不遺失資訊的情況下縮小資料規模的方法。
目前有一些更新的正式解決方案,例如TOON (標記導向的物件表示法),它可以將 LLM 的 JSON 壓縮高達 40%。它是一個完整的規範,包含基準測試和 SDK。如果您想要一種標準化的方法,值得了解一下。
但有時您需要掌控一切。您不希望增加額外的依賴項。您希望確切地了解發送給模型的內容,並根據您的特定用例進行調整。以下是我們用來減少令牌使用量而不增加複雜性的簡單技巧。
UUID無所不在。它們對資料庫來說很棒,但對令牌效率來說卻很糟糕。
// This UUID is 4-5 tokens
"550e8400-e29b-41d4-a716-446655440000"
// This is 1 token
"u-1"
當你在數百個站立會議記錄中引用同一個使用者時,這些額外的令牌會迅速累積起來。
解決方案:在處理資料時建立一個簡單的映射。遇到的第一個用戶記為u-1 ,第二個記為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。因此,當 LLM 在不同的答案中多次看到u-1時,它就知道這些條目屬於同一個人。為不同的實體類型使用不同的前綴,以便模型能夠區分使用者 ID 和問題 ID。
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(立法程序管理員)也不在乎格式。
仔細想想,這似乎顯而易見。對比一下:
// 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就很好。
不要發送不存在的資料:
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呢?
有時嵌套只是組織上的冗餘:
// Before
{
"user": {
"profile": {
"name": "Alice",
"team": "Platform"
}
},
"update": "Finished feature"
}
// After
{
"name": "Alice",
"team": "Platform",
"update": "Finished feature"
}
第二個版本以較少的結構標記傳達了相同的訊息。顯然,如果層級結構本身就具有意義,就不應該將其扁平化,但通常並非如此。
如果您有一系列相似專案,請考慮是否需要每個專案的完整物件結構:
// 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"]
]
}
這樣雖然犧牲了一些可讀性,但卻提高了效率。對於大型資料集來說,這是值得的。
人工智慧處理通常不需要時間戳、審計欄位和內部 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!"
}
問問自己:模型真的需要這個欄位才能產生有用的回應嗎?如果不需要,就把它刪掉。
對於布林標誌,請考慮當其值為 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 資料。它們不需要漂亮的格式或冗長的鍵名就能理解你的資料。它們只需要資訊本身。
哦,最後再厚顏無恥地推銷一下:如果你在敏捷開發團隊工作,不妨試試我開發的免費計劃撲克和回顧工具 Kollabe 。我們利用這些技巧來增強我們的 AI 總結功能。
原文出處:https://dev.to/mattlewandowski93/optimizing-json-for-llms-1dgf