
有没有遇到过这样的场景你对着一个“很聪明”的对话助手连续问了十几个问题前面的内容它还能记住但当你追问“刚才那份报告里提到的第三个结论是什么”时它要么给出似是而非的回答要么干脆说“我不记得了”。一开始你会以为是大模型能力不够后来做多了才发现大多数“失忆”根本不是模型的问题而是上下文管理的问题。这个坑我踩了很久最后折腾出一套叫 context-mode 的机制专门解决多轮对话里的上下文承载与切换问题。它能做的事听起来不玄乎把对话历史、系统指令、外部检索结果按策略分层管理让模型始终在有效上下文窗口内工作同时避免关键信息被截断或污染。如果你正在做聊天机器人、AI 助手、Agent 类的应用或者只是想把一个长对话跑得稳一点这篇文章应该能给你一些可直接落地的思路。1. 为什么需要 context-mode一次对话中“失忆”的本质再认识1.1 以为是模型不够聪明其实是上下文窗口不够用很多人对“上下文”的第一反应是上下文就是对话历史模型记不住就是历史太长。这个理解没全错但漏掉了两个关键点。第一上下文窗口不是无限的。以常见的商用模型为例就算窗口标称十几万 token真正跑到上限时表现也会明显变差。这个“变差”不是从窗口边界开始的而是在用到窗口七八成的时候就开始出现了指令遵循度下降、细节回忆出错、回答变得空洞。窗口越大这个衰减越隐蔽因为你以为还有空间实际上质量已经滑了。第二上下文不是简单的“越新越好”也不是“全都要留”。曾经我给一个文档问答助手做过一个粗暴方案把所有轮次的历史消息全部拼在一起发给模型。结果 token 用量猛增每次请求都接近窗口上限。更要命的是模型被海量历史“淹没”反而抓不住当前这轮问题的核心意图。这时候我才意识到上下文管理的核心不是“塞得下”而是“装得对”。1.2 context-mode 不是什么魔法而是一套管理机制我做的 context-mode本质上是一套上下文管理机制的组合。它把上下文拆成几个维度去处理系统层上下文角色设定、行为约束、输出格式要求这些是模型每轮都要遵守的优先级最高。业务层上下文当前任务需要的具体信息比如文档内容、数据库查询结果、工具返回值。对话层上下文用户和助手之间的历史交流包含意图演进和已确认的结论。临时层上下文当前这轮的输入、正在处理的中间状态用完就丢。context-mode 就是围绕这四个维度决定每轮请求往模型窗口里放什么、放多少、以什么顺序放。说得直白一点它不是一个模型而是一层“上下文调度器”。这套设计解决的不只是一个“失忆”问题它还顺带解决了几个很现实的问题token 成本控制、响应速度、以及多轮对话中“前后矛盾”的问题。后面我会详细拆它的工作原理先给一个直观的比喻如果把模型比作一个只能放四张桌子的会议室没有 context-mode 时你做的事情是把过去一百天用过的文件、照片、便签全部堆进会议室然后让新来的客人自己找重点。context-mode 做的就是把这次会议真正需要的三张桌子摆好其他东西归档到隔壁房间需要时再去取。2. context-mode 的工作原理拆解2.1 Token 是怎么被消耗掉的想理解 context-mode 的取舍逻辑首先得知道 token 花在哪。一个完整的请求token 消耗由几部分组成系统提示词、之前对话的摘要或原文、用户当前输入、模型回复的内容。很多人只盯着模型回得长不长忽略了系统提示词和对话历史才是大头。举个例子一个带角色设定的客服助手系统提示词可能就有 1500 token。如果对话历史直接拼接十轮每轮平均 800 token那就是 8000 token。再加上当前输入一次请求轻松冲上 10000 token。如果是高频率的实时对话场景一天下来光历史消息的重复传输就是一笔大开销。context-mode 的切入点就在这里对历史消息不做一刀切的保留而是按“对当前回答的贡献权重”来决定去留。这需要给每条历史消息打一个标记记录它的类型、时间、是否被后续消息修订过。我的实现在消息进入上下文管理器时会先做一次“价值分类”class ContextItem: def __init__(self, role, content, item_typemessage, timestampNone): self.role role # system / user / assistant self.content content self.item_type item_type # message / summary / retrieval / instruction self.timestamp timestamp or time.time() self.priority 0 # 优先级由 context-mode 策略动态调整 self.expired False # 是否可以被移出主窗口 def mark_priority(self, priority): self.priority priority每个 ContextItem 都携带元数据这样策略层才能做全局判断有些历史消息是用于理解用户习惯的有些是解决临时问题的有些则是对后续所有轮次都有影响的结论。优先级不同处理方式就不同。2.2 三级记忆结构在实际实现中我没有用单一的“历史消息列表”而是把记忆分成三级工作记忆、摘要记忆、检索记忆。工作记忆Working Memory当前对话轮次和最近几轮必须原文保留。这部分是模型理解当下语境的关键一般控制在总窗口的三分之一以内。摘要记忆Summarized Memory更早的对话定期被压缩成结构化摘要。比如“用户在上轮确认了运费计算方案选择按重量计费并约定下周五确认最终价格”。摘要不是把原文精简而是把与未来轮次相关的信息抽取出来。检索记忆Retrievable Memory更早期或更细节的内容不主动放进窗口但保留在本地索引中。当用户提到“刚才那个文件”“之前说的那个方案”这种模糊指代时通过意图识别触发检索再以临时上下文的形式注入当前轮次。这个三级结构的灵感其实来自人怎么记事情你做当前任务时脑子里只装这件事相关的信息更早的事情靠“想一下”才能想起来。context-mode 就是把“想一下”变成一个明确的模块而不是让模型在长文本里漫无目的地翻找。2.3 四种上下文策略context-mode 不只是“截断历史”那么简单。根据场景不同我实现了四种策略可以组合使用策略核心思路适用场景代价滑动窗口保留最近 N 轮原文更早的直接丢弃闲聊、FAQ、实时问答实现最简单但丢失长期信息滚动摘要超出窗口时旧内容合并进摘要摘要重写为新的“最旧一轮”连续任务、逐步推进的场景摘要会累积误差需要校验检索增强主窗口只放系统指令当前输入历史内容按需检索注入知识库问答、文档处理、Agent依赖检索质量增加一次检索延迟分层混合系统指令固定工作记忆保留部分摘要兜底检索补充绝大多数生产场景配置复杂需要调参我在项目里默认用的是分层混合因为单靠滑动窗口经常出现“几轮之前才说过的重要指令被挤掉”的问题单靠检索增强则会在用户没说清楚指代时漏信息。3. 落地实现从 API 接入到自定义 context manager3.1 依赖准备先交代一下环境。整个系统我用 Python 写的核心依赖不多openai的 API 客户端兼容其他模型的服务商的 OpenAI-compatible 接口也一样faiss或简单的sqlite-vec做向量检索也可以先用内存里的 numpy 余弦相似度凑合pydantic做配置和数据结构校验防止上下文管理器的输入字段漂移如果只是想先跑通流程检索部分完全可以用最“原始”的办法用模型为历史消息生成关键词标签存到 JSON 里再做简单的关键词匹配。这套方案虽然召回率一般但胜在任何环境都能跑。3.2 核心的 context-manager 代码context-mode 的核心是一个上下文管理器类逻辑其实不复杂关键是分层清楚。这里给一个可运行的简化版import time import json from typing import List, Dict, Optional from dataclasses import dataclass, field dataclass class ContextManager: system_prompt: str max_working_tokens: int 3000 max_summary_tokens: int 1000 max_total_tokens: int 8000 working_memory: List[ContextItem] field(default_factorylist) summary_memory: List[ContextItem] field(default_factorylist) retrieval_store: Dict[str, ContextItem] field(default_factorydict) def add_user_message(self, content: str): item ContextItem(roleuser, contentcontent) self.working_memory.append(item) self._maybe_compress() def add_assistant_message(self, content: str): item ContextItem(roleassistant, contentcontent) self.working_memory.append(item) self._maybe_compress() def _maybe_compress(self): # 粗估当前工作记忆的 token 占用 estimated_tokens sum( len(item.content) for item in self.working_memory ) * 1.3 # 中文字符与 token 的比例近似 if estimated_tokens self.max_working_tokens: self._compress_working_memory() def _compress_working_memory(self): # 保留最近两条其余合并成摘要 recent_items self.working_memory[-2:] older_items self.working_memory[:-2] if older_items: combined \n.join( f{item.role}: {item.content} for item in older_items ) summary_item ContextItem( rolesystem, contentf【历史摘要】{combined[:self.max_summary_tokens]}, item_typesummary ) self.summary_memory.append(summary_item) self.working_memory recent_items这段代码刻意做得简单演示的是“触发压缩”的时机。真实项目里_compress_working_memory不会硬截断而是调用一个摘要模型来生成高质量压缩甚至可以迭代式地把老摘要与新摘要合并。3.3 模式切换与注入光有存储还不够context-mode 的重头戏在“组装请求”这一步。每次调用模型前上下文管理器会生成一份注入计划def build_messages(self, current_input: str, retrieval_results: Optional[List[str]] None): messages [] # 第一层系统指令永远在最前面 messages.append({role: system, content: self.system_prompt}) # 第二层检索结果如果有标注来源 if retrieval_results: retrieval_block \n\n.join( f[检索片段 {i1}] {text} for i, text in enumerate(retrieval_results) ) messages.append({ role: system, content: f以下是相关知识片段仅作为参考\n{retrieval_block} }) # 第三层历史摘要压缩后的长期记忆 if self.summary_memory: summary_text self.summary_memory[-1].content messages.append({role: system, content: summary_text}) # 第四层工作记忆最近几轮 for item in self.working_memory: messages.append({role: item.role, content: item.content}) # 第五层当前输入 messages.append({role: user, content: current_input}) return messages这个注入顺序不是随便定的。系统指令在最前是为了让模型先建立行为框架检索结果紧跟系统指令是为了让它把外部信息当作“参考资料”摘要放在中间提供隐性背景工作记忆靠后保持最近对话的连贯性最后是当前输入确保模型注意力集中在最新问题上。类似这样一版代码就把“上下文按层级装入请求”这件事落地了。你完全可以用它替换掉原来的简单历史拼接效果会立竿见影。比如不再随机“忘记”几轮前确认过的信息不再把检索到的无关内容塞进窗口浪费 token。4. 上线之后踩过的坑与对应解法4.1 截断策略剪掉了系统指令第一次把 context-mode 接入线上环境时我犯了个典型错误压缩工作记忆时只按 token 量从前往后删结果一次用户连续追问后系统指令被“优化”出去了。现象是模型突然变得“没规矩”本来约定了必须按 JSON 格式输出突然开始输出多余的解释文字本来要求遇到不确定时回答“需要确认”突然开始编造。排查了很久最后在组装请求的日志里发现messages 的第一条变成了历史摘要系统提示词根本没进去。解决方式是在组装层加一个强制校验无论工作记忆怎么压缩系统指令都必须独立占一个位置任何压缩逻辑都不允许动它。def build_messages(...): # 禁止压缩系统指令每次都从原始配置读取 assert len(self.system_prompt.strip()) 0, system prompt is empty这看起来是小事但对生产环境来说这类问题非常隐蔽。因为模型不会报错只会“行为异常”而行为异常往往要到用户投诉才被发现。4.2 摘要机制把关键结论“压没了”后来摘要策略上线后又出现了新的问题。我原本以为摘要能保留“长期重要信息”结果发现摘要模型在压缩时有个倾向它会保留对话中的细节例子却把用户明确拍板的结论给省略了。举个例子用户和助手反复讨论三种方案后说“就按方案 B 来但价格上限控制在 5000 元。”这一句如果被摘要成“用户倾向方案 B”后续轮次就丢了“5000 元硬约束”这个关键信息助手可能会推荐 6000 元的东西。应对办法在摘要压缩时不是自由发挥而是要求模型按固定模板提取“决策、约束、待办、偏好”四类字段。summary_prompt 请将以下对话压缩为主题摘要必须包含以下字段 - 用户已确认的决策包括具体数字、时间、范围 - 未解决问题 - 明确的约束条件预算、期限、格式等 - 用户偏好 只提取与未来相关的信息不要保留寒暄和重复内容。 对话 {conversation_text} 摘要质量最怕的不是不完整而是“看起来完整但其实丢了硬约束”。我为这个加了一条自动校验规则压缩结果里如果出现了“用户说”“用户表示”这种模糊措辞就重新生成直到出现明确的值为止。 ### 4.3 检索注入导致上下文互相污染 使用检索增强模式后我又在一处栽了跟头。情况是这样的知识库里有内容说“接口限流策略每用户 100 次/分钟”另一处文档写的是“接口限流策略每用户 1000 次/分钟”。两者被检索出来后同时注入上下文模型直接懵了给出的答案取决于它在窗口里更“偏爱”哪一段的位置。 这其实不是检索的问题而是上下文组装的问题。检索结果并不是越多越好注入的每一段都要符合两个条件和当前问题明确相关且来源之间没有明显冲突。 后来我在检索注入前加了一道“一致性粗筛”从检索结果里抽关键词做互相比较命中率超过阈值时只保留相关性最高的一条如果两条都保留至少在 prompt 里同时标注它们的来源版本让模型自行遵循“最近更新优先”的规则。 提示如果你在做一个查询量很大的知识问答系统建议在检索结果里带上每条内容的“更新时间”和“置信度”。模型对带元数据的内容比光秃秃的文本片段要敏感得多错误率会明显下降。 ## 5. 调参思路与不同场景的组合策略 ### 5.1 参数选择建议 context-mode 里的几个关键参数没法用一个默认值应付所有场景只能给一个我觉得比较稳妥的起点 | 参数 | 建议值 | 说明 | |---|---|---| | 工作记忆 token 上限 | 总窗口的 30% 左右 | 太少会让最近对话断片太多会挤压检索和摘要空间 | | 摘要记忆 token 上限 | 总窗口的 10%-15% | 摘要只是为了兜底不需要塞太多 | | 检索结果条数 | 3-5 条 | 超过这个数量模型开始出现信息混淆 | | 摘要压缩触发阈值 | 工作记忆超过上限的 80% | 留出余量避免单轮超限后再被动处理 | 这些值真的要结合自己的场景去试。如果你的助手是客服型用户问题短、轮次多可以适当调大工作记忆占比因为每轮的有效信息量密度低如果你的助手是文档分析型检索结果才是主角工作记忆反而该缩小一点。 我自己在实际调试时有一套固定流程先用 20 条真实对话记录做测试开启 context-mode 前后对比回答质量如果发现模型出现“重复提问已经给过的信息”说明工作记忆太短如果发现模型把检索结果当成事实直接陈述说明检索注入缺少“仅供参考”的边界描述。 ### 5.2 场景化策略矩阵 最后整理一份场景与策略的对应表方便你拿到项目里快速做选择 - **智能客服 / 售前咨询**分层混合 滑动窗口工作记忆保留最近 6-8 轮摘要每 10 轮压缩一次。客服场景里用户说“我之前问过”很可能是 20 轮前的事这时候需要检索记忆兜底不能只靠最近几轮。 - **代码生成 / 代码审查**滑动窗口 系统指令。代码相关任务里系统指令里的代码规范最重要历史对话反而次要窗口里把当前文件内容和新需求放在最前就行。 - **知识库问答**检索增强 分层混合。主窗口不想保留太多历史关键是检索质量每次请求都要注入“检索内容可能有误请结合用户问题判断”。 - **Agent 多工具调度**分层混合 完整的摘要记忆。因为 Agent 的多轮行动会产生大量中间结果这些结果不适合全部放窗口但也不能丢建议每个工具调用的输入输出单独记录一份摘要时按“已完成动作 / 当前待办”分类保存。 ### 5.3 一个值得做的扩展上下文状态可视化管理 跑通了上述所有功能后我额外加了一个“体检”用的管理页面。它能在每次请求后展示这次请求里系统指令占了多少 token、检索注入占了多少、工作记忆里有几条、摘要压缩发生在哪一轮。作用是在调试时快速定位问题模型回答变差到底是检索坏了、摘要丢了、还是工作记忆被挤爆了。 这个可视化的实现很简单就是在 build_messages 的返回结果旁挂一个诊断结构 python diagnostics { system_tokens: len(system_prompt) * 1.3, retrieval_tokens: sum(len(r) for r in retrieval_results) * 1.3, summary_tokens: len(summary_memory[-1].content) * 1.3, working_items: len(working_memory), compression_count: compression_count, }每次请求都打印一份几分钟就能看出上下文分布的规律。很多人以为 context-mode 调优是玄学其实数据一摆出来问题都很直白要么是某项占比畸形要么是压缩太频繁导致历史反复丢失。从最初为了解决“模型失忆”到后面逐步发展成一套包含记忆分层、检索注入、策略切换、可视化的完整机制context-mode 在我的项目里已经取代了原来“无脑拼接历史”的做法。它的接入成本不算高核心就是一个消息组装器加一个压缩调度逻辑但带来的变化是实打实的长对话漂移少了token 开销降了响应也快了。如果你也在做这类应用我的建议是别急着上花哨的 Agent 框架先把上下文这一层管理好很多复杂问题自然会消失。