ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Agent长对话失忆怎么办?上下文管理实战:Context Editing、Compaction与Memory Tool

Agent长对话失忆怎么办?上下文管理实战:Context Editing、Compaction与Memory Tool 1. 为什么聊到第 20 轮Agent 就开始失忆1.1 一个几乎所有人都踩过的坑如果你正在做 Agent 开发大概率遇到过这个场景前 5 轮对话Agent 对答如流上下文记得清清楚楚到第 10 轮它开始把前面说过的约束条件忘掉到第 20 轮它直接失忆把你最开始交代的角色设定、任务目标、格式要求全丢了甚至开始胡编乱造。很多人第一反应是模型不行换个更大的模型试试。换完之后发现前几轮确实好了一点但到 20 轮还是崩。于是又怀疑是框架问题从一套 Agent 框架换到另一套问题依旧。我踩过这个坑不止一次。后来才想明白问题不在模型也不在框架而在于你把历史当成了上下文来管理。这两者听起来差不多实际差得很远。历史是用户和 Agent 说过的每一句话按时间顺序堆起来上下文是这一轮推理真正需要喂给模型的信息集合。前者是流水账后者是经过筛选、压缩、重排的作战地图。你如果只是把历史一股脑塞进 prompt那上下文窗口迟早会被撑爆模型注意力也会被稀释最后表现就是失忆。1.2 上下文窗口到底是个什么东西先把基础概念说清楚不然后面全是空中楼阁。大模型的上下文窗口Context Window指的是模型单次推理能看到的 token 总量上限。这个上限包含三部分系统提示词System Prompt、历史对话、当前用户输入。注意是总量不是每部分单独算。举个例子假设你用的模型上下文窗口是 128K token。你的 System Prompt 写了 2000 token工具定义Tool Schema占了 3000 token那么留给对话历史的只剩 123K。如果每轮对话平均消耗 1500 token用户输入 模型回复理论上能撑 80 轮左右。但实际情况远没这么乐观因为工具调用的返回结果往往很长一次搜索返回可能就是 3000-5000 token模型回复里如果带代码、表格、JSONtoken 消耗会翻倍多轮对话中模型会不断重复引用前面的内容产生冗余所以真实场景下很多 Agent 跑到 15-20 轮就开始逼近上限。一旦逼近上限要么框架直接报错比如你搜到的error running remote compact task: fatal error: remote compaction v2 expected这类要么框架悄悄截断最早的消息Agent 就失忆了。1.3 1M 上下文是不是就能解决问题最近1M 上下文是个热词很多模型宣称支持百万级 token。很多人觉得窗口够大就不会失忆了。我实测下来的结论是窗口大能延缓问题但解决不了问题。原因有两个。第一注意力稀释。上下文越长模型对单个 token 的注意力权重越低。你把 50 轮对话全塞进去模型对第 3 轮那句关键约束的注意力可能已经低到忽略不计。这就是所谓的lost in the middle现象——中间部分的信息最容易被漏掉。第二成本。1M token 的输入按现在的价格算一次调用可能就是几块钱。你一个 Agent 跑 20 轮成本直接起飞。而且延迟也会显著增加用户体验直线下降。所以真正的高手不会去赌窗口大小而是主动管理上下文。这就是 Context Editing上下文编辑、Compaction压缩、Memory Tool记忆工具这几个概念要解决的问题。2. 历史管理和上下文管理差在哪2.1 历史管理流水账思维历史管理的典型做法是这样的维护一个 messages 数组每轮把用户输入 append 进去把模型回复 append 进去把工具调用结果 append 进去。下一轮请求时整个数组原封不动发给模型。messages [{role: system, content: system_prompt}] def chat(user_input): messages.append({role: user, content: user_input}) response llm.invoke(messages) messages.append({role: assistant, content: response.content}) return response.content这段代码能跑前几轮效果也不错。但它的本质是只增不减迟早会撞墙。撞墙之后你会加一个截断逻辑比如只保留最近 10 轮。结果就是 Agent 彻底忘了 10 轮之前的事包括你最开始交代的任务目标。这就是历史管理的死结要么全留爆窗口要么截断丢信息没有中间态。2.2 上下文管理作战地图思维上下文管理的核心思路是每一轮推理前动态决定这一轮需要哪些信息然后把这些信息组装成最优的上下文。具体来说它要做几件事筛选哪些历史消息是这一轮真正需要的哪些可以丢压缩长对话、长工具返回能不能压成摘要外置不常用的信息能不能存到外部记忆里需要时再检索重排把最关键的信息放在上下文的首尾因为模型对首尾注意力最高这四件事对应了四个技术点Context Editing、Compaction、Memory Tool、以及上下文重排策略。下面逐个拆。2.3 一个类比会议纪要 vs 会议录音打个比方。历史管理就像把整场会议的录音原封不动交给新来的同事让他自己听。录音 3 小时他听到第 2 小时就晕了关键决策可能在第 30 分钟他早忘了。上下文管理就像给同事一份会议纪要决议事项、待办清单、关键背景一页纸搞定。需要细节时再告诉他第 30 分钟的讨论录音在附件 3。Agent 的上下文管理就是这个逻辑。主上下文放纪要细节放外部记忆按需检索。3. Context Editing主动编辑而不是被动截断3.1 Context Editing 到底编辑什么Context Editing 指的是在把消息发给模型之前主动对消息列表做增删改。它不是简单的保留最近 N 条而是有策略地编辑。常见的编辑动作有这么几类编辑动作说明适用场景删除移除已完成任务的中间步骤工具调用链很长时替换把长消息替换成摘要工具返回结果过长合并把多条相关消息合并成一条多轮闲聊、确认类对话保留标记为永不删除系统约束、任务目标重排调整消息顺序关键信息被淹没时关键在于保留这一项。很多人做 Context Editing 时只想着删结果把最关键的约束也删了。正确的做法是给消息打标签标记哪些是锚点消息永远不参与删除。3.2 锚点消息Agent 的宪法我在实际项目里会给每个 Agent 定义一组锚点消息相当于它的宪法。这些消息包括角色设定和核心约束你是谁、你不能做什么当前任务的最终目标用户最初交代的任务关键格式要求输出必须是 JSON、必须带引用等用户明确强调过的偏好我不喜欢用表格这类这些锚点消息在每一轮组装上下文时都会被强制放在最前面且永不删除。这样即使对话跑到 50 轮Agent 也不会忘记自己是谁、要干什么。实现上很简单给消息加一个pinned字段class Message: def __init__(self, role, content, pinnedFalse, priority0): self.role role self.content content self.pinned pinned # 是否锚点永不删除 self.priority priority # 优先级越高越先保留组装上下文时先放所有 pinned 消息再按 priority 从高到低填充剩余消息直到接近 token 预算上限。3.3 token 预算怎么算这里有个实操细节你不能等到撞上模型硬上限才处理要预留 buffer。我的经验是按模型上限的 70% 作为软预算。假设模型上限 128K软预算就是 90K。这 90K 里再分配System Prompt 工具定义预留 8K锚点消息预留 5K当前用户输入预留 3K剩余 74K 给历史消息74K 就是历史消息的硬预算。组装时从最新消息往前累加 token超过 74K 就停止更早的消息要么丢弃要么压缩后保留。注意token 估算不要用字符数除以 4 这种粗糙方法中文场景误差很大。用 tiktoken 或模型官方提供的 tokenizer 精确计算误差能控制在 2% 以内。3.4 一个容易忽略的点工具定义也吃 token很多人只盯着对话历史忘了工具定义Tool Schema也占大量 token。一个功能丰富的 Agent工具定义轻松超过 5000 token。如果你有 20 个工具每个工具的描述写得很详细光工具定义就能吃掉 10K。优化手段有两个。一是按需加载工具根据当前对话意图只挂载相关工具。二是精简工具描述把冗长的说明压到最简参数说明用短句。我见过一个项目工具定义占了 15K token对话历史才 5K结果 Agent 跑到第 8 轮就崩了。把工具定义精简到 4K 之后直接撑到 30 轮。这个优化性价比极高但很多人没意识到。4. Compaction把长对话压成记忆胶囊4.1 Compaction 的本质是摘要Compaction压缩的核心动作是当历史消息超过预算时把最老的一批消息交给模型让它生成一段摘要然后用这段摘要替换掉原始消息。比如前 10 轮对话有 8000 token压缩成一段 500 token 的摘要。这样既保留了关键信息又腾出了 7500 token 的空间。摘要的 prompt 设计很关键。我常用的模板是这样的请把以下对话压缩成结构化摘要保留 1. 用户提出的所有明确要求 2. 已达成的结论和决策 3. 未完成的任务和待办 4. 关键的事实性信息数字、名称、路径 丢弃寒暄、重复确认、中间推理过程。 对话内容 {history} 输出格式 - 用户要求 - 已达成结论 - 待办事项 - 关键事实这个模板的好处是输出结构化后续检索和引用都方便。而且明确告诉模型丢弃什么避免摘要里塞满废话。4.2 分层压缩不要一次压到底一次性把 50 轮压成一段摘要信息损失会很大。更好的做法是分层压缩。我的做法是维护三级记忆L1 原始消息最近 5-10 轮完整保留L2 段落摘要每 10 轮压缩成一段摘要L3 全局摘要整个会话的总体摘要几百 token组装上下文时L1 全放L2 放最近 2-3 段L3 永远放。这样既有细节又有全局视野。这个结构有点像操作系统的多级缓存越靠近 CPU 的越快越小越远的越慢越大。Agent 的上下文管理完全可以借鉴这个思路。4.3 压缩时机主动 vs 被动压缩什么时候触发两种策略。被动压缩是等 token 超预算了才压简单但容易出问题——压缩本身也要调用模型有延迟用户会感觉到卡顿。主动压缩是在对话进行到一定轮数比如每 10 轮就后台异步压缩等真正需要时摘要已经准备好了。用户体验更顺滑但实现复杂一些要处理并发和状态同步。我推荐主动压缩。具体做法是每轮对话结束后检查当前轮数是否是 10 的倍数是的话就起一个异步任务压缩最早的 10 轮。这样压缩和对话解耦互不阻塞。提示压缩任务失败不要影响主流程。我见过因为压缩接口超时导致整个对话卡死的案例。压缩是优化手段不是关键路径失败了大不了这轮不压下轮再试。4.4 压缩的坑摘要会漂移压缩有个隐蔽的坑多次压缩后摘要会漂移。因为每次压缩都是基于上一次的摘要再压误差会累积。压个五六次之后摘要可能已经和原始对话差很远了。解决办法是压缩时始终基于原始消息而不是基于上一次的摘要。也就是说L2 摘要永远从 L1 原始消息生成L3 摘要从所有 L2 摘要生成而不是 L3 从 L2 再压。这样误差不会跨级累积。另外关键事实数字、路径、ID不要依赖摘要传递直接存到外部记忆里需要时精确检索。摘要只负责传递语义不负责传递精确值。5. Memory Tool把记忆外置按需检索5.1 为什么需要外置记忆上下文窗口再大也是有限的而一个长期运行的 Agent 需要记住的东西是无限的。用户偏好、历史任务、领域知识、项目背景这些东西不可能全塞进上下文。Memory Tool 的思路就是把记忆存到外部向量库、KV 库、文件系统主上下文里只放索引需要时通过工具调用检索。这就像人脑。你不会把所有记忆都同时想起来而是需要时回忆起相关片段。Agent 也应该这样。5.2 记忆的三种类型我在项目里把 Agent 记忆分成三类分别用不同的存储和检索策略记忆类型内容存储检索方式事实记忆用户偏好、项目背景、领域知识向量库语义相似度检索情节记忆历史对话片段、任务执行记录文档库 向量时间 语义混合检索程序记忆工具使用经验、成功/失败模式结构化 KV精确匹配 规则事实记忆是我知道什么情节记忆是我经历过什么程序记忆是我会怎么做。三者配合Agent 才能表现出有记忆的样子。5.3 记忆写入什么时候存记忆写入的时机很关键。存太多会污染检索结果存太少又记不住东西。我的策略是事件驱动写入。具体触发条件用户明确表达偏好时我喜欢用 Python任务完成时把任务目标、执行路径、结果存成情节记忆工具调用失败时把失败模式存成程序记忆避免重复踩坑对话结束时把整段对话压缩后存成情节记忆写入时给记忆打标签类型、时间、重要性、来源。重要性高的记忆检索时优先返回。5.4 记忆检索怎么找得准检索是 Memory Tool 最容易翻车的地方。纯向量检索的问题是对精确匹配不友好比如用户问上次那个订单号是多少向量检索可能返回一堆语义相似但订单号不对的记忆。我的做法是混合检索先用规则提取查询里的关键实体订单号、人名、日期用实体做精确匹配命中则直接返回没命中再用向量检索做语义召回最后用重排序模型Rerank对结果排序这套组合拳下来检索准确率比纯向量高一大截。代价是多了一次 rerank 调用延迟增加 100-200ms但值得。5.5 记忆的遗忘机制人脑会遗忘Agent 也需要。不是所有记忆都值得永久保留。我设计了一个简单的遗忘策略记忆有新鲜度和访问频率两个维度。新鲜度随时间衰减访问频率随检索增加。两者加权得分低于阈值的记忆定期清理。这样常用的记忆会越来越牢固不用的记忆会自然淡出。避免记忆库无限膨胀检索越来越慢。6. 一套可落地的上下文管理架构6.1 整体架构把前面几个技术点串起来一套完整的上下文管理架构大概是这样用户输入 ↓ [意图识别] → 决定挂载哪些工具、检索哪类记忆 ↓ [记忆检索] → 从 Memory Tool 拉取相关记忆 ↓ [上下文组装] ├─ 锚点消息永远保留 ├─ 全局摘要 L3 ├─ 段落摘要 L2最近 2-3 段 ├─ 原始消息 L1最近 5-10 轮 └─ 检索到的记忆 ↓ [token 预算检查] → 超预算则触发 Compaction ↓ [模型推理] ↓ [记忆写入] → 异步写入新记忆 ↓ [异步压缩] → 每 10 轮触发一次这个架构的核心是组装这一步。每一轮推理前上下文都是动态组装的而不是简单 append。6.2 关键参数怎么定参数没有标准答案要根据你的场景调。我给出几个经验值供参考参数经验值说明软预算比例模型上限的 70%预留 buffer 防溢出L1 保留轮数5-10 轮太少丢细节太多占空间L2 保留段数2-3 段每段 10 轮压缩触发轮数每 10 轮太频繁浪费太稀疏来不及记忆检索条数Top 5-8太多稀释注意力锚点消息上限5K token超了要精简这些值不是拍脑袋定的是我在几个项目里反复调出来的。你可以从这些值起步然后根据实际表现微调。6.3 一个最小实现示例下面是一个简化的上下文组装函数展示核心逻辑def build_context(session, user_input, token_budget90000): context [] used 0 # 1. 锚点消息永远保留 for msg in session.pinned_messages: context.append(msg) used count_tokens(msg.content) # 2. 全局摘要 if session.global_summary: context.append(session.global_summary) used count_tokens(session.global_summary.content) # 3. 检索相关记忆 memories memory_tool.retrieve(user_input, top_k5) for mem in memories: if used count_tokens(mem.content) token_budget * 0.8: break context.append(mem) used count_tokens(mem.content) # 4. 段落摘要从近到远 for summary in reversed(session.segment_summaries[-3:]): if used count_tokens(summary.content) token_budget * 0.9: break context.append(summary) used count_tokens(summary.content) # 5. 原始消息从近到远 for msg in reversed(session.recent_messages): if used count_tokens(msg.content) token_budget: break context.insert(0, msg) # 插到前面保持时间顺序 used count_tokens(msg.content) # 6. 当前输入 context.append({role: user, content: user_input}) return context这段代码的关键在于按优先级填充锚点最优先然后全局摘要然后检索记忆最后才是原始消息。这样即使预算不够丢的也是细节不是核心。6.4 怎么验证效果架构搭好了怎么知道有没有用我一般用三个指标衡量失忆率随机抽查对话看 Agent 是否还记得 10 轮前的关键约束token 利用率实际使用的 token 占预算的比例太低说明浪费太高说明快溢出响应延迟上下文组装 检索 推理的总耗时超过 3 秒用户会明显感知我做过对比测试同一套 Agent用历史管理跑到第 18 轮开始失忆用上下文管理跑到第 60 轮还能准确引用第 3 轮的约束。token 消耗反而降低了 40%因为大量冗余历史被压缩掉了。7. 常见问题与排查技巧实录7.1 常见问题速查表现象可能原因排查方向第 10 轮后开始忘事历史未压缩注意力稀释检查是否启用 Compaction报错 remote compaction 相关压缩任务并发冲突检查压缩任务是否加锁Agent 忘记角色设定锚点消息被截断检查 pinned 标记是否生效检索记忆不相关向量检索精度不够加实体匹配 rerank响应越来越慢上下文越来越长检查 token 预算是否生效摘要和原文对不上多次压缩误差累积改为从原始消息压缩工具定义占太多 token工具描述冗余精简描述 按需加载7.2 几个我踩过的坑坑一压缩任务和对话任务抢资源。早期我把压缩做成同步的结果每次压缩时对话就卡住。后来改成异步但没加锁导致同一段历史被压缩两次摘要重复。解决办法是给每个会话加一个压缩锁同一时间只有一个压缩任务在跑。坑二锚点消息越加越多。一开始觉得什么都要保留锚点消息加到 20 条占了 15K token。后来发现真正需要永久保留的就那么几条其余可以降级成普通消息。锚点要克制5 条以内最好。坑三记忆检索返回太多。一开始 top_k 设成 20结果上下文里塞了一堆不相关的记忆模型反而被干扰。降到 5 之后效果明显变好。记忆不是越多越好精准才是关键。坑四忽略工具返回的 token 消耗。一次网页搜索返回的 markdown 可能有 8000 token几次搜索就把预算吃光了。后来我在工具层加了截断和摘要返回结果先压到 2000 token 以内再进上下文。7.3 一个实用的调试技巧上下文管理出问题时最难的是定位是哪一环出了问题。我的做法是把每轮组装的上下文 dump 到日志包含每部分占用的 token 数。logger.info(fContext breakdown: pinned{pinned_tokens}, fglobal{global_tokens}, memories{memory_tokens}, fsegments{segment_tokens}, recent{recent_tokens}, ftotal{total_tokens}/{budget})有了这个日志一眼就能看出是哪部分膨胀了。是记忆检索返回太多还是原始消息没压缩还是工具定义太大定位之后针对性优化比盲目调参高效得多。7.4 关于高手管理上下文的一点体会回到标题那句话。我做了几年 Agent 开发最大的体会是Agent 的能力上限很大程度上取决于你怎么管理它的上下文。模型能力是给定的框架是现成的唯独上下文管理是你可以深度定制的。同样一个模型上下文管理做得好能跑 60 轮不失忆做得差10 轮就崩。这个差距不是模型能弥补的。所以别再纠结换哪个模型、用哪个框架了。先把上下文管理这套东西搭起来锚点消息、分层压缩、外置记忆、动态组装。这四件事做到位你的 Agent 立刻上一个台阶。最后分享一个小技巧如果你的 Agent 经常在长对话中丢失关键约束试试在每轮用户输入前自动把最关键的 3 条约束以提醒的形式重新注入。这个动作成本很低但效果立竿见影。我管这叫上下文锚定比指望模型自己记住靠谱多了。
返回列表