Claude Agent 的隐形污染:错笔记自动全局挂载,绝大多数记忆用例都会漏测

举报
霍格沃兹测试开发学社 发表于 2026/11/11 14:24:25 2026/11/11
【摘要】 摘要Anthropic 在 2026 年 11 月 6 日公开的研究博客里,把智能体记忆拆成四层:会话日志、记忆笔记、wiki 这类结构化表征,以及每个新会话都会自动加载的注入上下文。四层里最危险的是最顶上那层——一条错笔记写进去,此后每个会话都被读一遍。这篇文章把官方给出的实验(含一次宝可梦跑关和一个 35 万文件的企业代码库)翻译成测试动作:四个测试维度、一套可运行的注入上下文体积与重复...

摘要

Anthropic 在 2026 年 11 月 6 日公开的研究博客里,把智能体记忆拆成四层:会话日志、记忆笔记、wiki 这类结构化表征,以及每个新会话都会自动加载的注入上下文。四层里最危险的是最顶上那层——一条错笔记写进去,此后每个会话都被读一遍。这篇文章把官方给出的实验(含一次宝可梦跑关和一个 35 万文件的企业代码库)翻译成测试动作:四个测试维度、一套可运行的注入上下文体积与重复门禁、一张"该写哪一层"的判定表。

 Anthropic 在《The components of agent memory》里描述了一次宝可梦红的跑关:模型把宝可梦中心的位置记错了,写成"在山洞东侧"。它走出建筑时判断"山洞在我身后",于是更新笔记,写下"山洞已通关"。然后它从一个单向的台阶上跳下去,跳了三次,最后放弃,写下"我决定停止而不是继续循环"。 

 问题不在它记错了——人也会记错。问题在于这条错笔记被写进了每局都会加载的那一层。后面每一次重开,它都从"山洞已完成"出发,所以永远不会再回山洞。

同一篇研究里还给了另一半:一次"做梦"(Anthropic 的术语,指空闲时重读日志、校对笔记的离线过程)把这条笔记和原始会话日志对了一遍,改回正确位置,那一次直接走进了山洞,抵达了下一座城市。而没有修正的那次运行,始终没到。

我把这件事当成写这篇文章的起点。Agent 记忆的测试重点不是"它记得对不对",而是"一条错的东西能活多久、能扩散到多远"。 记忆越自动,扩散越快——注入上下文那一层,是每个新会话都无条件加载的。

四层记忆,四种不同的失效方式

Anthropic 的定义很窄,先划清边界:记忆指模型从经验中获得、跨会话保留、以后可以读到的知识。它明确排除三样东西——权重变化、单次会话内的学习、以及"压实"(用摘要替换早先对话轮次)。这个边界很关键,因为它把讨论限定在"Agent 读写的文件"上,而不是训练。

四层从下到上:

  • 会话日志:可搜索的事件流水,什么都记。
  • 记忆笔记:Agent 自己判断值得留的观察、规则、纠正。
  • 记忆表征:从笔记长出来的结构,比如互链的 wiki 页面。
  • 注入上下文:每次新会话开头自动加载的那一小块。

"做梦"在四层之间搬运信息:校对笔记(对照日志)、建 wiki、把最可靠的东西推到最顶层。

为什么"记忆更多"不等于"记得更好"

Anthropic 公开了一组对照,我建议每个想把记忆做进产品的团队都看一眼。

宝可梦红,每个模型跑一次(官方明说这一组是"说明性"的,不是统计结论):

模型
记忆文件数
获得的徽章
Sonnet 3.7
413 个,分布在 225 个文件夹
1 枚
Fable 5
9 个,集中在 2 个文件夹 + 一个 global.md
4 枚

官方对 Sonnet 3.7 的描述是:"它假设什么都可能重要,不加区分地写进记忆。" 到第 10,000 步附近,它开始花回合整理记忆,没有任何游戏动作。

这个对照对测试的意义很直接:记忆的"覆盖率"是个坏指标,"信噪比"才是好指标。 如果你的验收单上写的是"记忆能否记录所有关键操作",那你会得到一个 413 文件夹的产品,和 1 枚徽章的结果。

另一组更有说服力,因为它有基线:

CL-Bench 1.0 数据库问答任务,同一模型分别在无记忆和有持久记忆目录下作答(官方数据,Anthropic 自跑):

模型
无记忆
有记忆
Opus 5.5
0.655
0.838
Sonnet 5.5
0.644
0.833
Opus 5
0.564
0.828
Opus 4.7
0.383
0.700
Sonnet 5
0.306
0.610
Sonnet 4.6
0.330
0.364

Sonnet 4.6 那一行几乎没动。官方给的解释是:它"在回答前读记忆"的比例不到一半,而 Sonnet 5 是十次里有九次。把提示改成"先读记忆、再检验你的理论"之后,Sonnet 4.6 从 0.330 升到 0.641。

