如何通过8种方法有效节省AI对话中的Token消耗?

摘要:前言 最近有球友问:“三哥,我们团队在做AI客服,对话一长token消耗扛不住。有没有一种方案,既能保留完整上下文记忆,又能省token?” 这位朋友的问题,恰恰戳中了当下AI应用开发最头疼的痛点。 既要马儿跑得快,又要马儿不吃草。 这听起
前言 最近有球友问:“三哥,我们团队在做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 List<Message> 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 Queue<Message> 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 List<Message> 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(List<Message> 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, Map<String, Object> metadata) { // 1. 调用embedding接口,将文本转为向量 float[] vector = embeddingClient.embed(content); // 2. 存入向量数据库 VectorRecord record = new VectorRecord(vector, content, metadata); vectorDb.insert(record); } // 检索与当前问题最相关的历史记忆 public List<Message> retrieveRelevantHistory(String currentQuestion) { // 将当前问题向量化 float[] queryVector = embeddingClient.embed(currentQuestion); // 在数据库中做相似度搜索,返回topK条最相似的历史消息 List<VectorRecord> 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) { List<Message> 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. 检索长期记忆(基于当前问题) List<Message> 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 Map<String, 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); Map<String, 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句话。