1. 项目缘起:当Agent的“记忆”成为负担

最近在折腾一个基于OpenClaw的智能客服Agent项目,遇到了一个典型的性能瓶颈:随着对话轮次的增加,上下文(Context)的长度像滚雪球一样膨胀。每次调用大模型API,不仅响应速度肉眼可见地变慢,token消耗的费用更是让人心惊肉跳。这让我意识到,对于长期运行、需要处理多轮复杂对话的Agent来说,上下文管理不是一个“有最好”的锦上添花功能,而是一个关乎成本、效率和稳定性的“必须有”的核心能力。

OpenClaw作为一个开源的AI Agent框架,其设计初衷就是处理复杂的、状态化的任务流。它默认会将整个对话历史、工具调用结果、系统指令等全部塞进上下文,传递给大模型。这在处理简单、短链路的任务时问题不大,但一旦任务流程变长,或者需要与用户进行深度、持续的交互,这个“全量记忆”模式就会迅速成为系统的阿喀琉斯之踵。大模型(无论是GPT-4、Claude还是国内的各种大模型)对上下文长度都有硬性限制和显著的性能衰减曲线。超过某个阈值后,模型的注意力机制会分散,理解核心指令的能力下降,甚至可能开始胡言乱语或遗忘早期的关键约定。

因此,我决定对OpenClaw的上下文处理机制动一次“外科手术”,目标很明确:在不显著牺牲Agent智能和理解连贯性的前提下,尽可能“瘦身”上下文,降低token消耗,提升响应速度。这个过程不是简单地截断或删除,而是一系列精密的实验和权衡。下面,我就把这三次关键的实验过程、背后的设计逻辑、踩过的坑以及最终的量化效果,完整地分享出来。

2. 实验一:基于关键信息提取的“摘要式”压缩

第一个实验的思路最为直观:既然完整的对话历史太长,那么我们能否像人做会议纪要一样,只保留最核心的信息,将冗长的原始对话“摘要”成一段精炼的文字,再放入上下文?这个方法的优势在于,它试图保留语义上的连贯性和关键事实,是一种“有损但智能”的压缩。

2.1 方案设计与工具选型

我并没有选择重新训练一个专门的摘要模型,那成本太高,周期也太长。我的策略是利用大模型自身的能力,让它来为自己生成摘要。具体来说,我在OpenClaw的对话循环中插入了一个新的“上下文整理器”(Context Summarizer)模块。这个模块会在上下文长度达到一个预设的阈值(例如,token数超过模型最大限制的60%)时被触发。

它的工作流程如下:

  1. 触发 :监控当前上下文的token数量。
  2. 分割 :将现有的长上下文按对话轮次或主题进行逻辑分割。例如,将“用户提问-助手回答”作为一个对话回合(Turn)。
  3. 指令构造 :构造一个清晰的提示词(Prompt),指令大模型将指定的对话历史压缩成一段连贯的摘要。这个提示词至关重要,它需要明确要求模型保留:用户的核心意图、助手做出的关键承诺或决策、双方确认的重要事实(如时间、地点、数字)、以及未解决的待办事项。
  4. 调用与替换 :调用一个大模型(这里我选择了一个成本较低的轻量级模型,如GPT-3.5-Turbo或国内同等能力的模型),将整理好的历史片段和摘要指令发送给它,获得摘要文本。
  5. 更新上下文 :用新生成的摘要文本,替换掉被摘要的那部分原始对话历史。同时,在摘要文本前加上明确的元信息标签,如 [历史对话摘要]:... ,让后续的模型知道这是一段摘要而非原始记录。

注意 :这里有一个关键细节,就是“摘要”的触发时机和频率。如果每轮对话都做摘要,开销太大且可能导致信息丢失过快。我最终采用的策略是“渐进式摘要”:只有当累积的原始对话token达到阈值时才触发,并且每次摘要后,新的上下文由“最新摘要 + 摘要点之后的原始对话”组成。这样既能控制长度,又保证了近期互动的细节不被丢失。

2.2 核心挑战与调优心得

这个方案听起来很美,但实操中遇到了几个棘手的问题:

第一个挑战是“摘要的摘要”问题 。随着对话进行,我们可能会对已经摘要过的内容再次进行摘要。这就像对一张JPEG图片反复压缩,信息损失会不断累积,最终可能导致关键事实扭曲或丢失。例如,用户最初说“预算5000元”,在第一次摘要中可能被保留为“预算约5000元”,第二次摘要可能变成“预算有限”,第三次可能就完全丢失了具体的数字信息。

我的应对策略是引入“关键事实锚点” 。在构造摘要指令时,我特别强调模型必须原封不动地提取并保留所有包含具体数字、日期、名称、布尔判断(是/否)的句子。在生成的摘要中,这些事实会被用特殊标记(如 {{预算: 5000元}} )包裹起来。在后续的摘要过程中,这些带标记的事实会被优先、无损地传递到新的摘要中,防止其在多次压缩中失真。

