1. 上下文管理到底在管什么第一次听到“上下文管理”这个词很多人会下意识觉得它是个很虚的概念像是某种管理学的黑话。但如果你真正动手做过对话系统、智能助手或者哪怕只是用过大模型接口写过一个小工具你就会发现上下文管理是决定一个应用“好不好用”的分水岭。它管的不是别的就是在有限的资源里让系统记住该记住的忘掉该忘掉的并且在需要的时候把正确的信息送到正确的位置。我打个生活化的比方。你去一家常去的面馆老板记得你“不要香菜、面硬一点”这是长期记忆你这次进门说“今天想喝点热的”这是短期上下文老板把这两条信息合在一起给你端上一碗不要香菜、面硬一点的热汤面这就是一次成功的上下文管理。如果老板把你三年前说过的一次“多加辣”也翻出来那这碗面就毁了。上下文管理的核心矛盾就在这里信息太少系统显得健忘信息太多系统又会跑偏还烧钱。这个内容适合谁来参考我认为有三类人。第一类是正在做对话类产品的开发者你大概率已经被“聊到第十轮模型就忘了前面说过什么”这个问题折磨过。第二类是做智能客服、知识问答、文档助手的工程同学你们面对的是长文档和长会话的双重压力。第三类是对大模型应用感兴趣、想自己搭个小工具的技术爱好者理解上下文管理能让你少走很多弯路。不管你是哪一类接下来的内容都会围绕一个目标展开把“上下文管理”从一个模糊的概念变成一套你能直接上手操作的方案。需要先说明一点下面涉及的具体参数、阈值、代码示例都是基于行业里常见的实践做法进行的合理补充不同模型和框架的细节会有差异但底层思路是相通的。你完全可以根据自己的场景做调整。2. 整体设计思路与方案选型2.1 为什么不能“全都塞进去”最朴素的做法是把所有历史对话原封不动地拼接到每次请求里。这个方案在对话只有三五轮的时候完全没问题甚至可以说是最优解因为它没有任何信息损失。但问题会随着轮次增长迅速暴露出来。首先是成本。大模型的计费基本都跟输入输出的token数量挂钩你每轮都把全部历史带上输入token会像滚雪球一样越滚越大。假设每轮对话平均200个token聊到第50轮单次请求的输入就接近1万个token而其中绝大部分信息对当前这轮回答根本没有帮助。其次是效果。这一点很多人会忽略。模型的注意力是有限的当上下文里塞了大量无关的旧信息模型反而更容易抓错重点。我实测过同样一个问题在干净上下文里回答得很准在塞了二十轮无关闲聊之后回答质量明显下降。这不是模型变笨了是噪声把它淹没了。最后是窗口限制。每个模型都有上下文窗口上限超过就直接报错或者被截断。截断这件事最坑的地方在于如果你截得不好可能把最关键的系统指令或者用户的核心诉求给截掉了。所以整体设计的第一条原则就是上下文不是越多越好而是越相关越好。我们要做的所有事情本质上都是在“保留信息”和“控制规模”之间找平衡。2.2 分层管理把上下文拆成几块来对待我在实际项目里最常用的思路是分层。不要把所有消息看成一锅粥而是按性质和生命周期分成几层每层用不同的策略处理。层级内容类型生命周期处理策略系统层角色设定、规则约束、输出格式要求整个会话不变永远保留放在最前长期记忆层用户偏好、关键事实、历史结论跨会话或长期摘要化存储按需召回短期对话层最近几轮问答当前会话滑动窗口保留即时层当前这轮的输入和检索结果单次请求动态拼装这么分的好处是每一层的处理逻辑是独立的你可以单独优化某一层而不影响其他层。比如系统层几乎不用动短期对话层用滑动窗口长期记忆层用摘要加向量召回。这种模块化的思路让整个系统更容易调试出问题的时候你能快速定位是哪一层出了岔子。2.3 三种主流策略的取舍具体到“怎么压缩上下文”行业里常见的有三种策略我分别说说它们的适用场景和坑。滑动窗口是最简单的只保留最近N轮对话。它的优点是实现成本极低几行代码就能搞定。缺点是硬截断如果用户在第3轮说了一个关键信息聊到第20轮时这个信息早就被挤出去了。它适合那种对话主题频繁切换、旧信息价值不高的场景比如闲聊机器人。摘要压缩是把旧对话用模型总结成一段简短的文字替代原始对话。优点是信息密度高能保留很久以前的关键点。缺点是有信息损失而且摘要本身也要消耗一次模型调用有额外成本和延迟。它适合长会话、需要记住用户长期诉求的场景比如个人助理。向量召回是把历史消息存进向量库每次根据当前问题检索最相关的几条拼进上下文。优点是精准能跨越很长时间找到相关信息。缺点是需要额外的存储和检索组件工程复杂度最高。它适合知识密集、需要频繁引用历史内容的场景比如文档问答。实际项目里这三种往往不是三选一而是组合使用。我通常的做法是短期用滑动窗口保证连贯性长期用摘要保证不丢关键信息再叠加一层向量召回处理“用户突然问起很久以前的事”这种情况。3. 核心细节解析与实操要点3.1 Token预算怎么算才不翻车上下文管理的本质是一个预算分配问题。你得先知道总共有多少预算再决定每一层分多少。这里的关键是给输出留足空间。假设你用的模型上下文窗口是8000个token你要预留至少1000到1500个token给模型的输出因为回答本身也要占位置。那么真正能用于输入的预算大概是6500到7000个token。在这个输入预算里我一般的分配比例是这样的系统层占10%到15%长期记忆层占20%左右短期对话层占50%到60%剩下的留给即时检索结果和当前输入。这个比例不是死的要根据场景调。比如客服场景系统层的规则约束很重要可以多给一点闲聊场景短期对话层可以占更多。关键是你要有一个明确的预算表而不是每次拼上下文的时候凭感觉。提示不同模型对token的切分方式不一样中文里一个汉字通常对应1到2个token英文一个单词大概1到1.3个token。做预算的时候最好用对应模型的分词工具实际算一下别拍脑袋估算误差会很大。3.2 摘要压缩的触发时机和写法摘要压缩最忌讳的是“每轮都摘要”那样成本和延迟都受不了。我的经验是设置一个触发阈值比如当短期对话层的token数超过预算的80%时才触发一次摘要。摘要的写法也有讲究。不要简单地让模型“总结一下”而要给它明确的指令告诉它保留什么、丢弃什么。我常用的提示词结构是这样的请把以下对话压缩成不超过200字的摘要重点保留用户明确表达的偏好、已确认的事实、待办事项和未解决的问题丢弃寒暄、重复确认和无关闲聊。这样得到的摘要才有用。我踩过的一个坑是早期让模型自由发挥结果它把“用户说今天天气不错”这种废话也总结进去了真正重要的“用户要求周五前交付”反而丢了。后来加了明确的保留项和丢弃项摘要质量稳定了很多。3.3 滑动窗口的边界处理滑动窗口看起来简单但边界处理不好会出问题。最常见的问题是把一问一答拆散了。比如窗口大小是10条消息正好在第5条是用户提问、第6条是助手回答的位置切开结果上下文里只剩一个孤零零的问题模型就会莫名其妙地重新回答一遍。解决办法是按“轮次”而不是按“消息条数”来滑动。一轮指的是用户提问加助手回答这一组。保留最近5轮就完整保留这5组问答不会出现半截对话。另外系统层的消息永远不参与滑动它始终在最前面。还有一个细节是如果某一轮的回答特别长比如助手输出了一篇长文这一轮就可能占掉大量预算。这时候可以对超长的单条消息做单独截断保留开头和结尾中间用省略标记因为开头通常是结论结尾通常是补充说明中间往往是展开论述优先级相对低。3.4 向量召回的检索质量把控向量召回的效果高度依赖检索质量。我见过很多项目向量库建了但召回出来的东西驴唇不对马嘴反而污染了上下文。问题通常出在两个地方。一是切块粒度。如果把一整轮长对话当成一个块存进去检索出来的块里可能只有一句话是相关的其余都是噪声。我的做法是把长消息按语义切成200到300字的小块每块单独存检索时按块召回这样精度高很多。二是相似度阈值。不要无条件地把检索到的前K条都塞进去要设一个相似度下限。低于阈值的宁可不要因为不相关的召回比没有召回更糟糕。这个阈值需要根据你的向量模型实测调一般从0.7左右开始试。注意向量召回和摘要压缩不要同时作用于同一批消息否则会出现信息重复。我的做法是已经被摘要覆盖的旧消息就不再进向量库两者职责分开。4. 实操过程与核心环节实现4.1 搭一个最小可用的上下文管理器下面我用Python写一个简化版的上下文管理器把前面说的分层、预算、滑动窗口、摘要触发都串起来。这段代码是示意性的你可以直接拿去改。class ContextManager: def __init__(self, max_input_tokens6500, window_rounds5): self.system_prompt self.long_term_memory # 摘要文本 self.short_term [] # 最近几轮对话 self.max_input_tokens max_input_tokens self.window_rounds window_rounds def set_system(self, prompt): self.system_prompt prompt def add_round(self, user_msg, assistant_msg): self.short_term.append({user: user_msg, assistant: assistant_msg}) # 超出窗口就触发摘要 if len(self.short_term) self.window_rounds: self._compress() def _compress(self): # 取出最旧的一轮合并进长期记忆 oldest self.short_term.pop(0) new_content f用户: {oldest[user]}\n助手: {oldest[assistant]} # 实际项目中这里调用模型做摘要这里用拼接示意 self.long_term_memory (self.long_term_memory \n new_content).strip() def build(self, current_input): parts [self.system_prompt] if self.long_term_memory: parts.append(历史摘要:\n self.long_term_memory) for r in self.short_term: parts.append(f用户: {r[user]}\n助手: {r[assistant]}) parts.append(用户: current_input) return \n\n.join(parts)这段代码里_compress方法是最简陋的版本直接把旧对话拼进长期记忆。真实项目里你应该在这里调用模型做摘要并且控制长期记忆的总长度超过一定字数就再压缩一次形成“摘要的摘要”。4.2 预算校验和动态裁剪光有结构还不够每次拼装完上下文一定要做一次预算校验。我通常会在build方法最后加一段逻辑估算拼装结果的token数如果超过预算就按优先级从低到高裁剪。裁剪的优先级顺序是先砍即时检索结果里相似度最低的再砍长期记忆里最旧的内容最后才动短期对话。系统层永远不砍。这个顺序背后的逻辑是越靠近当前问题的信息越重要越久远、越边缘的信息越可以先牺牲。估算token数的时候如果不想引入分词库可以用一个粗略的系数中文按字数乘1.5英文按单词数乘1.3加起来做个近似。虽然不精确但对于预算控制来说够用了留一点安全余量就行。4.3 一次完整的请求流程把上面的东西串起来一次完整的请求处理流程大概是这样几步。接收用户当前输入。用当前输入去向量库检索相关历史片段按相似度阈值过滤。检查短期对话层是否超出窗口超出则触发摘要压缩。按系统层、长期记忆、短期对话、检索结果、当前输入的顺序拼装。做token预算校验超预算则按优先级裁剪。发送给模型拿到回答。把这一轮问答加入短期对话层等待下一轮。这个流程里第2步和第3步是有先后依赖的因为摘要压缩会改变短期对话层的内容进而影响最终拼装。实际实现时要注意顺序别搞反了。4.4 参数选择的实测记录我在一个文档问答项目里调过一组参数记录一下供参考。模型窗口8000 token输入预算设6500。滑动窗口最初设10轮发现长会话下经常超预算后来降到5轮配合摘要压缩稳定性好了很多。向量召回的相似度阈值从0.6开始试0.6时召回太多噪声0.75时又经常召不回东西最后定在0.68比较平衡。摘要触发阈值设在短期对话层占预算80%的时候实测下来既不会太频繁调用模型也不会等到超预算才手忙脚乱。这些数字不是标准答案你的场景不同最优值也会不同。但调参的思路是一样的先定一个保守的初始值然后根据实际表现微调每次只动一个参数观察效果变化。5. 常见问题与排查技巧实录5.1 模型“失忆”了怎么办最常见的抱怨就是“聊着聊着它就不记得前面说过的了”。排查这个问题我一般按这个顺序看。先确认信息是不是真的被裁掉了。把每次实际发送给模型的完整上下文打印出来看很多时候你会发现关键信息在某一轮被滑动窗口挤出去了。如果是这个原因要么加大窗口要么把关键信息提前抽出来放进长期记忆。再确认信息是不是被摘要弄丢了。摘要压缩有信息损失如果关键信息正好在被压缩的那一轮里就可能丢失。解决办法是在摘要指令里明确要求保留“用户明确提出的要求”这类内容或者在压缩前先把关键信息单独提取出来存一份。最后确认是不是模型本身没抓住。有时候信息在上下文里但模型就是没用好。这种情况可以调整信息的摆放位置把最重要的内容放在上下文的开头或结尾这两个位置模型的注意力通常更强。5.2 回答越来越慢、越来越贵如果发现响应时间变长、费用上涨八成是上下文膨胀了。先看输入token的走势如果每轮都在涨说明裁剪逻辑没生效。检查一下预算校验那一步是不是真的在跑有时候代码里写了但条件判断写错了等于没裁。另一个常见原因是摘要没有二次压缩。长期记忆如果只增不减聊得越久它越大最后反而成了负担。解决办法是给长期记忆也设一个上限超过就再摘要一次把旧的摘要合并压缩。5.3 常见问题速查表现象可能原因排查方向解决思路模型忘记前文信息被裁或摘要丢失打印实际上下文调整窗口或摘要保留项响应变慢变贵上下文膨胀看输入token走势检查裁剪逻辑和二次压缩回答跑偏无关信息过多看召回内容相关性提高相似度阈值重复回答旧问题窗口切断了问答对检查滑动边界按轮次而非条数滑动摘要越来越长没有二次压缩看长期记忆长度设上限并合并摘要5.4 几个我踩过的坑第一个坑是把系统提示词也放进滑动窗口。早期图省事所有消息一视同仁地滑结果滑着滑着系统设定没了模型开始胡说。系统层必须单独拎出来永远置顶。第二个坑是摘要时把助手的话也当重点。助手说的话很多是展开论述真正重要的是用户表达的诉求。摘要时应该以用户信息为主助手内容只保留结论性的部分。第三个坑是向量召回不做去重。同一件事在短期对话和向量库里各有一份拼进去之后模型看到重复信息有时候会以为是两件事。召回前要先跟当前上下文比对已经存在的就别再塞了。第四个坑是忽略多轮之间的指代。用户说“就按刚才那个来”这个“刚才那个”如果被裁掉了模型就懵了。处理指代类表达时要特别小心宁可多留一轮上下文也别把指代对象弄丢。6. 进阶玩法与场景延展6.1 让上下文管理更聪明一点基础的滑动加摘要能解决大部分问题但如果你想做得更细可以试试给消息打标签。比如把消息分成“事实类”“偏好类”“待办类”“闲聊类”不同类型的消息用不同的保留策略。事实和待办优先级最高闲聊优先级最低预算紧张时先丢闲聊。这个思路实现起来不复杂就是在存消息的时候多存一个类型字段裁剪时按类型排序。另一个进阶方向是动态窗口。不要固定保留5轮而是根据当前问题的复杂度动态调整。问题简单就少留几轮问题复杂、需要大量背景就多留几轮。判断复杂度可以用当前输入的长度和是否包含指代词做个粗略估计。6.2 不同场景的策略差异客服场景和创作场景对上下文的需求完全不同。客服场景里用户的身份信息、订单信息、历史工单是关键这些应该抽出来放进长期记忆每轮都带上。创作场景里前面写过的内容、设定的风格、人物的设定是关键这些更适合用摘要加向量召回的组合。知识问答场景又不一样它的重点不在对话历史而在检索到的文档片段。这时候上下文管理的主要精力应该放在检索质量上对话历史反而可以压缩得很狠留个一两轮保证连贯就够了。6.3 监控和迭代上下文管理不是一次配好就完事的它需要持续监控。我一般会记录几个指标每轮实际输入token数、摘要触发频率、向量召回命中率、用户对回答的反馈。这几个指标能帮你判断当前策略是不是合适。如果发现输入token长期贴着预算上限跑说明预算太紧或者裁剪不够得调。如果摘要触发太频繁说明窗口太小或者摘要压缩率不够。如果召回命中率低说明切块粒度或阈值有问题。这些都需要你根据实际数据去调没有一劳永逸的配置。我个人在实际操作中的体会是上下文管理最难的从来不是技术实现而是判断“什么该留、什么该丢”。这个判断依赖你对业务场景的理解依赖你对用户真实需求的把握。技术方案只是工具真正决定效果的是你对场景的理解深度。所以别急着堆技术先想清楚你的用户最在意什么再倒推该保留什么上下文。这个顺序搞对了后面的事情会顺很多。
阅读完成 · 觉得有帮助?