大模型Token成本优化:5个上下文压缩与缓存实战技巧 1. 上下文窗口不是免费的午餐先搞清楚Token到底花在哪很多人第一次被账单吓到是在某个深夜盯着后台用量曲线发呆——明明只是让AI帮忙改了几段代码、读了两份文档怎么一天下来消耗的Token够买好几杯咖啡。问题往往不在你问了多少次而在于每次提问时你悄悄塞进去的上下文有多重。先把账算清楚。大模型的计费逻辑是按Token双向计费你发过去的输入prompt 上下文算一次模型吐回来的输出算一次。而上下文这个东西有个致命特性——它是累积的。在一个多轮对话或Agent任务里第N轮请求通常会把前面N-1轮的历史全部带上于是输入Token量会随着轮次近似线性甚至平方级增长。举个直观的例子假设每轮对话平均产生500 Token到第20轮时光历史就有近1万Token而你这一轮真正想问的可能只有50 Token。也就是说95%以上的钱花在了回忆上而不是思考上。更麻烦的是上下文塞满之后模型不只是变贵还会变傻。这不是玄学而是有明确机制的。主流大模型的注意力机制在处理长上下文时对中间位置的信息召回率会明显下降业界俗称lost in the middle。当你的上下文里混着大量无关的日志、重复的代码、过期的对话模型抓重点的能力会被稀释输出质量断崖式下跌。你花了更多钱得到了更差的结果这是最亏的一种情况。所以省Token这件事本质上是两件事的合体省钱和保智商。下面这5个做法是我在实际项目里反复验证过的从最粗暴的截断到相对精细的压缩都有你可以按自己的场景挑着用。2. 做法一给上下文设硬上限别让历史无限膨胀2.1 滑动窗口不是万能药但它是第一道防线最直接的办法是给对话历史设一个硬性的Token上限超过就丢弃最老的部分。这就是经典的滑动窗口策略。实现上很简单维护一个消息列表每次新增消息后从头部开始累加Token数超过阈值就把最早的消息删掉直到降到阈值以下。但这里有个坑很多人第一次做会踩粗暴地按条数删而不是按Token删。一条消息可能是一句好的也可能是粘贴进来的三千字文档按条数删会导致窗口大小剧烈波动。正确做法是用分词器tokenizer实际计算每条消息的Token数按Token预算来裁剪。# 以常见的消息列表为例按Token预算裁剪历史 def trim_history(messages, max_tokens, count_tokens): # 从最新往旧累加保留最近的对话 kept [] total 0 for msg in reversed(messages): t count_tokens(msg[content]) if total t max_tokens: break kept.append(msg) total t return list(reversed(kept))2.2 系统提示词要单独保护别被窗口挤掉滑动窗口有个隐蔽的副作用它可能把系统提示词system prompt也一起裁掉。系统提示词通常定义了角色、输出格式、安全约束一旦丢失模型行为会立刻跑偏。所以裁剪逻辑里必须把系统提示词排除在外永远保留只对用户和助手的对话轮次做窗口。我的习惯是给系统提示词单独留一个预算比如总预算8000 Token系统提示词固定占1000剩下7000给对话历史。这样即使对话很长角色设定也不会丢。2.3 什么时候该用滑动窗口什么时候不该用滑动窗口适合闲聊型、任务连续性不强的场景比如客服问答、日常助手。但如果你的任务是根据前面所有讨论逐步推导一个结论那丢掉早期上下文可能直接导致结论错误。这种情况下滑动窗口只能作为兜底真正的主力应该是后面要讲的摘要压缩。提示滑动窗口的阈值不要拍脑袋定。建议先用真实业务数据跑一遍统计平均每轮对话的Token分布再取一个覆盖80%场景的值。我一般会把它设成模型上下文上限的30%到50%留足余量给输出。3. 做法二把长历史压成摘要用信息密度换Token3.1 摘要压缩的核心思路让模型自己记笔记滑动窗口是忘掉旧的摘要压缩是把旧的浓缩成一句话。思路是当对话历史超过一定长度时调用一次模型把前面的历史总结成一段简短的摘要然后用这段摘要替换掉原始历史。这样原本几千Token的内容可能被压到几百Token信息密度大幅提升。关键在于摘要的粒度。我试过几种方案效果差别很大摘要策略Token节省信息保留适用场景全量一次性摘要高中长对话、主题集中分段滚动摘要中高超长任务、多主题只摘关键决策点高中低任务型Agent结构化摘要JSON中高需要精确回溯3.2 滚动摘要处理超长任务的正确姿势如果任务特别长一次性摘要会丢失太多细节。这时候用滚动摘要维护一个历史摘要字段每当新增的对话累积到一定量就把旧摘要 新对话一起喂给模型生成新的摘要。这样摘要本身也在不断更新既控制了长度又保留了演进过程。def rolling_summarize(old_summary, new_messages, llm): prompt f已有摘要 {old_summary} 新增对话 {format_messages(new_messages)} 请把以上内容合并成一段不超过300字的摘要保留关键决策、结论和未完成事项。 return llm.invoke(prompt)3.3 摘要最容易丢的三类信息实测下来摘要压缩最常丢的是这三样具体的数字和参数比如阈值设为0.75被摘成设置了阈值、否定性约束比如不要用递归被漏掉、未完成的待办。所以我在写摘要提示词时会明确要求模型保留所有数值、所有禁止事项、所有TODO。这一条小小的约束能救回很多后续的返工。注意摘要本身也要花Token调用模型生成摘要所以别太频繁地触发。我一般设置在历史超过预算的70%时才触发一次摘要避免为了省Token反而多花Token。4. 做法三RAG检索替代全量投喂只给模型看相关的4.1 全量投喂是最大的浪费源很多人做知识库问答时习惯把整个文档库塞进上下文或者把检索到的十几篇文档全部丢给模型。这是Token消耗的重灾区。一份技术文档动辄上万Token你塞五份进去光输入就五万Token而模型真正需要的可能只是其中两段。RAG检索增强生成的正确用法是先用向量检索或关键词检索从知识库里捞出最相关的少量片段只把这些片段放进上下文。检索质量决定了Token效率——检索得准三段就够检索得糙塞三十段也没用。4.2 检索片段的数量和长度怎么定这里有两个参数要调召回条数top-k和单条长度chunk size。我的经验值是top-k 先设3到5观察召回内容是否覆盖了答案。如果经常漏再往上加但一般不超过8。chunk size 控制在300到500 Token一段太大则单条浪费太小则语义不完整。加一个重排序rerank步骤把召回的片段按相关性重新排序只取前2到3条进上下文。这一步能砍掉一半以上的无效Token。4.3 检索结果要去重和截断实际检索出来的片段经常高度重复尤其是文档里有大量模板化内容时。进上下文之前先做一次相似度去重把重复度超过阈值的片段丢掉。另外如果某个片段特别长可以在句子边界处截断只保留最相关的部分而不是整段照搬。def dedup_and_truncate(chunks, sim_threshold0.9, max_len500): kept [] for c in chunks: if any(similarity(c, k) sim_threshold for k in kept): continue kept.append(truncate_at_sentence(c, max_len)) return kept这套组合拳下来同样的问答任务输入Token通常能降到全量投喂的十分之一甚至更低而答案质量反而更稳因为模型不用在一堆噪音里找信号了。5. 做法四让Agent少绕路工具调用别把结果全背回来5.1 Agent的Token黑洞工具返回结果做Agent开发的人都知道Token消耗的大头往往不是对话而是工具调用的返回结果。你让Agent去查一个接口返回一大坨JSON让它读一个文件返回整个文件内容让它跑一次搜索返回十条网页摘要。这些结果全部进入上下文几轮下来上下文就爆了。解决办法是在工具层做过滤而不是把原始结果直接丢给模型。具体来说接口返回的JSON只提取模型真正需要的字段其余丢弃。文件读取支持按行范围或按符号如函数名读取而不是整文件读。搜索结果先做摘要只把标题和关键句给模型需要详情时再二次调用。5.2 工具描述本身也占Token容易被忽略的一点每个工具的schema描述都占Token。如果你给Agent挂了20个工具光工具定义可能就两三千Token而且每一轮请求都要带上。所以工具要精简功能重叠的合并不常用的按需动态加载。我见过一个项目挂了40多个工具光工具描述就吃掉了上下文的三分之一纯属浪费。5.3 用计划-执行分离减少往返Agent绕路的另一个原因是边想边做每一步都要把完整上下文带上。改成先规划、再执行的模式第一轮让模型输出一个任务计划简短后续执行时只带当前步骤相关的上下文而不是全部历史。这样每一轮的输入都能控制在很小的范围。提示Agent的每一步都建议记录Token消耗做成监控面板。我自己的项目里就是靠这个面板发现某个工具调用平均返回8000 Token优化后降到500整体成本直接砍半。6. 做法五缓存与复用别让相同的上下文重复计费6.1 提示词缓存把不变的部分缓存起来现在很多模型服务商支持提示词缓存prompt caching对于反复出现的相同前缀比如固定的系统提示词、固定的知识库片段缓存命中后计费大幅降低有的甚至能降到原价的十分之一。这个特性对Agent和长系统提示词的场景特别友好。用法上关键是把稳定不变的内容放在前面把变化的内容放在后面。因为缓存是按前缀匹配的如果你的系统提示词每次都变缓存永远命中不了。所以系统提示词要尽量固定动态内容用户输入、检索结果放到后面。6.2 相同问题的结果缓存如果你的应用里有大量重复或高度相似的问题比如FAQ场景可以在应用层做一个语义缓存把问题和答案存起来新问题先查缓存相似度超过阈值就直接返回根本不调用模型。这一层能省下的Token是100%因为压根没请求。6.3 批处理合并请求如果有一批独立的短任务要处理别一个个发请求。把多个任务合并成一个请求让模型一次性输出多个结果可以省掉重复的系统提示词和上下文开销。当然要注意别合并太多导致单次输出过长一般控制在模型输出上限的60%以内比较稳。7. 五个做法怎么组合一套可落地的省Token流水线单独用某一个做法效果有限组合起来才能形成合力。我在实际项目里的流水线是这样的入口层语义缓存拦截重复问题命中直接返回。检索层RAG只召回top-3并重排序去重截断后再进上下文。历史层滑动窗口兜底超过70%预算触发滚动摘要。工具层工具返回结果在代码里过滤只给模型必要字段。请求层固定前缀开启提示词缓存动态内容后置。这套流水线跑下来同样的业务量Token消耗相比最初的全量投喂 无限历史版本通常能降到15%到25%而且因为上下文更干净模型输出质量反而更稳定。省Token和提质量在这里不是矛盾的而是同一件事的两面。最后分享一个我踩过的坑别为了省Token把上下文压得太狠。有一次我把摘要阈值调得过低结果模型丢失了关键约束输出了一堆不符合要求的代码返工花的时间远超省下的那点钱。省Token的前提是不损害任务成功率这个平衡点需要你用真实数据去调而不是拍脑袋。建议每次调整压缩策略后都跑一遍回归测试集确认成功率没有下降再上线。