尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

AI Agent上下文压缩技术:解决长对话记忆过载的工程实践

AI Agent上下文压缩技术:解决长对话记忆过载的工程实践 1. 项目概述当Agent的记忆不堪重负最近在折腾几个工具型Agent项目比如让它们帮我处理长文档摘要、分析多轮对话日志或者作为客服助手处理复杂的用户咨询。一个绕不开的痛点很快就浮现出来随着对话轮次增加上下文Context像滚雪球一样越积越大。模型的处理窗口比如常见的4K、8K、16K tokens很快被塞满导致后续的指令理解能力下降、回复质量滑坡甚至直接报错“上下文超长”。这就像让一个助手同时记住几十页的会议纪要和上百条用户反馈再让他处理新任务不混乱才怪。“Agent的上下文压缩”要解决的就是这个“记忆过载”问题。它的核心目标不是无限制地扩展模型的物理处理窗口而是在有限的上下文容量内通过智能的“信息提炼”技术保留对话历史中最关键、最相关的部分丢弃冗余和过时信息从而让Agent能够在一个“虚拟的”长会话中持续、稳定地运行。这不仅仅是技术优化更是决定一个Agent能否从“玩具”走向“生产力工具”的关键能力。无论是处理长文档的问答Agent还是需要记忆用户长期偏好的个性化助手都离不开高效的上下文管理策略。2. 核心需求与挑战拆解2.1 为什么长会话是刚需工具型Agent的应用场景决定了其对长上下文的依赖。我们来看几个典型场景复杂任务分解与执行一个用户指令可能是“帮我分析上季度销售报告找出下滑最严重的三个区域并对比它们本季度的营销活动预算”。Agent需要先理解“分析报告”调用工具读取报告可能长达数十页然后执行“找出区域”和“对比预算”两个子任务。整个过程涉及多轮“思考-行动-观察”的循环每一步的中间结果和原始报告的关键信息都需要保留在上下文中供后续步骤参考。如果上下文被截断Agent可能会忘记最初的目标或关键的中间数据。多轮对话与状态维持在客服或导购场景中用户的问题往往是递进的。“这款手机续航怎么样” - “和另一款A比呢” - “那我用移动网络多它的5G信号好吗”。一个合格的Agent需要记住整个对话脉络理解“这款手机”指代的是哪一款并且知道用户的核心关切是“续航”和“信号”。没有连贯的上下文每次回复都像是面对一个新用户体验会非常割裂。长期记忆与个性化高级的Agent应该能记住用户的偏好和历史交互。比如一个编程助手Agent如果它能记住用户之前提过“我习惯用Python的requests库而不是urllib”并在后续代码建议中体现这一点其体验将大幅提升。这种长期记忆的存取本质上也是对超长上下文信息的一种选择性保留和召回。2.2 原始消息结构的瓶颈目前主流的Agent框架如LangChain、AutoGen与模型API如OpenAI、Claude交互时上下文通常以线性序列的消息列表Message List形式传递。一个典型的对话轮次结构可能是[ {role: system, content: 你是一个有帮助的助手...}, {role: user, content: 问题1}, {role: assistant, content: 回答1}, {role: user, content: 问题2}, {role: assistant, content: 回答2}, ... // 几十轮后 {role: user, content: 最新问题} ]这种结构的瓶颈非常明显长度线性增长每一轮对话都不可逆地增加上下文长度。信息密度不均早期对话可能包含关键任务定义但后期大量是执行细节有些消息很长如工具返回的大段JSON或文本有些则很短。缺乏结构语义模型将所有消息视为平等的“历史记录”无法区分哪些是核心任务描述、哪些是临时中间结果、哪些是已解决的过时信息。当这个列表长度超过模型的上下文窗口限制时我们不得不做出取舍是丢弃最早的历史可能丢失任务目标还是丢弃中间某些轮次可能导致逻辑断裂无论哪种都会损害Agent的可靠性。2.3 压缩的本质在遗忘与记忆间寻找平衡因此上下文压缩不是一个可选项而是必需品。它的目标不是“记住一切”而是“在正确的时机记住正确的东西”。这引出了几个核心挑战保真度 vs. 压缩率压缩得越狠丢失的信息可能越多。如何确保压缩后的摘要能100%代表原始内容的核心意图和关键事实尤其是在涉及具体数字、名称、条款时。实时性开销压缩过程本身需要计算资源。是在每轮对话后异步压缩还是当长度接近阈值时同步压缩压缩操作例如调用另一个LLM进行总结会引入延迟如何不影响主对话的流畅性压缩策略的智能性是简单地总结所有历史对话还是只总结与当前任务最相关的部分或者区分“系统指令”、“用户目标”、“工具结果”、“内部思考”等不同部分进行差异化处理与工具调用的协同Agent的核心能力之一是调用工具。工具执行的结果可能是一大段数据往往是上下文膨胀的主因。如何压缩这些结果同时保留其可供后续工具调用或决策的关键字段3. 主流上下文压缩技术方案剖析面对上述挑战社区和业界已经探索出多种技术路径没有银弹需要根据具体场景组合使用。3.1 基础策略滑动窗口与关键消息保留这是最简单直接的策略可以作为第一道防线。固定窗口滑动只保留最近N轮对话。这适用于对话主题切换频繁的场景。但缺点很明显如果关键目标在早期设定它会被无情遗忘。关键角色消息保留永远不压缩或丢弃system角色消息和最早的一两条user消息通常包含核心任务描述。这保证了Agent不会“忘记自己是谁”和“要干什么”。基于Token计数的截断实时计算上下文Token数当接近阈值如预留10%空间时触发压缩或清理。实操心得在实际编码中我通常会实现一个ContextManager类它维护消息列表并提供一个add_message方法。在这个方法内部会检查当前总Token数使用tiktoken或类似库估算。如果超过阈值首先尝试丢弃role为tool且content过长的中间结果如果判断其已失效其次再考虑压缩历史user/assistant对话对。永远把system和初始user消息加入“保护名单”。3.2 核心武器智能摘要与提取式压缩这是目前最主流、最有效的压缩方式其核心思想是使用一个“裁判”LLM通常是同一个模型但也可以是更小、更快的专用模型来重新表述或提取历史信息。增量式摘要每进行K轮对话或当历史对话达到一定长度时触发一次摘要。将待压缩的历史消息块例如最近的10轮对话发送给LLM指令为“请将以下对话浓缩成一个简洁的段落保留所有与核心任务、关键决策、未解决问题和重要事实相关的信息。” 然后将这个生成的摘要段落以一条system或user消息的形式如{role: “system” “content”: “历史对话摘要...”}插入到上下文中的合适位置通常放在当前活动上下文之前并丢弃被摘要的原始消息。提取式关键点列表与生成连贯段落不同这种方法是让LLM从历史中提取一个结构化的关键点列表、待办事项或事实清单。例如“- 用户目标分析Q3销售报告。 - 已发现A区域下滑15%。 - 待办仍需对比B、C区域数据。” 这种方式的信息密度更高更易于后续步骤检索和引用。对话式记忆这是更高级的形式不仅总结“发生了什么”还总结“Agent认为发生了什么”。例如在摘要中加入“用户似乎对价格比较敏感已两次询问折扣信息”。这为Agent赋予了更丰富的“记忆”维度。技术实现要点选择摘要模型如果主模型是GPT-4可以用GPT-3.5-Turbo来做摘要以降低成本。对于本地部署可以专门微调一个小模型如Llama 3 8B来做摘要任务。设计提示词Prompt这是成败关键。提示词必须明确要求模型保留具体实体产品名、数字、日期、用户意图、未决问题和任务状态。一个不好的摘要提示词会导致信息严重失真。处理长文本如果要压缩的历史本身就很长超过摘要模型的窗口需要先进行文本分块然后采用“Map-Reduce”模式先对各块分别摘要再对分块摘要进行二次摘要。# 一个简化的增量摘要函数示例 import tiktoken from openai import OpenAI client OpenAI() def summarize_messages(messages_to_compress, modelgpt-3.5-turbo): 压缩一段消息历史 prompt f 你是一个高效的对话摘要助手。请将以下对话历史浓缩成一个简洁、准确的摘要。 要求 1. 保留所有关键事实、数据、决策和未解决的问题。 2. 保留涉及的具体人物、地点、产品、时间、数字等信息。 3. 摘要语言应客观使用第三人称。 4. 如果对话涉及多个任务请分点说明当前各任务状态。 对话历史 {messages_to_compress} 摘要 response client.chat.completions.create( modelmodel, messages[{role: user, content: prompt}], temperature0.1, # 低温度保证摘要的确定性和客观性 max_tokens500 # 控制摘要长度 ) return response.choices[0].message.content # 在ContextManager中调用 if context_tokens threshold: # 选择要压缩的旧消息保护前几条 to_compress self.message_history[protected_count:-5] # 例如压缩除了保护消息和最近5轮之外的部分 summary summarize_messages(to_compress) # 用一条摘要消息替换被压缩的原始消息 self.message_history self.message_history[:protected_count] [{role: system, content: f历史摘要{summary}}] self.message_history[-5:]3.3 高级策略向量检索与记忆库对于需要超长记忆如跨越多次独立会话的场景简单的摘要不够用了。这时需要引入外部记忆系统。向量数据库作为长期记忆将每一轮对话或对话片段转换为向量嵌入Embedding存储到向量数据库如Chroma、Weaviate、Pinecone中。当进行新对话时将当前查询也转换为向量从记忆库中检索出最相关的若干条历史记忆动态地插入到当前上下文中。这实现了“按需记忆”上下文长度只包含当前对话和最相关的几条历史而不是全部历史。分层记忆结构将记忆分为“工作记忆”当前会话的压缩上下文和“长期记忆”向量库。工作记忆保证连贯性长期记忆保证知识的持久性。定期将工作记忆中的重要信息如达成的结论、用户的重要偏好提取出来存储到长期记忆中。注意事项向量检索方案虽然强大但引入了一套新的基础设施和复杂性。你需要考虑嵌入模型的选择、检索的延迟、检索结果的相关性阈值避免召回无关记忆干扰当前对话、以及如何更新和清理长期记忆记忆也会过时。对于大多数单次长会话任务智能摘要已经足够对于真正的个性化、跨会话Agent向量记忆库才是归宿。3.4 框架级解决方案与工具集成许多Agent开发框架已经内置了上下文处理模块。LangChain的ConversationSummaryBufferMemory这是LangChain提供的一种记忆类型它结合了滑动窗口和LLM摘要。它会保留最近的K轮对话滑动窗口同时使用LLM对窗口之外的更早对话进行摘要。当你调用它时它返回的是“摘要 最近对话”的组合体。这开箱即用地解决了基础问题。AutoGen的GroupChat与摘要在AutoGen的多Agent群聊中当一个代理发言时它可以有选择地引用或总结之前的讨论框架也提供了摘要功能来管理过长的讨论记录。Claude的“上下文压缩”功能Anthropic的Claude模型在API层面提供了一些上下文管理的最佳实践建议虽然并非自动压缩但其文档强调了在长上下文中使用清晰的结构标记如XML标签来帮助模型定位信息这可以视为一种“前端压缩”或“结构化”提升了模型在长上下文中的信息提取效率。4. 实战为一个文档分析Agent实现上下文压缩让我们以一个具体的“长文档分析Agent”为例从头设计其上下文压缩系统。这个Agent的任务是用户上传一份长文档如一份50页的市场研究报告然后可以持续向Agent提问关于文档的问题。4.1 系统架构设计初始化阶段用户上传文档Agent调用工具如parse_document将文档解析成结构化的文本块。将这些文本块的向量嵌入存入向量数据库长期记忆。同时生成一个非常精简的文档元摘要如标题、作者、核心结论放入初始上下文。对话循环中的上下文管理输入用户新问题 当前上下文包含系统指令、历史问答等。检索将用户问题向量化从向量库中检索出最相关的3-5个文档片段。上下文组装组装最终发给LLM的上下文列表顺序如下system: 核心指令永不压缩。user: 文档元摘要基本不变。user: “根据以下文档片段回答用户问题” 检索到的片段动态变化是主要长度来源。user/assistant: 经过压缩的历史问答摘要见下文。user: 当前用户问题。触发压缩每次Agent生成回答后将本轮QA加入一个“待压缩历史池”。当池子大小超过3轮或总Token数预估将超过阈值时触发摘要函数。执行压缩将“待压缩历史池”中的所有QA交给摘要LLM生成一段新的摘要。然后用一条新的system: “历史问答摘要{新摘要}”消息替换掉上下文中旧的摘要消息和池子里的原始消息。清空“待压缩历史池”。4.2 关键代码模块详解1. 记忆管理器MemoryManagerclass DocumentQAMemoryManager: def __init__(self, llm_client, embedding_model, vector_store, summary_modelgpt-3.5-turbo): self.llm llm_client self.embedding_model embedding_model self.vector_store vector_store self.summary_model summary_model # 核心存储 self.system_instruction 你是一个专业的文档分析助手... self.document_meta_summary # 文档元摘要 self.qa_history_buffer [] # 待压缩的原始QA对 [(q1, a1), (q2, a2)...] self.compressed_history_summary # 当前的压缩后摘要文本 # 配置 self.qa_buffer_max_size 3 # 积累3轮QA后触发压缩 self.max_context_tokens 12000 # 假设模型窗口16K留出余量 self.retrieval_top_k 4 def add_document(self, doc_text): 处理初始文档 chunks self._split_document(doc_text) # 生成元摘要 self.document_meta_summary self._generate_meta_summary(doc_text) # 向量化并存储 embeddings self.embedding_model.embed_documents(chunks) self.vector_store.add_texts(chunks, embeddings) def _generate_meta_summary(self, text): 生成文档的极简元摘要 prompt f请用一句话概括以下文档的核心主题与目的\n{text[:2000]}... # 只取前部分 response self.llm.chat.completions.create( modelself.summary_model, messages[{role: user, content: prompt}], max_tokens100 ) return response.choices[0].message.content def process_query(self, user_query): 处理用户查询返回组装好的上下文消息列表 # 1. 检索相关文档片段 query_embedding self.embedding_model.embed_query(user_query) relevant_chunks self.vector_store.similarity_search_by_vector(query_embedding, kself.retrieval_top_k) chunk_context \n---\n.join([chunk.page_content for chunk in relevant_chunks]) # 2. 组装消息 messages [ {role: system, content: self.system_instruction}, {role: user, content: f文档概述{self.document_meta_summary}}, {role: user, content: f相关原文片段\n{chunk_context}}, ] # 3. 添加上下文历史压缩后的 if self.compressed_history_summary: messages.append({role: system, content: f之前问答的摘要{self.compressed_history_summary}}) # 4. 添加当前问题 messages.append({role: user, content: user_query}) return messages def add_qa_pair(self, question, answer): 添加一轮新的QA对并可能触发压缩 self.qa_history_buffer.append((question, answer)) # 检查是否触发压缩 if len(self.qa_history_buffer) self.qa_buffer_max_size: self._compress_qa_history() # 可选检查总Token数需估算如果接近阈值也触发压缩 # if self._estimate_tokens(messages) self.max_context_tokens * 0.9: # self._compress_qa_history() def _compress_qa_history(self): 压缩QA历史缓冲区 if not self.qa_history_buffer: return # 将缓冲区内容格式化为文本 history_text for i, (q, a) in enumerate(self.qa_history_buffer): history_text fQ{i1}: {q}\nA{i1}: {a}\n\n prompt f请将以下连续的问答记录压缩成一段连贯的摘要。 摘要需要 1. 保留用户所有问题的核心意图。 2. 保留助手回答中的关键结论、数据和事实。 3. 如果某些问题已被完全解决且后续未再提及可以简略提及。 4. 如果存在未解决或待跟进的问题必须清晰指出。 5. 摘要应简洁为后续对话提供足够背景即可。 问答记录 {history_text} 摘要 response self.llm.chat.completions.create( modelself.summary_model, messages[{role: user, content: prompt}], temperature0.1, max_tokens500 ) new_summary response.choices[0].message.content # 更新压缩摘要可以将新旧摘要合并也可以直接替换。这里采用合并以保留更长期记忆。 if self.compressed_history_summary: # 简单合并策略用新摘要覆盖旧摘要。更复杂的可以再对“旧摘要新摘要”做二次压缩。 self.compressed_history_summary new_summary else: self.compressed_history_summary new_summary # 清空缓冲区 self.qa_history_buffer []2. 主Agent循环class DocumentAnalysisAgent: def __init__(self, memory_manager, llm_modelgpt-4): self.memory memory_manager self.llm_model llm_model self.llm_client OpenAI() # 假设已初始化 def chat_cycle(self, user_query): 处理一轮用户查询 # 1. 从记忆管理器获取组装好的上下文 messages self.memory.process_query(user_query) # 2. 调用LLM获取回答 response self.llm_client.chat.completions.create( modelself.llm_model, messagesmessages, temperature0.7 ) assistant_reply response.choices[0].message.content # 3. 将本轮QA加入记忆管理器 self.memory.add_qa_pair(user_query, assistant_reply) return assistant_reply # 使用示例 memory DocumentQAMemoryManager(llm_client, embedding_model, vector_store) agent DocumentAnalysisAgent(memory) # 初始化文档 agent.memory.add_document(long_document_text) # 开始多轮对话 while True: user_input input(用户: ) if user_input.lower() quit: break reply agent.chat_cycle(user_input) print(f助手: {reply})4.3 效果评估与调优实现之后如何判断压缩系统是否有效不能只看“能跑通”要看效果。定量指标上下文长度控制监控每轮对话发送给LLM的Token数确保其稳定在窗口限制以下且没有持续增长的趋势。压缩比比较压缩前后文本的Token数或字符数比例。问答准确性设计测试集在开启压缩和关闭压缩使用完整历史如果窗口允许两种模式下对比Agent回答相同问题的准确性。准确性可以通过人工评估或使用LLM-as-a-Judge来评分。定性评估连贯性测试进行多轮指代性提问。例如先问“文档中提到的A公司主要业务是什么”几轮其他问题后再问“那家公司的市场份额是多少”看Agent是否能正确理解“那家公司”指代A公司。这考验压缩摘要是否保留了实体信息。遗忘测试在长对话末尾询问对话早期提及的关键信息或任务目标看Agent是否还记得。调优旋钮压缩触发时机qa_buffer_max_size和 Token阈值需要平衡。太频繁压缩如每轮都压缩开销大太久不压缩如10轮可能导致关键信息在压缩前就被滑动窗口丢弃。通常3-5轮是一个不错的起点。摘要提示词工程这是调优的重中之重。你的提示词决定了摘要的“风格”和“信息偏好”。多尝试不同的指令例如“以项目清单形式总结”、“聚焦于未解决的问题”、“强调所有出现的数字和日期”等观察哪种摘要风格最有利于后续问答。摘要模型的选择用大模型如GPT-4做摘要质量更高但成本高、速度慢。用小模型如GPT-3.5-Turbo或专用模型需要仔细评估其摘要是否会导致信息扭曲。可以在非关键路径上使用小模型在关键对话节点使用大模型复核。5. 常见陷阱、问题排查与进阶思考5.1 典型问题与解决方案问题现象可能原因排查步骤与解决方案Agent“忘记”了早期设定的核心任务。1.system指令在压缩过程中被意外丢弃或覆盖。2. 初始用户目标未被加入“保护消息”。1.检查代码确保system消息和初始user消息在上下文列表中始终存在且位置固定。2.强化保护将这些消息标记为protectedTrue在所有的压缩、截断逻辑中跳过它们。压缩后Agent的回答开始出现事实性错误或混淆实体。摘要过程丢失了具体细节如数字、专有名词或产生了“幻觉”捏造了不存在的信息。1.优化提示词在摘要指令中明确强调“保留所有具体数字、名称、日期和直接引用的事实”。2.引入校验对于关键事实可以尝试从摘要中反向提取实体与原始片段核对。3.分而治之对于工具返回的结构化数据如JSON不要直接扔给LLM做文本摘要。可以设计规则只提取其中的status、result等关键字段放入摘要原始数据可丢弃或存向量库。引入压缩后单轮响应时间显著变长。压缩操作调用摘要LLM是同步的阻塞了主对话流程。1.异步压缩将add_qa_pair中的压缩操作改为异步任务。触发压缩后主流程继续压缩在后台进行。下一轮组装上下文时使用最新的压缩结果即可可能有一轮延迟。2.预测性压缩在Agent思考/生成回答的同时在后台异步准备对历史缓冲区的压缩利用空闲时间。在多轮复杂推理中压缩导致逻辑链断裂。摘要过于笼统丢失了推理的中间步骤导致后续步骤无法衔接。1.保留推理链在摘要提示词中要求“保留主要的推理步骤和结论”。2.结构化记忆不采用自然语言摘要而是将Agent的“思考过程”如Chain-of-Thought以结构化的形式如列表、图谱保存。例如记录“步骤1识别问题A - 结论X步骤2基于X分析B - 结论Y”。向量检索召回的内容不相关干扰了当前回答。1. 嵌入模型不适合领域。2. 检索的top_k值太大。3. 查询问题本身模糊。1.领域微调嵌入模型如果文档非常专业如法律、医学考虑使用在该领域语料上微调过的嵌入模型。2.调整检索策略降低top_k提高相似度分数阈值。尝试混合检索关键词向量。3.查询重写在检索前用LLM将当前用户问题重写为更利于检索的格式例如补充对话历史背景“在讨论XX文档的背景下用户问...”。5.2 进阶优化方向基于内容类型的差异化压缩策略不要对所有消息一视同仁。可以定义不同的消息类型system_goal: 系统目标和约束永不压缩。user_intent: 用户核心意图低频压缩或只做提取。tool_result: 工具返回结果高压缩率只保留成功/失败状态和关键数据。assistant_reasoning: Agent的思考过程中等压缩保留逻辑链。 针对不同类型设计不同的压缩提示词和触发阈值。压缩质量的自评估与回滚让LLM在生成摘要后自我评估一下摘要是否涵盖了原始内容的所有关键点。如果评估分数低可以触发更保守的压缩策略如只丢弃最老的对话而不做摘要或者尝试换一种摘要方式。与流式输出的结合在Agent流式输出回答的同时后台就可以开始准备对当前轮次即将成为历史的压缩计算实现极致的性能优化。元数据辅助的压缩在消息结构中加入元数据如importance_score由模型或规则打分压缩时优先保留高分消息。也可以加入expires_after过期时间标签让临时性的工具结果自动过期。5.3 个人实践中的体会在我实际构建的多个Agent系统中上下文压缩从来都不是一个“设置好就一劳永逸”的功能。它是一个需要持续观察和调优的子系统。我最深的体会是压缩策略的激进程度必须与任务的信息密度和容错性相匹配。对于法律、医疗等高风险领域压缩必须极其保守宁可牺牲一些上下文长度也要保证事实零失真。这时向量检索保留原文比LLM摘要更可靠。而对于创意写作、头脑风暴等场景则可以更激进地摘要甚至允许一些信息的模糊化以换取更长的对话延续能力。另一个反直觉的点是有时故意“遗忘”比“记忆”更重要。一个常见的失败模式是Agent陷入了对某个早期错误或无关细节的无限纠结中。一个设计良好的压缩系统应该能够识别并淡化那些已经解决或不再相关的历史分支让Agent的“注意力”始终聚焦在任务的主线上。这不仅仅是技术更是一种对对话“认知管理”的艺术。最后不要过度设计。先从最简单的滑动窗口关键消息保留开始如果不够用再加上增量摘要。大多数情况下一个智能的摘要器加上一个外部的向量记忆库已经能解决90%的长上下文问题。剩下的10%可能需要更复杂的认知架构但那可能就是下一个研究课题了。
返回列表