大模型长对话上下文管理与记忆保持的工程实践 1. context-mode 到底是什么以及为什么系统需要它1.1 从一次线上事故说起前几天我接手了一个智能客服机器人的排查任务现象很典型用户在前面还聊得好好的到第 40 轮左右就开始“胡言乱语”甚至把已经确认过的收货地址又报错一遍。我第一反应是模型参数没调好prompt 写得不够严谨于是反复改 system prompt结果毫无起色。后来把线上 request 抓下来一看才发现真正的问题出在 context-mode 上我把整段对话历史原封不动地塞进上下文中间还包括十几条工具调用返回的 JSONprompt 体积膨胀到离谱模型在长文本里根本抓不住关键信息。这个案例其实每天都在无数 AI 应用里重演。model 本身很强但“喂给模型什么、按什么顺序、以什么形态”这部分却经常被当成一个不起眼的拼接逻辑。context-mode 这个名字听起来像是某个框架的功能开关但我在实际工作中越来越觉得它更像是一整套上下文管理的“中间层”策略决定哪些内容进入模型视野、哪些被压缩、哪些被直接丢弃、哪些要落盘持久化。你完全可以把它理解成给大模型准备“工作台”的方式而不是某个单一的开关。1.2 有限窗口与无限对话的矛盾大模型上下文窗口再大也是有限资源而对话在真实产品里是无限延伸的。就算你说有百万 token 的窗口也不可能永远堆下去延迟和成本都会先把你压垮。更麻烦的是上下文里不只是对话历史还有 system prompt、用户当前输入、检索回来的知识片段、工具调用的返回结果、临时推理过程。这些东西的“新鲜度”完全不一样用户这句话肯定要优先保留昨天的寒暄就可以扔掉工具返回的一长串 JSON 里可能只有三个字段真正有用。我习惯用一个生活化的类比上下文窗口就是一张办公桌。桌面空间永远有限你可以摆开正在处理的文件但档案柜里能存的东西多得多。context-mode 要做的就是在每次调用模型之前决定哪些文件摊在桌面上、哪些收进档案柜、哪些直接进碎纸机。如果桌面一直堆满所有历史文件你不仅找东西慢还容易盯错重点。理解了这层关系再去看各种上下文处理策略思路会清晰很多。2. 上下文窗口的量化与预算管理2.1 token 估算别再数“字数”了很多新手最容易犯的错就是拿“字符数”当“token 数”来规划上下文。中文字符和 token 不是一比一的关系一个汉字在常见分词器下大约对应 0.6 到 1.5 个 token英文单词平均也可能被拆成多个子词。如果按字数字面估算预算会严重偏离实际等到线上日志一查才发现超出窗口上限所有上下文策略等于白做。我现在的做法是在项目启动阶段就用模型配套的 tokenizer 或者开源分词库把真实语料跑一遍建立一份自己的“换算系数表”。这个系数跟业务领域强相关技术文档类内容、客服对话、代码片段系数都不一样。代码示例def estimate_tokens(text, tokenizerNone): if tokenizer is None: # 没有 tokenizer 时的兜底估算中文按每字约 0.7 token英文按每词约 1.3 token return int(len(text) * 0.7) return len(tokenizer.encode(text))线上环境我会在每次请求响应时把 prompt 的 token 总量、生成部分的 token 量、各内容区块的 token 占比全部打到日志里。这不只是用于监控成本更重要的是沉淀数据帮你在后续优化 context-mode 时有依据。没有这些数据你所谓的“上下文优化”就只能是拍脑袋。2.2 上下文预算分配公式与优先级规则既然窗口有限就必须做预算。我的经验是总预算不要顶满模型的硬上限要留出 10% 到 15% 的余量给模型生成回复用。如果模型窗口是 8000 token那 context-mode 里所有可管理内容的预算上限就按 6800 到 7200 来规划其余留给生成空间。在这个总预算内我一般会按档次分配各区块占比。下表是我在长对话场景里调过很多次之后沉淀下来的一个参考配置内容区块建议占比说明system prompt 与静态指令5%-10%只放角色定义、硬性约束、回复格式用户当前问题与意图15%-20%最新输入必须完整保留这是最高优先级近期对话原文30%-40%最近 3 到 5 轮的原文保留细节历史对话摘要20%-30%更早内容的压缩形态只保留关键事实检索增强知识10%-20%RAG 召回的片段按相关度筛选工具调用返回5%-10%只保留对当前回复有用的字段这个比例不是死的但你一定要有“预算内排序”的思维。我的优先级规则是当前意图高于关键约束关键约束高于近期原文近期原文高于历史摘要历史摘要高于无关闲聊。也就是说当预算不够时最先被删的应该是寒暄和与当前任务无关的历史内容而不是用户在第 2 轮提过的关键条件。很多“AI 失忆”现象追根溯源都是因为系统把用户的关键约束当普通历史消息一样滑出了窗口。3. 三种最常用的上下文组装模式3.1 滑动窗口模式简单直接适合短会话滑动窗口是最容易上手的一种 context-mode 实现核心思想就是一句话只保留最近 N 轮对话原文更早的直接裁剪。我早期做第一个聊天机器人时用的就是这个一行代码就能实现运行也快几乎没有额外开销。def build_sliding_window(messages, max_rounds6): return messages[-max_rounds:]但它的代价也很明显超出窗口的历史信息会“硬丢失”而且是不可恢复的丢失。如果用户在第 10 轮说过“我预算在 5000 块以内”到第 25 轮模型早把这条件忘了。所以我现在的判断是滑动窗口只适合短会话、一次性问答、客服转接这类场景或者作为其他模式的基础组件。在真正的长对话系统里单靠滑动窗口几乎必然导致失忆问题。3.2 摘要压缩模式记忆不失真适合长会话摘要压缩模式就是为了解决滑动窗口“硬丢失”的问题出现的。思路不复杂每过一定轮数把旧对话交给模型整理成一段结构化摘要然后只保留摘要和最近的原文。这样一来历史信息没有彻底消失只是换了一种更节省空间的形态。实际落地时有个关键选择固定步长压缩还是连续滚动摘要。固定步长是说每隔 10 轮触发一次压缩把前 10 轮压缩成摘要。连续滚动摘要则是每轮都在原摘要基础上增量更新像记账一样持续维护一份“最新版记忆”。我建议在大多数业务场景里用连续滚动摘要因为固定步长容易产生“断档期”刚压缩完还好跑到第 15 轮的时候第 5 轮之前的细节已经没了但距离下次压缩还有 5 轮这段时间上下文里全是原文很快又会撑爆预算。增量更新的代码思路大概是这样的def incremental_summary(old_summary, new_messages, llm): content f已有摘要\n{old_summary}\n\n新增对话\n{new_messages}\n\n请合并为一版新摘要。 return llm.generate(content)要注意摘要压缩是有损压缩。模型在压缩过程中可能丢掉数字、日期、人名这些精确信息也可能在归纳时顺手编造一些不存在的结论。所以我后来凡是做摘要都会在压缩 prompt 里明确要求“不要补充任何原对话中不存在的事实保留全部数字、日期、金额、地址等硬信息”。这一条能显著减少后续幻觉。3.3 分层混合模式我的首选工程方案真正到了生产级的长对话系统我的首选方案是把上面两种模式组合成分层混合结构这也是我对 context-mode 理解最深的一层。它跟计算机体系结构里的分层存储很像寄存器、缓存、内存、磁盘各管一段各有各的速度和容量。我目前常用的分层结构是这样四层核心记忆层Core Memory包含 system prompt、用户档案、业务硬约束、跨会话持久偏好基本上永远不裁剪token 占比控制在 10% 以内。工作记忆层Working Memory最近 3 到 5 轮对话原文模型推理时主要依赖这一层的内容。摘要记忆层Summary Memory更早对话滚动压缩出来的摘要按时间分段存放需要时再取。知识窗口层Knowledge WindowRAG 检索回来的外部资料只保留与当前问题强相关的片段。组装的时候不是简单把四层拼在一起而是先做预算检查。如果总 token 超出预算先砍知识窗口再压缩摘要记忆层最后才会动工作记忆层。核心记忆层原则上不参与裁剪。这套模式看起来比前两种复杂但稳定性和可维护性远高于单靠任何一种策略。4. 检索增强下的上下文注入策略4.1 检索内容该放在哪一区如果你的产品接入了知识库检索context-mode 就会多一个棘手问题检索回来的内容到底插在哪我见过不少团队直接把召回结果拼在 prompt 最前面结果模型把一大段背景资料当成主任务用户的问题反而被淹没了。我踩过几次坑之后形成了一套固定位置规则把检索内容放在用户问题之后、历史摘要之前。原因很简单模型对越靠近当前输入的位置注意力越敏感用户当前问题必须保持在它“视野”最近的区域。检索知识是辅助材料放在问题后面是让它紧接着当前上下文出现又不至于喧宾夺主。另外每条检索片段我都要求带上元信息例如来源编号、更新时间、置信度这既能帮助模型判断资料可信度也方便后续追溯到具体文档。检索块本身的长度也要控制。单条检索内容超过一定长度时必须先切片再注入。我的经验是单条片段控制在 150 到 300 token 之间太长的话细节密度反而下降模型容易看花眼。宁可多取几条短的也不要塞一大块冗长文本。4.2 相关片段重排与去噪的实操心得很多团队做 RAG 时向量检索完就直接把 Top K 结果全塞进上下文这是不对的。向量相似度只能代表语义层面的初步接近不代表每条结果对当前问题都真正有用。我在 context-mode 的检索注入环节里会额外加一道重排步骤而且重排不只看向量分数还要叠加几个惩罚项。第一个惩罚项是位置重复如果多个片段来自同一文档的相邻位置内容一定高度重叠留着纯粹浪费 token必须去重。第二个是时间衰减同样是讲产品政策半年前的政策和三天前的新政策后者权重应该更高。第三个是意图漂移检索结果如果偏离用户当前问题的核心意图即使语义分数不低也要降权。我还会在每个检索片段前面加标注比如“[资料1]”“[资料2]”然后告诉模型引用外部信息时必须在括号里标注来源编号。这样模型就不太敢凭空编造因为引用格式已经限制了它。代码层面重排的逻辑大致是这样def rerank(docs, query, threshold0.6): scored [] for idx, doc in enumerate(docs): score semantic_score(query, doc) if score threshold: continue if is_near_duplicate(doc, scored): score * 0.5 score * time_decay(doc.timestamp) scored.append((score, idx, doc)) return sorted(scored, reverseTrue)这套组合策略在问答准确性上的提升非常明显。同样的知识库不做重排时模型经常引用矛盾信息做完重排之后回复的一致性和可解释性都上了一个台阶。5. 常见问题与排查技巧实录5.1 现象模型“忘掉”了用户最初的要求这个问题几乎是每个长对话系统都会遇到的。用户在第 3 轮说“不要含糖的”第 28 轮问“那你推荐哪个”模型推荐了一个明显含糖的产品——这就是失忆。我排查时第一步不是看模型而是看 context-mode 的预算分配这个关键约束当时在哪个区块里如果它在滑动窗口模式的裁剪范围之外直接被丢了那模型再强也白搭。解决思路是把用户的关键约束识别出来提升到核心记忆层。具体来说就是在对话过程中用规则或一个小模型专门抽取“约束类信息”例如否定词、数字、价格上限、明确偏好一旦抽取到就写入核心记忆层的用户档案里。这样一来即使相关原文被滑出窗口约束依然还在。这个改动听着简单但效果立竿见影。5.2 现象上下文越长响应越慢上下文膨胀带来的问题不只是成本和注意力稀释响应延迟也会肉眼可见地增加。很多团队贪图省事每次请求都重新对全量历史做摘要等于每轮对话都跑一遍大模型压缩任务延迟自然高得吓人。我常用的优化手段有三招。第一招是缓存静态区块system prompt 和固定的用户档案基本不变完全可以提前拼好并缓存不用每次请求都重新构造。第二招是增量摘要不要每轮都全量重算只在累计新增内容达到一定阈值时才触发一次摘要更新。第三招是语义缓存如果用户近期问了类似问题直接复用上一次的完整回复和上下文组装结果省掉一次模型调用。这三招加起来我实际项目里的平均响应时间大概降了 40%。5.3 现象压缩后模型开始“胡编”摘要压缩模式跑一段时间后有些团队会发现模型开始出现“记忆错误”甚至一本正经地编造用户没说过的话。我排查这类问题有一个固定套路先调出压缩前后对比看看哪些硬信息在压缩中被弄丢了或者被模型自己“脑补”了。根因往往就集中在压缩 prompt 写得不够严谨。比如你只是说“请概括以上对话”模型就可能顺手把一些不确定的内容当成事实写进摘要。我的解法是在压缩 prompt 里加负面约束并且要求摘要按结构化格式输出用户诉求、关键事实、待办事项、涉及的数字信息分别列出来。压缩完以后我还会抽测其中几条跟原文比对一致性。如果发现有明显矛盾说明压缩策略需要调整而不是模型的问题。下面这个速查表是我整理给自己团队用的遇到类似问题可以按图索骥现象可能原因排查方法处理建议模型忘记早期关键约束关键约束被滑出窗口检查预算分配与优先级规则将约束抽取到核心记忆层响应越来越慢上下文全量组装、摘要重算频繁查看日志中 prompt token 趋势缓存静态区块、增量摘要、语义缓存压缩后出现事实错误摘要丢失硬信息或模型脑补对比压缩前后内容压缩 prompt 加负面约束、结构化输出回答引用矛盾资料RAG 结果未重排、重复内容多检查检索片段重复率和来源增加重排和去重逻辑我在实际项目里还有一个习惯把每一次压缩前后或裁剪前后的上下文快照都留一份日志。这样一旦线上反馈异常我能直接复盘“当时模型看到了什么”。这个习惯帮我解决过至少三次莫名其妙的线上问题比事后猜原因高效太多。6. 踩过这么多坑之后我的一些体会如果只能留一条经验我会说上下文不是缓存而是产品的一部分。你裁掉的每一段内容用户都可能感知为“AI 失忆”你塞进去的每一段冗余都可能稀释模型对关键信息的注意力。所以 context-mode 从来不是一个可以随便拼一下的代码模块它在很大程度上决定了用户对“AI 是否聪明”的整体印象。最后再分享一个小技巧每次上下文策略改动上线前我都会专门构造一组“穿越测试”用例把几十轮前的关键信息翻出来问模型看它还能不能准确回答。这种用例不用太多二十条就够但比看几个 demo 试玩靠谱得多。只要这组用例稳定通过你再去做预算压缩、摘要裁剪、检索注入心里都有底。context-mode 这件事没有一劳永逸的方案它是一个需要持续观察和调优的系统工程希望这篇整理能帮你少走一些弯路。