☰
Agent长期记忆与长程工具调用的上下文优化实战
2026/10/4 14:16:20 网站建设 项目流程

最近我在做一个能自动查资料、调内部API、最后生成调研报告的Agent,从单轮对话扩展到多轮长任务时,撞上了一堵很现实的墙:明明是想让AI记住上下文,工具一多、链路一长,模型就开始"失忆";把历史记忆塞回去,工具调用的中间结果又把窗口挤爆了。更难受的是,账单上的数字随着token一起飞涨——看起来没用到多少东西,每天的成本却高得离谱。

这类问题的核心,就是标题里说的那两件事:长期记忆和长程工具调用,它们天然在抢同一块上下文资源,而成本又在旁边等着收钱。这篇文章我打算把最近调试、重构这套系统的完整思路写出来,包括记忆该怎么做分层、工具调用怎么安排才能不挤占上下文、以及成本到底靠哪些手段能真正压下来。内容偏工程实操,正在做Agent、RAG应用或AI自动化工作流的同学,应该能直接拿去参考。

1. 记忆与工具调用抢上下文:这个矛盾到底卡在哪

1.1 同一个token预算,两家人在用

先说个基本事实:当前主流大模型的工作方式,是把"你给它的所有内容"都算进一次请求的上下文里。长期记忆的本质,就是把历史信息重新塞回这段上下文;而工具调用的本质,是让每一步工具返回结果也在上下文里留下痕迹。问题是上下文窗口就那么大,记忆占多了,工具调用的空间就少了;工具调用链一长,前面放进去的记忆又会被挤出去。

我在项目里第一次感觉到这个矛盾,是在跑一个"查三家数据源并交叉验证"的任务。系统先检索了用户前几次会话的偏好和历史结论,又连续调用了三个API,每个API返回都是一大段JSON。结果跑到第二个API时,模型开始重复问"我刚才查到的是什么"——它确实忘了,不是逻辑不行,而是早期的记忆已经被后边的工具结果冲出了窗口。

1.2 三种最常见的故障形态

这类冲突在真实系统里有三种典型表现,排查起来基本都能对上号:

  • 记忆污染执行:长期记忆内容太多、太杂,模型在执行工具调用时被无关历史干扰,做了多余操作,甚至直接跳过该调的工具。
  • 执行挤掉记忆:工具调用步数多,每一步的结果都留在上下文里,等到后面需要引用早期结论时,模型已经"不记得了"。
  • 反复注入导致失控:为了保住记忆,系统每轮都把完整历史重新塞进去,上下文长度越来越膨胀,成本按次翻倍上升。

1.3 为什么"加窗口"不是答案

很多人第一反应是换更大上下文窗口的模型。这个方法治标不治本。上下文变长之后,单次请求的费用和延迟都跟着涨,而且模型对长上下文中"关键信息"的注意力会稀释。我实测过同样的任务,在32k窗口下运行正常,换成128k窗口后,模型反而开始关注一些无关细节,因为塞进去的噪声变多了。

还有一个更隐蔽的问题:长上下文不等于长记忆。窗口是临时的,任务一结束就清空了;长期记忆需要跨会话保留,这必须依赖外部存储和检索,而不是单纯靠窗口大小硬扛。所以真正的解法,是同时做两件事:让记忆按需进入上下文,让工具调用不产生过多上下文残留。下面几节就是我按这个思路拆出来的方案。

2. 分层记忆:把AI的回忆按保鲜期拆开管理

2.1 三层记忆模型

我给这套系统设计的记忆结构,参照了认知科学里工作记忆和长期记忆的区分方式,分成三层:

  • 工作记忆:只存在于当前请求的上下文里,就是这轮对话中正在处理的内容,任务结束即消失。
  • 会话记忆:一次会话的摘要,保存"这次会话讨论了什么、结论是什么、用户关注点有哪些"。以结构化摘要的形式存储,下一轮同主题对话时召回。
  • 长期记忆:跨会话的事实、偏好、实体关系、历史决策记录。以向量索引为主,配合结构化字段,按需检索。

