☰
LLM应用上下文维护:主动遗忘与语义压缩实战
2026/10/22 2:42:51 网站建设 项目流程

做LLM应用开发这一行,迟早会在同一个问题上卡住一次——对话越聊越长,上下文越堆越满,模型开始“犯迷糊”:要么把早期零散信息当成重要事实翻出来反复强调,要么在长对话中段就把用户这几轮真正关心的需求抛到脑后。你接着往上堆token,模型倒是没丢掉,账单却先撑不住了,响应时间也变得让人怀疑网络是不是断了。这是所有需要“持续对话”的产品都会撞上的一堵墙:上下文窗口是有限的,必须有人负责维护,而维护的核心不是“全部记住”,而是“知道该忘什么、该压缩什么”。

这里要讲的主角,是一套我在真实业务里实现并反复验证过的上下文维护方案,内部代号就叫 Harness。如果你正在做智能客服、AI 编程助手、知识问答机器人,只要你的应用需要连续多轮对话、又不想无脑把全部历史塞进每一轮请求,那么这套思路大概率能帮你省下一大块 token 成本和调试时间。下面我从设计思路、遗忘机制、压缩机制、落地配置、踩坑记录五个部分来讲,整个方案不止能抄代码,更重要的是理解它背后的取舍逻辑。

1. 为什么“上下文维护”会成为LLM应用的生死线

1.1 上下文窗口的真相:越大不等于越好

先讲一个基础但容易被忽视的事实:大模型的上下文窗口看起来在不断增加,好几万甚至几十万 token,看起来足够装下很多轮对话。但窗口能装下和模型能有效利用是两回事。当上下文里堆满了冗余内容,模型需要在注意力分配上反复权衡,哪些是当前任务的关键依据,哪些只是历史背景。内容一多,这个权衡过程本身就会拉低准确性,尤其当任务需要精确引用早期某个字段时,过长的上下文反而会让模型像“大海捞针”一样漏掉关键信息。

成本方面就更直接了。上下文越长,单次请求的 token 消耗越高,推理耗时也随之上升。如果每个请求都无脑携带全部历史,一个用户聊了五十轮,你就得为每一轮请求支付五十轮历史的 token 成本。我在一个模拟项目 X 里做过对比:同一套问答逻辑,不做上下文管理时平均单次请求长度 8000 多 token,延迟大约 3.2 秒;接上维护层之后,平均请求长度压到 2500 token 左右,延迟降到 1.1 秒。最关键的是回答质量没有变差,必须记住的核心信息一次都没丢。这个对比基本能回答“为什么需要上下文维护”这件事,省下的不仅是钱,更是用户的耐心。

1.2 Harness 的定位:它是一套记忆调度方案

很多人一听到“上下文维护”,第一反应是接一个向量数据库,把历史消息全部向量化存进去,每轮请求做 TopK 相似度检索。这个思路本身没问题,但它只是整个方案里的一个环节。真正的问题在于,不同类型的信息应该用完全不同的方式保存:用户叫什么名字、刚才说过不爱吃辣、昨天要求退款的订单号,这三类信息的重要程度、持久性、被引用的方式都完全不同,单纯靠向量相似度很难自动区分。

Harness 的思路是把记忆分成工作记忆和长期记忆两层。工作记忆只保留最近几轮对话的原始内容,长期记忆则存放从历史里提炼出来的关键事实和压缩摘要。遗忘策略决定什么时候把某条信息从工作记忆里删掉,压缩策略决定往长期记忆里写什么、写成什么形态。这样设计的好处是,每一轮真正进入模型的上下文往往只有几百个 token,却能覆盖前面几十轮对话的关键信息。相当于给模型配了一份精简版会议纪要,而不是把几十个小时的录音全部丢给它听。

注意:这里说的 Harness 是我在自己的模拟项目 X 里实现的一套上下文维护服务,不是一个固定的商业产品。你完全可以用同样的分层设计思路,在自己的项目里搭出类似的一套东西。

2. 核心机制之一:遗忘策略的设计与落地

2.1 为什么必须主动“忘记”

人类记忆有个很有意思的特点:越久远的信息越模糊,越近的信息越清晰,而某些关键节点哪怕过了很久也记得一清二楚。模型的上下文管理也应该参考这个逻辑。如果对所有历史一视同仁全部保留,上下文很快就爆掉了;如果只保留最近 N 轮,用粗暴的滑动窗口一刀切,又会把早期关键信息直接丢掉。用户昨天反复强调的一个重要约束,今天开场就被系统忘了,这种体验比模型能力不足更让人恼火。

