详细讲解如何在固定 token 预算下管理 LLM 对话历史,包括精确 token 计数、消息压缩、优先级决策的代码实现。
上下文窗口有严格的 token 预算,而对话只会不断增长。如果放任不管,每次聊天最终都会越过上限,导致请求被拒绝或内容被静默截断。解决办法是进行上下文管理:每次发起请求前,先根据 token 预算处理历史记录,决定哪些轮次原样保留、哪些丢弃、哪些压缩。我实现了一个小型管理器,让它在每次调用模型前运行。下面是它的工作原理。
无法衡量,就无法分配预算。真正占用上下文窗口的是 token,而不是字符。请使用模型实际采用的 tokenizer,确保统计结果与模型一致;在初步估算时可以使用“字符数除以 4”,但这种方法会低估代码和非英语文本的 token 数量,因此需要留出余量。统计时,要计算每条消息的 token 数量,并加上少量的单条消息开销。
const countMsg = m => enc.encode(m.content).length + 4; // ~4 tok overhead
const countAll = msgs => msgs.reduce((n, m) => n + countMsg(m), 0);
上下文窗口同时容纳输入和输出。历史记录的预算应当是“窗口大小 − 回复预留量 − systemTokens”,这样才不会挤占回答所需的空间。system prompt 总是最先占用预算,而且必须保留;其余所有内容只能竞争剩下的空间。如果忘记为回复预留空间,完整的历史记录可能在技术上能够“放进去”,却没有给模型留下任何作答余地。
最简单的裁剪器会从最新消息向最旧消息遍历,在预算允许的情况下保留消息,其余全部丢弃。它无状态、成本低,但也很粗暴:它会毫不犹豫地删除早期包含需求的轮次;如果预算足够紧张,甚至连 system prompt 也可能被丢弃。一定要把 system prompt 放在这个循环之外,始终保留。
function truncateOldest(history, budget){
const kept = []; let used = 0;
for (let i = history.length - 1; i >= 0; i--){
const t = countMsg(history[i]);
if (used + t > budget) break; // older ones dropped
kept.unshift(history[i]); used += t;
}
return kept;
}
滑动窗口会保留最近 N 轮对话,无论它们有多长。这种方式行为可预测,而且极其简单,但它按轮数而不是 token 数量计算,因此最近几轮只要稍微长一些,依然可能溢出;如果历史消息都很短,又会浪费预算。
旧内容摘要是一种更聪明的折中方案:原样保留最近 K 轮,再使用成本较低的模型,把更早的所有内容合并为一份持续更新的摘要。它只需消耗很少的 token,就能保留整段对话的大意,代价是牺牲逐字逐句的细节,以换取更长的上下文覆盖范围。随着尾部对话不断增长,以增量方式重新生成摘要,就能让成本维持稳定。
首先为固定消息预留预算,包括 system prompt 和所有被标记的关键信息;然后用最新的对话轮次填满剩余空间。固定内容永远不会被丢弃,因此,即便对话持续很久,第一轮提出的需求也能保留下来。
这直接解决了截断策略最严重的问题:如果忘记了最开始选定的技术栈,那么最后用户问“提醒我一下我们选了什么”时,模型将无法回答。摘要和固定机制可以保留这些信息,而截断策略和普通滑动窗口都会把它们丢掉。
在生产环境中,你会组合使用这些手段:固定 system prompt、总结较早的中间内容、保留一个最近消息窗口,并在每一轮发送请求前确认所有内容都能放进上下文窗口。
其中的权衡就是整个问题的核心:丢弃过多内容或压缩过度,模型就会失去保持连贯性所需的细节;保留所有内容,又会耗尽预算。上下文管理不同于 system prompt——后者决定固定什么;也不同于 prompt caching——后者通过复用降低成本。上下文管理是在决定哪些内容能够继续保留,让对话始终装得进窗口。
在每次调用模型前运行管理器,历史记录就可以无限增长,而上下文窗口永远不会溢出。
async function manageContext(system, history, summarize){
const budget = historyBudget(system);
let msgs = await summarizeOld(history, 6, summarize); // fold the old middle
if (countAll(msgs) > budget) msgs = truncateOldest(msgs, budget);
return [system, ...msgs]; // system pinned, first
}
让一段对话增长到超出上下文窗口,然后实时观察每种策略如何保留、丢弃或压缩内容:
https://dev48v.infy.uk/prompt/day52-context-management.html
如需采取进一步措施,你可以考虑屏蔽此人和/或举报滥用行为。