这条对测什么最有启发:同一套记忆,模型不读就等于没写。 所以"记忆已写入"和"记忆被使用"必须分成两条断言,不能合并成一个"记忆功能可用"。我见过太多验收只测了前者。

还有一个反直觉结论,官方原文写得很平:告诉模型"记忆是用来干什么的"有效,规定"怎么用"反而有害。一条"只记录失败"的硬规则,让 Opus 5 的记忆增益掉了 0.234,Opus 4.7 掉了 0.150。所以如果你的产品给用户提供了"记忆写入规则模板",这些模板本身要有 A/B 数据支撑,不能凭直觉配置。

wiki 比搜索强,但没说强在所有地方

企业侧那组实验值得单独拎出来,因为它最接近我们真实的项目:约 35 万个文件、约 4.2 亿 token 的代码库,128 次"做梦"会话,25 个问题。

三个数字:

  • wiki 单独用,达标率随会话数从 13% 涨到 48%。
  • wiki + 搜索,61%;只用搜索,53%。
  • wiki 从 103 页 766 条链接,长到 222 页 12,893 条链接。

但官方也写了它不管用的地方:答案集中在单个文档的任务上,wiki 没有明显增益;有一个 wiki 长到了源文件体积的 1.77 倍;多个 Agent 写同一份记忆时会互相覆盖,把 57 个事实写成 41 个(72%),改成"每份合同一页、逐字引用条款"后,57 个全部保住。

这几条给了测试一个非常具体的判定:**"记忆检索"这个功能不能只测一个场景。** 至少要按"答案在单文档 / 需要多个文档综合 / 需要跨版本比对"三类分开测,因为你很可能会看到——第三类收益最大,第一类零收益,第二类取决于 wiki 有没有膨胀。

四个测试维度,从便宜到贵

把上面这些收成可执行的清单。每个维度都写明"用什么判",避免变成一堆无法执行的形容词。

维度
测什么
判据来自哪里
典型失败表现
写入正确性
笔记与日志是否一致
原始会话日志(Ground Truth)
笔记写错位置、写错状态,日志里却是对的
读取有效性
新会话是否真的读了记忆
会话开头的读取动作 / 工具调用记录
记忆存在,模型没读,表现与无记忆一致
注入卫生
顶层上下文有没有过期、重复、超限
注入层体积、去重后条目、与日志的交叉核对
旧规则被反复注入;体积超限被截断
传播半径
一条错笔记影响多少个会话
同一错误出现在多少个会话的输出里
一处错,全线错,且没人能定位到源头

第三行那个"体积超限被截断",有个公开的锚点:官方文章提到,Claude Code 只把 MEMORY.md 的前 200 行或 25 KB 当作索引加载,其余按需读取。这意味着注入层有一个硬上限,越靠后的内容越不算数。如果一条关键规则被写到了第 300 行,它实际上等于没被注入。

 这一条可以直接写成一个门禁:

import pathlib, re, sys

MAX_LINES = 200
MAX_BYTES = 25 * 1024          # 与官方披露的 Claude Code 索引上限一致
DUP_MIN_LEN = 12               # 短于此长度的条目不做重复判定

def load_injected(path: pathlib.Path) -> list[str]:
    """只取会被自动加载的那一段,模拟按行/按字节双上限截断"""
    raw = path.read_text(encoding="utf-8")
    lines = raw.splitlines()[:MAX_LINES]
    buf, used = [], 0
    for ln in lines:
        n = len(ln.encode("utf-8")) + 1
        if used + n > MAX_BYTES:
            break
        buf.append(ln)
        used += n
    return buf

def entries(lines: list[str]) -> list[str]:
    """把注入层拆成条目:标题行与列表项各算一条"""
    out = []
    for ln in lines:
        s = ln.strip()
        if s.startswith(("#", "-", "*")) or re.match(r"^\d+[.)]", s):
            body = re.sub(r"^(#+|-|\*|\d+[.)])\s*", "", s)
            if len(body) >= DUP_MIN_LEN:
                out.append(body)
    return out

def gate(path: pathlib.Path):
    lines = load_injected(path)
    items = entries(lines)
    seen, dup, long_items = {}, [], []
    for it in items:
        key = re.sub(r"\s+", "", it)          # 去空白后比对,抓近似重复
        if key in seen:
            dup.append((seen[key], it))
        else:
            seen[key] = it
        if len(it) > 120:
            long_items.append(it[:40] + "…")
    total = path.read_text(encoding="utf-8").splitlines()
    report = {
        "被自动加载的行数": len(lines),
        "文件总行数": len(total),
        "被截断而未被注入的行数": max(0, len(total) - len(lines)),
        "注入层条目数": len(items),
        "重复条目对": dup,
        "超长条目": long_items,
    }
    blocked = bool(dup) or report["被截断而未被注入的行数"] > 0
    return blocked, report