遗忘策略要回答的问题,本质上是:某条历史信息,现在还有多大保留价值?价值低到一定程度就删掉,或者降级到压缩队列。它不追求“记住一切”,而是追求“在有限的窗口里保留对当前任务最重要的信息”。明确这个目标之后,实现的思路反而清晰了。

2.2 三条遗忘判据:时间衰减、活跃度、关键性

我实践下来比较稳的组合,是同时用三条判据给历史消息打分。

第一条是时间衰减。每条历史消息按对话时间计算一个衰减系数,越老的分值越低。衰减曲线我推荐用指数衰减而不是线性衰减,因为对话里的“热度”下降非常快,前几天还在热聊的临时话题,今天可能已经完全不重要了。指数曲线能更真实地模拟这种状态。比如一条四轮之前提到的临时需求,如果话题已经切换,衰减分很快就会跌到阈值以下。

第二条是活跃度加权。如果一条信息在后面多轮对话里被反复引用,说明它对当前任务仍然重要,应该加分。这个信号实现起来很直接,就是统计引用次数。用户再次提到订单号、再次确认某个偏好,都算一次活跃命中。把命中次数折算成分值累加到基础分上,就能避免“重要但很久没出现”的信息被过早清理。

第三条是关键性标记。某些字段在业务规则里属于基础信息,比如用户 ID、收货地址、当前任务目标,这类信息不做时间衰减,只做会话有效期管理。会话结束、任务完成之后,这些字段才进入归档或者压缩流程。关键性标记的优先级高于前面两条判据,它是业务规则层面的人工干预,比纯统计更可靠。

2.3 遗忘阈值怎么定才不伤关键信息

遗忘阈值我习惯用百分制打分,默认设 60 分。大于 60 分的消息保留在工作记忆,低于 60 分但有引用关系的移动到压缩队列,完全低于 30 分的直接丢弃。为什么不等到 0 分再丢?因为上下文里的信息并不需要全都是“立刻有用”的,有一部分是铺垫性和辅助理解的,比如用户对某个功能的背景描述。这些内容直接丢掉很可惜,但也不值得长期占用工作记忆,把它们送去压缩队列换成一句精炼背景说明,比原样保留更划算。

实操心得:阈值不要一开始就定死。先跑一到两周真实流量,看工作记忆的平均占用率。如果长期低于 50%,说明阈值定得偏严,可以适当下调到 45 到 50 分,给上下文留更多余量;如果经常超过 80%,就得把阈值上调,或者降低单条消息的权重上限。调阈值一定要看数据,不要靠感觉拍脑袋。

3. 核心机制之二:压缩策略的语义蒸馏

3.1 压缩不是简单截断,而是提取关键骨架

如果说遗忘决定“删掉什么”,压缩决定“留下什么形态”。最粗糙的压缩做法是把超出窗口的历史消息直接掐头去尾,只保留最后几百个字符。这种方式通常会把用户的明确指令拦腰截断,而且丢失的是上下文里真正重要的结论性信息,性价比极低。

Harness 的压缩分两个层次:摘要压缩和事实抽取。摘要压缩针对的是对话过程本身,比如用户和机器人讨论了三种方案,最后选了方案 B,中间还有一段反复澄清需求的过程。这段对话原文可能有几千个字,对后续对话真正有意义的其实只有一句“最终确认使用方案 B,预算上限 5000 元”。摘要压缩的目标就是生成这样一句话,把分歧过程、尝试和修正全部丢掉。事实抽取针对的是实体和属性,比如用户的邮箱地址、希望的下单时间、指定的某项参数,从原文里提取成结构化键值对,存进长期记忆字段表。这种信息的价值不在于句式,而在于能被精确检索。后面任何一轮需要用到邮箱的时候,直接查字段表就行,不需要翻遍整个历史。

3.2 压缩触发条件:什么时候压缩才不会浪费成本

压缩不能每条消息都做,那会白白烧掉大量模型调用成本。我实践下来,触发条件有三条,满足任意一条就可以启动压缩。

第一,工作记忆的总 token 数超过配置上限。比如预设上限 8000,一旦超过立即触发整体压缩,把较早的一批消息批量摘要化。第二,某条消息在遗忘策略里被判定为低价值但尚可留存,进入压缩候选队列。这个队列适合用后台定时任务批量处理,比如每分钟扫一次,攒够一定数量再统一压缩。第三,出现明显的阶段性结束信号,比如用户说“好的谢谢”、订单支付完成、任务状态切到已完成。这时候压缩的收益最高,因为一段对话已经结束了大半,剩余价值只剩那些需要长期记住的结论。

