跳到主要内容

记住你 Bug 的智能体:为什么修复 Bug 是一次内存失效事件

· 阅读需 11 分钟
Tian Pan
Software Engineer

几个月前,你的一个下游 API 返回了一个格式错误的时间戳——在应当显示毫秒的地方返回了秒,或者在 Schema 承诺返回字符串的地方返回了 null。你的智能体(agent)遇到了这个问题,分析了故障原因,并制定了一个修复方案:乘以 1000,或者回退到默认值,或者使用不同的端点重试。它解决了眼前的麻烦。然后,它做了一件产生深远影响的事:它记下了这个变通方案(workaround)。

也许它在长期记忆中保存了一条笔记:“计费 API 返回的时间戳单位是秒;使用前需转换。” 也许这次交互被卷入了一个微调(fine-tuning)数据集,于是这个变通方案变成了一个习得的反射行为。无论哪种方式,智能体现在都对世界产生了一种认知。而就在上周,API 团队发布了一个修复补丁。现在时间戳正确了。但没人告诉这个智能体。

于是智能体继续执行乘法。它接收到一个正确的毫秒时间戳,忠实地应用其记忆中的修正方案,结果产生了一个错误千倍的值。Bug 修复了,但变通方案没有。智能体正在做一件你希望勤勉系统做的事情——记住它学到的东西并前后一致地应用它——而这恰恰就是它出错的原因。

智能体记忆是派生状态,而派生状态会过期

理解这一点的最清晰方式借用了系统工程的概念:智能体记忆是一个缓存(cache)。这不只是一个隐喻,在结构上也是如此。缓存是某个权威源(authoritative source)的快速副本,保留它是为了让你不必重新计算或重新获取。智能体记忆也是一样的。权威源是 API 的实际行为、数据形状和产品规则。记忆是一个派生副本:“这就是我学到的关于该系统如何运作的知识。”

每个工程师都知道,计算机科学中第二难的问题是缓存失效(cache invalidation)。之所以难,并不是因为让一个条目过期在技术上有多复杂,而是因为缓存和源之间被一种看不见的依赖关系耦合在了一起。一旦源发生变化,缓存就成了一个骗子——它自信、沉默,没有错误也没有异常。缓存不知道世界已经改变,它只是在继续提供服务。

智能体记忆具有令缓存变得危险的所有属性,甚至还多了一项。传统的缓存至少缓存的是你可以重新推导出来的东西:数据库里的那一行依然存在。而智能体对变通方案的记忆缓存的是对 Bug 的推论——一旦你修复了 Bug,那个推论就不再是过时的数据,而是对一个已不存在的事物的错误认知。没有什么可以重新获取的,因为记忆所描述的那个格式错误的时间戳已经消失了。这段记忆现在指向的是一个已被删除的世界。

最近的研究开始精确衡量目前的系统在处理这类问题时表现得有多糟。STALE 基准测试将这种失败定义为“隐式冲突”——即后来的观察使之前的记忆失效,但却没有明确地否定它。新的正确时间戳并不会宣告“旧的行为已结束”。它只是出现,看起来像个正常值。要检测到它应该杀死一段旧记忆,需要智能体进行推理:这与我所相信的事情相矛盾;应该让步的是信念,而不是观察。 在 400 个经过专家验证的冲突场景中,即使是最强大的前沿模型也只有约 55% 的正确率。这基本上相当于抛硬币,取决于你的智能体是否能注意到自己的记忆已经过期。

修复是无人安排的内存失效事件

这是一个组织上的差距,使得这个问题比纯粹的建模问题更糟糕。当你修复时间戳 Bug 时,你会做很多事情:编写补丁、添加回归测试、更新 API 变更日志。你可能还会通知调用方团队。你关闭了工单。

但在整个工作流程中,你绝对不会问:哪些智能体已经记住了针对此问题的变通方案,这些记忆是否需要失效?

Bug 追踪器里没有这个字段。部署流水线中也没有这一步。修复被视为一件纯粹有益的事情——系统变得更好了——但对于每一个依赖于错误行为的派生信念来说,它也是一次破坏性事件。用缓存术语来说,你更新了记录源,却未发出失效信号。派生层继续提供旧的产物。

这与团队从主数据库删除一行却忘记搜索引擎索引中仍有该行属于同类错误。记录系统是正确的,派生索引现在却在撒谎。区别在于,搜索索引是你拥有的基础设施,你可以运行重新索引(reindex)任务。而智能体的记忆是一个弥散的、半结构化的自然语言信念库,“重新索引”并不是一个按钮。更糟糕的是,如果变通方案泄露到了微调数据中,它甚至不是一个你可以编辑的存储库——它被固定在了权重中,唯一的失效路径就是重新训练。