分层的好处是:不用任何时刻都把"所有历史"搬进上下文。大多数情况下,工作记忆就够用了;需要回溯时,先召回会话摘要;只有当任务确实依赖更久远的细节时,才去翻长期记忆。这就把"全量重放"变成了"按需提取"。

2.2 写入策略:不是每轮对话都值得记住

之前我犯过一个典型错误:每轮对话结束后,都把全部内容写进长期记忆库。结果记忆库爆炸不说,检索时还经常召回一堆互相矛盾的历史。后来改成事件写入策略,核心是给记忆入库加触发条件:

  • 任务完结时,写入任务的目标、关键路径、最终结论;
  • 用户明确表达偏好或否定意见时,单独记一条"偏好变更";
  • 会话中出现新的实体(比如新项目、新人、新的约束条件)时,抽取实体关系入库;
  • 普通闲聊、中间过程、失败尝试,一概不写。

写入的时候我用了两层:一层是原始片段,保留原始消息的关键段落;另一层是摘要记录,用LLM把这次要记的内容压缩成一条结构化的JSON。原始片段进向量库用于语义检索,摘要记录同时存成结构化字段,方便按时间、按项目、按实体过滤。

2.3 召回策略:先想清楚"现在需要什么记忆"

召回做得不好,比不召回还糟糕——我在第6节会详细讲这个坑。这里先给出我最终采用的召回方案:

  • 每次需要记忆前,先用一个轻量步骤把当前需求改写成检索词。比如用户说"上次那个方案的成本部分再详细点",就直接提取"方案成本"作为检索query。
  • 做混合检索:向量相似度为主,再加上时间衰减权重和重要度加权。时间衰减是为了避免永远召回最旧的内容,重要度则是给"用户明确强调过"的信息加权。
  • 召回数量严格控制。我现在的默认值是Top-5,且带相关度阈值;不相关宁可不召回。召回回来的记忆再经过一次"记忆包"整理,按与当前任务的相关性排序,拼装成一小段结构化的"记忆提示",插入到系统提示词的固定位置。

这里有一个设计细节值得单独说:记忆包不是把原话复制回来,而是统一改写成"事实陈述+时间戳+相关度说明"的格式。比如"2025-05-20:用户确认项目优先目标是降低API调用成本,暂不追求响应速度(相关度0.92)"。这种格式让模型读取成本极低,能直接复用,不用再花时间去理解一段对话的上下文。

3. 长程工具调用:上下文腾挪的几种有效策略

3.1 全链ReAct为什么撑不住长任务

ReAct模式(推理-行动-观察循环)是工具调用最基础的做法:模型边想边调用工具,每一步的推理和观察结果都追加到上下文里。单步任务它很好用,但长任务一旦超过一定步数,上下文就会被"历史对话"和"中间观察"占满,最后要么任务执行不下去,要么模型开始幻觉。

我在一个"批量核对一百条数据"的任务里实测过:ReAct模式下跑到第20步左右,模型开始忘记最初的数据校验规则;跑到第40步,上下文基本被工具返回填满。这个教训让我明白,长程工具调用不能靠"记忆硬扛执行痕迹",必须从架构层面做腾挪。

3.2 规划与执行分离:用任务清单代替对话轨迹

我最终采用的是规划-执行分离结构:

  • 规划器(Planner):接收用户目标和记忆包,输出一份任务清单,比如["调用数据源A获取原始数据", "调用数据源B获取交叉数据", "按规则A执行校验", "生成差异报告"]。规划器只负责"要做什么",不负责"每步怎么做"。
  • 执行器(Executor):按清单逐项执行。每一步都是相对独立的函数调用,只接收这一步需要的参数,只返回这一步的结构化结果。
  • 每完成一个任务项,就把"该项目标+核心结果+状态"写进执行日志,而单步的详细中间过程直接丢弃。

