Agent 记忆越滚越乱:会话内、任务内、跨任务这三层不能混着存

2026-07-29

数据截至 2026-07,各产品的额度与报错口径以官方最新说明为准。

Agent 跑到一半开始犯浑,绝大多数人第一反应是换模型或者改提示词,这个归因通常是错的。真正的病灶在于:会话内的对话流、任务内的工作状态、跨任务的长期偏好,这三样东西性质完全不同,却被拼成同一个大字符串每轮送进模型。它们的生命周期不同、可信度不同、更新方式也不同,混在一起之后你既没法单独失效某一层,也没法判断模型是”忘了”还是”记错了”。

站内已经有两篇相邻的文章:Agent 记忆是什么 讲的是记忆这件事的整体图景和常见实现路子,属于总述;Agent 的上下文怎么管 讲的是单轮请求里那个窗口该装什么、怎么裁。这篇不重复它们——它只解决一个具体问题:当你已经知道要做记忆、也知道窗口要裁,但系统仍然出现”记忆污染”和”莫名失忆”时,怎么按层定位、按层动手。换句话说,前两篇是”是什么”和”一轮怎么装”,这篇是”三层怎么分、坏了怎么修”。

一、先划清三层:各存什么,谁负责失效

分层的意义不在于名字好听,而在于每一层有各自明确的写入者、读取时机和失效条件。这三样定不下来,分层就是摆设。

第一层:会话内记忆(conversation)。 装的是当前这段对话的原始流水——用户说了什么、模型回了什么、调了哪个工具、工具返回了什么。特点是原始、按时间排列、只增不改,失效条件是会话结束或被裁剪滚掉。它不该承担”结论”的角色,因为里面同时躺着已经被推翻的中间猜测。你从这一层读到”数据库端口是 5432”,并不知道这句话是三分钟前用户确认的,还是二十轮前模型自己猜的。

第二层:任务内记忆(task state)。 装的是为完成当前这个任务而积累的结构化工作状态:目标是什么、已完成哪几步、当前待办、已确认的关键事实、已排除的方案、产出物路径。它的特点是被覆盖式更新、有结构、可校验。失效条件是任务结束或被显式重置。这一层是最容易被漏掉的一层,很多实现里它根本不存在——所有状态都靠模型自己从对话流水里”重新读一遍推出来”,于是每多十轮,推错的概率就上一个台阶。

第三层:跨任务记忆(long-term)。 装的是跨越单个任务仍然成立的东西:这个仓库的构建命令、团队的代码风格约定、用户偏好用中文回复、某个接口的鉴权方式。特点是沉淀慢、写入要有门槛、读取要按相关性检索而不是全量注入。失效条件是显式修订或过期复核。这一层最危险的不是丢,而是——一条早已废弃的约定被写进去之后,会在之后所有任务里持续误导。

三层的关键分界线是一句话:会话内是”发生过什么”,任务内是”现在是什么状态”,跨任务是”一贯如此的规矩”。 一条信息要往哪层放,先问它属于哪一句。

二、症状对照表:从现象倒推是哪一层坏了

排查的第一步不是改代码,是把现象对上层。下面这张表按我实际调 Agent 时的高频顺序排列,从上往下试。

现象大概率成因怎么验证处置动作
跑到中后期开始重复调用刚调过的工具任务内层缺失,已完成步骤只存在于对话流水里,被裁掉后模型无从判断把某一轮送出的完整内容 dump 到文件,检查里面有没有一份显式的”已完成步骤”清单建立独立的任务状态对象,每步结束后覆盖写入,并在每轮请求里完整带上
模型坚持一个早被否定的结论会话内层被当成结论层用,中间猜测和最终确认没有区分在流水里搜这个结论第一次出现的位置,看它是模型自述还是工具/用户确认已确认事实单独进任务内层的 facts 字段,裁剪时优先保留该字段而非原始流水
换了个任务,模型还在用上个任务的项目路径或技术栈任务内层没有随任务边界重置,被误当长期记忆持久化新开一个任务,打印注入内容,看有没有上个任务的产出物路径给任务状态加任务 ID,切任务即丢弃;只有显式提炼的条目才允许升入跨任务层
模型执行的规矩跟当前仓库对不上跨任务层脏了:旧约定没失效,或多个项目的规矩混存直接打开长期记忆的存储文件逐条读,按仓库/项目维度分组核对长期记忆按作用域分片存储,注入前先按当前工作目录过滤
长对话后期”忘了”最初的目标会话内层裁剪策略把最早几轮滚掉了,而目标只写在第一轮检查裁剪逻辑是否保护首轮;对比裁剪前后的注入内容目标不放在对话流水里,提炼成任务内层的固定字段,永不参与裁剪
同一个事实在不同轮次被说成两个值三层都存了这个事实且没有单一来源把两个值分别全文搜一遍(含各层存储文件和当轮注入内容),看它们各自落在哪一层定一个单一可信来源,其余位置改为引用而非拷贝
重启进程后状态全丢任务内层只在内存里,没有落盘杀掉进程重跑,看能否续上任务状态序列化到磁盘,按任务 ID 命名,启动时按 ID 恢复

