
最近有个词在 AI 应用开发圈子里越聊越热——context-mode。如果你也在做 Chatbot、智能体或者 AI 工作流大概率已经遇到同一类问题对话一长模型就开始失忆回着回着就把用户几分钟前说过的话忘得一干二净回答质量断崖式下跌。我在好几个项目里都踩过这个坑后来才意识到问题根本不在于模型选得不够好而在于我们压根没有认真对待上下文管理这件事。这篇文章想跟你聊清楚 context-mode 到底是什么、常见的实现方式有哪些、我在真实项目里是怎么落地和调优的以及那些说出来都是泪的翻车场景。适合刚想把 AI 接入业务、又不想在上下文处理上栽跟头的朋友也适合已经在跑多轮对话但总觉得哪里不对劲的开发者。读完你会发现上下文管理不是附加题而是必答题。1. 从一次对话翻车说起为什么上下文成了新的性能瓶颈1.1 一个典型的上下文溢出事故先讲个真实案例。大概半年前我在做一个客服问答机器人模型用的是一线主流的大模型 API配置上自认为没什么问题。一开始用户问的是你们退货政策是什么这种单轮问题机器人答得漂漂亮亮。可一旦用户开始连续追问比如先问笔记本能退货吗再问如果已经激活了呢接着问那运费谁出我发现模型开始出现自相矛盾的回答。第一次注意到这个问题是在一条对话日志里。用户在第 17 轮问了一句我刚才说的型号是哪个机器人居然回复了一段礼貌的道歉说自己没有找到相关记录而那个型号就明明白白出现在第 3 轮的消息里。不是模型傻是我发给模型的上下文里第 3 轮早就被挤掉了。当时我用的还是最粗暴的方式把最近 20 轮对话全部塞给模型。但由于每条消息还带着时间戳、用户 ID、来源渠道等元信息换算成 token 之后20 轮下来已经逼近模型的上下文窗口上限。后排消息被系统静默丢弃而这种丢弃完全没有规律连我的日志里也看不出来。1.2 上下文窗口不是越大越好很多人听到这里的第一反应是那换一个上下文窗口更大的模型不就行了这个思路方向对但做产品的人不能这么天真。窗口大只代表能装的容量变大了不代表值得装的内容变多了。模型拿到超长上下文时对中间部分的注意力权重会显著下降注意力机制会偏向开头和结尾。心理学上有个序列位置效应在 LLM 身上也观察得到开头的内容被记得相对牢首位效应结尾的内容刚刚看过也记得牢近因效应中间部分最容易被忽略或者读不细。而且窗口变大成本不是线性增长。那个 O(n²) 的注意力计算一直在那里虽然现在工程优化做了很多但长上下文请求的延迟和费用依然非常可观。我算过一笔账一个日活 5000 的客服机器人如果每轮请求都传 20 轮历史外加知识库资料光是输入 token 的成本一个月就能吃掉我一半的服务器预算。这还是用的中等价位模型如果换成更强的旗舰模型账单会直接起飞。1.3 被忽略的时间维度其实大多数开发者的上下文管理都停留在空间维度也就是看窗口够不够大、内容塞不塞得下。但上下文本质上还有时间维度哪些信息是长期稳定的哪些信息只在这轮对话里有效哪些信息已经过期了举个直观的例子用户在一个购物助手里说了我预算 5000 以内这句话在第 2 轮有效到第 20 轮依然应该被记住。但如果用户在第 12 轮又说了预算提到 8000 了那么第 2 轮的旧预算就是过期信息必须被覆盖。不做时间维度的管理模型会把新旧预算同时视为有效约束然后给出一个让你哭笑不得的回答5000 到 8000 之间都可以选。所以 context-mode 这个名字虽然听起来像某个具体功能但本质上是一整套让模型看到什么、记住什么、忘掉什么的策略组合。2. 拆开 Context Mode 的黑箱四种典型模式与底层取舍在我接触过的项目和同行交流里实际在用的 context-mode 基本可以归纳成四种各有各的适用场景也各有代价。模式核心思路适合场景主要代价失败表现Full Context 全量直传所有历史消息原样带上传给模型短会话、对完整性要求极高的场景成本高、超窗丢消息无规律失忆Sliding Window 滑动窗口只保留最近 N 轮或最近 M 个 token多轮闲聊、题目不依赖太久远信息早期关键信息丢失用户说过的话被忘掉Summarization 摘要压缩把旧历史先总结成摘要再携带摘要超长会话、需要长期记忆细节、数字、代码会丢回答变成了大概齐RAG-Context 检索增强不传全部历史按需检索相关内容知识库问答、客服系统检索不准就带错上下文一本正经地胡说八道2.1 Full Context简单但有天花板Full Context 是最容易理解也最容易实现的模式。就是把 messages 数组整个拼好一骨碌发给模型接口。它的最大优点是忠实模型能看到每一个字理论上只要窗口够大就不会出现历史被截断的问题。但它的天花板特别明显。第一是成本每轮对话都在重复发送全部文本这些历史消息每次都要重新计费。第二是延迟请求体越大首字返回时间越长。第三是干扰上下文越长模型越容易被无关细节带偏回答反而不如短上下文更聚焦。我的使用经验是当会话轮数少于 8 轮或者单次任务对前后文一致性要求极高时比如代码审查用 Full Context 是没问题的。一旦聊到十几轮还继续用就要做好账单和延迟一起涨的心理准备。2.2 Sliding Window最常见的偷懒方案Sliding Window 是我最开始用的方案。维护一个固定大小的队列比如最近 10 轮新消息进来就把最老的挤出去。实现起来大概十几行代码确实能控制成本也不会超窗。但它的问题很隐蔽它均匀地遗忘所有旧消息。用户在第 2 轮说的重要约束、在第 5 轮提供的身份背景到了第 11 轮就一视同仁地被挤掉了。而模型不会告诉你你给我的历史里少了东西它只会基于残缺信息一本正经地补全最后补出来的内容往往让你莫名其妙。这类场景最典型的就是客服里问我上次买的那个东西。如果上上次对话已经发生在 20 轮之前滑动窗口早就把它丢掉了。你以为是模型记忆力不行其实是你的滑动窗口策略做了减法但毫不知情。2.3 Summarization为记忆做压缩后来我转向了 Summarization。逻辑很直观当历史消息超过某个阈值先把旧内容交给一个总结模型生成一段摘要再把摘要当做一个系统消息放在对话头部。这样既保留了长程信息又严格控制了 token 消耗。这个方法救回了不少失忆场景但很快暴露出新的问题摘要会丢掉精确细节。用户之前说我要深圳发货、预算 8000、周二之前送到摘要可能会被压缩成用户有物流和预算要求。单独看这句话没毛病但模型一旦需要具体执行就会无从下手。再一个坑是摘要模型本身也会受长度限制。当历史超过几十万字摘要本身也需要分片压缩再归并这个链路一旦做不好摘要质量就越来越差最终变成一个幻觉浓缩机。2.4 RAG-Context让上下文变成按需取用RAG 已经不算新概念了但我这里想强调的是把 RAG 和对话历史结合起来的模式不是一股脑地把历史发给模型而是先判断这条问题需要哪些历史信息再用向量检索和重排序把最相关的几段历史拿出来。这样上下文永远保持精简而且带着明确相关性。我之前在一个法律咨询项目里用过这种组合。用户的对话历史特别长但每一轮的核心问题都集中在某一个具体合同条款上。用 RAG 把相关段落捞出来之后模型回答的准确率明显提升而且 token 消耗降到了原来的四分之一。但 RAG 的代价是把上下文管理又变成了检索质量的问题。检索 Top-K 取多少、向量是否区分了不同的语义空间、重排序模型怎么选每一步都有细节坑。检索一旦捞错模型就会拿着不相关的上下文给出一个自信但错误满满的回答。3. 落地 Context Mode 的完整方案从日志分析到关键路径改造理论说了不少回到实操。我把我现在做的一个项目拆出来给你看这套方案在我自己的客服、知识库、写作辅助工具三个场景里都跑通过你可以直接借鉴。3.1 第一步先装上上下文探针没有日志就没有优化别急着改逻辑先看清楚你的系统现在是怎么处理上下文的。我强烈建议先给对话接口加一套探针日志记录以下关键字段请求唯一 ID、会话 ID、用户 ID当前消息总长度token 数上下文结构历史消息条数、每条的 role、每条的 token 数是否发生截断、截断位置在哪模型返回的回答、总 token 数、耗时用户的后续反馈点赞/点踩、是否追问同一话题有了这批数据你才能回答三个核心问题我的对话一般第几轮开始劣化劣化时上下文里到底还有多少内容模型答错的时候上下文里缺了什么我当时在项目里用了一个非常轻量的方案把探针日志写到一个单独的 JSONL 文件里每天跑一次统计分析再用脚本把模型回答被用户点踩的样本和上下文结构关联起来。这一步救了我因为我发现 70% 以上的点踩样本都发生在上下文被截断的请求上。有了这个数字后续优化才有方向。3.2 第二步按场景拆分对话路径动态选择模式一个真实的业务系统里不可能只有一种上下文模式。我给项目设计了四条路径用一个简单的规则引擎做分流def route_request(session, query): # 会话轮数小于 8全量直传 if session.turn_count 8: return full # 会话轮数中等但用户有明确长期偏好摘要模式 if session.turn_count 30 and session.has_preferences(): return summarization # 会话轮数多但问题高度分化检索增强 if session.turn_count 30 and session.query_diversity 0.7: return rag # 兜底滑动窗口 return sliding_window这个规则看起来很粗糙但实际跑起来很稳。判断依据不是看上去很智能的机器学习模型而是可解释的启发式规则。你完全可以按自己的业务特点调整参数比如客服场景里用户偏好信息很关键那has_preferences()的判定就要做得更细。3.3 第三步写一个 ContextManager统一管理所有模式我最后把四种模式统一封装成了一个ContextManager类对外只暴露两个方法build_messages(query)和update(session, answer)。内部维护会话状态和上下文构建逻辑。下面是简化后的骨架代码class ContextManager: def __init__(self, modefull, max_tokens3000): self.mode mode self.max_tokens max_tokens self.history [] # 已处理的对话历史 self.summary # 摘要模式的压缩结果 self.vector_index None # RAG 模式的检索索引 def build_messages(self, query): if self.mode full: return self._build_full(query) elif self.mode sliding: return self._build_sliding(query) elif self.mode summarization: return self._build_summarized(query) elif self.mode rag: return self._build_rag(query) else: raise ValueError(fUnknown mode: {self.mode}) def _build_summarized(self, query): messages [{role: system, content: f历史摘要{self.summary}}] messages self.history[-self.keep_recent:] messages [{role: user, content: query}] return messages def update(self, messages, answer): # 记录对话必要时触发摘要刷新 self.history.append({user: messages[-1][content], assistant: answer}) if self.mode summarization and self._needs_compress(): self._refresh_summary()封装的直接好处是你以后不用在业务代码里到处塞上下文逻辑想换模式或者调参数只改一个文件。而且对上层 LLM 接口来说入参永远是干净的 messages 数组。3.4 第四步用回归集守门别让优化变成劣化改完代码最担心的问题就是这次优化到底有没有变好用感觉来判断最容易翻车。我会固定一批测试对话集包含至少 50 个真实场景分成三类短期一致型第 5 轮以内的信息模型必须准确复述长期记忆型第 30 轮以上的信息模型必须准确引用信息更新型用户后续修改过约束模型必须以最新值为准然后定期跑一遍用规则打分。规则不复杂比如长期记忆型的样本如果模型回答的内容和标准答案的语义相似度低于 0.7就算失败。我个人的容忍线是整体通过率不低于 90%如果有一类低于 80%那说明某个模式的参数需要调而不是直接放弃整个方案。4. 实测数据与调参经验哪些参数真的值得动很多同行跟我说看完理论方案后最大的困惑是别人跑得好好的换到我的项目里怎么就不行了。原因大部分出在参数没调对。这一节我把我试过的参数和实测结果列出来基本排除了玄学成分。4.1 窗口长度先调这个收益最直接Sliding Window 里最核心的参数就是保留轮数N。我试过 5、8、12、20 四组数据结论是N5token 消耗最低但第 3 轮之前的信息几乎全部丢失用户长期偏好类的问题正确率只有 62%N8各项指标比较均衡正确率到了 85%性价比最高N12正确率提升到 88%但 token 消耗比 N8 高了 45%N20正确率反而降到 83%原因是噪声信息过多模型开始被无关历史带偏所以我的建议是不要贪心N 一般设在 8-10 轮最稳。所谓轮指的是一个完整的 user/assistant 往返。如果单轮消息特别长建议改成按 token 控制比如保留最近 6000 token 的历史。4.2 摘要压缩的触发阈值什么时候开始压缩收口Summarization 模式里最关键的是触发压缩的时机。压缩太早新历史还没积累足够信息摘要质量跟不上压缩太晚又可能超窗。我从失败经验里得到一个经验值当历史消息 token 总量 新 query token 数 预留输出 token 数超过窗口上限的 70% 时就该触发压缩了。举个例子模型窗口是 8000 token历史已经攒了 4000新问题大概 500再留 1000 给输出加起来 5500刚到 70% 的警戒线这时候压缩是合适的。另一个容易被忽略的点压缩不要只做一次。我见过很多人是一次性把整个历史压缩成一段摘要之后就再也没更新过。正确做法是每次触发压缩时把上一次的摘要和新积累的消息合并再生成新的摘要。这样能保证摘要永远覆盖到最新状态。我管这个叫增量滚动压缩代码里就是每次裁剪掉最老的部分而不是清空重建。4.3 检索 Top-K 与重排序RAG 模式的核心战场RAG 模式调参的重点完全不同。最常见的问题是 Top-K 取太小或太大。K3 时召回太窄经常漏掉关键历史K15 时噪声大增重排序模型都救不回来。我在实测里发现对长对话历史比如超过 200 条消息来说K8 是个不错的起点。但比 K 更重要的是重排序这一步。不夸张地说同样的 K 值加不加 rerank正确率能差 12 个百分点以上。重排序模型可以先粗后细第一轮用 BM25 混着向量召回前 30 个候选再用交叉编码器精排选出 Top 8效果非常明显。4.4 给 token 预算建一个任务会计前面说的都是某一种模式的参数但从项目全局看我建议你给每次请求设计一个 token 预算表。我的通用模板长这样预算项占比说明系统提示词20%角色设定、指令、工具说明历史摘要/检索结果40%长期信息与即时关联信息最近几轮对话25%当前会话的直接上下文当前用户问题5%核心 query预留输出10%保证模型有足够空间生成这个比例不是死规则但它逼着你做一件事给每一部分内容设定上限。没有预算的上下文构建就像一个无限增长的背包最后一定会超载。我见过很多线上事故追根溯源都是某个字段被拼接进上下文却没人管它有多大。5. 最容易翻车的五个场景与补救手段配置再合理、模式再先进也架不住真实业务里的各种刁钻情况。下面这几个坑我全踩过而且每一个都花了不少时间才排查出来。5.1 多轮身份问题模型忘记自己是谁摘要压缩模式跑了几天后我收到反馈说机器人说话越来越公事公办经常忘了自己是个活泼的客服助手。排查半天才发现问题出在摘要合并逻辑上新的摘要只总结了用户和模型之间的对话内容完全没有保留系统提示词里的角色设定。于是模型等于裸奔了很久。补救方法很简单把角色设定拆成两块一块是永远不变的 system prompt单独放另一块才是动态摘要只压缩对话历史。两块不要混在一起。5.2 精确数字与代码细节丢失摘要模式会把退款金额 138.5 元手续费 2%压缩成用户有退款诉求这种信息丢失对大部分闲聊无所谓但对支付场景是致命的。有一次测试用户问我之前查的那个订单号和金额是多少模型回答的订单号完全对不上因为摘要里只保留了订单状态。到了这一步我才认清一个原则要精确保真的信息绝对不能只靠摘要。常用的补救方式是摘要 关键字段抽存双轨。摘要管语义概述关键字段订单号、金额、日期、代码片段单独存结构化字段构建消息时作为硬约束拼回去。5.3 历史指令被压没用户在第 6 轮说过以后都用简体中文回答这个指令到了第 30 轮在摘要里可能变成了用户有语言偏好这样模棱两可的话。模型一旦开始自行理解就可能切换成另一种语言风格。解决这个问题的思路是区分用户指令和对话内容。用户指令属于持久化偏好应该抽出来存到 profile 里每一次请求都作为系统级约束携带。对话内容才属于可压缩的上下文。我后面所有项目都强制要求把这两层逻辑分开省掉了无数麻烦。5.4 并发场景下的上下文串线这个坑特别隐蔽。我之前有一个阶段把会话状态存在内存里的全局字典中想着反正是单机服务问题不大。上线后用户 A 和用户 B 的对话内容开始互相串A 的问题里夹着 B 的历史记录。排查原因是升级 context-mode 后新增了一个merge_context方法把当前内存里的全局实例当成了共享上下文两个会话同时进来时直接串了。从那以后我给自己定了一条铁律任何会话状态都必须带 session_idContextManager 实例要么按会话分实例要么所有读写都显式传会话 ID。这个问题在单用户 demo 里永远不会出现但一上生产就爆炸。5.5 长文档对话的准确性质疑最后一个场景是关于长文档问答的。做知识库问答时用户上传了一篇几十页的产品手册连续问了很多问题。前面几个问题答得不错但问到一个藏在附录里的参数时模型开始大幅输出高度疑似编造的内容。后来我用 RAG 模式修复对文章做分块、向量化每次提问先检索再回答。但该场景最让我惊讶的是模型即使在提供检索片段后偶尔还是会自行脑补片段里没有的细节。这说明任何上下文模式都无法完全消除幻觉模型本身的生成倾向摆在那里。我的最终补救手段是在提示词里明确要求只能引用提供的资料原文未提供的内容必须明确说不知道并且对关键参数的答案做一轮规则校验。一段时间的观察下来错误率降到可接受的范围。写到这里我回想自己从最开始的无脑全量直传到现在的多模式动态路由中间走了不少弯路。最有价值的教训有两条第一上下文管理没有银弹业务场景会直接影响模式选择照着别人的方案抄一遍大概率会水土不服第二测量永远先于优化没有探针日志就别谈改造。如果你正打算动手我的建议是先从把日志建好看清楚系统当前是怎么处理上下文的然后选一个最让你痛的模式试改配上回归集验证效果。等你把这套流程跑顺再遇到context-mode这个词你看到的就不再是一个概念而是一串很具体的、可以调试的参数了。