大模型上下文管理模式解析:从摘要压缩到token成本优化的实战指南
最近在折腾大模型应用的时候经常被问到同一个问题多轮对话聊着聊着模型就开始“忘事”前面说好的条件后面全不认账。有人以为是模型太笨有人以为是提示词写得不对其实大多数情况下问题出在上下文管理上。你给它看了多少上下文、以什么方式组织上下文、什么时候该压缩、什么时候该丢弃这些统称起来就是 context-mode 要做的事。准确说context-mode 不是某个单一技术或某个产品的专属功能而是所有会话式 AI、Agent、RAG 应用里都必须面对的一套上下文组织与管理策略。它决定了模型在这个“回合”能看到什么历史信息以多高的保真度去理解当前意图以及最终生成的内容是否连贯可信。这篇文章我打算把 context-mode 从概念到实现完整拆一遍它到底在解决什么问题、业界常见有哪几种模式、每种模式适合什么场景、以及如果我手头正在做一个聊天机器人应该怎么一步步设计自己的上下文管理器。适用人群很明确正在做大模型应用开发、API 接入、智能客服、Agent 工作流又总被“上下文太长、token 太贵、模型失忆”折磨的朋友。不管你是刚入门的小白还是已经踩过几个坑的进阶玩家这篇文章都能给你一套可以拿去直接用的落地思路。1. context-mode 到底是什么先搞懂上下文管理在解决什么问题1.1 从一个真实“翻车”现场说起我之前做一个行业问答机器人最开始是特别朴素的写法把用户所有历史对话都拼在一起一股脑塞给模型。结果第 5 轮之后就开始出问题。用户在第 2 轮说过“不考虑华东地区”到第 8 轮机器人推荐方案时偏偏推荐了华东的供应商。你说模型不知道吗它知道因为历史记录里明明白白写着“不考虑华东地区”。但问题在于新旧信息混在一起之后模型不知道该以哪条为准注意力被分散最后优先采信了更靠后但相关性其实更低的信息。这个场景特别典型。它揭示了一个核心事实大语言模型的“记忆”不是数据库里的字段而是一个有容量上限、有注意力偏向的工作台面。你在这个台面上放太多杂物它反而找不到最重要的那个零件。context-mode 要解决的就是“如何保持工作台面整洁让模型每次都能在有限空间里看到最优信息组合”。1.2 上下文窗口与 token模型的“工作台面”到底有多大要理解各种 context-mode先得理解上下文窗口context window这个东西。它指的是模型单次生成时能利用的最大输入 token 数量。比如某个模型的上下文窗口是 200K token听起来很大但你要知道几百行代码可能就是 10K token一份三四页的文档就是 20K token再加上工具返回结果、系统提示词、历史对话200K 真的不算阔绰。更关键的是上下文窗口越大你为每一步付出的计算成本就越高。注意力机制的时间复杂度是 O(n²) 量级token 数翻一倍计算量涨四倍这不是线性关系。也就是说让模型“多看点内容”不是无代价的它同时牺牲了响应速度和单次调用成本。所以 context-mode 本质上是在做一道权衡题在有限的窗口预算内决定哪些信息必须保留、哪些信息可以压缩、哪些信息可以彻底扔掉。1.3 context-mode 的几种常见模式从“全保留”到“结构化记忆”业界目前常见的 context-mode 大致有四类我按信息保留的粒度从粗到细排一下。第一种是全量历史模式Full History。所有对话原文一股脑传进去实现最简单效果在小规模、短对话场景下也没问题。但一旦对话轮次多了token 成本直线上升模型注意力也会被稀释。很多新手踩坑都是从这里开始的。第二种是滑动窗口模式Sliding Window。只保留最近 N 轮对话更早的直接丢弃。这个方式相当于给工作台面设了一个物理边界放不下的旧东西直接扔掉。优点是简单、稳定、快缺点也明显模型会彻底遗忘窗口之外的信息跨窗口的长期依赖完全断裂。第三种是摘要压缩模式Summarization / Compaction。系统定期把早期对话压缩成一段摘要然后用摘要加最近几轮原文共同组成新的上下文。这是目前生产环境里最主流、效果最均衡的方案。模型不再需要逐字逐句记住所有历史但保留了核心事实和决定既控制 token 成本又维持了长期一致性。第四种是外部记忆模式External Memory / RAG。把历史信息、知识库内容向量化后存到外部数据库里每次按照相关性检索出一小部分放回上下文。这个模式适合超大规模知识场景它不追求把上下文塞满而是追求“按需取用”。难点在于检索质量决定了效果上限。我个人的看法是不要把这四种模式当成互斥选项成熟的系统往往是混合使用的。比如先滑动窗口兜底再叠加摘要压缩遇到特殊实体查询时再走 RAG。整个 context-mode 的设计其实就是把这些策略组合成一套流水线。2. 各种 context-mode 的选型对比数据说话避免拍脑袋2.1 四种模式的实测表现对照为了不空谈理论我把四种模式在一个固定测试集上跑过一轮对比。测试集是一个 30 轮的客服对话中间包含用户偏好变更、订单号、地址修改等关键信息最后在第 28 轮到第 30 轮追问细节。我统计了三个指标关键信息召回率模型能否答出第 5 轮之前提到的订单号、平均单轮 token 消耗、以及响应延迟。模式关键信息召回率平均单轮输入 token延迟感受实现复杂度全量历史92%每轮递增后期每轮超 15K后期明显变慢极低滑动窗口保留最近 5 轮41%稳定在 3.5K 左右稳定低摘要压缩每 5 轮压缩一次87%稳定在 5K 左右稳定中外部记忆 RAG78%稳定在 4.5K 左右受检索耗时影响高从结果可以看得很清楚全量历史看起来召回率高但代价是 token 成本无上限地涨滑动窗口成本最低但关键信息损失严重摘要压缩在成本和召回率之间取得了最好的平衡。RAG 的召回率反而不如摘要压缩原因也简单——测试集里的关键信息是对话过程中产生的动态变更向量检索对这种“临时事实”的捕捉能力不如直接保存在摘要里稳定。这里要提醒一句上面是我的测试结果不同模型、不同提示词风格、不同对话复杂度下数据会有波动。但趋势是稳定的。如果你正在做方案选型我建议也按这个思路建一个小规模评测集别只看官方宣传。2.2 为什么不能一味堆长上下文可能有人会问现在不是有 1M 上下文窗口的模型了吗直接把所有历史都塞进去不就行了还搞这些花里胡哨的模式干嘛这个想法我特别能理解因为我一开始也是这么想的。但实测下来你会发现问题没那么简单。第一长上下文不代表高注意力质量。模型在处理超长输入时对中段信息的注意力会明显衰减这就是业界常说的“lost in the middle”现象。你把关键信息放在第 100K token 的位置模型就算“看到”了也未必会“用上”。第二成本是硬约束。即便把 1M token 全塞进去能跑单次调用的费用也高得离谱生产环境根本烧不起。我之前做过一个实验同一段 20K token 的历史分别用全量传入和压缩成 2K 摘要传入。结果压缩后的答案不仅不比全量差反而在某些细节一致性上更好。原因就是模型不用费力在 20K 里捞关键信息2K 摘要已经把关键事实摆在了它面前。所以我的结论是长上下文窗口是“上限保障”但不该成为你设计 context-mode 时偷懒的借口。2.3 成本账怎么算一个具体的 token 消耗估算选型的时候成本账一定要算。我拿一个实际例子来演示计算过程。假设某模型输入价格是 3 美元 / 1M token输出价格是 15 美元 / 1M token。一个日活 1 万、每人每天 10 次请求的客服机器人每天就是 10 万次请求。如果采用全量历史模式假设平均每次请求输入 6K token、输出 500 token那么每天输入成本是100000 × 6000 ÷ 1000000 × 3 1800 美元。输出成本是100000 × 500 ÷ 1000000 × 15 750 美元。一天光模型费用就是 2550 美元一个月超过 7 万美元。如果改用摘要压缩模式假设平均输入降到 2.5K token、输出不变那么每天输入成本变成100000 × 2500 ÷ 1000000 × 3 750 美元一个月光输入成本就省下 31500 美元大约能压缩 58% 的总体费用。这只是最粗略的估算还没有算压缩本身产生的额外调用成本但量级已经很说明问题context-mode 不是一个“优化项”而是一个直接决定项目能不能盈利的“成本项”。3. 从零实现一个自定义 context-mode完整流程与代码解析3.1 整体链路设计从会话输入到上下文回写这一节我直接分享一套我实际在用的 context-mode 实现方案。先说整体链路一共五个环节。第一接收新消息。用户新输入进系统后不直接发给模型先进入上下文管理器。第二计算当前上下文 token 数。对已有历史、系统提示词、最新消息做 token 估算判断是否触达压缩阈值。第三触发压缩决策。如果未触达阈值走常规路径把历史拼接后发给模型如果触达阈值启动摘要压缩流程。第四执行压缩。用一个专门的“压缩提示词”把旧历史改写成结构化摘要与最近几轮原文合并成新的上下文。第五调用模型并回写。模型生成回复后把本轮对话追加进存储并更新 token 统计。这五步看着简单真正落地时每一步都有坑。下面我逐个拆开讲。3.2 关键参数设计max_tokens、阈值、保留轮数与摘要策略参数设计是这个方案的核心骨架。我先把一段常用的配置代码放出来再解释每个参数为什么这么定。CONTEXT_MODE_CONFIG { max_context_tokens: 6000, # 单次请求向模型传参的token上限 summary_trigger_ratio: 0.7, # 当前上下文达到max的70%时触发压缩 reserve_recent_rounds: 6, # 压缩时保留最近几轮原文 summary_max_tokens: 600, # 摘要部分允许占用的最大token数 summary_model: claude-sonnet-4-20250514, # 执行摘要的模型 compaction_template: summarize_v2, # 摘要提示词模板编号 }max_context_tokens 怎么定不是拍脑袋写个 6000 就完事。我建议你先统计自己的业务场景找 100 条真实用户对话计算平均的“用户输入 系统提示词 预期回复”的 token 数然后在这个中位数基础上加 50% 到 100% 作为上限。比如你的业务里单条用户消息平均 80 token系统提示词 800 token预期回复 500 token那单轮基线大概在 1380 token 左右考虑多轮累积max 设到 5000 到 6000 是合理区间。summary_trigger_ratio 设 0.7 而不是 1.0是因为模型输出也需要 token你不能让上下文把窗口全占满留 30% 的余量给生成结果。reserve_recent_rounds 保留最近 6 轮是经过验证的经验值。太少了摘要还没来及吸收新信息模型就丢失短期记忆太多了压缩没意义。6 轮覆盖了大多数业务场景中“用户刚刚说过的话”。summary_max_tokens 设 600对应大约 450 到 600 字的摘要空间足够放下主要的决策结论和关键实体。还有一个容易忽略的参数摘要模型的选择。我倾向于用更便宜、更快的模型执行压缩而不是用和主对话相同的高端模型。因为压缩任务本身不复杂把一堆对话变成要点归纳中小模型完全能胜任成本能省一大截。3.3 核心代码实现一个带摘要压缩的上下文管理器下面这段代码是我从一个真实项目里简化出来的保留核心逻辑。它实现了上面说的五个环节你可以直接拿走改。import tiktoken class ContextManager: def __init__(self, config): self.config config self.history [] # 每项为 {role: ..., content: ...} self.summary # 压缩后的历史摘要 self.encoder tiktoken.get_encoding(cl100k_base) def estimate_tokens(self, messages): # 粗略估算token数不用非常精确够用即可 total 0 for msg in messages: total len(self.encoder.encode(msg.get(content, ))) return total def add_message(self, role, content): self.history.append({role: role, content: content}) def build_context(self, new_user_message): # 1. 构造当前完整上下文 system_prompt 你是一个专业助理请基于上下文与最新问题作答。 messages [ {role: system, content: system_prompt} ] if self.summary: messages.append({role: system, content: f[历史摘要]\n{self.summary}}) messages.extend(self.history) messages.append({role: user, content: new_user_message}) # 2. 判断是否触发压缩 current_total self.estimate_tokens(messages) max_tokens self.config[max_context_tokens] if current_total max_tokens * self.config[summary_trigger_ratio]: self._compact() # 压缩后重新构造上下文 messages [ {role: system, content: system_prompt} ] if self.summary: messages.append({role: system, content: f[历史摘要]\n{self.summary}}) messages.extend(self.history[-self.config[reserve_recent_rounds] * 2:]) messages.append({role: user, content: new_user_message}) return messages def _compact(self): # 3. 将被压缩的历史除保留的最近轮次交给摘要模型 reserve_count self.config[reserve_recent_rounds] * 2 to_compress self.history[:-reserve_count] or [] # 如果摘要已存在把旧摘要也纳入压缩范围 combined [] if self.summary: combined.append({role: system, content: f已有摘要:\n{self.summary}}) combined.extend(to_compress) if not combined: return prompt self.config[compaction_template] summary self.call_summary_model(prompt, combined) # 实际调用模型API self.summary summary # 4. 删除已被压缩掉的历史 self.history self.history[-reserve_count:] def call_summary_model(self, prompt, messages): # 这里接入你的模型API返回摘要文本 # 注意被压缩内容为空时不调用这是别忘的边界条件 pass这段代码有一个地方需要额外留意压缩触发的判断发生在 build_context 阶段但你 new_user_message 还没有写入 self.history所以不会重复计算。压缩后重建 messages 时我用了 self.history[-reserve_count * 2:] 这个切片指的是保留最近的 6 轮每轮对应一条 user 和一条 assistant共 12 条消息。还有一个特别容易踩的坑是如果你已经有旧的 self.summary一定要把旧摘要也作为输入喂给摘要模型让它“合并摘要”而不是“重新生成摘要”否则之前压缩出来的要点在二次压缩时可能会丢。很多上下文管理越做越糟糕就是因为每次只拿原文压缩忽略了历史摘要本身也是高价值信息。3.4 摘要提示词设计压缩效果好不好全看这里摘要压缩的效果不取决于模型而取决于你的 summary prompt。我调试过很多版最后总结出一套比较稳定的压缩提示词模板核心思路有三个提取事实、保留决定、标注未决问题。下面是一版可以直接用的模板请对以下对话历史做结构化摘要要求 1. 提取所有已确认的事实信息如订单号、地址、价格、时间、用户偏好保留原文数值。 2. 保留所有明确的决策结论格式为“用户要求/双方确认...”。 3. 列出尚未解决的问题或待办事项格式为“待办...”。 4. 不要添加原文没有的信息不要猜测用户意图。 5. 如果已有摘要作为输入请在保留旧摘要核心信息的基础上与新历史合并更新。 6. 总输出不超过 XXX token。最核心的是第 1 条和第 2 条。“保留原文数值”这一点尤其关键。我见过很多摘要把“订单号 839201”压缩成“订单号见上文”模型之后完全无法引用这是最常见的摘要失效原因。只要涉及数字、日期、金额、ID一定要要求模型原样保留。第 5 条也很重要它就是解决上一节说的“二次压缩丢信息”问题的关键。整个摘要模型调用流程一定要把旧摘要和待压缩历史一起传进去让模型做增量的“更新合并”而不是每次从零重写。4. 常见问题与排查技巧实录踩过坑的人才写得出来4.1 模型“失忆”明明压缩了关键信息还是丢了这是反馈最多的一个问题。排查思路要按顺序来。第一步检查摘要里到底有没有这个信息。把压缩后的摘要打印出来看看订单号、人名、具体的限制条件是否还在。很多时候是摘要模型执行不到位数值没有原样保留。解决办法是强化提示词第 1 条甚至在提示词里显式声明“所有数字必须出现且不得改写”。第二步检查摘要是否真的被注入到了系统提示词里。我调试时经常发现因为消息列表顺序写错摘要被放在了 user 消息之前模型把它当普通聊天内容而忽略了它的系统级权威性。第三步检查顶层上下文是否被截断。如果上下文窗口不够某些 SDK 会静默丢弃最早的消息你把摘要放在历史最前面反而被丢掉了。解决办法是把摘要放到 system prompt 里紧跟在主系统提示词之后。4.2 上下文“串味”模型被过时的摘要带偏摘要的更新滞后会造成信息不一致。举一个真实案例用户在第 3 轮说“我下周出差机器人先托管”第 8 轮又说“出差取消了我直接处理”按摘要优先的逻辑系统应该以第 8 轮为准但旧摘要仍然写着“用户出差托管模式”模型就会持续给出托管模式下的回复。这类问题的根源是摘要更新策略太死板。提高可用的经验策略是在构建上下文时把“最近保留的几轮原文”放在最后并且强制模型遵循“对话尾部优先于摘要”的规则。同时在摘要提示词里加上一句“如果已有摘要与后续对话冲突以对话原文为准并在新摘要中明确标注变更。”把变更检测纳入压缩流程而不是只做事实汇总。4.3 token 突然暴涨小心循环调用把摘要越压越大这个问题隐蔽性很强。系统跑了一段时间后摘要越来越大最后比原历史还长。原因多半是压缩提示词里没有给“摘要长度上限”模型在每次压缩时倾向于输出更长的文本更新合并后就不断膨胀。解决办法有两个。第一在压缩提示词里明确写死摘要长度上限我一般设“摘要不超过 400 token”并且持续在同一数值区间内。第二在代码层做硬限制调用完摘要模型后对返回的文本重新跑一次 token 估算如果超过阈值重新调用一次模型提示“请缩短上一版摘要至 400 token 内”。我用这种方式把摘要体积控制得很稳不会越滚越臃肿。4.4 独家避坑清单最后分享一份我自己总结的避坑清单都是测试时踩出来的。第一不要贪心保留太多历史轮次。你以为 10 轮原文加摘要是对模型好实际上模型更依赖最近的 3 到 5 轮内容保留太多原文反而干扰。第二压缩动作不要放在请求关键路径上同步执行。压缩本身也是一次模型调用会增加几百毫秒到几秒的延迟。可以改成先带着未压缩的上下文直接回复在后台异步执行压缩下一轮再用压缩结果。第三所有摘要和上下文管理逻辑都要有开关。上线前先做 A/B一组开摘要压缩一组纯全量历史对比业务指标确认摘要没有伤害核心效果再全量放开。第四raw token 估算用 tiktoken 这类工具别自己数汉字。一个中文字通常对应 1 到 2 个 token自己数很容易低估。我在实际项目中反复验证过这套方案最近一次是给一个电商客服机器人做上下文升级从全量历史改成摘要压缩模式后单日 token 成本降了差不多一半同时关键信息召回率只下降了不到 5 个百分点整体效果完全在可接受范围内。如果你也正在被上下文问题困扰建议先别急着换更大窗口的模型把你现有的 context-mode 好好打磨一遍这一层优化好了才是真正的降本增效。