注意:压缩动作本身要防止把信息压坏。抽象生成结果在落库前需要经过校验,不能直接信任模型的输出,这个我在后面的踩坑部分会专门展开。

4. 实际操作:用 Harness 跑通一套上下文维护流程

4.1 记忆区设计与数据结构

实际动手之前,先把数据模型定清楚。我划分了三块存储区:

短期区,也就是工作记忆,是一个按时间排序的消息列表,每条记录包含 message_id、角色、内容、时间戳、引用次数这些字段。长期区,也就是事实库,是一个键值表,存储从历史里抽取出来的实体和偏好。摘要区,是一个压缩摘要列表,每条记录包含摘要文本、起止时间、来源消息 ID 列表。

对应到存储选型上,我用 Redis 比较多,因为它要处理并发读写,Redis 的 ZSet、Hash、List 三种结构刚好能对上。消息列表用 ZSet 按时间戳排序,事实库用 Hash,摘要区用 List。表结构本身很简单,关键是每条摘要记录都要保留 source_ids,也就是生成这条摘要的来源消息 ID 列表。没有这层追溯关系,排查问题的时候会非常痛苦。

4.2 遗忘与压缩的调度流程

整个流程是“事件触发 + 定时任务”的混合调度。事件触发面向新增消息,新消息进入短期区之后先跑一次快速打分,分值低于遗忘阈值就直接进压缩候选队列;分值高于阈值但短期区总量超标,则触发一次批量压缩。定时任务负责兜底,每隔五分钟扫一次短期区的总 token 量,超过上限就按遗忘策略分批清理。

这里附一段核心伪代码,结构清晰,方便你套到自己的项目里:

def on_new_message(message): score = scoring_service.evaluate(message) if score < FORGET_THRESHOLD: compression_queue.push(message) else: working_memory.add(message) if working_memory.token_size >= MEMORY_LIMIT: trigger_compaction() def trigger_compaction(): candidates = working_memory.get_oldest(ratio=0.3) summaries = summarizer.batch_compress(candidates) for summary in summaries: summary_store.add({ "text": summary.text, "source_ids": [m.id for m in candidates], "start_time": candidates[0].timestamp, "end_time": candidates[-1].timestamp }) working_memory.remove(candidates)

这套流程的实际执行成本很低,因为它不是每一轮都对全部历史做处理,只在边界条件触发时才做维护操作。常规对话场景下,我评估过,额外增加的模型调用量占比不超过总调用量的 5%,完全可以接受。

4.3 参数计算示例与模型请求组装

给一个完整可参考的参数计算例子。假设单轮对话平均 token 数按 300 算,工作记忆上限设为 6000 token,大约能覆盖 20 轮对话。一轮完整对话结束后,这 20 轮原始记录需要被压缩,摘要策略是每 10 轮生成一条不超过 200 token 的摘要。这样 20 轮最终会变成 2 条摘要,占用约 400 token。即便用户连续聊了 50 轮,进入上下文的核心内容也只是“最近 20 轮原始信息中剩下来的部分”加上“之前 30 轮压缩成的 3 条摘要”,总量可能只有 2500 token。相比把 50 轮全量塞进去的 15000 token,这是数量级的差距。

Harness 在实际链路里的位置,是在把上下文交给模型之前做最后一道组装。组装规则是:先取长期区里与当前问题最相关的事实字段,再取与当前轮最相关的摘要,最后拼接最近 N 轮工作记忆。拼接顺序不是随便排的,我把长期事实放最前面、摘要次之、工作记忆放最后,是因为很多模型的注意力对提示词开头和结尾更敏感,把关键事实放在开头能有效降低丢失概率。

5. 不同场景下的调优配置

5.1 三类典型场景的推荐参数

我把调好的配置模板给到三类常见场景,你可以直接抄,再根据实际情况微调。

智能客服场景,工作记忆上限建议设 5000,遗忘阈值 55 分,压缩摘要长度上限 150,事实抽取字段包括用户 ID、订单号、地址、诉求类型。客服场景的关键是快速定位问题,所以事实库的权重比摘要高,宁可多存几个字段,也不能在关键时刻找不到用户身份。

AI 编程助手场景,工作记忆上限建议设 8000,遗忘阈值 60 分,摘要上限 250,事实抽取字段包括当前任务目标、文件路径、用户技术栈偏好。编程场景涉及多文件信息,摘要需要保留和代码结构相关的上下文,所以上限比客服场景高一些。