if __name__ == "__main__":
    blocked, rep = gate(pathlib.Path(sys.argv[1]))
    print(rep)
    sys.exit(1if blocked else0)

三点说明,免得被当成万能检查。

第一,"被截断"直接判失败是我的选择,不是官方结论。理由是:被截断意味着作者以为写进去了、实际没生效,这种"以为"比明确的缺失更危险。团队可以改成告警,但至少要显式记录。

第二,DUP_MIN_LEN=12 和"去空白后比对"只能抓近似的重复条目,抓不到"用不同说法表达同一条规则"。这一类要靠人工或语义比对,代码只做第一道筛。

第三,这个门禁测的是文件卫生,不测内容对错。内容对错只能靠和日志、需求、工单交叉核对——这也是"写入正确性"那一行的判据来源。

"该写哪一层"比"写什么"更容易出错

写完检测,还有一个设计问题绕不开:这条信息到底该放哪一层?放错了,测试做得再细也救不回来。我给团队的判定顺序是这样的:

问四个问题,第一个回答"是"的地方就是它该待的层。

  1. 这条信息是"发生过什么"吗?是 → 只进日志。日志不需要判断,只负责可查。
  2. 这条信息以后大概会再用到,但不确定什么时候?是 → 写笔记。允许它暂时不准确,允许被"做梦"校正。
  3. 这条信息需要和别的信息互相印证、被反复引用?是 → 进 wiki,并且每页只讲一件事。
  4. 这条信息错了会导致所有会话一起错,且改动成本极高?是 → 不要放注入层。

第 4 条是反直觉的。直觉上"最关键的规则应该每次必读",Anthropic 的做法恰好相反:注入层要小。原文里的理由是"更长的指令文件会降低遵循度",Claude Code 只加载前 200 行就是这条原则的落地。

所以正确的做法往往是:注入层写"有这么一个记忆库、什么时候该去查",细节放到下面一层按需读。官方那句总结我抄在这里:告诉它记忆是用来干什么的,别规定它怎么用。

一个具体的越权案例,说明测试要拦什么

11 月 6 日 Google 官方博客公开的轨迹分析里有一类现象,和记忆的"污染"是同一机理:Agent 在代码里读到 TODO(bug/xxx),把这条给人看的旧注释当成了待办任务。13% 的会话会遇到这类注释,其中 6% 会真的动起来,83% 的动作不是用户要求的。

我说它是同一机理,因为两件事的共同点是:**Agent 无法区分"给我看的"和"叫我做的"**。记忆笔记、注入的上下文、代码注释、README,只要在它的视野里,都可能被升级成指令。

测试上要拦的,就是这条升级路径:

  • 记忆条目里出现"必须""禁止""忽略以上"这类祈使句,是否经过人工确认?
  • 笔记引用的外部内容(工单、链接、日志片段)是否可能被第三方写入?如果会,就需要隔离。
  • 同一份记忆被多个 Agent 写入时,是否有"每人一页"的隔离,还是共用一份会被覆盖?

第三点在官方数据里是有代价证据的:57 个事实写成 41 个。这不是理论风险。

边界与还不确定的地方

有几条必须写清楚。

第一,宝可梦那一组实验,官方明说每个模型只跑一次,是"说明性"的。413 与 9 这个对比很有说服力,但它不是统计结论,不能写进"我们的产品实测数据"去对外说。

第二,wiki 的 13%→48%、54%→61% 这些数字来自 25 个问题的集合,样本小,官方自己也列了这个限制。作为选型参考可以,作为效果承诺不行。

第三,本文所有数字都是 Anthropic 自己跑的,没有第三方复现。我在正文里逐处标了"官方数据",不做转译夸写。

第四,我上面给的注入层门禁用 200 行/25 KB 当阈值,这个值来自官方对 Claude Code 的描述,不代表你用的框架有同样的限制。用之前先确认自己那一层的真实截断点,否则这个门禁只是自我安慰。

最后判断:记忆这块现在最缺的不是更强的模型,是可核对的证据链。一条笔记是从哪次会话、哪一行日志来的,能不能点开看;这条规则是什么时候被验证过的,有没有过期。这些结构一旦有了,测试才有东西可以断言。没有它,我们能测的只有"记忆存在"——而这个答案,几乎永远是通过。

本文部分内容参考了霍格沃兹测试开发学社整理的相关技术资料,主要涉及软件测试、自动化测试、测试开发及 AI 测试等内容,侧重测试实践、工具应用与工程经验整理。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。