尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

聊天记录都在,模型为什么还是会“忘记”?

聊天记录都在,模型为什么还是会“忘记”? 从上下文投影、提示缓存到会话恢复拆解 Coding Agent 的三层记忆系统你可能遇到过这样的场景一个 Coding Agent 已经连续工作了几个小时界面里的聊天记录仍然可以一路向上滚动刚才读取过的文件、执行过的命令也都历历在目可当你继续追问时它却像突然失忆一样忘了早先确认过的结论。这通常不是模型“记忆退化”也不一定是历史记录丢失。更可能的原因是你在界面中看到的历史并不等于这一轮真正发送给模型的上下文。一旦接受这个前提许多看似矛盾的现象就能得到解释。为什么聊天记录完整模型却看不到其中一部分为什么系统宁可暂时多占一些 Token也不愿修改早先的一段文字为什么有些压缩可以找回原文有些压缩却会改变后续会话的起点为什么“恢复会话”远比重新读取一个日志文件复杂答案藏在一套容易被忽略的系统设计中成熟的 Agent 并不维护一份唯一的聊天历史而是在同时维护多种彼此关联、用途不同的历史视图。一段会话其实有三本账理解 Agent 上下文管理最重要的不是先研究某个压缩算法而是先区分三类数据。视图它回答的问题典型内容是否直接发给模型交互历史用户在界面上看到了什么用户消息、助手回复、工具调用与结果不一定请求投影模型这一轮实际看到了什么筛选、折叠、外置和摘要后的消息是逻辑执行链下一轮应该从哪里继续活动分支、压缩边界、父子关系、运行状态间接决定交互历史面向人目标是完整、可读、可追溯。请求投影面向模型目标是在有限窗口中保留最有价值的信息。逻辑执行链面向运行时目标是让会话能够分叉、回退、压缩、恢复并且在下一轮继续沿正确的时间线推进。这三者可以完全不同。界面里仍然可见的大段日志可能早已不在请求投影中请求投影里被替换成摘要的内容磁盘上可能一个字都没少磁盘中的所有事件虽然完整保存但当前逻辑链只会选择其中一个活动分支。因此评估任何“上下文压缩”机制时都应该先问三个问题它改动的是哪一层它在什么时候触发原始信息还能否恢复如果这三个问题没有说清楚“压缩”只是一个过于宽泛的词。真正的矛盾窗口预算与前缀缓存语言模型没有天然的跨请求记忆。每一轮推理都需要重新提交系统指令、对话消息、工具描述、文件内容和工具结果。假设模型窗口为 (W)实际可用于历史的预算并不是 (W) 本身而更接近[B_{history}W-R_{output}-R_{system}-R_{tools}-R_{safety}]其中输出预留、系统指令、工具定义和安全余量都必须提前扣除。对 Coding Agent 来说最容易挤爆窗口的往往不是自然语言对话而是搜索结果、构建日志、长文件、测试报告和批量工具返回值。直觉上的解决方案是不断删除旧内容但这里还有另一项成本提示缓存。许多推理服务会复用上一轮请求中相同的前缀。只要新请求开头与旧请求保持逐字节一致前缀对应的中间计算就可以复用。一旦在历史前部改动了一个字符后续缓存都有可能失效。于是系统面临一组方向相反的优化目标减少 Token 需要改写历史命中缓存却要求前缀尽量不变。一个成熟实现不会简单地选择“删”或“不删”而是根据缓存冷热、内容价值和窗口压力在不同层上采取不同动作。压缩不是一个动作而是一组投影策略将原始会话变成请求投影可以理解为一条逐级收缩的流水线。越靠前的策略越轻量、越可逆越靠后的策略影响越大也越可能改变逻辑执行链。阶段主要对象请求中的变化原文是否保留是否改变逻辑起点大结果外置单个超大工具结果全文变为预览与位置引用完整保留否细粒度回收过期工具结果旧结果变为占位或从投影移除通常保留否区间裁剪中间低价值历史指定消息不再进入请求保留否上下文折叠一段连续历史原消息变为摘要占位保留否全量压缩边界之前的历史历史被一条工作摘要替代保留是失败后压缩被服务端拒绝的请求压缩后重新尝试保留通常是大结果外置不要总结先把正文搬出去当工具一次返回数万行日志时最稳妥的处理并不是立刻总结因为总结会损失细节。更好的做法是把完整结果写入独立存储在请求投影中只保留一段预览、结果规模、内容类型和可再次读取的位置。这种方式本质上是一种无损间接寻址。模型先看到“这份结果是什么”只有在确实需要细节时才重新读取对应片段。它节省的是请求空间不牺牲磁盘上的可追溯性。外置后的占位内容一旦进入请求前缀后续轮次最好稳定复用同一份文本。反复调整预览措辞虽然可能再省几十个 Token却可能破坏一大段已命中的前缀缓存得不偿失。细粒度回收先清理最便宜的噪声随着任务推进最早的搜索输出、旧版本文件内容和已经排除的错误日志会逐渐失去价值。此时可以只回收工具结果而不总结整段会话。缓存已经变冷时直接重写本地请求投影通常代价较小缓存仍然很热时系统更倾向于维持本地前缀不变。如果推理后端支持缓存裁剪或缓存编辑也可以让服务端处理旧块从而避免客户端前缀发生变化。这类策略揭示了一个很实用的工程判断内容是否应该保留不只取决于语义价值还取决于它在缓存结构中的位置。一段已经没有业务价值的文本如果位于高复用前缀中短时间内仍可能值得保留。区间裁剪与折叠改变“怎么看”而不是修改过去有些排查过程很长但最终只得到一句有效结论。系统可以把这段历史折叠成简短摘要也可以根据消息标识精确排除一段已经确认无用的支线。关键在于持久化记录不必真的删除旧消息。系统只需要追加一条“投影提交”说明恢复和构建请求时应当如何解释过去的数据。下一次加载会话时再重放这些投影规则就能得到相同的请求视图。这种设计类似数据库中的事件溯源事实只追加当前状态通过重放事件计算得到。它既保留审计能力也让删除、折叠和撤销变得可重放、可测试。全量压缩摘要不仅省空间还会建立新边界当轻量回收已经不够上下文仍逼近硬上限时系统才需要进行全量压缩。它会把某个边界之前的历史总结为一份“工作状态摘要”其中通常包含当前目标、已经确认的事实、关键决策、未完成事项、重要文件与必要约束。下一轮请求不再携带边界之前的原始消息而是从摘要继续。原始记录仍保存在磁盘上但逻辑执行链已经建立了新的起点。换句话说旧历史是“仍然存在但默认不再参与推理”而不是被物理删除。为了降低摘要请求的计算成本一种常见优化是让摘要生成任务复用主会话的相同前缀和配置。这样虽然会额外生成一些输出 Token却可能复用大段已计算前缀。它再次体现了同一原则局部多花一点输出可能换来更大的输入复用收益。失败后压缩它是保险丝不是常规路径客户端对 Token 数量的估计不可能永远精确。序列化差异、工具协议开销和服务端计数规则都可能导致一个看似安全的请求被判定为超长。因此系统还需要一道失败恢复策略当服务端明确以“上下文过长”拒绝请求时执行一次压缩然后重新提交。这里必须设置严格的重试上限通常只允许一次。否则恢复过程自己生成的新消息可能继续推高上下文最终形成无限压缩、无限重试的回路。协议完整性比 Token 数更重要上下文不能在任意位置切开。一次工具调用与对应结果构成协议上的完整单元一条助手消息中的多个结构块也可能必须整体保留。如果压缩边界把调用和结果拆散模型收到的消息就可能违反接口约束。因此边界选择不是简单地“保留最近 N 个 Token”。系统需要先找到满足预算的候选位置再把边界向外移动到合法的结构边界。必要时宁可略微超过名义预算也不能制造一份语义或协议上不完整的请求。这是一条很重要的设计优先级协议完整性高于局部 Token 最优确定性高于一次性的极限压缩率。会话记录应当像事件日志而不是可变数组如果历史会被频繁折叠、裁剪、分叉和恢复把它存成一个不断原地修改的 JSON 数组会很快变得脆弱。更稳健的方式是采用只追加事件日志。一个简化后的记录可能包含如下事件{type:message_appended,id:m42,parent:m41,role:assistant}{type:tool_result_externalized,message:m42,artifact:logs/run-17.txt}{type:projection_committed,hidden:[m18,m19,m20]}{type:compaction_boundary_created,id:c3,parent:null,summary:...}{type:runtime_state_checkpointed,recent_files:[src/index.ts]}这里的重点不是字段名称而是数据模型消息通过父指针形成一张可分叉的图投影规则决定哪些节点进入本轮请求压缩边界切断默认回溯运行时检查点保存聊天文本之外的工作状态。在这种模型下会话恢复不是“读取全部消息并放回内存”而是一次确定性的重放过程defresume(session_id):eventsload_events(session_id)tipfind_active_branch_tip(events)chainwalk_parent_links(tip)chainstop_at_latest_compaction_boundary(chain)projectedreplay_projection_events(chain,events)repairedrepair_incomplete_protocol_units(projected)runtimerestore_runtime_state(events)returnSession(messagesrepaired,runtimeruntime)真正的实现还需要处理并行工具调用、回退后产生的分支、未完成的助手消息、孤立工具结果和中断写入。恢复器的职责不是把过去机械地照搬回来而是把一个可能在任意时刻中断的执行现场修复成可以安全继续的状态。Resume 与 Fork 不是同一种恢复继续原会话和从旧会话分叉看起来都需要加载历史但它们的所有权语义完全不同。行为ResumeFork会话标识沿用原 ID创建新 ID事件日志继续追加原日志写入新的日志活动分支接管原分支从选定节点创建新分支外置结果与投影规则原样恢复复制必要的解释规则目标回到原工作现场带着上下文开启新时间线Fork 时不能只复制聊天文本。假如旧会话曾把一段大型日志外置而新会话没有继承对应的外置映射请求投影就可能突然恢复成全文不仅窗口占用会变化提示缓存前缀也会改变。需要迁移的不是全部运行状态而是足以保证同一段历史得到同一解释的最小状态集合。为什么“消息恢复了”行为仍可能改变会话连续性不只由文字决定。Agent 最近读取过哪些文件、使用过哪些工具、选择了哪种工作模式、当前工作目录在哪里、是否位于独立工作树中这些状态都会影响下一轮行为。如果恢复器只重建聊天消息界面看起来可能完全正常模型接手的工作现场却已经变化。更准确地说Resume 是一次运行时迁移而不只是一次文件读取。这一点也解释了很多“恢复后突然忘记”的问题。故障可能不在摘要质量而在其他层投影事件没有重放活动分支找错了压缩边界丢失了外置结果路径失效了或者运行状态没有恢复。只有把问题定位到具体层才能避免把所有异常都归咎于模型。三条值得复用的系统原则前缀决策必须可重复同一份持久化记录在相同配置下应当产生字节级稳定的请求前缀。占位文本、摘要边界和投影顺序都应确定化否则即使语义相同缓存也会因细微差异而失效。结构完整性优先于极限压缩工具调用与结果不能被拆开消息内部的关联片段不能被任意切断。预算控制应该服从协议合法性而不是反过来。磁盘只追加逻辑视图可重建物理记录描述“发生了什么”投影事件描述“现在应该如何看待过去”。压缩和删除尽量表现为新事件而不是回头篡改旧数据。这样才能同时获得可恢复性、审计能力和确定性。结语Agent 的记忆本质上是一次受控重建我们常把上下文窗口想象成模型的短期记忆把磁盘记录想象成长期记忆。但对一个能够读文件、运行命令、并行调用工具并恢复工作的 Agent 来说这个类比还不够准确。它真正维护的是一套多层状态系统交互历史负责向人解释过去请求投影负责在有限预算内组织当下逻辑执行链负责决定未来从哪里继续事件日志负责让这一切可以被重放。所谓“记住”不是把所有内容永远塞进窗口所谓“恢复”也不是把聊天记录重新显示出来。更准确的定义是在资源受限的前提下让系统能够稳定地重建下一步行动所需的现场。下次再看到“上下文压缩”时不妨先问它压缩了哪一层下次再遇到“恢复后失忆”时也不要只检查聊天记录。真正决定 Agent 能否继续工作的往往是那套用户看不见、却一直在重建现场的投影与运行时系统。说明本文讨论的是成熟 Coding Agent 可采用的通用架构抽象。不同产品与版本的具体命名、阈值和实现路径可能不同。
返回列表