对比一下就知道区别在哪:ReAct模式是"每一步的思考过程都留在现场",而规划-执行模式是"现场只留清单和已完成的结论"。长任务跑下来,上下文里只有几十个任务项的状态摘要,而不是几百条工具返回。

3.3 子代理:让子任务在独立上下文里跑

有些任务单个执行器处理不过来,比如"让AI把一个PDF里的所有表格提取出来并做数据清洗",这种子任务的中间过程特别长。我的做法是再套一层:把这类独立子任务交给子代理。子代理自己拥有一个独立的上下文窗口,在里面做完整的工具调用循环;它结束后,只把结论摘要返回给主执行器。

主代理的世界里,"这个PDF已经处理完了,提取出18个表格,清洗后剩16个有效表,已保存到目录…"——这就够了。至于子代理中间调了什么清洗函数、踩了什么坑,那是它自己的事,不需要污染主上下文。子代理的上下文用完就释放,成本也是"只花在真正需要的任务上"。

3.4 工具结果的压缩与结构化

工具返回结果往往是成本大头。一个典型的查询接口可能返回几千token的JSON,但当前任务真正需要的可能只有其中三个字段。所以在我的系统里,每次工具调用后都会接一个压缩器:从原始结果里提取当前任务需要的字段,重组成一个紧凑的结构,然后才放进上下文。

这个组件可以用一段规则代码实现,也可以调用一次小模型。我的经验是:能用规则就用规则,因为稳定且没有额外成本;只有结果是非结构化文本时才上小模型。压缩时要特别注意两点:

  • 保留必要的错误信息。如果工具调用失败,错误码和失败原因必须原样保留,不能压缩成"出错"两个字,否则模型无法做下一步决策。
  • 保留数据的可追溯标识,比如数据来源ID、时间戳。后面生成报告时要引用,丢了就得重新查。

4. 成本控制:压缩、复用、分级三板斧

4.1 先搞清楚token都花在哪

在动手优化成本之前,我把一次完整任务的开销拆了一遍,主要有五块:记忆检索、规划、工具调用、执行上下文、最终生成。不做拆解直接砍成本,往往会把不该砍的砍掉。我当时的账单构成大致是:工具返回占45%,历史记忆重放占25%,规划与生成占20%,其他占10%。这个比例基本指向两个主攻方向:压缩工具结果、减少记忆重放。

4.2 记忆只喂"够用的":检索量是成本调节阀

记忆重放是最容易失控的。很多系统为了确保模型"记住",把召回结果不加限制地全塞进去,一次塞几千甚至上万token。我的做法是给记忆包设硬性预算,比如默认不超过600token。超过预算时,优先保留高相关度项,相关度不够的直接丢弃。配合第2节说的"摘要优先、原始片段兜底"策略,大多数场景下600token已经能让模型拿到足够上下文。

这里给个实测对比:改造前,每次任务固定重放约8000token记忆,占整个请求的四分之一;改成分层召回后,平均重放1100token,相关度反而更高,因为冗余项少了,模型更容易抓住重点。单这一项,就能省下约15%的输入token。

4.3 缓存复用:系统提示词和工具Schema别重复付钱

现在不少模型接口提供了提示词缓存能力,简单说就是:如果请求的前缀部分和之前某个请求完全一致,这部分token会按折扣价格计费。很多团队知道这个功能,但不知道怎么用出效果。我的落地方案是:

  • 把系统提示词、工具定义(包括工具Schema)、记忆包的固定格式部分,全都拼在请求的最前面,并保证它们在一段时间内稳定不变。
  • 把每次变化的用户输入和任务内容放在后面。缓存命中的前提是前缀稳定,所以把变化的部分尽量往后移。
  • 经常变动的部分,比如最新的执行日志,不要塞进前缀。

这块优化在长任务场景效果非常显著。一个任务跑N步,每步都带同样的系统提示词和工具定义,缓存命中后能省掉一大块重复的输入费用。我实测下来,带缓存的接口在长链路任务里能省20%到35%的输入token,具体取决于系统提示词和工具定义占整个请求的比例。

4.4 模型分级:小模型当过滤器,大模型当决策者