第二个挑战是摘要的“客观性”偏差 。大模型在摘要时,可能会无意中注入自己的理解或倾向,从而“改写”了用户的原始意图。比如,用户说“我觉得方案A和B都可以,再想想”,模型摘要可能变成“用户倾向于方案A和B,暂无定论”,这里的“倾向于”就是添加的主观色彩。

为了解决这个问题,我调整了提示词工程 。在指令中明确要求:“请进行事实性摘要,避免任何推测、解释或添加未在原文中明确表述的情感倾向。直接引用原文中的关键词和短语。” 同时,我会在摘要后附上少量(例如最近2-3轮)的原始对话片段,作为“近期原始记录”供模型参考,确保其对最新上下文的理解是基于最原始的信息。

第三个挑战是成本与延迟 。每次摘要都需要额外调用一次大模型API,这增加了单次交互的延迟和成本。虽然摘要用的是较便宜的模型,且触发频率不高,但在高并发场景下仍需考虑。

我的权衡点是设置一个较高的触发阈值 。经过测试,我将阈值设置为目标模型上下文窗口的70%。这意味着系统会容忍上下文变得比较“臃肿”,直到真的快触及上限前才进行一次“大扫除”。这样,在多数对话轮次中,是没有摘要开销的。对于延迟,我将摘要操作设计为异步任务,触发后,系统会继续用当前的“臃肿”上下文处理用户的下一句话,同时后台并行生成摘要,生成完毕后再更新上下文状态。对于用户而言,感知到的延迟几乎没有增加。

2.3 实验一效果评估

实施“摘要式”压缩后,效果是立竿见影的。在一个模拟的20轮技术咨询对话中,未压缩前,上下文token数最终超过了8000。启用摘要压缩后,token数被稳定地控制在3000-4000的区间内。API调用成本下降了约35%,平均响应时间减少了约20%。

然而,副作用也很明显。在涉及复杂逻辑推理或多步骤任务规划的对话中,Agent偶尔会出现“记忆模糊”的情况。例如,用户可能在第五轮对话中提出了一个约束条件,在第十五轮对话中又引用了它。如果这个约束条件在中间的某次摘要中被过度简化或丢失,Agent在第十五轮的回答就可能出现偏差。这证明了纯粹的摘要是一种“粗粒度”的压缩,它保留了“故事梗概”,但可能丢失了推动情节发展的“关键伏笔”。

3. 实验二:基于向量检索的“按需提取”策略

鉴于摘要方法可能丢失关键细节,第二个实验转向了另一种思路:我们不主动压缩历史,而是把所有的对话历史都存起来,但不在每次请求时全部发送给大模型。只有当大模型需要“回忆”某个特定信息时,我们再从历史库中精准地提取相关片段送过去。这就像是给Agent配了一个外部知识库(向量数据库),它的“工作记忆”(上下文)很短,但可以随时查阅“长期记忆”(向量库)。

3.2 架构改造与数据流设计

这个方案需要对OpenClaw的数据流进行更深的改造:

  1. 历史存储 :每一个完成的对话回合(包括用户输入、助手思考、工具调用结果、最终回复),在生成后都会被立即存储到两个地方:
    • 元数据存储 :如数据库,用于记录对话ID、时间戳、轮次等。
    • 向量数据库 :将这一轮对话的文本内容(通常是将用户输入和助手回复拼接)通过嵌入模型(Embedding Model)转化为一个高维向量(Vector),并存入向量数据库(如Chroma、Milvus、Qdrant)。存入时,会关联这一轮对话的元数据(如对话ID、轮次)。
  2. 上下文组装 :当需要构造发送给大模型的上下文时,我们只放入以下几部分:
    • 系统指令 :永远保留。
    • 最近N轮原始对话 :例如最近3轮,确保模型对即时对话有连贯理解。
    • 当前用户问题 :本次需要处理的新查询。
    • 检索到的相关历史片段 :这是核心。将当前用户的问题也转化为向量,然后在向量数据库中进行相似度搜索(Similarity Search),找出与当前问题最相关的K个历史对话片段(例如K=3)。将这些片段的原始文本作为“补充上下文”插入。
  3. 指令增强 :在系统指令中明确告诉模型:“你拥有一个外部记忆库。以下是基于你当前问题检索到的相关历史对话片段,供你参考。请主要依据这些片段和最近的对话来回答问题。”

3.3 相似度搜索的陷阱与优化

