ARTICLE DETAIL

资讯详情

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

长程智能体上下文管理:从压缩到结构化驱逐的范式转变

长程智能体上下文管理:从压缩到结构化驱逐的范式转变 1. 从“压缩”到“结构化驱逐”长程智能体上下文管理的范式转变最近在折腾一些长程任务Long-Horizon Tasks的智能体Agent时我遇到了一个经典难题上下文窗口Context Window又不够用了。这几乎是所有基于大语言模型LLM构建的复杂智能体都会撞上的天花板。无论是处理一份冗长的技术文档、进行多轮代码调试还是规划一个包含数十个步骤的复杂工作流智能体需要记住和参考的信息量很快就会超出模型预设的上下文长度限制。传统的解决方案比如简单的“最近N条对话历史压缩”或者“摘要式压缩Summarization”在短对话里还行但在长程任务中往往显得力不从心。它们就像为了腾出书架空间要么扔掉最旧的书最近最少使用要么把几本书的内容强行缩写成一段话结果导致关键细节丢失、任务依赖断裂智能体开始“失忆”或做出前后矛盾的决策。这正是论文《Beyond Compaction: Structured Context Eviction for Long-Horizon Agents》所直面的核心挑战。它没有停留在如何“压缩”信息上而是提出了一个更根本的思路转变结构化上下文驱逐Structured Context Eviction。这个想法让我眼前一亮——它不再把上下文管理看作一个被动的、损失信息的“压缩”问题而是一个主动的、基于任务结构的“记忆管理”问题。简单来说它不是把信息“压扁”而是根据信息之间的依赖关系和未来效用聪明地决定“忘记”什么、以及如何“记住”被忘掉的信息的线索以备未来需要时能快速找回。对于任何正在构建或研究能够处理复杂、多步骤任务的LLM智能体的开发者来说理解并实践这种“超越压缩”的思维至关重要。它直接关系到智能体在长程任务中的可靠性、一致性和最终成功率。本文将深入拆解“结构化驱逐”的核心思想、技术实现并结合我个人的实验经验分享一套可落地的实践方案与避坑指南。2. 为什么传统上下文管理在长程任务中会失效在深入新方法之前我们必须先搞清楚老方法为什么不行。长程智能体任务例如自动化软件测试、多步骤研究分析、持续性的客户支持会话通常具备几个关键特征步骤多、周期长、信息间存在复杂的依赖关系。一个后续步骤可能依赖于很早之前步骤产生的某个具体数据或状态。2.1 朴素驱逐策略的致命缺陷最常见的传统方法是滑动窗口Sliding Window或最近最少使用LRU驱逐。它们只保留最近N个Token或对话轮次。在长程任务中这会导致灾难性的“关键信息丢失”。例如一个智能体在步骤1中从用户那里获取了项目的核心API密钥在步骤20中需要调用该API。如果采用简单的滑动窗口密钥信息早已被驱逐智能体将无法继续任务。更糟糕的是它可能不会意识到自己忘记了而是尝试生成一个没有密钥的API调用导致错误。另一种常见方法是摘要压缩Summarization。定期用LLM将历史上下文总结成一段更短的文本。这听起来不错但问题在于细节丢失摘要过程必然丢失具体参数、精确数值、异常堆栈等关键细节。成本高昂每次压缩都需要调用一次LLM增加了成本和延迟。依赖模糊摘要文本很难清晰保留信息之间的精确依赖关系。例如“系统在早期进行了配置”这样的摘要无法告诉智能体“配置的具体参数是什么”以及“哪个步骤需要用到哪个参数”。2.2 长程任务对上下文管理的核心需求因此一个适用于长程智能体的上下文管理系统必须满足以下需求保持关键信息对任务完成至关重要的信息如目标、约束、关键凭证必须长期保留或可检索。维护依赖完整性确保当一个步骤需要早期信息时该信息是可用的。高效利用Token预算在有限的上下文窗口内最大化有用信息的密度。支持动态重聚焦当任务分支或回溯时能快速将相关的历史上下文重新引入当前窗口。传统方法几乎无法同时满足这些需求而“结构化驱逐”正是为了解决这些痛点而设计的。3. 结构化上下文驱逐的核心架构与工作原理“结构化驱逐”不是一个单一的算法而是一套方法论和系统设计思想。其核心在于引入一个外部结构化记忆体并与LLM的内部上下文窗口协同工作。我们可以将其理解为智能体拥有了一个“外部笔记”和一套“笔记管理方法”。3.1 系统组件拆解一个典型的结构化上下文驱逐系统包含以下关键组件工作上下文Working Context即LLM当前的上下文窗口。这是智能体进行即时推理和决策的“工作记忆区”。它容量小但访问速度极快零延迟。外部记忆存储External Memory Store一个在LLM外部的存储系统用于保存被从工作上下文中“驱逐”的信息。它可以是一个向量数据库、图数据库或者更简单的键值存储。其容量远大于工作上下文。依赖关系图Dependency Graph这是“结构化”的灵魂。它是一个图结构节点代表信息块例如一个任务步骤的输出、一个用户指令、一个工具执行结果边代表信息之间的依赖关系例如“步骤B需要步骤A的结果作为输入”。驱逐管理器Eviction Manager负责决定何时、以及如何从工作上下文中移除信息。它的决策基于一套启发式规则或学习到的策略通常会考虑信息的“年龄”、“访问频率”、“未来效用预测”以及它在依赖关系图中的位置。检索器Retriever当智能体在执行当前步骤需要历史信息时检索器负责根据当前查询例如“用户之前提到的API密钥是什么”从外部记忆存储中快速找到最相关的信息片段。重写器/占位符管理器Rewriter/Placeholder Manager为了保持工作上下文的连贯性当信息被驱逐时并非简单地删除。系统可能会插入一个结构化占位符例如[ref:step_5_output]。这个占位符提示LLM这里曾经有一块信息如果需要细节可以参考某个标识符。当检索器找回信息后系统可能需要将详细信息重新插入或让LLM知晓。[工作上下文 (LLM窗口)] --(驱逐低优先级/旧信息)-- [驱逐管理器] | (存入结构化记忆) v [外部记忆存储 依赖关系图] ^ (按需检索) | --(插入占位符/召回详细信息)-- [检索器 重写器]3.2 依赖关系图的构建与维护依赖关系图是实现智能驱逐和精准检索的基础。构建它主要有两种方式显式声明在智能体框架设计时强制要求每个工具Tool或动作Action声明其输入和输出。系统自动根据执行流构建依赖边。这种方式精确可靠但对框架设计有要求。# 伪代码示例一个工具定义 tool def call_api(api_endpoint: str, api_key: str) - dict: 调用指定API。 # ... 执行逻辑 return response_data # 系统记录call_api 依赖于 api_endpoint 和 api_key 这两个信息节点。隐式推断利用LLM本身来分析对话或任务历史自动推断信息块之间的依赖关系。例如在每一步执行后让一个轻量级LLM分析该步骤的输出与历史中哪些信息相关。这种方式更灵活但可能引入噪声。依赖关系图的边可以有权重表示依赖的强度。例如“必须拥有”的依赖权重为1.0“可能有帮助”的依赖权重为0.3。驱逐管理器在决定是否驱逐一个信息节点时会检查是否有高权重的边指向它即它被未来步骤强烈依赖如果是则优先保留。3.3 驱逐策略如何决定“忘记”什么这是结构化驱逐策略的核心算法部分。一个好的驱逐策略需要在“保留重要信息”和“腾出空间”之间取得平衡。常见策略包括基于未来效用预测的驱逐这是最理想但也最复杂的方法。系统尝试预测每个信息块在剩余任务中的被需要概率。可以基于一些启发式规则进行简化预测信息类型目标声明Goal、约束Constraint、凭证Credential通常具有高未来效用。依赖图中心性在依赖关系图中出度高的节点被很多后续步骤依赖效用高。创建时间与访问模式最近被访问过的信息短期内再次被访问的概率可能更高局部性原理。基于依赖关系的安全驱逐这是一种更保守但安全的策略。只有当系统确认某个信息块的所有直接依赖者即需要它的后续步骤都已经执行完毕该信息块才被标记为“可安全驱逐”。例如一个只在步骤1用到、步骤2之后再也不用的中间结果在步骤2完成后就可以被驱逐。混合策略在实际中通常结合多种策略。例如首先驱逐那些已被标记为“安全”的节点如果空间仍不足则根据预测的效用分数从低到高驱逐那些“非安全”但效用较低的节点核心高效用信息如目标永远保留在工作上下文中或给予最高保护级别。4. 从理论到实践构建一个简易的结构化驱逐系统理解了原理后我们如何动手实现一个简化版下面我将基于Python和一些常用库勾勒一个可运行的原型框架。这里我们选择显式依赖声明和基于依赖关系的安全驱逐策略因为它相对容易实现且行为确定。4.1 环境准备与核心数据结构首先定义我们的核心数据结构InformationNode信息节点和DependencyGraph依赖图。# context_eviction.py from dataclasses import dataclass, field from typing import Any, Dict, List, Optional, Set from enum import Enum import uuid class NodeType(Enum): GOAL goal CONSTRAINT constraint ACTION_INPUT action_input ACTION_OUTPUT action_output USER_MESSAGE user_message SYSTEM_MESSAGE system_message dataclass class InformationNode: 表示一个信息块节点。 id: str field(default_factorylambda: str(uuid.uuid4())[:8]) content: str # 信息文本内容 node_type: NodeType created_at_step: int # 在哪个任务步骤创建 # 元数据可用于效用预测 metadata: Dict[str, Any] field(default_factorydict) # 显式声明的依赖本节点依赖于哪些其他节点 depends_on: Set[str] field(default_factoryset) # 显式声明的被依赖哪些其他节点依赖于本节点 depended_by: Set[str] field(default_factoryset) class DependencyGraph: 管理信息节点及其依赖关系的图。 def __init__(self): self.nodes: Dict[str, InformationNode] {} # 用于快速查找哪些节点是“叶子”当前未被任何未完成步骤依赖 self._pending_dependencies: Dict[str, Set[str]] {} # node_id - 依赖它的未完成节点集合 def add_node(self, node: InformationNode): self.nodes[node.id] node # 初始化时假设所有依赖都未完成 self._pending_dependencies[node.id] set(node.depended_by) def mark_step_complete(self, step_id: int): 标记一个任务步骤完成。遍历所有节点移除对该步骤的依赖。 nodes_to_check [n for n in self.nodes.values() if n.created_at_step step_id] for node in nodes_to_check: # 这个节点是步骤step_id产生的现在步骤完成了所有依赖它的节点需要更新 for depender_id in list(node.depended_by): if depender_id in self._pending_dependencies: self._pending_dependencies[depender_id].discard(node.id) # 同时这个节点本身的依赖也可能清空了如果它的依赖者都完成了 # 但更简单的策略是一个节点是否可驱逐取决于是否还有“未完成的步骤”依赖它。 # 我们通过 get_safe_to_evict_nodes 来实现。 def get_safe_to_evict_nodes(self, current_step: int) - List[InformationNode]: 获取当前可安全驱逐的节点列表。 安全驱逐条件该节点没有被任何“创建于当前或未来步骤”的节点所依赖。 简化判断如果 _pending_dependencies[node.id] 为空集则说明所有依赖它的节点都已处理完毕。 safe_nodes [] for node_id, pending_deps in self._pending_dependencies.items(): if not pending_deps: # 没有未完成的依赖者 node self.nodes[node_id] # 额外检查确保这个节点本身不是当前或未来步骤创建的核心信息如目标 # 这里简单排除GOAL类型 if node.node_type ! NodeType.GOAL: safe_nodes.append(node) return safe_nodes def get_nodes_by_type(self, node_type: NodeType) - List[InformationNode]: return [n for n in self.nodes.values() if n.node_type node_type]4.2 智能体执行循环与上下文管理器的集成接下来我们创建一个StructuredContextManager它将集成依赖图、工作上下文模拟的Token列表和驱逐逻辑。# context_manager.py from typing import List from .context_eviction import DependencyGraph, InformationNode, NodeType class StructuredContextManager: def __init__(self, max_working_tokens: int, dependency_graph: DependencyGraph): self.max_working_tokens max_working_tokens self.dependency_graph dependency_graph self.working_context: List[str] [] # 模拟LLM的上下文存储节点ID或内容 self._token_count 0 self._node_id_to_token_count {} # 记录每个节点在working_context中占用的token数估算 def _estimate_tokens(self, text: str) - int: 简单的token估算函数。实际应用中应使用与LLM匹配的tokenizer。 # 这里用单词数近似实际请使用 tiktoken 或 transformers return len(text.split()) // 0.75 # 近似估算 def add_to_context(self, node: InformationNode, force_keep: bool False): 将信息节点添加到工作上下文。 node_tokens self._estimate_tokens(node.content) # 检查是否需要驱逐 while self._token_count node_tokens self.max_working_tokens: if not self._evict_one_node(): # 无法再驱逐可能都是强制保留的需要压缩或报错 raise RuntimeError(工作上下文已满且无法安全驱逐任何节点。) # 添加上下文 self.working_context.append(f[{node.id}]: {node.content}) self._token_count node_tokens self._node_id_to_token_count[node.id] node_tokens # 如果该节点被强制保留如目标可以打上标签驱逐策略会跳过它 if force_keep: node.metadata[force_keep] True def _evict_one_node(self) - bool: 尝试驱逐一个节点。返回是否驱逐成功。 # 1. 优先驱逐“安全”节点 safe_nodes self.dependency_graph.get_safe_to_evict_nodes(self._current_step) for node in safe_nodes: if node.id in self._node_id_to_token_count: self._remove_node_from_context(node.id) # 在上下文中放入一个占位符 self.working_context.append(f[Evicted Reference: {node.id}]) # 占位符token数很少这里忽略不计或可以估算一个固定值 print(f安全驱逐节点: {node.id} ({node.node_type})) return True # 2. 如果没有安全节点尝试基于效用预测驱逐简化版驱逐最旧的非强制保留节点 # 获取当前在工作上下文中的所有节点按创建步骤排序 nodes_in_context [] for node_id in list(self._node_id_to_token_count.keys()): node self.dependency_graph.nodes.get(node_id) if node and not node.metadata.get(force_keep, False): nodes_in_context.append(node) nodes_in_context.sort(keylambda n: n.created_at_step) # 最旧的在前 for node in nodes_in_context: self._remove_node_from_context(node.id) self.working_context.append(f[Evicted Reference: {node.id}]) print(f基于LRU驱逐节点: {node.id} ({node.node_type})) return True # 3. 所有节点都是强制保留的无法驱逐 return False def _remove_node_from_context(self, node_id: str): 从工作上下文中移除一个节点。 # 在实际实现中需要找到包含该node_id的上下文条目并移除 # 这里简化处理从working_context中移除包含该id的字符串 for i, ctx in enumerate(self.working_context): if f[{node_id}]: in ctx: removed_tokens self._node_id_to_token_count.pop(node_id, 0) self._token_count - removed_tokens self.working_context.pop(i) break def get_context_for_llm(self) - str: 返回格式化后的工作上下文供LLM使用。 return \n.join(self.working_context) def set_current_step(self, step: int): 设置当前任务步骤用于依赖图更新。 self._current_step step4.3 一个完整的任务执行示例让我们模拟一个简单的长程任务“编写一个Python函数从API获取数据进行过滤然后保存到文件。”# main_demo.py from context_eviction import DependencyGraph, InformationNode, NodeType from context_manager import StructuredContextManager def simulate_long_horizon_task(): # 初始化 graph DependencyGraph() context_manager StructuredContextManager(max_working_tokens500, dependency_graphgraph) # 步骤0定义任务目标强制保留 goal_node InformationNode( content任务目标编写一个Python脚本从https://api.example.com/data获取JSON数据过滤出status为active的条目并将结果保存到active_items.json文件中。, node_typeNodeType.GOAL, created_at_step0 ) graph.add_node(goal_node) context_manager.add_to_context(goal_node, force_keepTrue) context_manager.set_current_step(0) # 步骤1用户提供API密钥关键信息被后续步骤依赖 api_key_node InformationNode( content用户提供的API密钥sk_live_1234567890abcdef。, node_typeNodeType.USER_MESSAGE, created_at_step1 ) # 假设步骤2编写获取函数依赖此密钥 api_key_node.depended_by.add(step2_action) # 这里用字符串ID模拟实际应是节点ID graph.add_node(api_key_node) context_manager.add_to_context(api_key_node) # 步骤2智能体生成获取数据的函数动作输出 fetch_code_node InformationNode( contentpython\nimport requests\ndef fetch_data(api_key):\n url https://api.example.com/data\n headers {Authorization: fBearer {api_key}}\n response requests.get(url, headersheaders)\n return response.json()\n, node_typeNodeType.ACTION_OUTPUT, created_at_step2 ) fetch_code_node.depends_on.add(api_key_node.id) # 显式声明依赖API密钥节点 graph.add_node(fetch_code_node) # 更新API密钥节点的被依赖集合 api_key_node.depended_by.add(fetch_code_node.id) context_manager.add_to_context(fetch_code_node) print( 步骤2后工作上下文 ) print(context_manager.get_context_for_llm()) print(*50) # 模拟上下文已满触发驱逐 # 我们手动添加一些“低效用”信息来模拟 for i in range(3, 10): dummy_node InformationNode( contentf这是步骤{i}产生的一些中间日志信息重要性较低。, node_typeNodeType.ACTION_OUTPUT, created_at_stepi ) graph.add_node(dummy_node) context_manager.add_to_context(dummy_node) print( 添加多个中间节点后工作上下文 ) print(context_manager.get_context_for_llm()) print(当前Token估算:, context_manager._token_count) print(*50) # 步骤10需要执行过滤操作但可能依赖早期信息 # 假设过滤需要知道数据格式这来自于步骤2的输出 filter_node InformationNode( content接下来需要编写过滤函数。已知fetch_data返回一个包含items列表的JSON每个item有status字段。, node_typeNodeType.ACTION_INPUT, created_at_step10 ) filter_node.depends_on.add(fetch_code_node.id) # 过滤依赖fetch_data的代码了解返回结构 fetch_code_node.depended_by.add(filter_node.id) graph.add_node(filter_node) # 在添加filter_node前上下文可能已满会触发驱逐 print( 尝试添加步骤10的过滤节点可能触发驱逐) context_manager.set_current_step(10) # 标记步骤2完成fetch_code_node的依赖者可能减少但filter_node又依赖它所以它不会被标记为安全 # 在我们的简单逻辑中mark_step_complete 会更新内部状态。 # 这里为了演示我们假设驱逐管理器运行。 context_manager.add_to_context(filter_node) print( 最终工作上下文包含占位符) print(context_manager.get_context_for_llm()) print(*50) # 演示检索当LLM遇到占位符时如何获取详细信息 # 在实际系统中LLM生成内容中若包含对占位符的引用一个后处理模块会从外部存储中检索并补充。 evicted_ref [Evicted Reference: step2_action_output] # 假设LLM输出需要细节系统检索器根据ID从graph.nodes中找回完整内容。 # print(f检索到被驱逐的内容: {graph.nodes.get(step2_action_output, {}).get(content, Not Found)}) if __name__ __main__: simulate_long_horizon_task()运行这个示例你会看到上下文管理器如何在Token预算有限的情况下优先保留目标Goal并在需要空间时根据依赖关系和简单的LRU策略驱逐节点并用占位符替代。当后续步骤需要被驱逐的信息时占位符提供了检索的线索。5. 实战中的挑战、优化与避坑指南纸上得来终觉浅绝知此事要躬行。在将结构化驱逐应用到真实智能体项目中时我遇到了不少坑也总结了一些优化思路。5.1 依赖关系推断的准确性与噪声问题如果采用隐式推断用LLM分析依赖其准确性严重依赖于提示词Prompt质量和LLM的能力。它可能错误地建立依赖假阳性或遗漏关键依赖假阴性。假阳性会导致大量无用信息被长期保留浪费空间假阴性则会导致关键信息被过早驱逐引发任务失败。解决方案与心得混合模式启动对于关键路径如工具输入输出坚持使用显式声明。对于更自由的对话或推理过程可以辅助使用隐式推断。例如在智能体执行一个工具后可以自动声明该工具的输出依赖于其输入参数对应的节点。置信度过滤让LLM在推断依赖时输出一个置信度分数。只采纳高置信度例如 0.8的依赖边。低置信度的边可以存储但不作为安全驱逐的依据。定期依赖图验证在任务的关键检查点如子目标完成时让智能体主动回顾并确认关键依赖关系人工或自动修正图中的错误。5.2 占位符对LLM推理的影响问题简单地将被驱逐内容替换为[ref: id]这样的占位符可能会干扰LLM的理解。LLM可能无法正确解析这个占位符的含义或者在其生成的内容中不恰当地引用它。解决方案与心得结构化、可读的占位符不要只用ID。占位符应包含最小化的、人类和机器都可读的摘要。例如[Evicted: Step 5 Output - ‘fetch_data‘ function definition. Ref ID: abc123]。这样即使不检索LLM和开发者也能大致知道这里有什么。在系统提示中教育LLM在给LLM的系统指令中明确说明上下文中可能出现[Evicted: ...]这样的占位符并告知它当需要详细信息时可以输出一个特殊的检索指令如[RETRIEVE: abc123]由系统层拦截并处理。主动预检索根据当前步骤和依赖图预测可能需要的信息在生成前就将其从外部存储检索回来替换或补充到上下文中。这需要更复杂的预测逻辑但能提供最流畅的体验。5.3 外部记忆存储的选型与检索效率问题当外部记忆存储了大量信息节点后如何快速精准地检索简单的全文搜索或基于关键词的匹配在复杂任务中效果很差。解决方案与心得向量数据库是标配为每个InformationNode的content字段生成嵌入向量Embedding。当需要检索时将当前查询或上下文向量化进行相似性搜索。这能有效找到语义相关的历史信息即使字面不匹配。元数据过滤结合向量搜索仅用向量搜索可能召回无关内容。结合节点类型node_type、创建步骤created_at_step等元数据进行过滤可以大幅提升精度。例如当需要找一个“API密钥”时可以限定node_typeNodeType.USER_MESSAGE并进行向量搜索。分层存储对于超长任务可以考虑分层记忆。最近、最相关的信息放在快速向量存储如内存或Redis较旧的信息归档到更经济的存储如磁盘数据库并建立更粗粒度的索引。5.4 效用预测模型的构建问题如何准确预测一个信息块的“未来效用”这是结构化驱逐中最具挑战性的一环。实践经验与启发式规则基于规则的基线这是我最初采用的方案简单有效。永久保留NodeType.GOAL,NodeType.CONSTRAINT。高权重包含特定关键词的节点如“密码”、“密钥”、“地址”、“配置”。中权重动作输出ACTION_OUTPUT尤其是那些在依赖图中被多次引用的。低权重中间日志、确认消息。轻量级机器学习模型如果任务模式相对固定可以收集历史任务数据训练一个简单的分类器如逻辑回归、XGBoost特征可以包括节点类型、文本长度、包含的关键词、在依赖图中的度中心性、创建时间距离现在的步数、历史被访问次数等。这个模型可以预测该节点在接下来N步内被需要的概率。在线学习与适应智能体可以在运行中学习。如果一个信息块被驱逐后很快又被检索回来说明效用预测低了下次类似的信息块应给予更高权重。6. 性能考量与扩展方向引入结构化驱逐系统必然会增加复杂度。在性能上需要关注以下几点延迟驱逐决策、依赖图更新、检索操作都会增加循环延迟。需要优化算法复杂度尤其是依赖图的操作应接近O(1)。对于检索可以考虑异步预取。Token估算精度不准确的Token计数会导致上下文窗口管理失控。务必使用与后端LLM完全一致的Tokenizer进行精确计数。状态持久化对于可能中断重启的长程任务整个依赖图和工作上下文的状态需要能够序列化保存和加载。未来的扩展方向也很有趣与反思Reflection机制结合让智能体定期回顾外部记忆存储进行总结、提炼或发现新的依赖关系动态优化依赖图。多模态上下文管理当智能体能处理图像、音频时如何定义这些模态信息之间的依赖关系如何估算它们的“Token”开销分布式记忆在多个智能体协作的场景下如何共享和协调一个全局的结构化记忆体从我个人的实验来看结构化上下文驱逐不是银弹但它为长程LLM智能体的发展提供了一个坚实且必要的工程基础。它迫使我们从设计之初就思考信息的生命周期和价值从而构建出更健壮、更可靠的自主系统。开始在你的下一个智能体项目中尝试引入最简单的依赖跟踪吧即使只是一个记录“关键信息ID”的列表也能显著提升其在长对话中的表现。
返回列表