不是每一步都需要调用最贵最强的模型。我现在的策略是给不同环节配置不同档位的模型:

  • 检索改写、结果压缩、格式校验:用小参数模型就够了。成本低、速度快,而且这些环节逻辑相对固定,不依赖复杂推理。
  • 规划器、最终报告生成、复杂工具参数组装:才用大参数模型。
  • 执行器:大多数情况下用中档模型,只有在工具参数需要复杂推断时才升级。

这个策略带来的成本下降很直观:同样一个任务,优化前每一步都用同一个高档模型,优化后大模型只承担规划、生成等少数环节,整体成本下降大约40%。要注意的是,切换模型不是一个纯省钱动作,需要对比输出质量。我踩过的坑是:让小模型做"工具结果压缩"时,它偶尔会把关键数值丢进省略号里,后来加了正则校验才稳住。

5. 混合架构落地示例:一次真实改造的完整过程

5.1 最终采用的模块划分与数据流

我把这套系统整理成下面几个模块,它们之间的数据流就是一次完整任务的生命周期:

  1. 用户请求进入Orchestrator(调度器),调度器先调用记忆检索模块,拿到当前任务需要的记忆包;
  2. 带着记忆包,Planner(规划器)生成任务清单;
  3. Executor(执行器)按清单逐项执行:调用工具 -> 压缩结果 -> 判断是否完成 -> 进入下一项;
  4. 每完成一个任务项,写入执行日志;执行日志累积到阈值时,触发Summarizer(摘要器)压缩一次;
  5. 全部任务项完成后,Writer(生成器)基于执行日志和记忆包生成最终报告;
  6. 报告完成后,记忆写入模块按事件触发规则,把本次任务的关键结论写入长期记忆库。

5.2 记忆模块的工程实现

记忆库我用的是向量数据库,每条记忆项包含这些字段:

字段类型说明
idstring唯一标识
contenttext记忆内容(原始片段或摘要)
summarytext结构化摘要,JSON格式
event_typestring触发事件类型(任务完成/偏好变更/实体新增等)
importancefloat重要度评分,用户强调或多次出现会加权
timestampdatetime写入时间
source_refsarray关联的任务ID或消息ID,便于回溯

检索时,我先把当前请求改写成一个检索query,然后做向量检索加元数据过滤(比如限定项目ID),得到候选集后再按"相关度0.6 + 重要度0.3 + 时间衰减*0.1"排一次序,取Top-5,拼成记忆包。

5.3 长程执行引擎的伪代码

整个执行引擎的核心逻辑其实很短,这里给一份简化版伪代码:

def run_task(request): # 1. 检索记忆,控制预算 memory_pack = recall_memory(request, budget_tokens=600) # 2. 规划 plan = planner.generate_plan(request, memory_pack) # 3. 执行循环 exec_log = [] for step in plan: result = tool_call(step) compact_result = compressor.extract(result, fields=step.required_fields) exec_log.append({"step_name": step.name, "result": compact_result}) # 4. 日志压缩:执行日志超过阈值时,做一次摘要合并 if token_count(exec_log) > 3000: exec_log = summarizer.squash(exec_log) # squash保留的是每个任务项的结论,不保留中间细节 # 5. 生成最终响应 response = writer.generate(request, memory_pack, exec_log) # 6. 写回长期记忆(仅事件触发) memory_store.save(event_from_task(request, response)) return response

这套逻辑看起来简单,但每个函数内部都有一层工程细节。比如summarizer.squash不能把执行日志压成一句话,而要压成"任务项清单+每项结论"的结构,否则模型最后生成报告时缺少细节支撑。我在这块调了很久,最终固定输出格式为:每个任务项保留"名称/关键输出/状态/来源ID",总共限制在800token以内。

5.4 改造前后的成本对比