向量检索听起来很“智能”,但它高度依赖于嵌入模型的质量和检索策略的设计。我踩的第一个大坑就是 直接使用通用嵌入模型 。我用一个开源的通用句子嵌入模型来处理我的客服对话,结果发现检索效果很不稳定。当用户问“刚才说的那个价格是多少?”时,模型很可能检索出历史中所有包含“价格”二字的片段,而不是特指“刚才说的那个”的具体价格条目。

优化点一:使用领域适配或指令微调的嵌入模型 。我转而尝试了专门为对话检索优化的嵌入模型,或者利用少量标注数据对通用模型进行微调(Fine-tuning),让模型更能理解对话中的指代(如“这个”、“那个”、“你刚才说的”)和对话逻辑的连贯性。

优化点二:优化检索查询(Query)的构造 。直接拿用户当前的一句话去检索,信息量可能不足。我改进了查询构造方式,将“最近一两轮对话 + 当前用户问题”一起作为检索查询。这样,查询中就包含了更多的上下文线索,能显著提升检索的相关性。例如,用户当前问“它呢?”,结合上一句助手说的“A方案优点是快,B方案优点是便宜”,构造的查询就是“A方案优点是快,B方案优点是便宜。它呢?”,这样就更可能检索到关于B方案的信息。

优化点三:引入元数据过滤(Metadata Filtering) 。单纯的向量相似度搜索可能把很久以前的不相关但文本相似的历史找出来。我增加了过滤条件,比如只检索本对话session内的历史,或者只检索最近100条历史记录,这样可以避免“穿越”式的错误回忆。

3.4 实验二效果评估与局限性

“按需提取”策略在控制上下文长度方面表现极其出色。无论对话进行多少轮,每次发送给大模型的上下文主体(系统指令+最近N轮+检索结果)基本保持恒定,token数稳定在2000左右。成本节约高达50%以上,响应速度也因上下文变短而提升。

但是,它的局限性在于 对复杂、逻辑链长的任务支持不足 。大模型的推理能力,尤其是在进行多步骤规划、解决复杂问题时,往往需要在一个完整的、线性的上下文中进行“思考”。向量检索提供的是一些离散的、相关的“记忆碎片”,模型需要自己将这些碎片拼凑成一个完整的故事线,这对模型的要求很高,容易导致推理不连贯或遗漏关键的逻辑步骤。

例如,在帮用户规划旅行行程的对话中,用户可能先确定了目的地,然后讨论了预算,接着提出了时间约束,最后询问景点推荐。这是一个典型的递进式逻辑。如果只靠检索,当用户问景点推荐时,系统可能会检索出“目的地”和“预算”的片段,但“时间约束”这个片段如果与“景点”文本相似度不高,就可能被漏掉,导致推荐的景点不符合时间安排。而传统的长上下文则能完整地呈现这个决策链。

4. 实验三:混合策略与动态上下文图管理

前两个实验各有优劣:摘要法能保持叙事的连贯性但可能丢失细节;检索法能精准提取细节但可能破坏逻辑整体性。最终的解决方案,必然是一种混合策略。我的第三次实验,就是尝试构建一个 动态的、结构化的上下文管理机制 ,我称之为“上下文图”(Context Graph)。

4.1 “上下文图”的核心思想

这个想法源于对人类对话的观察。我们的对话不是扁平的文本流,而是由多个话题(Topic)、实体(Entity)、决策点(Decision)和事实(Fact)相互关联构成的网络。例如,在一个购物对话中,“商品A”、“价格”、“库存”、“配送地址”都是实体或事实,它们通过“用户询问”、“客服答复”、“用户确认”等关系连接起来。

“上下文图”旨在将扁平的对话历史,实时地解析成这样一个图结构。图中的节点是对话中提取出的关键元素(如实体、意图、承诺),边是这些元素之间的关系(如“属于”、“导致”、“否定”)。

4.2 实现路径与组件拆解

实现这个机制需要几个核心组件协同工作:

  1. 实时信息抽取器 :这是一个轻量级的模型或规则引擎,在每轮对话结束后运行。它的任务是从最新的对话回合中,快速抽取出:
    • 命名实体 :人名、地名、产品名、时间、金额等。
    • 用户意图 :是询问、确认、否定、还是提出新需求?
    • 助手动作与承诺 :调用了什么工具?输出了什么结果?向用户承诺了什么?
    • 关键事实与状态变更 :例如,“用户已登录”、“订单已创建”、“预算从5000改为6000”。
  2. 图结构管理器 :负责维护和更新这个上下文图。它将抽取器输出的元素作为新节点加入图中,并根据对话逻辑建立边。例如,用户说“把商品A加入购物车”,那么就会建立“用户” -> “执行” -> “动作:加入购物车” -> “目标” -> “实体:商品A” 这样一条关系链。同时,它还需要能解决指代,比如当用户说“它太贵了”,“它”指代的是上文中刚提到的“商品A”,这就需要将“它”这个节点关联到“商品A”节点上。
  3. 上下文组装引擎 :当需要构造大模型的输入时,这个引擎根据 当前用户的问题 当前的对话状态 ,动态地从上下文图中选取最相关的子图,并将其“翻译”回一段结构化的文本描述。

