ARTICLE DETAIL

资讯详情

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

Agent长期记忆与长程工具调用的上下文优化实战

Agent长期记忆与长程工具调用的上下文优化实战 最近我在做一个能自动查资料、调内部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 最终采用的模块划分与数据流我把这套系统整理成下面几个模块它们之间的数据流就是一次完整任务的生命周期用户请求进入Orchestrator调度器调度器先调用记忆检索模块拿到当前任务需要的记忆包带着记忆包Planner规划器生成任务清单Executor执行器按清单逐项执行调用工具 - 压缩结果 - 判断是否完成 - 进入下一项每完成一个任务项写入执行日志执行日志累积到阈值时触发Summarizer摘要器压缩一次全部任务项完成后Writer生成器基于执行日志和记忆包生成最终报告报告完成后记忆写入模块按事件触发规则把本次任务的关键结论写入长期记忆库。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_tokens600) # 2. 规划 plan planner.generate_plan(request, memory_pack) # 3. 执行循环 exec_log [] for step in plan: result tool_call(step) compact_result compressor.extract(result, fieldsstep.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也卡在类似的地方建议先从每一步上下文里到底多了哪些没用的东西查起往往能找到突破口。
返回列表