做AI Agent开发的朋友,十有八九都会卡在同一个地方:对话一长,模型就开始“失忆”。你说东它说西,前面确认过的信息后面又反复问,甚至把用户早就不想要的旧设定当成最新指令执行。刚接触上下文管理时,我也天真地以为这不过是把历史消息拼一拼、塞进提示词就算完事,直到自己搭的Agent在连续对话超过二十轮后彻底“精神分裂”,才意识到这里面藏着多少坑。
这篇文章是AI Agent系列的第三篇,专注聊上下文管理。它不是什么理论科普,而是我在实际搭建多轮对话系统、跑角色扮演Agent、做知识库问答机器人这些项目里,反复折腾上下文之后踩出来的经验。内容会按五个部分展开:先解读上下文管理到底在解决什么,再把窗口取舍、记忆分层、遗忘机制这些核心环节逐项拆开,然后给出可落地的实现思路,接着结合案例讲典型故障的排查方法,最后说说我自己的体会。如果你想搭一个真正稳定、能听懂人话的Agent,而不是只会处理单轮问答的玩具,这篇文章值得读完。
1. 上下文管理的核心需求:并不是把所有话都喂给模型
先说一个最容易踩的误区。很多人觉得上下文管理就是把所有聊天记录一股脑塞给模型,把“记忆”和“堆历史”画上等号。实际上这里面的核心矛盾是:模型上下文窗口有限,而真实对话产生的信息是无限的。用一个固定容量的杯子去装一条奔流的长河,强灌只会溢出,关键是要学会分流和沉淀。
我做过一个具体的对比实验。A方案是把最近五十轮对话完整拼进提示词,B方案是用摘要压缩历史、只保留最近五轮的完整原文,两个方案跑同一套多轮任务,结果是B方案的指令跟随准确率高出大约三成,响应延迟还降了四成。原因并不玄妙,模型注意力会随着超长上下文衰减,无关的寒暄、冗余的中间推理过程,都在抢占真正重要的信息位。
所以上下文管理从设计层面就要回答三个问题:哪些信息必须记住、哪些信息可以丢弃、哪些信息需要在不占空间的条件下快速找回。这就像人脑不是把所有经历都永久存储在“工作台面”上,而是分成工作记忆、短期记忆、长期记忆几个层次。Agent也一样,必须在设计的一开始就把记忆分层想清楚,而不是等到上下文不够用了再临时压缩。
从应用场景看,上下文管理的好坏直接决定了Agent的可用性下限。客服机器人记不住用户刚报的订单号,就得多轮追问;编程助手忘了用户项目里指定的技术栈,就会输出一堆风马牛不相及的代码;角色扮演Agent忘了设定里的关键性格,聊到后面会OOC(脱离角色)到没法看。可以说,任何需要多轮协作的Agent,上下文管理就是地基中的地基。
2. 上下文窗口的取舍策略:有限空间里的信息分治
2.1 窗口不是越大越好,关键是让“重要信息”始终在线
大模型厂商都在卷上下文窗口长度,从32K到128K再到200K,听起来很唬人,但真实部署过就会发现,窗口是越大越容易“铺张浪费”。这里有一个隐含规律:超长上下文里,模型对中间部分的关注度天然偏低,对开头和结尾的信息最敏感。业内人士常说的“Lost in the Middle”指的就是这个问题——你把订单号、用户偏好这些关键信息淹没在二十轮废话中间,模型照样看不见。
我的取舍思路是:把上下文窗口划分为两个逻辑区。第一个是“硬核区”,负责存放当前必须执行的指令、最近几轮的核心约束,以及用户最高频强调的需求;第二个是“背景区”,负责存放压缩后的对话摘要、知识库检索片段、历史任务结论。硬核区保持精简稳定,背景区允许滚动覆盖。这样窗口虽然也在往里塞东西,但优先级一清二楚,模型每次做决策时面对的始终是“提纲挈领”的信息流。
实操时我会给硬核区设置一个比例上限。比如窗口总长度8K,硬核区不超过2K,背景区留6K去承载摘要和检索片段。别小看这个分配动作,它能让指令的稳定性大幅提升。因为每次请求前背景区的内容都重新组装,相当于让模型拿着一份动态更新的简报去干活,而不是抱着整卷聊天记录去考古。
2.2 摘要压缩是必需品,但压缩本身也会引入风险
压缩摘要几乎是人人都能想到的方案,但真做起来,细节决定成败。我试过直接用模型把一大段历史对话压缩成一段话,看起来挺合理,实际用起来问题不少。关键在于,摘要如何与原始内容搭配,而不是摘要本身的质量。
压得太狠,信息丢失。比如用户之前在第三轮提过“我只能接受Python的技术方案”,摘要压缩成“用户偏好Python”,到了后面某个环节,模型就可能默认“只要是Python就行”,忽略了“只能接受”这个排他性前提。压得太松,摘要又跟原文一样长,失去了压缩的意义。
我现在更推荐分级压缩方案。先把每一轮对话独立压缩成结构化条目,保留核心实体、明确的约束、决策结论;再按话题段落把若干轮合并成一段摘要;最后在上下文中同时保留“最近N轮原文+更早历史的分层摘要”。这样近处细节完整,远处脉络清晰,模型需要追溯细节时,近期原文里能找到;需要理解来龙去脉时,摘要里也能看到演进的骨架。
2.3 Token分配机制里,隐藏着上下文管理的真正成本
做项目不能不算账。上下文管理直接跟成本挂钩,每一轮对话塞进去的Token数量决定了你在模型API上的支出,也决定了响应速度。一个常见的问题就是过度检索:为了“确保不漏信息”,把知识库里搜索出来的五十个片段全部拼进去,结果真正的答案只有一句,其他四十九句全是噪音。
我自己的Token分配原则是“三步走”。第一步,预估任务类型,判断这轮交互到底需要多少历史信息和外部知识;第二步,按预估的预算上限去检索和组装,不盲目塞满;第三步,每轮请求结束后审查实际用量,对超出预算的调用打标记,回溯是哪里溢出了。做久了之后你会发现,大部分上下文爆炸都不是单一原因,而是多轮积累后没人清理导致的。
在这里也提醒一下,不同家模型对上下文长度的计费规则不一样。有的按Prompt和Completion分开计,有的按总Token计,在设计缓存和持久化时要提前把这些规则考虑进去,否则很容易出现“对话没多少,费用先暴涨”的尴尬。
3. 记忆的分层设计:别让你的Agent变成金鱼
3.1 显式记忆与隐式记忆,两者都要管
记忆分层是上下文管理的核心骨架。我把它分为显式记忆和隐式记忆两条线。
显式记忆好理解,就是用户明说出来的信息。用户说“我叫李明,帮我记住”,这是显式;用户说“我家小朋友三年级了”,这是隐式——他没有说“记住我有个上三年级的孩子”,但这句话隐含了这个事实。许多Agent只处理显式记忆,用户说“记住”才记,用户没说的就当作过眼云烟。这会让体验大打折扣。
我的经验是,对每一轮用户输入都要做一个“记忆潜力评估”。先用模型提取实体和关系:人、物、时间、偏好、限制条件、目标状态,再把这些结构化数据写进记忆库。重点要标注信息的置信度和时效性,别把用户随口一说的“我可能下周去上海”当成确定行程存进去。置信度低的记忆用完之后要及时降级或清理,否则积攒一批过期假设,后续推理会被带偏。
隐式记忆的另一个价值是发现用户的表达习惯。比如用户习惯用缩写,习惯在每段话前先给结论,习惯夹带少量英文词汇。如果Agent能记住这套风格并在输出里延续,交互的“人味儿”会立马上来。这并不需要什么高深算法,只要在对话回合间隙异步抽取出风格特征,写进一个独立的显性配置里就行。
3.2 缓存在会话里的记忆,和企业级的持久化记忆,根本不是一回事
很多人把缓存和记忆混为一谈。Redis里存了几条聊天记录就号称“Agent有记忆”,这是理解偏差。缓存的本质是临时存储,和上下文管理配合使用,解决的是“快速访问”的问题;而持久化记忆解决的是“跨会话复用”的问题。
举个例子,用户昨天在对话里要求“所有周报都用表格结构输出”,如果这只是一个会话内的临时状态,那么今天新开一个会话,这个偏好就丢失了。要让Agent连续几天保持这个输出习惯,就必须把这条规则写入持久化记忆库。我这里用的比较朴素的方案是SQLite加一个memory表,字段包括用户ID、记忆类型、内容、置信度、来源轮次、时间戳、最后访问时间。看起来简单,但配合一个定期清理脚本,效果已经非常能打。
在做记忆持久化时,还要区分“用户级记忆”和“会话级记忆”。用户级记忆跟随账号走,跨会话长期生效;会话级记忆只作用于当前对话组。这两个层级如果不分开,容易出大笑话——用户在A会话里设置了个性化偏好,跑到B会话问完全无关的问题,结果AI还在讲A会话里的旧设定。我遇到过类似场景,当时排查了很久,最后发现就是层级没分清楚,所有记忆一股脑混进了同一个存储桶。
3.3 长期记忆的召回策略,直接决定Agent的“聪明程度”
有记忆库只是第一步,关键是怎么把记忆准确找回来。每次对话时不可能把用户所有历史记忆全部塞进上下文,所以必须依赖召回机制,而且要有优先级排序。
我常用的召回排序算法是“时间衰减+相关性加权”。每个记忆条目都有一个最后访问时间和一个被引用次数,这两个变量会影响它在召回列表里的排名。相关性计算简单一点,就是用户当前输入与记忆内容做向量相似度,超过阈值的才进入候选。排完序后按分数从高到低取前N条,再配合一定的随机扰动,防止每次都召回完全相同的记忆导致对话单调。
时间衰减公式也不复杂,基本思路是分数乘上一个衰减因子,越久没被激活的记忆权重越低。但这里有一个边界情况:用户偶尔会隔了很久再提起一个旧话题,如果衰减太激进,旧记忆早就被清出候选区,AI会表现为“彻底忘了”。所以我在衰减公式里设定一个保底分,只要是置信度标记为“重要”的记忆,哪怕时间很久也强制参与召回。这一招在角色扮演类Agent里尤其好用,角色之间的核心关系和关键事件,不会因为对话频率低就被模型淡忘。
4. 记忆的遗忘与长期迁移:该忘的别硬留,该记的别漏掉
4.1 遗忘并不全是坏事,主动遗忘是保证稳定性的手段
和直觉相反,适当遗忘反而能让Agent更稳定。想象一个场景:用户连续讨论了十个话题,其中最前面的话题早已结束,但Agent在后续输出时还会不自觉引用前面的话题设定,导致回答“跑偏”。这就是没有主动遗忘机制造成的——上下文一直在背景里,模型无法判断哪些已完成、哪些还在进行。
我引入了一个“话题生命周期”概念。每个话题在打开时记录开启时间,在用户明确转换话题后标记关闭。关闭后的话题只保留摘要,原文从上下文中逐出。这样Agent在回答新问题时,不会被已经关闭的话题内容干扰。同时,关闭状态的话题如果后续又被用户重新开启,再通过召回机制把详细记录调回来,而不是始终挂在上下文里。
主动遗忘另外一个应用是“自我修正”。模型在一些场景下会产生错乱信息,如果这些错乱信息被当作正常记忆缓存下来,后续就会被自我强化,越错越远。所以我会在记忆写入时加一道一致性校验,用另一条提示让模型自己判断,这段新增记忆是否与已有的核心设定冲突。如果冲突,新记忆会被拦截并放入待确认区,而不是直接覆盖旧设定。
4.2 长短迁移:把临时对话的产出,沉淀为长期可复用经验
短期记忆和长期记忆之间不是割裂的,中间需要有一个迁移机制。最简单的做法是会话结束后触发生成“会话小结”,由模型把本次对话中“新的、值得长期保留的”信息提炼出来。但注意,这个提炼过程很考验提示词设计。
我最初写的小结提示词只是说“总结用户的需求”,结果每次都是从对话里摘录原文,毫无结构化价值。后来改成让模型按四个维度输出:可长期记忆的事实、用户表达出的偏好、本次对话中建立的约束条件、未完成但还有后续价值的事项。每个维度限定只输出关键短语,不写废话。这个调整做完之后,沉淀下来的记忆复用率明显提高,不再是一堆空泛的“用户提到...”。
迁移时间点也很重要,不要只在会话自然结束时才迁移。整个对话时长超过一定分钟数,或轮次达到某个阈值,就可以先做一次阶段性迁移,防止对话中途进程崩溃后所有信息丢失。我踩过一次进程异常重启的坑,那一刻才意识到,实时迁移比写多少个花哨的记忆算法更基础。
可靠的迁移策略也应有升级机制。如果某条记忆在多个不同会话里被反复激活,说明它对用户很重要,可以考虑把它的置信度标记上调,并提到更高优先级列表。这样用户虽然每次说的都是零散内容,但Agent能通过“高频记忆”自动识别出核心主线。
4.3 会话管理的状态机设计,让上下文始终处于可控状态
从工程角度看,上下文管理本质上是一个状态机。每次对话都应明确处于哪个阶段:是在收集信息、还是在执行任务、还是在确认结论。阶段不同,上下文里放的内容比重就应该不同。
我给会话设计了五个状态:初始化、需求收集、方案执行、结果确认、收尾归档。初始化时上下文里放的是系统提示和用户偏好;需求收集阶段侧重于历史记忆召回和约束记录;方案执行时大量注入工具调用结果和中间推理;结果确认状态压缩过程信息,突出展示最终结果;收尾归档时把整个对话浓缩成结构化记忆写入长期库。
这种状态机的好处是,上下文组装不再是一团无规则的堆叠,而是有章法的动态编排。模型在指定的状态里面对的上下文内容更聚焦,它回答的准确性自然更高。而且状态切换本身也可以交给模型来判定——每一轮结束时,让模型输出一个状态字段,下一轮组装逻辑就按新状态走。
5. 系统性衰退场景分析:当Agent开始“胡言乱语”
5.1 症状一:模型对最新指令不敏感,还在回应老设定
这是最常见的一种衰退,表现为用户明确改了要求,但Agent依然沿用旧规则。通常不是模型笨,而是旧规则在上下文中的优先级没被正确设置。如果旧设定的描述更长、位置更靠后,或者与系统提示靠得更近,就可能盖过用户新输入的简短指令。
我排查时第一步会看上下文的组装顺序。原则上,系统提示、核心约束、最新指令、短期记忆、长期记忆摘要,这五个模块的顺序不能乱。最新指令必须放在短期记忆之前,紧贴系统提示或紧随其后,用分隔符明确区分。如果组装顺序乱了,再智能的模型也会被前后矛盾的信息搞晕。
另外,最新的指令如果和旧设定冲突,要在上下文里显式标注“此条覆盖之前的规则”。一句话就能解决的问题,很多人却漏掉。模型不是人,不会自动判断“最新意味着优先”,它只会按信息显著性分配注意力。标注本身就是为了制造显著性。
5.2 症状二:细节张冠李戴,李四的信息安到了张三头上
多用户系统里,最怕串记忆。比如用户A的偏好被应用到用户B的对话里,或者某个项目环境的配置被套到另一个项目的分析中。排查这类问题的方向是记忆隔离。所有写入记忆库的数据都必须带用户和场景标签,在召回时,查询条件强制过滤匹配标签。
还有一种张冠李戴发生在单会话内部:模型把前几轮某个实体属性错配到了当前实体上。比如用户先聊了“张经理的日程”,又聊“李经理的日程”,模型可能把张经理的日程安排当成李经理的输出出来。针对这种场景,我在实体识别后强制做一次消解,把新的实体属性和已有实体分开存储,构成独立记忆单元,而不是以“日程”这个笼统概念去存储。
5.3 症状三:重复信息在上下文里反复出现,把有效空间挤没了
有些信息是天然会被重复提及的,比如用户的工作岗位、家庭角色、某些固定偏好。如果每轮对话都原样塞进上下文,积累几十轮以后重复Token占比会高得吓人,有效容量被通货膨胀消耗殆尽。
我的解法分两步。第一步是信息归一化,用户如果这轮提的内容和长期记忆里的条目高度重合,就不必新写一条,只更新原条目的最后访问时间。第二步是输出去重,在组装上下文时扫描候选片段,与已选片段计算重叠度,重叠度高且信息量低的片段直接跳过。这一步在知识库问答场景效果尤其明显,检索出来的多个片段经常内容重复,去重之后上下文长度能缩减百分之二三十,回答反而更清爽了。
5.4 症状四:工具调用结果把上下文变成了垃圾场
现代Agent基本离不开工具调用,但工具返回结果往往很臃肿。一次数据库查询可能返回几十行日志,或是一大段网页正文,原样拼进上下文既占空间又会误导推理。工具结果不能拿来即用,必须经过提炼。
我所有工具调用的返回结果都强制经过一个“结果压缩层”。这个层做的事情包括:只保留关键字段、删除空行和时间戳噪音、数值统一格式、必要时额外调用一次轻量模型做要点总结。压缩后的结果再进入上下文,同时原始完整结果存储到外部日志,以备审计和排查。这个流程跑通之后,工具密集型的Agent任务稳定性和速度都有可感知的提升。
6. 实操实现的几个核心环节
6.1 上下文组装器的模块设计
上下文管理落到代码实现,可以抽象成一个组装器。每轮请求进来,组装器接收当前状态和用户输入,然后从多个数据源拉取信息,包括但不限于:系统提示模板、用户长期记忆候选、会话存储、知识库检索结果、工具输出缓存。拉完之后按照固定顺序拼接成模型输入。
我实现的组装器是三个方法的组合:load_prompt_template负责加载提示模板;load_memory_candidates负责召回记忆并按规则排序;build_context执行最后拼接。得益于模块化设计,后续如果要加新能力,比如文档引用片段或者多模态输入,不会影响已有的核心流程。
有一个细节值得分享:在拼接时,各个内容模块之间必须使用清晰的分隔符标记,比如用带尖括号的标签区分。模型对隔离良好的模块理解度远高于所有内容堆成一团。尤其是在系统提示和用户输入之间、记忆区域和工具结果之间,分隔符作用非常大。
6.2 记忆写入的前置提取与后置校验
记忆写入往往是异步进行的,避免阻塞对话主链路。在用户每一轮输入结束后,触发一个结构化提取任务:识别实体、关系、约束、偏好、情绪倾向。提取结果进入内存队列,批处理写入持久化存储。
但这只是前置提取,真正重要的还有后置校验。校验分两道,第一道检查格式问题,例如空字段、JSON解析失败、日期格式不统一;第二道检查冲突,新记忆与旧记忆内容相斥时,根据时间戳和置信度决定保留谁。我见过很多项目把精力全放在提取上,忽略了校验,结果记忆库越建越臃肿,垃圾和精华混合在一起,反而破坏上下文质量。
6.3 对话历史存储的结构化改造
如果还在用纯文本数组存对话历史,上下文管理做起来会非常痛苦。纯文本没有索引,没有语义标记,检索只能靠截取,想做精细化召回基本不可能。我的建议是给对话存储加结构:每条消息除了role和content,还要附带额外的结构化字段。
比较实用的字段包括:消息类型(用户陈述、用户提问、Agent说明、工具结果)、话题标签、关键实体列表、重要性分数、指令覆盖标记。有了这些字段,组装上下文时就能按重量级筛选,而不是无脑全取。我之前的项目在没做结构化改造前,代码里到处是find和slice来抠信息,改成结构化存储之后,组装逻辑简单了不止一倍,排查问题时定位也更快。
6.4 系统提示词在上下文管理中的角色
很多人把系统提示词当摆设,随便写两句“你是一个有用的助手”就完事。但在上下文管理里,系统提示词扮演着“总指挥”的角色,它需要告诉模型这个上下文是怎么组织的,信息以什么优先级存在,处理冲突时该怎么做。
我系统提示词里会显式写入类似这样的声明:最近的用户指令优先于早前设定,记忆摘要仅供参考,工具结果可能有延迟,若发现冲突以用户当前表述为准。这么写看似是废话,但对模型的推理行为约束非常有效。模型在模糊地带做选择时,系统提示里的优先规则能显著提高它与开发者期望的一致性。
7. 常见故障排查与调试速查表
| 问题现象 | 大概率原因 | 排查方向 | 对应调整策略 |
|---|---|---|---|
| 最新指令被忽略 | 上下文组装顺序有误,旧设定干扰 | 检查提示词模块拼接顺序 | 最新指令提前,加覆盖声明 |
| 长时间对话后记忆混乱 | 摘要压缩丢失关键约束 | 还原压缩前的关键轮次 | 分区分级压缩,保留近期原文 |
| 用户信息张冠李戴 | 记忆库未按用户隔离 | 检查记忆写入和召回过滤条件 | 强制按用户标签过滤 |
| 上下文Token暴涨 | 重复片段过多或工具结果臃肿 | 统计各内容模块占用的Token比例 | 引入去重与工具结果压缩层 |
| Agent翻旧账决策跑偏 | 已关闭话题仍占上下文 | 检查话题状态管理 | 引入话题生命周期机制 |
| 模型过分自信给错答案 | 上下文里可验证信息不足 | 查看知识库检索覆盖率 | 增强检索召回阈值与排序 |
| 多轮后风格漂移 | 用户偏好未在上下文中稳定占位 | 检查偏好模块是否被新内容挤掉 | 偏好区固定长度并保持置顶 |
调试时我有一个习惯:给组装后的上下文单独开一个日志输出。每次请求都完整打印最终拼装结果到本地文件,标注每个模块的起始位置和Token数目。遇到问题,先看这个日志,通常一眼就能定位是哪个模块出了问题。很多人图省事只记网络请求参数,等到排查时才发现关键是“实际发给模型的内容长什么样”。
8. 回到源头,上下文管理的本质还是三个问题
做了这么多项目,回头再看,上下文管理的复杂性都来自三个基础问题的叠加:模型窗口是有限的、对话状态是动态的、用户意图是分层的。所有策略无非是在这三个现实约束中间找平衡。
记忆分层解决的是“动态状态”的存储问题,让Agent既能处理当下任务,又能维持长期关系;摘要压缩解决的是“有限窗口”下信息密度的取舍,让重要信息尽可能留存;召回排序解决的是“分层意图”的响应问题,判断当前对话最需要哪些历史支撑。
实践里没有统一的最优解,只有根据场景做的取舍。知识库问答Agent可以容忍不压缩历史、只做检索增强;而角色扮演类Agent则必须把记忆的长期一致性放在第一位;工具密集型Agent要优先保证工具结果的精简和稳定。理解了这三种场景的差异化需求,再去看各种上下文管理方案,就不会被框架名和术语牵着鼻子走了。
我在最近一次项目中还有个体会,单独谈一下。前几轮我一直在帮某位用户搭建一个偏生活服务类的智能助手,涉及日程安排、偏好记录、提醒设置。一开始我把上下文管理做得很复杂,又是分层又是向量召回,结果发现用户真正得到的体验提升并没有想象中那么明显。后来我砍掉不必要的复杂模块,只保留了三个最简单的能力:核心指令永远在窗口前排、用户偏好单独区域稳定存储、历史信息压缩成摘要。效果反而更符合预期。这个经历让我开始反思,很多Agent项目的瓶颈并不在技术复杂度不够,而在于开发者是否真正理解,用户当下得到准确、自然、连贯的反馈,比任何华美的架构设计都重要。
对于还没开始做上下文管理的朋友,建议从最小闭环开始:先实现窗口内裁剪,再做一层摘要压缩,最后加持久化记忆。别一上来就追求百科全书式记忆系统,那只会让项目陷入没完没了的调试泥潭。等跑通了基础链路,你自然会发现它往哪个方向需要加厚。
这个系列下一篇我打算聊聊AI Agent的工具调用,就是Agent怎么决定调哪个函数、传什么参数、以及怎么消化工具返回值。如果你正在做这方面开发,欢迎在评论区讨论自己遇到的怪问题,越是奇怪的现象越能激发出有价值的技术解法。