给一个具体的数字参考。我拿"调研10个竞品官网并输出对比表"这个任务做过一轮对比测试,模拟50次运行:

  • 改造前:全链ReAct + 每轮全量注入记忆。平均每次任务消耗输入token约4万,输出token约8000,输入成本按当前主流接口的中档价位估算约0.04元/千token,单次成本约1.6元(输入)+0.5元(输出),合计约2.1元/次。50次约105元。
  • 改造后:分层记忆 + 规划-执行 + 工具结果压缩 + 缓存。平均每次任务消耗输入token约1.2万,输出token约7000,加上缓存折扣,单次成本降到约0.7元/次,50次约35元。

这个对比不是严谨的基准测试,但趋势很明显:改造后单次成本大约降了三分之二,任务质量反而更稳定,因为模型始终在"够用"的上下文里工作,不会因为上下文太乱而跑偏。

6. 反复踩坑后的调优记录

6.1 记忆检索太积极,成本不降反升

第一次做分层记忆时,我让系统在每一步执行前都检索一遍记忆。出发点是好的:确保模型每一步都有足够上下文。结果每一步都带来额外的检索请求token和注入token,整个任务的请求量翻了三四倍,成本比改造前还高。

后来改成"只在两个时机检索记忆":任务开始时检索一次,任务方向发生重大变化时再检索一次。多数任务全程只需要一次记忆检索就够了。这个改动让检索调用量下降了80%,成本自然跟着回落。问题根源是"检索频率"没有和"记忆增量"匹配——每一步执行时,模型需要的是上一步的工具结果,而不是再翻一遍历史。

6.2 摘要记忆会"板结",关键细节被压丢

长期记忆用摘要方式存储,最怕的就是摘要过于概括。举个例子:用户明确说过"这个项目的预算是3万封顶,超出部分需要额外审批",如果摘要里只留"用户对预算有要求,项目预算有限",那下次任务调用时模型就不知道具体数字,可能导致做方案时给出一个超预算的建议。

解决办法是:摘要只能压缩语义,不能压缩数字、日期、名称、条件。我在记忆写入时加了一个"关键字段保护"机制——凡是识别为数字、价格、日期、实体名称、布尔条件的片段,都必须原样保留在结构化字段里,不进摘要压缩。这样即使上下文再紧,这些硬信息也不会丢。

6.3 工具结果压缩得太狠,错误信息被压没了

有一次任务执行失败,模型却没有报错,而是编了一个"结果正常"的结论。查了很久才发现问题出在压缩器上:工具返回的一段错误提示因为不是任务需要的字段,被压缩器过滤掉了。模型根本没看到错误,自然以为工具调用成功了。

从那以后,我把压缩器改成白名单模式:默认保留工具返回的"状态、错误码、错误信息、数据来源、数量统计"这几类元信息,再按需提取业务字段。宁可多留几个token,也不能把失败信号吞掉。这是本轮调优中最重要的一次修整。

6.4 缓存命中率不高,先查前缀稳定性

缓存省钱的逻辑很简单,但实际效果往往取决于前缀的稳定性。我曾经把每次请求都会变化的"当前时间"写进了系统提示词里,结果缓存命中率直接归零,因为前缀每秒钟都在变。后来把所有动态信息统一移出前缀,固定在请求尾部,缓存才重新生效。

另外要注意,缓存一般都有有效期,可能是几分钟到几小时。长任务执行间隔超过有效期的,缓存会自动失效,属于正常现象。所以设计时尽量让一个长任务内的各步执行保持连续,中间不要穿插太长的等待时间,否则缓存帮不上忙。


这套方案我从最初的"记住一切+现调现用"改成"分层记忆+规划执行+压缩复用",整个过程里印象最深的不是哪个算法多精妙,而是很多问题出在"上下文里放了不该放的东西":记忆、工具结果、错误信息、中间推理,这些都有各自的去留标准。现在回头看,长期记忆和长程工具调用从来不是二选一,关键是让记忆按需出现、让工具调用轻装前进,成本自然也就跟着下来了。如果你的Agent也卡在类似的地方,建议先从"每一步上下文里到底多了哪些没用的东西"查起,往往能找到突破口。

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

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

立即咨询