用这张表有个前提要先排除:如果现象是输出整体变差而不只是记性问题,那先按模型侧排查。记忆分层解决的是”记什么”,解决不了”想得对不对”。

三、按层动手:三种完全不同的处置手法

定位到层之后,动作不能一套模板走天下。三层的技术手法差别很大。

会话内层:只压体量,不改结论

这一层唯一该做的事是在保真的前提下控制体量。常见做法是保留最近 N 轮完整内容,更早的内容折叠成摘要,同时无条件保留首轮的任务描述。这里说的”不做加工”指的是不在这一层做结论提炼——摘要只压缩篇幅、保留”谁在第几轮说过什么”的痕迹,不能把”模型猜测”和”用户确认”合并成一句结论性陈述。真要提炼结论,那是任务内层的活儿。折叠时有个容易犯的错:把工具返回的长结果也一并摘要掉,结果模型丢失了唯一的原始证据。更稳的做法是长工具结果落盘,流水里只留一行路径和摘要,模型需要时再读回来。

关于窗口内每一类内容怎么排、怎么裁,Agent 的上下文怎么管 那篇讲得更细,这里不重复。你也可以先看看 多轮对话遗忘 里的现象归类,判断自己碰到的是不是纯裁剪问题。

任务内层:结构化、覆盖式、每轮全量注入

这一层建议直接用一个显式的 JSON 对象,字段固定,每步结束后由代码(不是由模型自由发挥)更新。一个够用的最小结构:

{
  "task_id": "20260729-refactor-auth",
  "goal": "把鉴权中间件从会话改成无状态令牌,不改对外接口",
  "confirmed_facts": [
    "现有中间件在 src/middleware/auth.ts",
    "线上仍有旧客户端依赖旧接口,不能删除路由"
  ],
  "done": ["读完现有实现", "补齐单元测试基线"],
  "todo": ["实现令牌签发", "灰度开关"],
  "ruled_out": ["改用第三方托管鉴权:合规评估没过"],
  "artifacts": ["docs/auth-migration.md"]
}

四个要点:

  1. 每轮请求把整个对象原样注入,不做裁剪。它本来就小,而且它是唯一能对抗”越跑越糊”的锚。
  2. ruled_out 字段常被忽略但极其值钱。 没有它,模型会反复提议同一个已经被否掉的方案,你得反复解释,纯浪费轮次。
  3. 写入权收在代码里。 让模型输出结构化的状态更新,由你的程序校验后合并——而不是让模型直接改一整份状态文本。前者出错会被 schema 挡住,后者出错就是静默污染。
  4. 落盘。 一个任务一个文件,进程重启能续上。序列化时统一用 UTF-8,Windows 上尤其容易在这里踩编码的坑。

跨任务层:写入设门槛,读取按作用域

这一层的核心原则是宁缺毋滥。每多一条,未来所有任务都要为它付出注入成本和被误导的风险。

写入门槛我一般定三条:这条信息在至少两个不同任务里都被用到过;它不带时间戳也仍然成立;它能一句话说清且可验证。三条不全满足的,留在任务内层就行。

读取上不要全量注入。按当前工作目录、当前技术栈、当前任务类型做过滤,注入前打印一次实际选中的条目——这一步很多人跳过,然后花几个小时怀疑模型,最后发现注入的是另一个项目的约定。

存储上按作用域分文件是最省事的做法,比如:

memory/
  global.md          # 跨项目都成立的偏好
  repo/<repo-name>.md  # 单仓库约定
  user/<user-id>.md    # 个人偏好

分文件的好处是清理的时候能整块删,不用在一个大文件里做外科手术。日常维护节奏可以参考 Agent 日常运维 里的巡检思路。

四、什么情况下别再折腾了

分层这件事有明确的边际收益递减点。下面几种情况,继续调记忆是白费力气,该止损。

止损点一:任务本身超过合理长度。 如果一个任务需要几十上百步才能收尾,问题不在记忆而在任务拆分。再精巧的记忆结构也扛不住这种长度带来的误差累积。正确动作是把它拆成若干个有明确交付物的子任务,每个子任务独立起一份任务状态,上一个的产出作为下一个的输入。这比继续优化压缩算法划算得多。