4.3 动态组装策略示例

假设当前用户问:“那我刚才选的那个商品的库存还有吗?”

  • 上下文图 中可能包含节点: [用户] [动作:选择] [实体:商品X] [属性:库存] [值:未知]
  • 组装引擎 的工作:
    1. 识别当前问题中的核心实体“商品”和属性“库存”。
    2. 在图中找到最近被“选择”动作关联的“商品”节点(即商品X)。
    3. 找到与该商品节点相连的“库存”属性节点。
    4. 发现“库存”节点的值是“未知”。
    5. 于是,它组装出这样一段上下文描述,插入到大模型的提示词中:“ [对话状态图] 用户当前正在关注商品 X (该商品是用户最近一次选择的)。关于商品 X 的库存信息,目前状态为 未知 。用户的最新问题是询问该商品的库存情况。”
    6. 同时,组装引擎依然会附上最近2-3轮的原始对话,以供模型理解最新的语言风格和细微语气。

这种方法的精髓在于,它传递给模型的不是原始的历史文本流,而是一个 高度结构化、语义化的状态描述 。模型无需再从冗长的历史中费力寻找相关信息,它直接面对的是一个已经整理好的“事实清单”和“问题焦点”。

4.4 实验三的复杂性与阶段性成果

构建一个健壮的上下文图管理系统是三次实验中最复杂的,它几乎是一个小型的研究项目。我目前实现的是一个简化版本,主要依赖规则和预训练好的NER(命名实体识别)模型进行信息抽取,图的关系也比较简单(主要是“提及”、“属于”、“属性”等)。

即便如此,这个混合策略也展现出了巨大的潜力。在测试中,它能够非常精准地在多话题交织的对话中维持状态,上下文长度控制得比摘要法更好(因为传递的是结构化描述而非自然语言摘要),同时逻辑连贯性又远优于纯检索法。对于需要追踪多个实体状态的任务(如订餐、行程规划、故障排查),效果提升尤为明显。

当然,它的缺点也很突出:实现复杂度高,非常依赖于信息抽取的准确性。如果抽取器错误地将“商品A的价格是500元”理解成“商品A和500元”,整个图的状态就会出错。这需要持续地优化抽取模型和规则。

5. 总结与选型建议

回顾这三个实验,本质上是在探索上下文管理的不同维度:

  • 实验一(摘要) 是在 时间序列 上进行压缩,保留主干,丢弃细节。
  • 实验二(检索) 是在 语义空间 上进行索引,按需取用,放弃线性。
  • 实验三(图管理) 是尝试在 逻辑结构 上进行重构,将线性叙事转化为网络状态。

对于正在面临OpenClaw或类似Agent框架上下文膨胀问题的开发者,我的选型建议如下:

  1. 如果你的任务简单直接,对话轮次不多 :或许不需要复杂的压缩,只需简单设置一个上下文长度上限,采用FIFO(先进先出)的滑动窗口,丢弃最老的历史即可。这是成本最低的方案。
  2. 如果你的任务对话较长,但话题相对集中,逻辑链不强 “摘要式”压缩 是一个非常好的起点。它实现相对简单,能有效控制成本,只需精心设计摘要提示词和触发策略,就能在大多数场景下取得不错的效果。优先推荐从这个方案入手实践。
  3. 如果你的任务需要频繁、精准地回溯历史中的具体事实、数字、条款 “向量检索”策略 更为合适。特别是在知识问答、文档核对、基于历史记录进行分析等场景。你需要投入精力选择合适的嵌入模型和优化检索查询。
  4. 如果你的任务极其复杂,涉及多实体、多状态、长逻辑链的交互 :那么值得考虑向**“混合策略”或“上下文图”** 方向探索。这通常是大型、复杂Agent系统的终极解决方案。你可以从简化版的图模型开始,例如,先实现关键实体和状态的追踪,再逐步丰富关系和推理逻辑。

最后,一个至关重要的心得是: 没有银弹 。上下文“瘦身”的本质是在“信息完整性”、“计算成本”和“响应延迟”之间寻找最佳平衡点。最有效的方法往往是结合业务场景的混合模式。例如,在我的最终部署方案中,我结合了摘要法和检索法:系统默认使用摘要法维持对话主线,但当检测到用户的问题明显是在查询一个具体历史事实(如“你刚才说的那个数字是多少?”)时,会自动触发一次向量检索,将最相关的原始片段插入上下文。这种动态策略,实测下来在成本和效果上取得了最好的平衡。

更多推荐