这种不对称性就是陷阱。写入智能体记忆是廉价的、自动的,并且是正常运行过程中的副作用。使智能体记忆失效则是昂贵的、手动的,只有在有人想起要考虑它时才会发生。记忆系统研究将智能体生命周期描述为“写入-管理-读取”循环,并指出几乎每个人都很好地实现了写入和读取,却完全忽略了管理。比 Bug 活得更久的变通方案,正是“管理”环节缺失的具象化。

为什么这些陈旧的记忆异常难以察觉

正常的陈旧缓存项往往会自我暴露。缓存的价格错误,客户投诉,你就能发现它。但变通方法(workaround)记忆更为隐蔽,原因有三。

它会静默且看似合理地失败。 偏移了 1000 倍的时间戳可能会落在一个仅仅是看起来很奇怪、而非明显损坏的日期上。针对一个现在已经健康的端点进行防御性重试,只会增加延迟。一个字段重命名的变通方法会将一个现在正确的字段映射到一个现在错误的字段上。这些都不会抛出异常。它们产生的输出虽然错误但格式正确,而格式正确的错误输出是最难被察觉的一种。

它在编写时是正确的。 你不能将其作为错误进行审计,因为它当时并不是错误。这种记忆是对 真实 缺陷的 正确 推论。这挫败了寻找错误决策的通常本能——当时并没有错误的决策,只有一个其过期日期在无人看管的情况下流逝了的决策。

它只在现已修复的路径上触发。 变通方法会保持休眠状态,直到智能体(Agent)遇到它专门针对的特定端点或数据形状。如果该路径很少见——比如每月账单运行、错误分支——陈旧的记忆可能会沉睡数周,然后针对完全健康的基础设施激活。当它触发时,使该记忆失效的修复工作早已成为陈旧的历史,没有人会将其与新的症状联系起来。

这种组合意味着变通方法记忆通常表现为 看起来很新鲜的事故。昨天还运行良好的智能体,今天在本身健康的系统上产生了错误的输出,且智能体近期没有任何改动。实际的根本原因——六周前发布的修复程序——远在调查窗口之外,以至于团队在有人说出“等等,API 是不是改了什么?”之前,会浪费数小时的时间。

将智能体记忆视为需要显式失效的缓存

如果记忆是一个缓存,那么修复方法就是分布式系统工程师已经在使用的那些。它们都不陌生。只是必须有意识地采用它们,因为智能体框架本身并不会强制你这样做。

为记忆标记来源和依据。 变通方法记忆不应只说“转换时间戳”。它应该记录 它为什么存在:哪个端点、观察到了什么行为、哪个日期、哪个版本。一个声明依据为“v2.3 版本的账单 API 返回了秒数”的记忆,你可以通过机械方式比对 v2.4 版本进行检查。而一个只说“转换时间戳”的记忆是一个孤儿信仰,与它所描述的世界没有任何联系。

让记忆可证伪,并重新测试它们。 每个变通方法都编码了一个预测:如果我发送这个输入,系统会以这种方式表现异常。这个预测是可测试的。定期地——或者在关键路径上依赖某个记忆之前——智能体可以探测该假设:该端点是否仍然返回秒数?一个不再针对当前行为触发的变通方法,就是一个应当退役的记忆。STALE 的作者将这描述为 检索 更新信息与根据信息 行动 之间的差距;显式的重新测试通过强制检查来弥补这一差距。

将修复作为失效触发器。 这是组织层面的工作,也是真正起作用的一半。缺陷跟踪器、变更日志、部署流水线——这些应该是失效的源头,因为这正是世界发生实际变化的地方。已解决漏洞的工作流应该包含一个像“你添加测试了吗?”一样常规的问题:是否有任何智能体记忆或训练数据编码了针对此行为的变通方法? 当答案为“是”时,修复程序会发出一个“墓碑(tombstone)”,就像一个构建良好的系统中的删除操作会向每个派生索引发出墓碑一样。智能体不必去 推断 它的记忆已过期——它会被 告知

对派生信念倾向于选择过期而非永久。 持久的记忆应该是门槛,而不是默认选项。在任务执行过程中发现的变通方法,正是那种应该是瞬态或短期的信念,除非有明确的操作将其提升。一个记忆被持有的信心越强、持续时间越长,它最终过期后的代价就越高。因此,记忆框架越来越多地支持衰减和有效期——一个附带“截至日期”和软过期的事实,远比一个永久断言的事实危险性小。

最深层的修复方案是文化上的。目前大多数团队都有一个单向模型:代码是事实来源,记忆是它的被动反映。但两者之间没有连线。修改代码,记忆并不会随之移动。在这一链接存在之前——在发布修复程序习惯性地询问它刚刚使哪些信念失效之前——你修复的每一个漏洞都会留下一个仍然记得它的智能体。你会让系统变得正确,却让智能体变得错误,而直到变通方法苏醒之前,你都不会发现这一点。

缓存失效一直是个难题。我们只是没想到缓存会是模型的记忆,也没想到失效事件竟然就是我们引以为傲地发布的修复程序本身。

References:Let's stay in touch and Follow me for more thoughts and updates