节省Token的8种方案

张开发
2026/4/16 11:36:12 15 分钟阅读

分享文章

节省Token的8种方案
前言最近有球友问“三哥我们团队在做AI客服对话一长token消耗扛不住。有没有一种方案既能保留完整上下文记忆又能省token”这位朋友的问题恰恰戳中了当下AI应用开发最头疼的痛点。既要马儿跑得快又要马儿不吃草。这听起来像是矛盾但经过这两年的摸索我发现在某些条件下确实存在“相对两全”的解法。今天这篇文章就专门跟大家一起聊聊这个话题希望对你会有所帮助。更多项目实战在Java突击队网susan.net.cn一、为什么记忆必然消耗token很多小伙伴可能觉得大模型就像一个人你说过的话它应该天然记得住。错大模型本质上是一个无状态的函数。每次调用都是独立的它没有任何“记忆细胞”。为了让AI记住之前聊过什么唯一的办法就是把历史对话拼接到下一次请求里。这就是所谓的“上下文注入”。看到没第N次请求携带的历史是前N-1轮的总和。token消耗随着对话轮数线性增长——更准确地说是O(n)级别的增长。但事情没这么简单。Transformer模型的核心是自注意力机制它的计算复杂度是O(n²)。也就是说输入长度翻一倍计算量翻四倍。更可怕的是当输入过长时模型会患上“中间迷失症”——位于长文本中间的信息被严重忽略。所以我们的真实困境是保留全部历史 → token爆炸 注意力稀释 → 又贵又笨丢弃历史 → 信息丢失 → AI变“金鱼脑”有没有一条中间道路有。下面我会介绍8种方案从简单到复杂从廉价到智能你可以根据自己的场景按需选择。二、方案一全量记忆简单粗暴但不推荐。这是最直觉的实现把所有对话都存下来每次请求全部带上。public class FullMemorySession { // 使用LinkedList存储全部对话轮次 private ListMessage fullHistory new ArrayList(); public void addTurn(String userMsg, String assistantMsg) { fullHistory.add(new Message(user, userMsg)); fullHistory.add(new Message(assistant, assistantMsg)); } public String buildContext() { StringBuilder sb new StringBuilder(); for (Message msg : fullHistory) { sb.append(msg.getRole()).append(: ).append(msg.getContent()).append(\n); } return sb.toString(); } // 消息实体 static class Message { String role; String content; // 构造器、getter省略 } }代码解析逻辑非常直接——fullHistory列表保存所有消息buildContext()把它们全部拼接成字符串。没有任何优化。优点信息零损失完美保留每句话实现极简单5分钟写完缺点token消耗随轮数线性增长100轮可能几万token达到模型上下文上限后比如8K/128K旧消息会被截断响应时间越来越慢账单越来越高适用场景只适合Demo演示、调试测试或者保证对话不超过10轮的极短场景。生产环境慎用。三、方案二滑动窗口省token但记性差。滑动窗口只保留最近N轮对话超出窗口的直接丢弃。public class SlidingWindowMemory { private final int windowSize; // 窗口大小比如5轮 private final QueueMessage window; // 用队列自动淘汰旧数据 public SlidingWindowMemory(int windowSize) { this.windowSize windowSize; this.window new ArrayDeque(); } public void addTurn(String userMsg, String assistantMsg) { // 加入新消息 window.offer(new Message(user, userMsg)); window.offer(new Message(assistant, assistantMsg)); // 如果超过窗口大小注意一轮2条消息就移除头部 while (window.size() windowSize * 2) { window.poll(); } } public String buildContext() { return window.stream() .map(m - m.getRole() : m.getContent()) .collect(Collectors.joining(\n)); } }代码解析ArrayDeque作为队列offer()在尾部添加当大小超出windowSize * 2因为一轮包含用户和助手两条消息时poll()移除最旧的。这样窗口始终保持固定大小。优点token消耗严格可控不会无限增长实现简单性能高响应速度快缺点早期信息永久丢失。用户第一句说“我是VIP会员”第20轮问“我的会员权益”AI已经忘了无法处理需要长期记忆的任务适用场景客服快速问答、闲聊机器人、临时对话——那些不需要记住早期信息的场景。四、方案三摘要压缩让AI自己总结记忆。这个方案的想法很巧妙不保留原始对话而是定期让大模型把旧对话“压缩”成一段摘要只保留关键信息。public class SummaryMemory { private String summary ; // 历史摘要 private ListMessage recentMessages new ArrayList(); // 未压缩的新消息 private final int compressThreshold; // 触发压缩的token阈值 private final LLMClient llm; // 大模型客户端 public SummaryMemory(LLMClient llm, int compressThreshold) { this.llm llm; this.compressThreshold compressThreshold; } public void addTurn(String userMsg, String assistantMsg) { recentMessages.add(new Message(user, userMsg)); recentMessages.add(new Message(assistant, assistantMsg)); // 估算当前token数超过阈值则触发压缩 if (estimateTokenCount() compressThreshold) { compress(); } } private void compress() { // 构建待压缩的内容旧摘要 新消息 String toCompress summary \n formatMessages(recentMessages); String prompt 请将以下对话历史压缩成一段简洁的摘要保留 1. 用户的关键信息姓名、偏好、身份、已做出的决定 2. 重要的任务状态和上下文 3. 任何后续对话可能需要的事实 原始对话 %s 摘要 .formatted(toCompress); // 调用大模型生成摘要 String newSummary llm.chat(prompt); this.summary newSummary; // 压缩后清空近期消息但可以保留最后一轮作为锚点防止断层 this.recentMessages.clear(); } public String buildContext() { // 上下文 摘要 最近未压缩的消息 if (summary.isEmpty()) { return formatMessages(recentMessages); } return 【历史摘要】\n summary \n\n【最近对话】\n formatMessages(recentMessages); } private int estimateTokenCount() { // 简单估算中文1字≈2token英文1词≈1.3token这里粗略用字符数/2 int totalChars summary.length() formatMessages(recentMessages).length(); return totalChars / 2; } private String formatMessages(ListMessage msgs) { return msgs.stream() .map(m - m.getRole() : m.getContent()) .collect(Collectors.joining(\n)); } }代码详解summary字段存储压缩后的历史摘要初始为空。recentMessages存储尚未被压缩的新消息。每次添加消息后估算总token数摘要新消息如果超过阈值调用compress()。压缩时将旧摘要和新消息一起发给大模型让它生成一个新的、更精炼的摘要。压缩完成后清空recentMessages但也可以选择保留最后1-2轮防止摘要丢失近期细节。buildContext()返回摘要最近消息的拼接作为下次请求的上下文。优点压缩比极高100轮对话可能压缩成200字摘要token节省90%以上关键信息被提炼出来比滑动窗口聪明得多成本可控摘要生成的调用次数不多每隔N轮一次缺点摘要可能失真模型可能漏掉重要细节或产生幻觉每次压缩需要额外调用LLM增加几十毫秒延迟摘要的“信息密度”随着压缩次数增加而下降反复压缩会丢失细节适用场景长周期对话几十到几百轮对信息完整性要求不是100%严谨的场景比如教育辅导、角色扮演。五、方案四向量记忆RAG检索相关而不是保留全部。这是目前工业界最主流的方案。思路是不保存全部历史而是把历史消息向量化后存入数据库每次只检索最相关的几条历史。public class VectorMemory { private final EmbeddingClient embeddingClient; // 向量化模型如OpenAI embedding private final VectorDatabase vectorDb; // 向量数据库如Milvus、Chroma private final int topK 5; // 每次检索几条最相关的历史 public VectorMemory(EmbeddingClient embeddingClient, VectorDatabase vectorDb) { this.embeddingClient embeddingClient; this.vectorDb vectorDb; } // 存储一条历史消息每个消息单独存储带上元数据 public void saveMessage(String role, String content, MapString, Object metadata) { // 1. 调用embedding接口将文本转为向量 float[] vector embeddingClient.embed(content); // 2. 存入向量数据库 VectorRecord record new VectorRecord(vector, content, metadata); vectorDb.insert(record); } // 检索与当前问题最相关的历史记忆 public ListMessage retrieveRelevantHistory(String currentQuestion) { // 将当前问题向量化 float[] queryVector embeddingClient.embed(currentQuestion); // 在数据库中做相似度搜索返回topK条最相似的历史消息 ListVectorRecord results vectorDb.search(queryVector, topK); // 转换成Message对象 return results.stream() .map(r - new Message((String)r.getMetadata().get(role), r.getContent())) .collect(Collectors.toList()); } // 构建上下文检索结果 最近几轮可选 public String buildContext(String userQuestion) { ListMessage relevantHistory retrieveRelevantHistory(userQuestion); StringBuilder context new StringBuilder(相关历史记忆\n); for (Message msg : relevantHistory) { context.append(msg.getRole()).append(: ).append(msg.getContent()).append(\n); } return context.toString(); } }代码详解每条消息单独存储调用embeddingClient.embed()将其转为高维向量比如1536维。向量数据库存储向量原始文本元数据角色、时间戳等。当用户发来新问题时同样将问题向量化然后到数据库中做余弦相似度搜索找到最相似的topK条历史消息。这些检索出的历史消息就是“与当前问题最相关的记忆”拼接进上下文。优点token消耗极低每次只带5-10条最相关的历史而不是几百条可以访问非常久远的记忆只要存储了就能检索到不受窗口限制灵活性高可以混入知识库、FAQ等外部知识缺点检索可能不准确如果embedding模型质量差或者问题与历史的相关性未被捕捉到就会漏掉关键信息需要额外组件向量数据库、embedding服务增加系统复杂度有延迟embedding调用向量检索大约增加50-200ms适用场景绝大多数生产环境——智能客服、AI助手、个性化推荐等。这是目前最推荐的方案。六、方案五分层混合记忆它是工业级最强方案。没有单一方案是完美的。真正的工业级系统往往会组合多种策略形成分层记忆。下面这张图展示了一个典型的混合记忆架构Java实现的核心骨架public class HierarchicalMemory { private SlidingWindowMemory shortTerm; // L1 短期 private SummaryMemory midTerm; // L2 中期摘要 private VectorMemory longTerm; // L3 长期向量 private final int SUMMARY_INTERVAL 10; // 每10轮触发一次摘要压缩 private int turnCount 0; public HierarchicalMemory(LLMClient llm, EmbeddingClient embed, VectorDatabase db) { this.shortTerm new SlidingWindowMemory(5); // 保留最近5轮 this.midTerm new SummaryMemory(llm, 2000); // token超2000压缩 this.longTerm new VectorMemory(embed, db); } public void addTurn(String userMsg, String assistantMsg) { turnCount; // 存入三层记忆 shortTerm.addTurn(userMsg, assistantMsg); midTerm.addTurn(userMsg, assistantMsg); longTerm.saveMessage(user, userMsg, Map.of(turn, turnCount)); longTerm.saveMessage(assistant, assistantMsg, Map.of(turn, turnCount)); // 每10轮额外触发一次摘要同步可选 if (turnCount % SUMMARY_INTERVAL 0) { midTerm.compress(); // 强制压缩 } } public String buildContext(String currentQuestion) { // 1. 短期记忆最近对话—— 最重要直接拼接 String shortContext shortTerm.buildContext(); // 2. 检索长期记忆基于当前问题 ListMessage longMemory longTerm.retrieveRelevantHistory(currentQuestion); String longContext formatMessages(longMemory); // 3. 中期摘要如果摘要非空 String midContext midTerm.getCurrentSummary(); // 4. 按重要性组装短期 检索结果 摘要 StringBuilder finalContext new StringBuilder(); finalContext.append(【最近对话】\n).append(shortContext).append(\n); if (!longContext.isEmpty()) { finalContext.append(【相关历史】\n).append(longContext).append(\n); } if (midContext ! null !midContext.isEmpty()) { finalContext.append(【历史摘要】\n).append(midContext).append(\n); } return finalContext.toString(); } }代码解析L1 短期滑动窗口保留最近5轮保证对话连贯性延迟最低。L2 中期摘要压缩当消息积累到一定程度比如token超2000或每10轮就压缩一次保留全局脉络。L3 长期向量数据库存储每条消息支持按语义检索解决“长尾记忆”问题。构建上下文时优先保证短期最可靠然后加上向量检索出的相关历史弥补窗口丢弃的最后补充摘要作为兜底。优点兼具短时连贯、长时检索、全局摘要覆盖几乎所有场景token消耗可控短期固定检索topK摘要即使检索失败摘要和短期窗口也能兜底缺点实现复杂需要维护多个组件需要精细调参窗口大小、摘要频率、检索数量适用场景大型生产系统、企业级AI应用对体验和成本都有高要求的场景。七、方案六状态变量提取该方案需要极致的结构化压缩。有些场景下真正需要记忆的不是整个对话而是几个关键状态变量。比如订票机器人只需要知道{目的地: 北京, 日期: 2026-05-01, 人数: 2}。public class StateVariableMemory { private MapString, Object state new HashMap(); // 核心状态 // 通过LLM从对话中提取结构化状态 public void updateState(String userMsg, String assistantMsg, LLMClient llm) { String extractPrompt 从以下对话中提取关键状态变量以JSON格式输出。 当前已有状态%s 用户最新消息%s AI回复%s 请更新状态只输出JSON不要其他内容。 .formatted(toJson(state), userMsg, assistantMsg); String jsonResponse llm.chat(extractPrompt); MapString, Object newState parseJson(jsonResponse); state.putAll(newState); // 合并更新 } public String buildContext() { // 上下文只需要展示当前状态而不是历史对话 return 当前会话状态 toJson(state); } }优点极致省token几KB的状态就能代替几十KB的对话结构化模型更容易理解缺点只适合高度结构化的任务订票、填表、参数收集提取状态本身需要调用LLM有额外成本适用场景任务型对话、表单填写、配置向导。八、方案七工具/函数调用把记忆“外包”给外部系统。大模型不是万能的记忆完全可以交给外部数据库。模型只需要学会调用“保存记忆”和“查询记忆”的工具。public class ToolBasedMemory { // 定义两个工具函数 Tool(name save_memory, description 保存一条重要信息到长期记忆) public void saveMemory(String key, String value) { externalDB.put(key, value); } Tool(name recall_memory, description 根据关键词回忆之前保存的信息) public String recallMemory(String key) { return externalDB.get(key); } // 在对话循环中让模型自主决定何时调用这些工具 }这种方案让模型自主管理记忆——它觉得重要就存需要就用。这是目前AI Agent的主流做法。优点极其灵活模型可以按需存取token消耗几乎为零只传工具调用结果。缺点依赖模型自身的函数调用能力容易出错或漏存。适用场景Agent系统、自主决策类应用。九、终极对比方案token节省效果信息保留能力实现复杂度推荐指数全量记忆0%无节省100%⭐❌ 不推荐滑动窗口极高固定差只留近期⭐⭐⭐ 短对话可用摘要压缩高70-90%中可能失真⭐⭐⭐⭐⭐⭐⭐ 长对话向量检索(RAG)高每次topK高语义检索⭐⭐⭐⭐⭐⭐⭐⭐⭐ 首选分层混合高极高⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐ 工业级状态变量极高近乎0中仅结构化⭐⭐⭐⭐⭐ 任务型工具调用极高中靠模型⭐⭐⭐⭐⭐⭐⭐⭐ Agent场景更多项目实战在Java突击队网susan.net.cn总结回到最初的问题有没有方案既能保留上下文记忆又能省token我的答案是有但不存在“免费午餐”。每一分token的节省都换来了系统复杂度的增加或记忆精度的下降。根据我的实战经验给你几条直接的建议如果你刚开始做MVP直接用滑动窗口最近10轮上线跑起来再说。先验证产品价值再优化成本。如果你做的是通用客服/AI助手首选向量检索RAG。这是当前最成熟、性价比最高的方案。配合一个小的滑动窗口3-5轮保证对话连贯性效果已经很好了。如果你的对话轮次非常长100且信息密度高上分层混合记忆。短期窗口中期摘要长期向量三者配合才能既省token又不丢信息。如果你做的是表单/订票/参数收集状态变量提取是王道。几十个字段就能代表整个会话token几乎不增长。永远不要在生产环境用全量记忆——除非你预算无限且用户只聊5句话。

更多文章