首页 / 资讯中心 / 文章详情

大模型上下文管理实战:分层记忆、压缩与检索策略

大模型上下文管理实战:分层记忆、压缩与检索策略 ★ FEATURED ARTICLE
1. 上下文管理到底在管什么第一次听到“上下文管理”这个词很多人会下意识觉得它是个很虚的概念像是某种架构师才需要关心的东西。但如果你真正写过一个稍微复杂点的对话系统、代码助手或者多轮任务代理你就会发现上下文管理才是决定这套系统能不能用得下去的那根命脉。它管的不是别的就是模型在每一次推理时到底“记得什么”“忘掉什么”“优先看什么”。说白了大模型本身没有记忆。它每次生成回复都是把当前收到的全部文本重新读一遍然后预测下一个词。所谓“多轮对话”其实是把历史消息拼成一个越来越长的字符串再一股脑塞进去。问题就出在这个“越来越长”上——上下文窗口是有硬上限的而且就算没到上限塞得越多推理越慢、越贵关键信息还越容易被淹没。我拿一个实际场景举例。假设你在做一个客服助手用户先问“我的订单什么时候到”你查了订单号回复了物流信息接着用户说“那帮我改一下收货地址”这时候模型必须记得上一轮的订单号否则它根本不知道要改哪个订单。这个“记得订单号”的能力就是上下文管理要解决的核心问题之一。但如果你把用户前面闲聊的二十句话、系统提示词、工具返回的一大段JSON全都原样保留那订单号很可能被挤到角落里模型反而抓不住重点。所以上下文管理要干的事可以拆成三个层面存什么、怎么存、什么时候取。存什么决定了信息的选择性怎么存决定了压缩和结构化的方式什么时候取决定了检索和注入的时机。这三件事做不好系统就会表现出典型的“失忆”“串台”“答非所问”。适合看这篇内容的人我大致分三类。第一类是正在做对话应用或者智能体的开发者你大概率已经被上下文超限或者信息丢失折磨过第二类是做RAG或者知识问答的工程师你需要理解检索结果和对话历史怎么共存第三类是对大模型应用感兴趣、想搞清楚“记忆”这件事到底怎么落地的技术爱好者。不管你是哪一类接下来的内容都会尽量落到可操作的层面而不是停在概念上。2. 上下文管理的整体设计思路2.1 为什么不能“全都留着”最朴素的做法是把所有历史消息原封不动地保留每次请求都全量发送。这个方案在对话轮次少的时候完全没问题甚至是最省心的。但只要轮次一多三个问题会同时爆发。第一是成本。上下文长度直接和token消耗挂钩而token是要花钱的。假设每轮对话平均500个token聊到第50轮单次请求就是25000个token。如果每轮都要重新发送全部历史总消耗是累加增长的不是线性的。我实测过一个中等复杂度的助手聊到三四十轮之后单次请求成本能涨到最初的十几倍。第二是延迟。上下文越长模型处理输入的时间越长。对于需要实时响应的场景比如语音助手或者在线协作工具多出来的几百毫秒到几秒是致命的。第三是效果衰减。这一点最容易被忽略。学术界有个说法叫“迷失在中间”意思是当上下文很长时模型对开头和结尾的信息记得比较牢中间部分的信息容易被忽略。你把关键信息埋在第三十轮模型很可能根本没用上。所以“全都留着”不是不行而是不可持续。上下文管理的第一性原理就是在有限的空间里放最有价值的信息。2.2 分层记忆的设计思路我比较推荐的一种思路是分层。把上下文拆成几个不同优先级、不同生命周期的层每层用不同的策略管理。最底层是系统层放系统提示词、角色设定、全局约束。这部分基本不变优先级最高永远保留。中间层是工作记忆层放当前任务相关的关键信息比如用户正在处理的订单号、当前编辑的文件路径、最近几轮的核心意图。这部分需要动态更新任务结束就清理。最上层是历史层放完整的对话记录但只保留摘要或者按需检索不全部注入。这个分层的好处是你可以针对每一层设计不同的压缩和检索策略。系统层不动工作记忆层用结构化字段存历史层用摘要或者向量检索。这样既保证了关键信息不丢又控制了总长度。2.3 压缩与检索的取舍压缩和检索是两条主要的技术路线各有适用场景。压缩的思路是把长文本变短比如把十轮对话总结成一段摘要或者把工具返回的大段JSON提取成几个关键字段。优点是简单直接注入的时候不需要额外检索延迟低。缺点是压缩是有损的摘要可能丢掉细节而且摘要本身也需要模型生成有额外成本。检索的思路是不压缩而是把历史存到外部需要的时候再查。典型做法是把每轮对话向量化存进向量库当前问题来了之后做相似度检索把最相关的几条捞出来注入。优点是信息不丢可以存很久。缺点是需要额外的检索步骤而且检索质量依赖embedding模型和索引策略有时候会捞出不相关的内容。我的经验是两者结合。近期对话用压缩保证连贯性远期历史用检索保证可追溯。工作记忆用结构化字段保证关键信息精确。具体比例可以根据场景调但纯用某一种往往会在某个环节出问题。3. 核心细节解析与实操要点3.1 系统提示词的稳定性设计系统提示词是上下文的锚点它的稳定性直接影响模型表现。我见过很多项目把系统提示词写得很长很细恨不得把所有规则都塞进去。但系统提示词越长占用的上下文预算越多而且模型对它的注意力也会被稀释。我的做法是分层写系统提示词。第一层是角色和核心约束比如“你是一个订单处理助手只能处理订单相关问题”。第二层是输出格式要求比如“回复必须包含订单号和预计时间”。第三层是边界情况处理比如“如果用户问的问题和订单无关礼貌拒绝”。这三层里第一层永远保留第二层和第三层可以根据当前任务动态调整。还有一个细节是系统提示词的位置。有些框架会把系统提示词放在最前面有些放在最后。实测下来放在最前面更符合模型的训练分布效果更稳。但如果你的框架支持把最关键的约束在最后再重复一遍能进一步提升遵循率。注意系统提示词里不要放会频繁变化的内容比如当前时间、当前用户ID。这些应该放在工作记忆层否则每次变化都会导致缓存失效增加成本。3.2 工作记忆的结构化存储工作记忆是上下文管理里最容易被做砸的部分。很多人把它当成普通的历史消息混在一起存。结果就是关键信息被淹没模型抓不住重点。我的建议是用结构化字段存工作记忆而不是纯文本。比如用一个JSON对象存当前任务状态{ current_task: 修改收货地址, order_id: ORD-2024-XXXX, user_intent: change_address, last_action: query_logistics, pending_confirmation: false }这个对象在每次请求时序列化成文本注入模型能非常清晰地看到当前状态。相比把“用户说要改地址订单号是XXX”混在对话历史里结构化存储的召回率高得多。更新策略上我倾向于每轮对话后让模型输出一个状态更新指令而不是让模型自由发挥。比如让模型返回一个update_memory字段明确告诉系统要改哪个字段。这样可控性强不会出现模型自己乱改状态的情况。3.3 历史对话的摘要策略历史对话的摘要核心是保留什么、丢弃什么。我的经验是保留四类信息用户的明确偏好、已经确认的事实、未完成的承诺、以及情绪转折点。丢弃的是寒暄、重复确认、以及已经被后续对话覆盖的中间状态。摘要的触发时机也很关键。不要每轮都摘要那样成本太高。我一般设置两个阈值轮次阈值和token阈值。比如超过10轮或者历史token超过2000就触发一次摘要。摘要时把最老的几轮合并成一段保留最近几轮原文。摘要的prompt我一般这样写“请把以下对话压缩成不超过150字的摘要保留用户偏好、已确认事实、未完成事项丢弃寒暄和重复内容。输出格式为纯文本不要加任何前缀。”实测下来这个prompt生成的摘要质量比较稳定不会丢关键信息。3.4 检索注入的时机与排序检索注入最怕的是捞出一堆不相关的内容反而干扰模型。我的做法是加两层过滤。第一层是时间过滤。优先检索最近的历史比如最近7天或者最近50轮。太老的历史除非明确相关否则不注入。第二层是相似度阈值。设定一个最低相似度分数低于这个分数的直接丢弃宁可不注入也不要注入噪声。排序上我一般按相似度乘以时间衰减来排。相似度高且时间近的排前面相似度高但时间远的排后面。这样既保证了相关性又保证了时效性。还有一个细节是注入位置。检索结果不要直接拼在对话历史后面而是单独放在一个“相关历史”区块里用明确的分隔符隔开。这样模型能区分“当前对话”和“参考历史”不会混淆。4. 实操过程与核心环节实现4.1 一个最小可用的上下文管理器我拿一个实际做过的对话助手项目来拆解。这个助手要处理多轮订单查询和修改上下文管理是核心模块。整体流程分四步接收消息、更新工作记忆、压缩历史、组装请求。第一步接收用户消息后先做意图识别。这一步可以用小模型或者规则判断用户是在查询、修改还是闲聊。意图识别的结果直接写入工作记忆的user_intent字段。第二步根据意图决定是否调用工具。比如查询意图就调订单查询接口修改意图就调地址修改接口。工具返回的结果不要全量塞进上下文而是提取关键字段写入工作记忆。比如查询返回一大段物流信息我只提取status和estimated_time两个字段。第三步检查历史长度。如果超过阈值触发摘要。摘要时把最老的几轮对话合并保留最近三轮原文。摘要结果存到一个单独的history_summary字段。第四步组装请求。顺序是系统提示词、工作记忆JSON、历史摘要、最近三轮原文、当前用户消息。这个顺序经过多次调整实测下来模型对工作记忆的遵循率最高。4.2 参数计算与阈值设定阈值设定没有绝对标准但可以根据成本和效果做权衡。我一般这样算假设模型上下文窗口是128k token但实际可用预算我一般只用到60%也就是约76k。留出40%是为了应对突发长输入和模型输出的不确定性。在这76k里系统提示词占2k工作记忆占1k历史摘要占2k最近三轮原文占3k当前消息占1k。加起来约9k剩下的67k是缓冲。这个缓冲不是浪费而是为了应对工具返回的长结果或者用户粘贴的长文本。摘要触发阈值我设在历史超过15轮或者历史token超过8k。这两个阈值是根据实际对话的平均长度调的。如果你的场景对话很短可以调低如果对话很长可以调高。4.3 组装请求的代码实现下面是一个简化的组装逻辑用Python写def build_context(system_prompt, working_memory, history_summary, recent_turns, user_message): parts [] parts.append(f[系统指令]\n{system_prompt}) parts.append(f[当前状态]\n{json.dumps(working_memory, ensure_asciiFalse)}) if history_summary: parts.append(f[历史摘要]\n{history_summary}) if recent_turns: parts.append(f[最近对话]\n{format_turns(recent_turns)}) parts.append(f[用户消息]\n{user_message}) return \n\n.join(parts)这个函数的关键是用明确的分隔符标记每个区块。模型对结构化标记的识别能力比纯文本强很多。实测下来加了区块标记之后模型引用工作记忆的准确率提升了大概三成。4.4 状态更新的闭环设计上下文管理不是一次性的而是每轮都要更新的闭环。我的做法是在模型输出里强制要求一个状态更新字段。比如让模型返回JSON{ reply: 好的我已经帮您查到订单..., memory_update: { last_action: query_logistics, order_id: ORD-2024-XXXX } }系统解析这个JSON把memory_update合并进工作记忆reply返回给用户。这样每轮对话后工作记忆都是最新的不需要额外推理。这个设计的难点是让模型稳定输出JSON。我的经验是在系统提示词里明确要求输出格式并给一个示例。如果模型偶尔输出格式错误加一层解析容错解析失败就跳过更新不要中断整个流程。5. 常见问题与排查技巧实录5.1 模型“失忆”的几种典型表现模型失忆是最常见的问题但失忆也分几种。第一种是完全忘记比如用户说了订单号下一轮模型问“请问订单号是多少”。这通常是工作记忆没更新或者更新了但没注入。排查方法是打印每次请求的完整上下文看订单号到底在不在里面。第二种是记错比如把订单号记成了另一个。这通常是历史摘要时把不同订单的信息混在一起了。排查方法是检查摘要prompt看是否明确要求保留订单号以及摘要结果里订单号是否正确。第三种是选择性遗忘比如记得订单号但忘了用户要求改地址。这通常是工作记忆字段设计不合理只存了订单号没存意图。排查方法是检查工作记忆的字段是否覆盖了所有关键信息。5.2 上下文超限的应急处理上下文超限是另一个高频问题。有时候用户突然粘贴一大段文本或者工具返回了超长结果直接把上下文撑爆。我的应急处理分三步。第一步是截断。优先截断历史摘要和最近对话保留系统提示词和工作记忆。截断时从最老的开始不要从中间截否则会破坏对话连贯性。第二步是压缩。如果截断后还是超限就对当前用户消息做压缩提取关键信息。比如用户粘贴了一篇长文我只提取前500字和最后500字中间用省略号代替。第三步是拒绝。如果压缩后还是超限就明确告诉用户“输入内容过长请精简后重试”。不要硬塞硬塞的结果是模型输出质量急剧下降。5.3 常见问题速查表问题表现可能原因排查方法解决思路模型忘记订单号工作记忆未更新或未注入打印完整上下文检查检查状态更新闭环模型记错订单号摘要时信息混淆检查摘要结果优化摘要prompt明确保留字段模型答非所问检索注入噪声过多检查检索结果相关性加相似度阈值和时间过滤响应变慢上下文过长统计每次请求token数触发摘要或截断成本飙升历史全量注入统计token消耗趋势启用分层管理和摘要模型忽略系统指令系统提示词被淹没检查系统提示词位置放在最前关键约束重复5.4 几个踩过的坑第一个坑是摘要过于激进。早期我为了省token把摘要压得很短结果模型丢失了关键细节用户不得不重复说明。后来我把摘要长度下限设在100字上限设在200字效果稳定很多。第二个坑是工作记忆字段太多。一开始我设计了几十个字段结果模型根本记不住反而容易混淆。后来精简到十个以内只保留最关键的召回率反而提升了。第三个坑是检索结果直接拼接。早期我把检索到的历史直接拼在对话后面模型经常把检索内容和当前对话搞混。后来加了明确的分隔标记问题就解决了。第四个坑是忽略缓存。系统提示词如果频繁变化会导致缓存失效成本翻倍。后来我把不变的部分和可变的部分分开不变的部分单独缓存成本降了不少。6. 进阶优化与场景延展6.1 多任务场景下的上下文隔离当一个助手要同时处理多个任务时上下文管理会变得更复杂。比如用户一边问订单一边问退货政策两个任务的信息不能混在一起。我的做法是给每个任务分配独立的上下文空间用任务ID做隔离。当前活跃任务注入完整上下文非活跃任务只保留摘要需要时再切换。这个设计的难点是任务切换的识别。我的经验是用意图识别加关键词匹配判断用户是否在切换任务。如果切换就把当前任务的状态存起来加载新任务的状态。实测下来这个方案在多任务场景下能把串台率降到很低。6.2 长文档场景的上下文策略如果场景涉及长文档比如让模型基于一份几十页的文档回答问题上下文管理策略要调整。我的做法是不把全文注入而是先做分块索引用户提问时检索最相关的几个块注入。这样既控制了长度又保证了相关性。分块的大小我一般设在500到1000字块之间有重叠避免边界信息丢失。检索时取top 3到top 5个块按相似度排序注入。如果问题涉及全局理解比如“这份文档主要讲了什么”就先做一次全局摘要把摘要注入。6.3 上下文管理的评估指标怎么判断上下文管理做得好不好我一般看四个指标。召回率关键信息在上下文里出现的比例精确率注入的信息里真正被用上的比例成本每次请求的平均token消耗延迟从收到消息到返回回复的时间。这四个指标要平衡。召回率高但精确率低说明注入太多噪声成本低但召回率低说明压缩太狠。我的经验是召回率优先因为丢信息的代价比多注入的代价大。在召回率达标的前提下再优化成本和延迟。6.4 后续可以扩展的方向这个内容后续还可以往几个方向扩展。一是自适应阈值根据对话的复杂度和用户行为动态调整摘要和检索的阈值而不是用固定值。二是多模态上下文把图片、音频也纳入管理这需要新的压缩和检索策略。三是跨会话记忆让助手记住用户跨会话的偏好这需要持久化存储和隐私保护的设计。我个人在实际操作中的体会是上下文管理没有一劳永逸的方案它是一个需要持续调优的过程。不同场景、不同模型、不同用户行为最优策略都不一样。关键是建立一套可观测、可调整的机制能快速定位问题并迭代。踩过几次坑之后我越来越倾向于“简单可解释”的方案而不是追求复杂的自动化。因为出问题的时候简单的方案更容易排查也更容易修复。
阅读完成 · 觉得有帮助?
咨询建站