知识问答机器人场景,工作记忆上限建议设 3000,遗忘阈值 50 分,摘要上限 300,事实抽取字段较少,重点是摘要覆盖的层次。问答场景的对话通常比较短,但问题背景可能跨度很大,摘要层次分明比字段丰富更重要。

场景工作记忆上限遗忘阈值摘要上限事实抽取重点
智能客服5000 token55 分150用户ID、订单号、地址
AI 编程助手8000 token60 分250任务目标、文件路径
知识问答3000 token50 分300摘要层次、背景信息

5.2 用两个指标验证调优效果

调参不能只看单次对话的体验,那样很容易被个例误导。我每次改配置之后,都会盯两个指标做回归验证。第一个是事实保留率,指会话结束后用户明确提到的关键信息是否还能从长期区找到。第二个是摘要利用率,指后续轮次中模型是否真的用到了之前生成的摘要。如果摘要利用率很低,说明摘要生成粒度太粗,或者位置放得不合适;如果事实保留率低,说明遗忘阈值太高,把重要信息提前清理掉了。我会持续跑数据,直到这两个指标都稳定在 90% 以上才收手。实测下来,只要守住这两个指标,线上基本不会出现“用户觉得模型失忆”的投诉。

6. 上线三个月踩坑实录

6.1 摘要压缩导致信息失真

最大的坑是第一次直接把大模型摘要结果落库,结果在一次测试里把“退款金额 598 元”写成了“退款金额 5980 元”。回去翻原始对话才发现,用户先问“5980 元的套餐能不能退款”,后来改成“只退 598”,摘要模型把两次出现的数字混在了一起。从那以后,我在压缩结果落库前加了一道规则校验:抽取所有金额、日期、订单号等关键字段,与原文交叉比对,不一致就重新生成或者直接回退到原文保留。这一步看起来简单,但在生产环境里能挡掉相当大一部分数据事故。

6.2 遗忘策略把进行中的约束删了

另一个问题出在“活跃度加权”信号太弱的时候。用户在第 5 轮提到“所有邮件都抄送给我”,后面 10 轮都没再提,系统按时间衰减把它清理了。结果第 16 轮用户突然抱怨“不是说了邮件都抄送我吗”。修复方案是增加一种“资源型约束”标签,凡是带有“所有”“每次”“一律”这类全称量词的消息,一律进事实库,不做时间衰减,直到用户主动修改。这类语义信号往往比打分器更可靠,遇到这类表达,规则优先级要高于统计权重。

6.3 并发处理导致摘要覆盖旧记录

还有一次线上事故,是定时压缩任务和手动触发任务同时跑,对同一批历史消息生成了两条摘要,后写入的覆盖了先写入的,导致前面一段对话的内容直接丢失。后来给摘要区加了版本号和合并机制,写入新摘要之前先检查 source_ids 是否有重叠,有重叠就向上合并而不是覆盖。这个改动看起来不起眼,却让数据可靠性和可追溯性上了一个台阶。上下文维护这种模块,看起来只是调度和存储,但一旦并发场景没想清楚,线上出问题的时候很难排查。

6.4 降低压缩成本的隐藏技巧

压缩摘要的模型调用并不便宜,尤其是对话量大的时候。我的经验是优先用相对小一些的模型做摘要,而不是每次都调最大的模型。摘要任务对语言理解要求不低,但核心要求是“忠实提取”而不是“创意生成”,小模型在指令清晰的情况下表现足够。另外一个技巧是批量摘要,攒够 10 轮历史消息再统一处理一大段,比每条消息生成一次摘要能降低一半以上的模型调用成本。压缩这类纯后台操作,本来就不需要追求低延迟,完全可以用更便宜的模型和更长的时间换取更低的成本。

7. 收尾:上下文维护是长期对话应用的护城河

我个人体会是,上下文维护不是一个“锦上添花”的性能优化,而是决定长期对话质量的地基。模型本身的长上下文能力再强,如果不做分层记忆,最终都会被 token 成本和注意力分散拖垮。Harness 这套方案的核心价值可以归结为八个字:主动遗忘、语义压缩。遗忘负责把该放下的放下,压缩负责把该留下的提炼好,两层配合才能让模型在有限的窗口里始终盯着真正重要的信息。

最后再分享一个小技巧:做遗忘打分时,除了考虑消息本身的特征,还可以把用户当前的意图纳进权重体系。比如用户正在咨询退款,那与退款相关的历史消息权重就应该临时调高,哪怕它已经比较久了。这样能让上下文维护从“静态规则”变成“动态感知”,模型在关键时刻总能拿到它真正需要的那一块记忆。这个改进我在模拟项目 X 里实现了之后,对话体验的提升比单纯调大窗口明显得多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询