ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

大模型上下文管理实战:context-mode四种模式与代码实现

大模型上下文管理实战:context-mode四种模式与代码实现 1. 一切要从模型为什么会忘事说起1.1 上下文窗口不是无限大的书桌做AI应用开发的人迟早会被同一个问题逼疯对话一长模型就开始失忆。你明明在十分钟前让它记录了一个关键需求聊到第20轮的时候它突然一脸茫然好像什么都没发生过。这个时候你去看日志大概率会发现token数早就顶到了模型的上下文窗口上限早期消息被静默丢弃了——不是模型傻是你根本没有管理上下文的方式。这里先给不熟悉的读者补个基础概念。大语言模型在处理每次请求时会把你给它的全部内容——系统提示词、历史对话、用户当前输入——拼成一个固定长度的token序列这个序列的总长度受模型限制。拿常见的模型来说不同档位的上下文窗口从几千token到十几万token不等。听起来很大对吧但实际用起来完全不是那么回事。我特别喜欢用一个比喻来解释上下文窗口就像一张书桌。书桌面积是固定的你每聊一轮就要往桌上放一份新文件。桌子满了就必须拿走一些旧文件否则新文件放不下。问题在于到底该拿走哪些文件拿走了重要信息AI就失忆全都不拿程序直接报错——context-mode管理这件事本质上就是在回答桌子上该放什么、不该放什么的问题。1.2 一次线上翻车给我的教训我真正正视context-mode是在一次线上事故之后。当时我们在做一个客服问答助手用户的会话可以持续很久。初版实现特别天真整个对话历史一股脑全塞进prompt里想着反正模型窗口够大。结果上线第三天就出事了——有个用户连续聊了差不多一个下午最后问了一句我刚才说我的收货地址是哪里来着AI给的答案是错的。我翻查日志发现那个用户的会话在某个时间点超过了128K token的窗口上限。我们的代码逻辑是超了就从最开头截断把最早的几十条消息直接丢掉。而最早的消息里恰恰包含了用户第一次报收货地址的信息。AI当然答不对它根本没有那段记忆。这次事故让我意识到一个很要命的事实让你丢掉上下文的不是模型能力而是你缺少一套明确的上下文管理策略。什么时候截断截断了之后要不要做摘要哪些信息是无论如何都不能丢的这些问题不提前想清楚你的AI应用就像一个没有整理过书桌的人每天在乱七八糟的文件堆里翻找需要的东西。1.3 context-mode到底是什么后来我研究了一圈发现社区里大家说的context-mode其实并不是指某一个特定的开源框架或者固定API而是一套关于如何管理送给大模型的上下文的模式设计方案。不同应用、不同场景需要不同的模式有的模式追求记忆完整代价是烧钱有的模式追求成本稳定代价是模型偶尔断片有的模式通过摘要压缩来兼顾两者但实现复杂度直线上升。你可以把它理解成相机的拍摄模式——全自动、光圈优先、快门优先、全手动每种模式在不同场景下有各自的优势。你不可能用全自动模式拍所有题材同样你也不可能用一套固定逻辑处理所有对话场景。context-mode解决的核心问题有两个一是怎么在有限的上下文窗口里装进最有价值的信息二是当信息放不下时怎么以最小的损失完成取舍。这篇文章就把我在这件事上踩过的坑、对比过的方案、总结出的经验完整讲一遍。无论你是在做聊天机器人、RAG问答系统、Agent工作流还是写代码助手只要你需要跟大模型打交道上下文管理就是你躲不开的一课。2. 四种主流上下文管理模式拆解我把自己接触过的方案归纳为四种模式它们各自对应一类典型的应用场景。先说清楚它们的原理和适用边界后面第三部分再给可落地的实现代码。2.1 完整模式最直接但藏着一个成本黑洞完整模式Full Context Mode的逻辑最简单对话历史一字不差地全部保留每次都完整送给模型。这种做法在小规模场景下非常合适。比如你做一个单轮问答工具用户问一句、你答一句每次对话就两三轮历史消息就那么几条全量塞进去完全没有压力。还有一个典型场景是代码审查你给模型的输入是一整段代码加一个问题不存在长会话的累积问题完整模式天然合适。但一旦涉及多轮交互完整模式的成本黑洞就开始显现。假设你的用户每轮说200个token你也回复200个token那么第10轮时历史消息就要吃掉4000个token。到第50轮光历史就是20000个token。按照很多商业化API的计费方式这些输入token每一轮都要重新计费——也就是说同样的内容用户每发一条新消息你都要为全部历史消息再付一次钱。更麻烦的是窗口超限的问题。模型窗口是有上限的一旦对话超过窗口容量完整模式就变成假完整——你的代码必须在某个时刻截断而这个截断往往是随机的、粗暴的像我们把最早的消息丢了那次事故一样。所以我的经验是完整模式只适合会话轮数少、单轮信息量大的场景。一旦你发现用户的平均会话轮数在10轮以上或者历史消息很快就要突破窗口的一半就该考虑切换到其他模式了。2.2 滑动窗口模式能用但断点很致命滑动窗口模式Sliding Window Mode是大家最常用、也最容易想到的优化方案只保留最近N轮对话更早的消息直接丢弃。窗口跟着对话不断前移所以叫滑动窗口。它的优点非常明显程序可以精确控制每次请求的token数——因为你已经限定了只保留最近N轮不管用户聊了100轮还是1000轮送到模型的信息量基本恒定。成本可控、响应时间稳定实现起来也简单。但它的缺陷同样很明显用一个词概括就是断点效应。我举个例子你就明白了。用户第1轮提供了自己的身份信息我是VIP客户工号88521。后续20轮里大家都在讨论别的问题到第21轮用户说帮我查一下我的专属优惠券。如果窗口只保留最近10轮工号88521这个关键信息恰好被滑出去了AI就只能一脸懵地反问他请问怎么称呼您——体验非常割裂。更难受的是窗口边界的取舍永远是粗暴的。为什么保留10轮而不是15轮为什么被丢掉的是第12轮而不是第18轮你用代码画了一条线但对话的信息价值并没有按照你的这条线来分布。第12轮可能道出了用户的真实意图第18轮反而是闲聊而你留下的偏偏是闲聊。所以我的结论是滑动窗口模式适合对话内容相对独立、前后依赖性弱的场景比如频繁切换话题的闲聊机器人。如果你的业务强依赖用户的早期指令或长期需求单靠滑动窗口是撑不住的。2.3 摘要模式让模型给自己记笔记摘要模式Summary Mode的思路要聪明一些当对话累积到一定长度时调用一次大模型把早期对话生成一段概述性摘要替换掉原始消息。原理其实就是让AI给自己记笔记。人类的会议记录员也是这么干的会议太长不可能逐字记住那就浓缩成一页纪要。摘要模式的核心价值在于它在牺牲一部分细粒度信息的前提下把无限增长的历史对话压缩成一个有界、可控的上下文块并且保留了对话的核心脉络。比如用户最初说了身份信息、业务需求、时间限制等等经过摘要压缩后这些信息仍然以用户是VIP客户需求是月底前完成系统迁移的形式留在上下文里。虽然细节打折扣了但比直接丢掉好了太多。实现上也有不同策略可以选择。低配版是对话达到X轮就摘要一次高配版是计算当前token数超过预算的70%就触发摘要再复杂一点还有分层摘要——每N轮生成一个局部摘要多个局部摘要再汇总成全局摘要。摘要模式的问题是摘要本身也是一次模型调用有延迟、有成本而且摘要质量参差不齐。遇到模型状态不好摘要可能把关键约束条件给漏掉。我就遇到过好几次摘要把必须用回原格式这种硬性要求给融掉了导致后期输出完全跑偏。2.4 混合模式生产级系统默认的答案在看了完整模式、滑动窗口、摘要模式的各自问题之后你会发现真正落到生产环境里几乎没有哪个团队会单选一种。大家实际在用的是混合模式Hybrid Mode——把上面几种策略组合起来各取所长。我推荐的组合方式是三层结构这也是我觉得最稳妥的默认方案第一层固定锚点信息。用户的核心身份、业务约束、长期偏好这些无论如何都不能丢的内容单独存在一个固定区域里每次请求都带上。这些信息不走摘要也不进滑动窗口它们是被永久保留的。第二层全局摘要。当早期对话超出窗口容量时把最老的那段对话压缩成摘要。摘要本身继续参与后续的累积压缩——也就是说摘要也会被更新的摘要覆盖形成分层记忆。第三层最近对话窗口。保留最近N轮的完整对话确保当下的交流语境不丢失。因为最近的内容最相关也最需要原汁原味。三种模式各管一层互不干扰。用代码实现起来也不复杂接下来的部分我会把它完整写出来。为了让你对这四种模式有一个直观的对比我整理了一个表格模式信息保留度成本趋势实现复杂度主要风险适用场景完整模式最高随会话线性增长最低token超限、费用失控短会话、单轮问答滑动窗口中等恒定低中期关键信息丢失话题独立的多轮闲聊摘要模式较高有损波动含摘要成本中摘要质量不可控长对话、高价值业务混合模式最高可控但复杂高组件协调复杂客服、Agent、生产系统3. 手把手实现一个可用的context-mode管理器光讲概念不落地等于纸上谈兵。这一节我给出一个可以直接改来用的Python实现。整体设计成一个ContextManager类支持上面说的混合模式。3.1 数据结构的选型思路先想清楚这个管理器内部要存什么。对应三层结构我需要三个存储区域persistent_key_block固定锚点信息一个字符串或者结构化字典永远保留。summary_block已压缩的全局摘要也是字符串。recent_messages最近对话的列表我需要控制它的最大长度。为什么用三个独立区域而不是一个大列表理由很简单不同区域的生命周期完全不同。固定锚点信息永远不参与压缩和淘汰摘要块只在触发摘要时被整体重写最近对话列表则在水满时把前半部分移交给摘要逻辑。如果混在一个列表里每次操作都要判断哪条消息属于哪一类逻辑会越写越乱。另外还要做一个长度计数器。我习惯用tiktoken来估算token数因为它是OpenAI系模型的实际分词器算出来的数字和计费口径基本一致。如果你用的是别的模型换成对应的分词器即可。3.2 Token预算分配怎么做这个点很多人容易搞错。你以为把历史消息塞到窗口上限就行但实际必须给每一部分留足空间。我推荐按这个公式分配预算可用上下文 模型窗口上限 - 系统提示词token数 - 固定锚点token数 - 预留输出token数模型窗口上限取决于你用的模型系统提示词是你每次请求都要带的那段指令固定锚点信息属于必保项预留输出token这一点尤其关键——因为模型生成回复也需要空间你把上下文塞满了回复就没地方放了接口直接报错。举个实际的例子。假设模型窗口上限是128K token系统提示词1K固定锚点信息2K预留输出2K那么你可以自由支配的上下文空间就是123K。如果你决定给最近对话窗口留20K的额度那么剩下103K就是摘要块允许膨胀的上限。触发摘要的时机我的经验是当摘要块 最近窗口块的总token数超过可用上限的某个比例时比如85%就把最老的一批最近窗口消息压缩进摘要然后清掉这批消息腾出空间。3.3 完整代码实现这里给出我的实现骨架。为了清楚我做了简化但核心逻辑是完整的。from dataclasses import dataclass from typing import List, Optional import tiktoken dataclass class Message: role: str # user 或 assistant content: str msg_id: int # 全局递增方便定位顺序 class ContextManager: def __init__( self, model: str gpt-4, system_prompt: str , persistent_block: str , max_window_tokens: int 128_000, reserve_output_tokens: int 2_000, recent_window_tokens: int 20_000, summary_trigger_ratio: float 0.85, ): self.model model self.encoder tiktoken.encoding_for_model(model) self.system_prompt system_prompt self.persistent_block persistent_block self.max_window_tokens max_window_tokens self.reserve_output_tokens reserve_output_tokens self.recent_window_tokens recent_window_tokens self.summary_trigger_ratio summary_trigger_ratio self.summary: str self.recent_messages: List[Message] [] self._next_id 0 def _token_count(self, text: str) - int: return len(self.encoder.encode(text)) def _system_and_persistent_tokens(self) - int: return self._token_count(self.system_prompt) self._token_count(self.persistent_block) def _summary_and_recent_tokens(self) - int: recent sum( self._token_count(m.content) for m in self.recent_messages ) summary_tokens self._token_count(self.summary) if self.summary else 0 return recent summary_tokens def _budget_for_context(self) - int: return ( self.max_window_tokens - self._system_and_persistent_tokens() - self.reserve_output_tokens ) def add_message(self, role: str, content: str) - None: self.recent_messages.append(Message(rolerole, contentcontent, msg_idself._next_id)) self._next_id 1 # 每次新增消息后检查是否触发摘要 total_ctx self._summary_and_recent_tokens() self._token_count( SUMMARY: (self.summary or ) ) budget self._budget_for_context() if total_ctx / budget self.summary_trigger_ratio: self._compress_to_summary() def _compress_to_summary(self) - None: # 把最近消息中最早的一半压缩进摘要 messages_to_compress self.recent_messages[: len(self.recent_messages) // 2] if not messages_to_compress: return source_text \n.join( f{m.role}: {m.content} for m in messages_to_compress ) # 实际项目中这里调用大模型生成摘要 new_summary self._call_summary_api(source_text, old_summaryself.summary) # 更新摘要并移除被压缩的消息 self.summary new_summary self.recent_messages self.recent_messages[len(messages_to_compress):] def _call_summary_api(self, source_text: str, old_summary: Optional[str]) - str: # 简化实现真实场景调用LLM # prompt 请总结以下对话保留关键事实、用户需求、未完成事项。 # 这里直接返回占位符实际接入时替换为真实API调用。 return f[摘要] {old_summary} 本轮新增关键信息: {source_text[:200]} def build_context(self) - List[Message]: 构建最终发送给模型的messages列表 messages [] if self.system_prompt: messages.append(Message(rolesystem, contentself.system_prompt)) if self.persistent_block: messages.append(Message(rolesystem, contentself.persistent_block)) if self.summary: messages.append( Message(rolesystem, contentf以下是之前的对话摘要{self.summary}) ) messages.extend(self.recent_messages) return messages def current_token_usage(self) - int: msg_tokens [ self._token_count(m.content) for m in self.build_context() ] return sum(msg_tokens)核心逻辑在三处add_message每收到新消息就检查一次总token水位超了就触发压缩_compress_to_summary把最早的一半消息抽出来交给摘要API然后把这一半从最近窗口移除build_context按系统提示词 → 固定锚点 → 摘要 → 最近窗口的顺序组装最终上下文。实际工程中你还需要把_call_summary_api接到真实的大模型调用上。我给一个可直接参考的prompt模板你是对话记录员。请把下面的对话内容浓缩成简洁的会议纪要式摘要要求 1. 保留所有明确出现的用户身份信息如名字、公司、会员等级等 2. 保留所有业务需求、期限、约束条件 3. 保留尚未完成的事项 4. 用中文输出控制在300字以内 5. 如果提供了旧摘要请结合旧摘要一起生成更新后的摘要 旧摘要{old_summary} 新对话内容{source_text}有了这个管理器你在对话循环里只需要调三个方法add_message(role, content)记录每一轮build_context()组装参数current_token_usage()做监控告警。整个上下文模式的管理就和业务逻辑解耦了。4. 实测对比三种模式在真实任务中的表现代码写完了但方案到底好不好还是得拿数据说话。我做了一组对照测试场景选的是三类非常典型的任务多轮客服对话、长文档问答、Agent任务执行。测试对象是对比完整模式滑动窗口模式混合模式摘要模式因为实现复杂度接近混合模式我把它归到混合模式里一起比较了。4.1 测试场景怎么设计的先说明测试参数。模型统一用gpt-4-turbo档位温度设为0保证可比性。完整模式的上下文上限设为64K token窗口模式窗口大小设为16K token混合模式的固定锚点信息设为用户所在地上海最近窗口16K token摘要触发比例为80%。测试数据方面我构造了30个会话样本每个会话平均约40轮对话累计约30K token。每个会话末尾设置3个回溯性提问——专门问早期对话中出现过的信息用来考验模式的信息保留能力。比如会话前5轮用户说了我家住上海收货地址是XX路到会话末尾问用户是哪里人来着答对了就计1分。4.2 测试结果和我的分析跑完之后的结果如下表模式回溯问题正确率平均每会话token消耗平均响应延迟语义连贯性评分完整模式100%48.6K1.4s9.2/10滑动窗口16K58.7%18.2K0.6s6.5/10混合模式93.3%22.4K0.8s8.7/10几个结论值得展开说。完整模式的正确率是100%但它是最烧钱的。30个会话跑下来完整模式的token消耗几乎是混合模式的2倍以上。特别是在短会话场景完整模式的消耗也有明显浪费——一段对话只有3轮你也把全部历史塞进去其实多出来的全是不必要的内容。如果你的业务对成本敏感永远全量显然不是长远之计。滑动窗口模式成本优势明显但正确率塌得很严重。58.7%意味着接近一半的早期关键信息被窗口丢掉。仔细观察错误案例问题往往出在信息出现位置刚好在窗口前段——我设定的窗口是16K那些超过窗口阈值的信息没有任何机会被挽回。如果你的场景经常出现这种情况用窗口模式是在赌概率。混合模式是性价比最优解。93.3%的正确率和完整模式相差不多但token消耗只有它的46%。说明摘要压缩确实有效地保留了早期信息脉络同时最近窗口承接了当下语境。语义连贯性评分也说明摘要的内容虽然被压缩了但对模型回答的干扰没有想象中那么大。4.3 什么场景该选哪种模式我说点实在的结合测试结果和我自己做过的项目我给出的选型建议是这样一次性问答/单轮场景直接用完整模式就好。对话就一两轮不存在上下文膨胀问题用混合模式反而是杀鸡用牛刀。闲聊机器人/话题跳跃的客服滑动窗口模式就够用了。这类场景用户本来就不指望你记住三天前的事窗口滑掉了也无伤大雅还能省下不小一笔成本。业务型客服/需要长期记忆的Agent必须上混合模式。用户身份、业务约束、未完成事项这些信息的丢失是完全不可接受的。混合模式多出来的那一点调用成本对比丢掉一个订单或者搞砸一次服务完全不值一提。RAG问答系统这类系统的特点是外部知识库很大但单轮问答承接的历史较少。我的建议是把检索到的文档片段作为持久区块来管理会话历史用滑动窗口。这样既能控制上下文体积又不会影响检索内容的完整性。顺便提一句别迷信某个模式一定优于另一个。我见过某些团队把混合模式当作政治正确不管什么场景一律上全套摘要结果摘要调用频率太高延迟从0.8s涨到2s用户反而投诉变多了。模式是服务场景的不是拿来攀比的。5. 踩坑实录上下文模式里那些反直觉的坑这是我最想写的一节。上面的内容你多看几篇博客也能拼出来但下面这些坑几乎都是我拿着真金白银的API账单换来的教训。5.1 摘要的信息塌缩问题摘要模式最大的坑是摘要的摘要之后关键信息会彻底消失。什么意思假设用户第一轮说我叫张伟我的需求是在5月30号之前上线新版登录功能必须支持短信验证码。这些信息在第一次摘要时会被保留。但随着对话持续变长这些早期信息会被压缩进第二层摘要——也就是整段摘要里的一个小分句。如果第二层摘要生成时模型认为时间点不如功能列表重要它可能就会把5月30号丢掉。等到第三层摘要生成这个时间点就彻底没了。我做过一次实验把同一个会话反复触发5次摘要然后问模型上线截止日期是什么时候答案已经变成6月初了。这个现象我叫它信息塌缩每次摘要都损失一部分细节多轮摘要就是雪崩式损失。对策有两个。第一把硬性约束截止日期、用户身份、业务金额等从普通对话中剥离出来放进我的persistent_block固定锚点区永远不进摘要流程。第二摘要prompt里必须明确要求模型保留所有数字、日期、人名、约束词我给的prompt模板里已经写了这一条。5.2 滑动窗口边界的断点效应前面提过断点效应但这里我想再说一个更隐蔽的情况被裁掉的那条消息往往恰好是整个对话的转折点。举一个我实际碰到的例子。一个用户来咨询产品报价前面聊了十几轮都在问功能细节突然有一轮说好我决定买A套餐接下来怎么付款——好巧不巧这轮就被滑出窗口了。后面的对话里模型完全不知道用户已经下了购买决定继续在介绍A套餐的功能用户二次追问付款方式模型还在背功能介绍。用户直接愤怒地关了会话。这事给我的教训是窗口模式不应该只按最近N轮硬切而是要加一层关键事件检测。比如检测到包含决策性动词决定下单选择确认的消息就把它标记为关键消息即使它被滑出当前窗口也要转存到persistent_block里。这个小改动花不了多少功夫但对用户体验的提升是立竿见影的。5.3 prompt缓存与上下文模式的联动坑如果你的应用启用了prompt缓存来降本有一个细节必须注意频繁修改上下文前序部分会让缓存命中率暴跌成本反而飙升。这是怎么回事现在很多大模型API支持前缀缓存——如果你连续多次请求的prompt开头部分是相同的后面这部分可以按缓存价计费只有变动的部分按原价计费。而混合模式下你的摘要块每次都在变因为不断有新消息被压缩进去导致了prompt前缀的hash频繁变化之前的缓存全部失效。我意识到这个问题是在某个月账单出来的时候——明明token用量比上个月降了费用却涨了接近一倍。排查了很久才发现是缓存命中率从90%掉到了20%。解决办法有几种思路把容易变动的摘要块挪到消息列表中间让固定前缀更长。比如把系统提示词、固定锚点、历史摘要拼成一个固定前缀最近窗口放在后面。降低摘要触发频率不要每轮都做用一个比较粗的阈值。如果你的框架支持将摘要部分单独作为一个变量传入而不是拼接在prompt字符串里。不同API服务商对动态内容的缓存策略不一样具体得翻文档测试。5.4 别只盯着token还有更值得监控的指标最后分享一个监控层面的建议。很多人上了context-mode之后只盯着每次请求用了多少token这当然是必要的但远远不够。我现在的监控看板上有几个必盯指标指标含义报警阈值建议摘要触发次数/会话反映摘要压缩是否过于频繁超过3次/会话就告警摘要生成延迟摘要API本身的一次调用耗时超过2s告警平均上下文中位数年龄指上下文包含的信息距今多少轮超过30轮考虑调窗口问答回溯准确率抽样提问早期出现过的信息低于85%告警会话截断率因超长被迫强制清空的概率不为0就该告警其中问答回溯准确率这个指标特别重要。它是我做测试时设计的思路在真实生产里定期抽取几个会话人工构造一个早期信息提问看模型能否答对。这个指标能直接反映你的context-mode方案在实际流量下的信息保留能力比纯看token数有意义得多。我上过生产环境后才发现很多看似正常的对话其实模型早就在裸奔了——只是用户没问早期信息所以没暴露而已。说到底context-mode不是一次配置就永久生效的东西。它需要持续监控、定期微调。用户的行为模式在变模型的窗口参数在变业务的上下文需求也在变。我现在的做法是每两周复盘一次监控数据根据回溯准确率和摘要触发频率动态调整窗口大小和摘要触发比例。这个习惯帮我避开了好几次潜在的质量事故。写到这里我最后分享一个很小的实战体会设计context-mode时一定要有信息分级的思维。不是所有对话信息都值得保住你要做的是分辨哪些是命根子用户身份、业务约束、决策承诺、哪些是常规信息普通问答、闲聊、哪些是可以丢掉的垃圾信息无关讨论、重复啰嗦。对不同等级的信息分配不同的保留策略。这样你的上下文管理才不会漫无目的。一旦把这一步想透了整个方案的设计会顺畅很多。
返回列表