止损点二:同一个错误连续修三次仍复现。 这基本说明你在治症状不治因。停手,把某一轮实际送出的完整内容原样 dump 到文件里,从头读一遍。我见过的大部分”玄学记忆问题”,在真的读完一遍注入内容之后五分钟内就定位了。相反,不看注入内容而靠猜着改,可以耗掉一整天。

回滚点:改完记忆结构后行为反而更差。 这种情况比想象中常见——比如你加了摘要压缩,结果模型丢了关键细节。判断依据很简单:准备一组固定的回归任务,改动前后各跑一遍,成功率没升就回滚。没有这组基准,你的每次”优化”都是盲改。

# 改结构之前先打标签,方便整块回退
git tag memory-baseline-before-compaction
# 确认变差之后
git revert <commit>   # 保留历史;不要在共享分支上 reset --hard

换条路的判断依据: 如果你发现自己在实现一套向量检索、相关性排序、遗忘曲线的完整体系,而当前任务的状态其实几百字就能说清——那方向错了。绝大多数工程场景里,一份结构化的任务状态 JSON 加一份按作用域分片的长期记忆文件,效果好于一套复杂的检索式记忆,而且可调试性高出一个量级。复杂检索方案的适用前提是长期知识条目数量大到人已经读不完,在那之前它带来的不确定性大于收益。

顺带提一句:有些海外的记忆类工具和托管服务,官方对中国大陆是有区域限制的,不支持直连使用。市面上存在第三方中转,但稳定性和数据合规都要自己承担,这里不做任何推荐。做技术选型时把这一条算进可用性风险里,别等到上线才发现依赖不可控。

五、避坑清单:为什么会踩,怎么避

坑一:把长期记忆当草稿本,什么都往里写。 会踩是因为写入太顺手——模型说”我记住了”,一条就进去了,没有任何评审。三个月后这个文件里一半是过期信息,而它每轮都在注入。避法:写入必须过前面那三条门槛,并且给每条记上写入日期,定期把超过一定时长没被命中的条目移出去。宁可让模型重新问一次,也别让它按错的规矩执行。

坑二:让模型自己维护整份记忆文本。 会踩是因为省事:把当前记忆丢给模型,让它输出更新后的完整版本。问题在于模型在重写长文本时会静默改写、丢条目、合并语义相近的两条。避法:模型只输出增量操作(新增哪条、废弃哪条),由代码校验后写入;改写记忆的权限不交给模型。

坑三:三层各存一份同样的事实。 会踩是因为分层实现是逐步长出来的,加新层的时候没清理旧的。结果同一个事实三处有,改了一处另两处还在,模型看到互相矛盾的信息会选哪个完全不可预测。避法:每个事实定唯一来源,其他层只放引用键,不放值。落地时用一遍全文搜索就能查出重复。

坑四:裁剪策略把首轮目标滚掉。 会踩是因为最简单的裁剪就是”保留最近 N 条”,而目标恰恰在最早。避法:目标从来不该只活在对话流水里,它属于任务内层的固定字段,永不参与裁剪。

坑五:任务切换不重置任务内层。 会踩是因为任务边界在代码里往往没有显式表达——用户换个话题就算新任务了吗?避法:让任务边界显式化,用任务 ID 标记;ID 变了就整块换状态对象。宁可多起一个任务,也别让两个任务共用一份状态。

坑六:记忆里混进了敏感信息。 会踩是因为工具返回里带了令牌、连接串,被原样写进了持久化记忆,之后可能进日志、进仓库。避法:写入前统一过一遍脱敏,配置类信息只存引用名不存值;相关处置可参考 日志敏感信息

六、收个尾

三层记忆的价值不在于”多存点东西”,而在于让每一层都能被单独观察、单独失效、单独回滚。混成一个大字符串之后,你连”模型是没看到还是看错了”都分不清,所有排查都退化成猜。

下次 Agent 又开始犯浑,按这个顺序过一遍:

  1. 先把某一轮实际送出的完整内容 dump 到文件,用眼睛读一遍——不要跳过这步。
  2. 对照第二节的表,把现象归到具体某一层。
  3. 检查任务内层是否存在、是否结构化、是否每轮全量注入、是否落盘。
  4. 检查目标和已确认事实是否被裁剪保护。
  5. 打开长期记忆文件逐条读,按当前作用域核对有没有串项目。
  6. 确认同一个事实没有在多层各存一份。
  7. 改动前打好标签,改完用固定的回归任务比一次,没变好就回滚。

七条里前两条能解决大半问题,剩下的是让它不再复发。

想系统学会用 AI?报名体系课或加入会员,照着学、照着用。