
1. 曾让我失眠的问题文档问答里那句“他似乎忘了我们在聊什么”年初接了一个企业知识库问答的项目客户要求不高就是让员工能用自然语言查制度、查流程。我最初的做法也很“标准”把文档切片、做向量检索把命中的片段拼进Prompt一次性丢给大模型。demo阶段跑得很顺所有人都觉得上线稳了。结果灰度测试第一周就出事了。一个员工连续问了三个问题“报销差旅费需要什么材料”“我们部门是市场部申请流程有什么不同”“那我上周提交的那单财务说缺发票还能补吗”第三个问题模型答得支离破碎甚至反问“您指的是哪一单”。原因显而易见——前面的对话历史和检索到的文档片段全被塞进同一个上下文里互相打架模型根本分不清哪些是“本次要处理的单据”哪些只是“历史提问中的例子”。那个周末我把自己关在房间里开始认真研究一个东西context-mode上下文模式。说句实话这不是某个开源项目的名字也不是某家大厂发布的新功能。它是你在做大模型应用时绕不开的一组设计决策——上下文窗口里的内容到底以什么形态组织、按什么顺序排列、什么时候保留、什么时候丢弃、什么时候去外部检索。它决定了你的应用是“看着聪明”还是“真的聪明”。这篇文章就围绕我在这个项目里踩过的坑、做过的手术和最终沉淀下来的方案展开。如果你是做RAG、智能问答、Agent这类应用被多轮对话“失忆”、上下文溢出、成本飙升这些问题折磨过这篇应该能给你一些能直接抄的作业。2. 上下文窗口的三个隐藏瓶颈长度只是第一道坎2.1 注意力塌缩模型真正“记住”的远没有你以为的那么多最开始我以为上下文窗口就像一个更长的草稿纸只要放得下模型就能读到。但翻了几篇关于长上下文模型评测的论文之后我发现事情没那么简单。业界早就有一个被反复验证的现象学术上叫Lost in the Middle翻译过来就是“中间丢失”。模型对上下文开头和结尾的内容记忆最好对中间部分的利用效率明显下降。你辛辛苦苦把一百页产品手册塞进窗口模型真正有效利用的可能只有前几千和最后几千token的范围内。这意味着什么如果你只是把历史对话、检索片段、系统指令一股脑按顺序拼接排在中间偏后的检索内容很可能处于“视觉盲区”。它会读到这些字但注意力和权重分配已经不够了生成时就表现为忽略、混淆、或者答非所问。2.2 成本与延迟的账单每多一轮对话都在烧钱第二个瓶颈是算账。我们用的是按token计费的大模型API上下文内容要重复计费。简单算一下就能看出问题假设一次完整回答需要生成300个token如果上下文固定塞了8000个token实际账单上消耗量是8300个token。看起来单次不多但一个用户一天对话20轮每轮都把历史全文带上上下文还会越滚越长——第20轮的时候可能已经带着四五万token在跑。我拉过一次账单纯上下文的费用占了总消耗的70%以上。更麻烦的是延迟输入token一多首字返回时间肉眼可见地变慢从几百毫秒涨到三五秒。客户的反馈很直接“这个机器人转圈圈的时间比我翻纸质手册还久。”2.3 注入噪声无关内容会主动拉低回答质量第三个问题最隐蔽也最容易忽略。你以为“内容多等于信息全”实际上无关的上下文对模型来说不是中性的而是负面的。我在测试里做过一个对照一份技术文档里混入一段毫无关系的行政通知结果模型在回答技术问题时开始“一本正经”地引用行政通知里的措辞。你给它什么它就会倾向于顺着什么风格和范围来回答哪怕那些信息与当前问题毫无关系。这就是context-mode里最核心的对抗点——你要控制的不是“放得下吗”而是“该放进去吗”。上下文不是水龙头不能只想着开大它更像一个行李箱每一件行李都要问一句这次旅途真的需要它吗这三重瓶颈叠在一起让我意识到与其抱怨模型笨不如承认自己的上下文管理模式是粗暴的。与其追求更大的窗口不如先把手里的窗口用得好一点。3. 一套可落地的上下文分层管理方案我现在的标准姿势想清楚了问题所在我开始把上下文拆成四个层每一层各司其职。这个方法我用了大半年稳定性和效果都明显好过之前“一锅炖”的做法。3.1 第一层系统指令层负责扮演“人设”和“铁律”这一层是最贴近窗口开头的部分专门放系统的固定指令比如角色设定、回答风格、安全红线、输出格式。它全程不变每个轮次都会携带但因为设计得紧凑通常能压到500到800个token以内。这里有个容易被忽略的细节系统指令不要写废话。“你是一个有用的AI助手”这种话纯属浪费窗口。真正有用的是约束规则比如“如果用户问的是报销流程必须引用知识库中的具体条款编号不得凭经验作答”。我带过的新人常常在指令里堆形容词结果就是指令越长模型越抓不住重点还不如直接给它“当A时做B遇到C就D”这种句式。3.2 第二层外部检索层按需注入而不是全量塞入这一层放的是和当前问题强相关的文档片段、知识库结果、数据库记录是RAG的主要战场。关键原则是“少而准”。我在项目里把检索策略从“取Top 5片段全塞”改成了“取Top 10片段、重排后只取Top 3”。一开始大家都不放心担心信息不够。实际上精准的三段内容远比五段里混着两段边角料的效果好。因为模型生成时能享受到的“注意力预算”是有限的你把预算浪费在低相关片段上核心信息反而得不到足够的权重。3.3 第三层会话记忆层摘要滚动加关键事实提取这一层解决多轮对话的“记忆连续感”。但我的做法不是把历史对话原文全堆进去而是维护两条线滚动摘要每经过若干轮让模型用一句话总结之前的对话主线。关键事实列表抽取对话中出现过的实体和重要状态比如“用户的市场部员工”“上周提交了一单差旅报销”“该单缺发票”。这样第三层始终能控制在1000个token以内。模型既知道你们聊到哪了又不会被历史原文里的细枝末节带偏。至于具体怎么实现我在后面第4章会展开。3.4 第四层工作记忆层只服务“当前这一步”这一层对应的是当前问题的即时上下文包括用户刚刚输入的query、当前Agent正在调用的工具返回结果、正在处理的临时数据。它是最靠近窗口末尾的部分也是最容易被模型“记住”的区域所以一定要留给当前最关键的内容。我自己的习惯是把窗口末尾的几百个token专门留出来只放“本次要回答的问题”和“必须完成的动作”相当于给模型一个锚点看到最后你就知道自己当下要做的是什么。这四层落地的顺序和比例我用一张表来总结方便你对照自己的场景做调整层级位置主要内容目标token预算系统指令层窗口开头角色、规则、输出格式500-800外部检索层指令层之后重排后最相关的Top 3片段800-1500会话记忆层检索层之后滚动摘要关键事实列表500-1000工作记忆层窗口末尾当前query、工具返回、临时状态300-800这个预算只是一个起点关键是你要意识到每一层都应该有明确的预算上限而不是让上下文自由膨胀。没有了预算意识context-mode就无从谈起。4. 上下文压缩的实战取舍该丢就丢该留的必须留4.1 三种压缩策略摘掉什么保留什么分层管理解决的是“结构问题”接下来还有一个“容量问题”——就算分了层用了两万token窗口对话拉长到五十轮还是会满。这时候就需要压缩。我实际试下来主流的压缩策略有三条路各有各的适用场景全文摘要式压缩把一段对话历史直接让模型总结成一两句话。优点是通用缺点是丢细节尤其容易丢具体的数字、日期和人物称谓。关键信息抽取式压缩不生成摘要而是抽取对话中的结构化信息比如“用户IDu_1024”“订单号SO20240311”。优点是精确、适合检索和后续处理缺点是读起来不像自然语言。裁剪丢弃式压缩按重要性给消息排序把低优先级内容直接丢弃。比如用户礼貌用语、无关寒暄、重复提问。优点是保留完整原文的片段缺点是可能丢掉隐线。我现在的做法是三者混合。寒暄和重复内容直接裁剪剩下的对话主干做关键信息抽取再配一条滚动摘要兜底。这样既保住了对话的“骨架”又留了“脉络”还不至于让摘要把细节全吞掉。4.2 一个可用的滚动摘要实现L1缓存式的记忆维护这里放一段我项目里实际用过的伪代码逻辑思路是“摘要的摘要”——每一小段对话先产出一条小结等小结攒多了再condense成更高层的摘要避免每次都拿全部历史去重新总结class ContextMemory: def __init__(self, max_turns6, max_summary_len300): self.recent_turns [] # 原始对话消息 self.rolling_summary # 滚动摘要 self.key_facts {} # 关键事实字典 def add_turn(self, user_msg, assistant_msg): # 1. 最新一轮原文进入缓存 self.recent_turns.append({user: user_msg, assistant: assistant_msg}) # 2. 攒满阈值就做一次摘要合并 if len(self.recent_turns) self.max_turns: combined_text self.rolling_summary \n self._json_dumps(self.recent_turns) new_summary self._summarize(combined_text, max_lenself.max_summary_len) self.rolling_summary new_summary self.recent_turns [] # 3. 抽取关键实体更新事实表 entities self._extract_key_facts(user_msg \n assistant_msg) for k, v in entities.items(): self.key_facts[k] v # 同名实体覆盖保留最新状态 def build_context_block(self): # 最终返回给LLM的会话记忆层文本 return { rolling_summary: self.rolling_summary, key_facts: self.key_facts, recent_turns: self.recent_turns[-2:] # 只保留最近两轮原文 }这套逻辑里的关键心法是“摘要的摘要”而不是“全文的摘要”。每隔几轮就压缩一次你的摘要不会因为对话太长而失控。每次压缩的输入是上一次的摘要加增量的原文成本固定不会随着对话轮数无限增长。4.3 压缩的红线这三类内容千万不能动压缩不是万能药我吃过的亏可以帮你画几条红线第一身份与约束类信息不能压缩。比如用户一开始说了“我是公司财务部的需要走特殊审批流程”这类信息一旦被摘要丢掉模型后面就可能按普通流程回答而且你很难发现哪里错了。第二时间顺序不能乱。对话里的先后关系经常是隐性的逻辑链比如“先申请、后审批、再打款”。做摘要时模型容易把顺序模糊化所以我要求摘要中加入时间标记或步骤序号。第三否定与反悔信息要显式保留。用户说“我不需要发票了改为电子凭证”如果没有显式记录“状态变更”模型会继续引用旧的发票规则。我把这类信息单独维护成一条“状态变更记录”不合并到摘要里确保每次组装上下文时它都在。压缩的本质是取舍而取舍的底线是那些一旦丢失就会导致结论反转的信息。宁可多留几百个token也不能让模型拿着一个残缺的事实去做推理。5. 检索增强与上下文模式的协同该“外挂”就别硬塞5.1 判断标准哪类内容必须走检索哪类可以常驻上下文很多团队的误区是明明有向量知识库却还是什么都往上下文里塞。我用一个简单的二分法来判断如果内容是“全局不变”的规则和指令比如公司制度、系统操作手册放向量库按需检索即可。如果内容是“本次会话特有”的状态和事实比如用户刚提交的单号、用户所在部门、正在审批的环节放上下文里的记忆层。这个判断标准帮我解决了一个长期困惑为什么文档一起检索出来模型还是答不准因为制度条文本身不会变但用户的具体状态每轮都在变。混合在一起时模型需要从大量静态文本里捞出动态状态难度陡增。拆开之后静态知识走检索动态状态走记忆层各管各的准确率明显提升。5.2 查询改写和重排为了让检索层“听懂”当前的上下文检索层的效果上限很大程度上取决于你给向量库的query是什么。原始用户问题往往太口语、指代不明。比如用户问“那单后来怎么样了”如果直接拿去检索向量库必然什么都匹配不到。我在context-mode里加了一步查询改写def rewrite_query(user_query, context_memory): prompt f 请根据已知的会话上下文把用户的最新问题改写成一个独立的、可检索的查询。 用户问题{user_query} 已知关键事实{context_memory.key_facts} 输出要求只输出改写后的查询不要解释。 return llm.call(prompt)改写完的query再去做向量检索召回率提升明显。另一个值得注意的环节是重排召回的Top 10片段里可能有两条都是同一个章节的重复内容不重排的话模型会看到两遍相同信息新内容反而没位置。我用的工具是bge-reranker实测下来把重排后的Top 3放进上下文比直接塞Top 5的效果更好而且token成本低了40%。5.3 实测对比一次真实的模型效果改善记录我在项目里做过一次A/B对比。对照组是“全量历史Top5片段”的老方案实验组是“分层上下文重排Top3滚动摘要”的新方案。测试集是200个真实用户问题由两个同事按1到5分独立打分。评估维度老方案平均分新方案平均分回答准确率3.24.6多轮一致性2.84.5首字响应延迟4.1秒1.8秒单次会话token消耗约35k约12k数据摆出来之后团队里最反对“动上下文结构”的同事也服了。准确率的提升其实不意外因为模型没被噪声干扰了延迟和成本的下降则是分层带来的意外之喜。这件事给我的启发是做AI应用别急着换更大参数的模型先把上下文结构理顺往往性价比更高。6. 生产环境中的两次惨痛教训并发隔离和上下文污染6.1 多用户上下文串台差点把A公司的数据回给B公司员工方案成型之后我一度觉得问题都解决了直到上线第二周收到一个投诉——用户问“我们公司食堂补贴是多少”机器人回答的是另一家公司的福利制度。排查后确认代码里用了全局单例的上下文缓存对象。两个用户共用了同一个ContextMemory实例后一个用户的关键事实覆盖了前一个用户的。这是非常典型的“局部变量没想清楚就上线”的坑。修复方式并不难把上下文管理类设计成按会话ID隔离的实例池class ContextStore: def __init__(self): self._sessions {} def get_session_memory(self, session_id: str) - ContextMemory: if session_id not in self._sessions: self._sessions[session_id] ContextMemory() return self._sessions[session_id]但真正值得写下来的教训是context-mode不是只在前端组Prompt它涉及整个会话生命周期的状态管理。如果后端没有做好隔离再精巧的上下文分层也会被数据污染瞬间击穿。上线之前多用户并发压测是你必须做的事。6.2 工具调用把上下文撑爆当Agent开始调用一连串API第二个坑是我做Agent化改造时遇到的。流程是用户提问、Agent规划、调用API查数据、把结果写进上下文、再让模型生成答案。听起来没问题但一个复杂的查询可能触发五六个工具调用每个工具返回几千token的数据。所有返回都堆进上下文之后窗口瞬间爆掉。我最终的解决思路是两层给工具返回设置上限单次工具返回超过一定长度就强制截断或先做摘要只把结构化关键字段留在上下文里。不把工具结果直接当最终答案的素材而是作为中间数据。模型需要先基于工具结果生成一个“brief”再基于brief做最终回复。这一层改动之后Agent在复杂任务里的稳定性提高了不少。工具调用的本质是“外挂”外部系统和检索一样也要遵守“按需注入”的原则不能因为接了API就让上下文的胃口跟着变大。6.3 上下文健康度监控量化指标带我避开“悄悄变差”最后再分享一个我最常推荐给身边人的习惯——给上下文建立监控指标。不要等用户投诉了才去翻日志我日常盯三个数字上下文总token数超过预算的90%就告警说明压缩策略没跟上。每次请求的检索命中率检索层拿回来的片段如果经常不相关先检查query改写和重排。多轮会话的“信息熵”同一会话内关键事实表的条目数是否只增不减如果摘要越压越短但关键事实没增长说明系统正在遗忘重要上下文。这些指标不需要什么重型监控平台用最朴素的日志统计就能做。但有了它们context-mode就不再是设计时的一次性工程而是运行时可观测、可调节的系统能力。7. 关于context-mode我最后想说的三句话踩过那么多坑之后我对context-mode的理解已经不只是“把上下文管理好”这么简单了。它本质上是在做信息资源的分配模型每一轮能获得的注意力是有限的预算系统设计者的工作就是把这笔预算花在最值当的地方。我在项目里也犯过“过度设计”的错把分层搞得极其复杂最后维护成本比收益还高。所以如果你刚开始改造我建议从最简单的两件事入手一是把系统指令精简到只留行为约束二是把多轮历史改成滚动摘要加关键事实。这两步做完你大概率就能感受到明显变化。在动手调整时还有三个小建议值得记在工位上每次改动上下文结构都要准备至少几十条真实用户问题做回归测试光靠两三句demo验证不出问题的。上下文管理的代码要单独抽成模块不要散落在业务逻辑里否则后面换模型、调策略时会痛不欲生。多关注模型在不同窗口位置的表现差异把最重要的信息放在开头和结尾别让它在中间地带“等死”。技术迭代很快今天的主流做法可能过半年就会被新方案替代但“如何分配模型的注意力”这个问题会一直存在。如果你也在做类似的项目希望这篇踩坑记录能让你少走几步弯路。