SSD Nodes Learn Hosting plans →
指南 Matt Connor作者: Matt Connor · 更新于 2026-09-10

代理记忆投毒是什么?攻击原理与防护方法

提示注入会在会话结束时消失,记忆投毒却会持久化为可信事实。本文解释恶意文本如何写入代理记忆,并介绍隔离、过期与人工审核方法。

什么是代理记忆投毒

代理记忆投毒是一种攻击。攻击者将恶意文本写入代理的长期记忆,使代理在后续会话中将其检索为可信事实。提示注入只存在于一个上下文窗口中,窗口关闭后就会结束。记忆投毒不会结束,因为代理已经记录了攻击者的文本,并会在第二天将其读回。

这一区别会改变防护重点。提示注入是一次重启即可清除的事件。记忆投毒则会改变系统状态。它类似于数据库中的错误行:重启后仍然存在,下一个查询者仍会获取它,而后续会话对使用者而言看起来并无异常。

OWASP 在 2025 年 12 月发布的 代理应用 Top 10 中,将此问题列为 ASI06“记忆和上下文投毒”。目前最清晰的公开测量结果来自 MINJA,即 通过仅查询交互对 LLM 代理实施记忆注入攻击。该研究于 2025 年 3 月发布,并在 NeurIPS 2025 上进行介绍。下文关于攻击的论述来自这两个来源。后文的防护措施属于运维实践,适用于任何自行托管的存储系统。

为什么提示注入会结束,而记忆投毒不会

上下文窗口是每次运行期间的状态。模型在不同调用之间没有记忆,因此它在一次运行中知道的所有内容,都由您的代码放入其中:系统提示、工具输出以及当前为止的对话内容。运行结束后,该状态就会被丢弃。注入到抓取网页中的指令也会随之消失。针对编码代理的提示注入在运行期间很危险,运行结束后则无害,因此“启动新会话”在这种情况下确实是一种有效的缓解措施。

长期记忆的存在,正是为了有意打破这一特性。您可能希望代理记住该用户使用 Debian,或生产数据库只能从此主机读取。因此,记忆步骤会写入持久记录,检索步骤会在下一次运行中加载匹配的记录。如果您从未亲自编写过这一套写入后再检索的流程,那么实际构建一次,是理解它为何属于信任边界而不只是一个功能的最快方法;循序学习代理基础会在您手动编写循环后,再进入记忆阶段。

攻击者希望实现同样的特性,原因也相同:写入一次,读取多次。只要问题与恶意记录足够相似,该记录就会在每次检索中被加载;对于该存储服务的每个用户,它会在每个会话中持续生效,直到有人将其删除。

恶意文本如何变成已存储的事实

写入路径分为4个步骤。必须明确每一步,因为防护措施分别作用于不同步骤。

  1. 代理读取不受信任的内容:网页、支持工单、电子邮件正文,或克隆仓库中的 README。
  2. 记忆步骤决定哪些内容值得保留。在大多数设计中,这是另一次模型调用,用于将会话总结为简短的事实陈述。
  3. 提取出的句子被存储。典型的数据行包含文本、嵌入向量、时间戳,通常还包含用户 ID。
  4. 后续会话执行相似度搜索,并将排名靠前的匹配项粘贴到提示中,通常位于用户消息上方。

对于代码扫描代理而言,第1步属于日常工作:自托管的 open-kritt 扫描器会读取您指定的任何仓库,因此它可能记住的每条发现都受他人编写的文本影响。搜索会进一步扩大这一步的范围,因为为代理提供 SearXNG 搜索后端意味着到达提取器的页面由查询结果排名决定,而不是由您选择。

第2步是跨越信任边界的地方,因为提取器的输入包含不受信任的内容,而其输出却被视为代理自己的结论。第3步是损害变得持久的地方,因为提取过程会丢弃文本来源。用户输入的句子和从恶意页面提取的句子会以相同的形式存储:文本、向量、时间戳。完成写入后,数据行中没有任何字段能区分它们。

检索随后会完全按照其设计运行。它根据相似度而不是信任级别进行排序,并将排名靠前的结果交给模型,放入提示中用于存放既有上下文的区域。模型无法知道其中某一行是陌生人写的。

MINJA 论文测量了什么,未测量什么

MINJA 的贡献在于其威胁模型。攻击者从不接触数据库。他们向代理发送查询并读取回答,这正是共享代理的普通用户已经拥有的访问权限。恶意记录由代理自身的记忆步骤写入。

ChartMINJA published success rates by configuration, percent
The data behind this chart
[
  {
    "config": "EHRAgent GPT-4 MIMIC-III",
    "injection_success_pct": 95.6,
    "attack_success_pct": 57.0
  },
  {
    "config": "EHRAgent GPT-4 eICU",
    "injection_success_pct": 98.5,
    "attack_success_pct": 90.0
  },
  {
    "config": "RAP GPT-4 Webshop",
    "injection_success_pct": 96.3,
    "attack_success_pct": 77.4
  },
  {
    "config": "RAP GPT-4o Webshop",
    "injection_success_pct": 99.3,
    "attack_success_pct": 98.9
  },
  {
    "config": "QA GPT-4 MMLU",
    "injection_success_pct": 100.0,
    "attack_success_pct": 68.9
  },
  {
    "config": "QA GPT-4o MMLU",
    "injection_success_pct": 100.0,
    "attack_success_pct": 68.9
  },
  {
    "config": "Paper average",
    "injection_success_pct": 98.2,
    "attack_success_pct": 76.8
  }
]

请分别看这两个指标。注入成功率表示恶意记录是否成功写入记忆库。攻击成功率表示该记录随后是否让代理对后续的不同查询生成受其影响的回答。论文报告的前一个指标平均为 98.2%,后一个指标平均为 76.8%。将文本写入记忆几乎总能成功。但让这些文本改变后续回答并不可靠,而且不同代理之间差异很大:基于 GPT-4o 的检索增强购物代理达到 98.9%,而基于 MIMIC-III 的临床记录代理达到 57.0%。

这些已发布数据表示什么,不表示什么

这些数据来自一篇论文的实验平均值。实验使用 GPT-4 和 GPT-4o,覆盖三种代理设计和四个数据集。原始表格中的每个单元格都带有标准差,攻击成功率的波动范围很大。这些数据衡量的是论文中的系统,而不是您的系统。应将其视为查询级内存注入可在真实代理设计上奏效的证据,而不是您部署环境中的概率。

为什么一个用户的输入会变成另一个用户的记忆

多租户场景会将一个小问题变成安全漏洞。许多自托管部署让一个代理使用一个记忆存储,并通过每条记录上的元数据字段区分用户:例如 tenant_iduser_id,然后在查询时进行过滤。

这种设计通常会通过两种方式失效。

第一种是读取时缺少过滤条件。检索可能从多个代码路径调用:聊天处理程序、每夜运行的摘要任务,以及某人匆忙编写的评估脚本。如果其中一个路径忘记添加过滤条件,不会触发任何错误。系统只会返回更多相似度排序后的记录,而多出的记录属于其他租户。缺少过滤条件会导致系统默认放行。

第二种是写入问题。租户 A 的工单文本经过提取器处理后,生成的“事实”会使用代码当时设置的租户 ID 保存。如果摘要程序在共享任务中运行,或者该记录读起来像通用知识,因此被写成全局偏好,那么 A 的文本现在就会成为 B 检索到的上下文。读取过滤条件本身从未出错。错误在于记录被写到了边界的另一侧。

因此,只要攻击者能向任意一个租户的代理输入内容,就可以针对该存储服务的所有租户。检索路径不会检查记录由谁写入。

防护措施 1:每个信任边界使用一个独立存储

确定信任边界后,为每个边界分配独立存储。不要使用带过滤条件的单个集合。应使用独立数据库;至少也要使用独立 schema,并通过独立凭据访问。

原因在于每种故障的影响方向不同。遗漏过滤条件会返回其他用户的数据,而且通常不会报错。连接字符串错误则会返回空结果,你能在第1分钟内发现问题。能够以安全失败方式隔离故障,值得额外运行一个容器。

在大多数部署中,以下边界值得分开:

  • 多租户 agent 中的每个客户或团队。
  • 从公开内容派生的数据,与从自有用户输入内容派生的数据分开保存。
  • 共享同一主机时的每个 agent 角色,例如特权 agent 与面向公众的 agent。
  • 开发环境和生产环境分开,避免测试运行写入真实会话将读取的数据行。

如果必须使用共享表,应将检查下沉到数据库中,而不是依赖每个调用点。PostgreSQL 行级安全策略可以通过以下三个语句实现:

ALTER TABLE agent_memory ENABLE ROW LEVEL SECURITY;
ALTER TABLE agent_memory FORCE ROW LEVEL SECURITY;

CREATE POLICY tenant_isolation ON agent_memory
  USING (tenant_id = current_setting('app.tenant_id', true));

之后,每个请求在读取数据前,都要在连接上指定其租户:

BEGIN;
SELECT set_config('app.tenant_id', 'acme', true);
SELECT content FROM agent_memory ORDER BY created_at DESC LIMIT 20;
COMMIT;

第三个参数 true 会使该设置仅在当前事务中生效。这一点很重要,因为事务结束后,连接池会将该连接交给下一个请求。如果某条代码路径从未设置该值,则会返回0行,因为未设置时 current_setting('app.tenant_id', true) 为 NULL,tenant_id = NULL 永远不会为 true。这正是所需的安全失败行为。你可以运行第二个代码块,并删除其中的 set_config 行来验证这一点。

实际使用时,有两点会导致此机制失效。FORCE ROW LEVEL SECURITY 不是装饰性配置:如果缺少它,表所有者会绕过所有策略。因此,使用表所有者身份连接的应用可以读取所有租户的数据,导致策略看起来失效。具有 BYPASSRLS 的角色也会出于同样原因忽略策略,superuser 默认具有该属性。请使用不拥有任何对象的普通角色进行连接。

防御 2:记录来源,并将每条记忆视为不可信输入

为数据行添加提取过程丢弃的字段。

CREATE TABLE agent_memory (
  id          bigserial   PRIMARY KEY,
  tenant_id   text        NOT NULL,
  content     text        NOT NULL,
  source      text        NOT NULL,   -- user_message, tool_output, web_page, operator
  source_ref  text,                   -- url, ticket id, session id
  written_by  text        NOT NULL,   -- which agent or job wrote the row
  created_at  timestamptz NOT NULL DEFAULT now(),
  expires_at  timestamptz,
  reviewed    boolean     NOT NULL DEFAULT false
);

CREATE INDEX agent_memory_tenant_idx ON agent_memory (tenant_id, created_at DESC);

source 是能够自行证明其价值的字段。发生疑似事件后,唯一值得回答的问题是:哪些存储事实来自用户未编写的内容?有了该字段即可回答这个问题。没有它,每一行看起来都同样可信,你只能删除整个存储,并连同恶意记忆一起丢失有效记忆。

来源信息还可以让你用一行规则定义检索策略。sourceuser_messageoperator 的行,才可检索到能够调用工具的运行中。其他所有行都必须先由人工设置 reviewed。该规则具有可移植性:它是一个 WHERE 子句,无论存储使用 Postgres、SQLite,还是支持元数据过滤的向量数据库,行为都相同。

然后,不要将存储文本放入系统提示词。检索到的记忆应放在单独且边界清晰的区块中,并标记为召回数据。标记并不能阻止模型执行其中找到的指令,假装标记可以阻止指令执行,最终只会让人信任一个不起作用的控制措施。你可以依赖它实现两点:提示词中信任级别最高的区域不会包含攻击者可触达的文本;当你检查模型看到了什么时,日志中也能清楚看到这条边界。

防护措施 3:让写入内容过期,审核真正重要的内容

永不过期的记忆会不断增长,直到无人能够读取。为每条提取的记录设置 TTL(生存时间),并按计划删除:

DELETE FROM agent_memory
 WHERE expires_at IS NOT NULL
   AND expires_at < now();

使用 systemd 定时器即可运行该任务:

[Unit]
Description=Delete expired agent memory rows

[Timer]
OnCalendar=daily
Persistent=true

[Install]
WantedBy=timers.target

匹配的 .service 单元只需包含一行 ExecStart,调用 psql -d agentmem -f /etc/agent-memory/expire.sql,并以维护角色运行。使用 systemctl list-timers agent-memory-expire.timer 检查该定时器;输出中应包含下一次运行时间和上一次运行时间。定时器从不触发,通常是因为创建后没有执行 enable --now

TTL 决定了被污染记录最长可被检索多久。因此,默认设置为 30 天时,暴露范围最多为 30 天的会话记录。提取的记忆应使用较短的默认期限;只有用户明确提升的记录才应保留更长时间。相同的过期任务也应负责清理过期的代理记忆,因为在这里,正确性和安全性要求相同。

应审核那些可能改变行为的记录,而不是全部记录,因为无人读取的队列只是摆设。值得人工检查的记录包括包含凭据、工具名称、URL,或带有“始终”和“从不”等词语的记录。将写入日志保持为仅追加,并与记忆表分开,这样删除被污染的记录时,不会同时丢失其出现时间以及写入该记录的会话。

防护措施 4:将记忆存储置于代理的影响范围之外

该存储是代理访问的数据库,因此应按数据库的标准进行保护。为它使用独立的容器或主机,不发布端口,并使用不与代理其他用途共用的凭据:

docker network create agent-mem

将数据库和代理连接到该网络,不发布任何端口。只能通过内部 Docker 网络访问的存储,即使代理进程已完全遭到入侵,也无法从互联网读取。

然后进一步限制代理自身角色可以执行的操作:

CREATE ROLE agent_rw LOGIN PASSWORD 'generate-this-do-not-type-it';
GRANT SELECT, INSERT ON agent_memory TO agent_rw;
GRANT USAGE ON SEQUENCE agent_memory_id_seq TO agent_rw;

不授予 UPDATE,也不授予 DELETE。当有人诱导代理“修正这段记忆”时,代理无法重写历史,因为它没有相应授权,而过期计时器由另一个角色运行。读者测试时会遇到的故障是更新操作返回 permission denied for table agent_memory,这说明控制措施正在生效。这些独立的数据库密码必须存放在某处。如果存放位置是自行托管的密码库,就应单独进行加固,因为 Vaultwarden 的实际暴露面是管理令牌和备份文件,而不是存储的密文本身。

最后一点经常被忽略。如果代理进程在与数据库密码相同的环境中持有云密钥或部署令牌,那么一次被篡改的记忆引导某个工具调用后,就可能访问全部这些凭据。应将它们分开,例如 不要将机密信息放入 AI 代理的运行环境,并将重要操作置于 明确的审批步骤之后。如果运行专用的记忆服务,例如 在 VPS 上自行托管 Mem0 服务器,这些原则仍然适用:它本质上是一个前面接入模型的数据库,数据库和模型两部分都需要采用相同的安全措施。

如何判断内存是否已经被投毒?

先给出诚实的答案:仅凭阅读文本无法判断。MINJA 作者在介绍其自身记录时也强调了这一点。他们认为,注入内容对输入和输出审核而言都看似合理,因此内容过滤很可能会漏过这些内容。在这里,扫描已存储句子、查找疑似恶意内容的过滤器并不是可靠的控制措施。

账务记录比人工检查更有效:

  • 按来源查询。列出 source 不为 user_message 的行,并按最新时间优先排序。在正常的存储中,这个列表通常足够短,便于人工阅读。
  • 监控增长速率。存储每天增加五行,却突然增加两百行,无论这些行的内容是什么,都值得关注。
  • 保留一组固定的评估提示词,并在内存发生变化后运行它们。如果代码没有变化但行为发生变化,问题可能出在存储中。
  • 每天为存储创建快照,并比较快照差异。文本差异便于阅读,而向量搜索不具备这一点。

这些方法都不是检测器。它们的作用在于让你能够自行发现被投毒的行,而不是等客户报告问题。

无法解决的问题

隔离、来源记录和过期机制可以缩短恶意记录的存留时间,并限制其传播范围。但在记录仍保存在存储中时,这些措施无法阻止模型相信该记录。限制损害的关键在于代理被允许执行的操作:如果代理未经人工批准不能部署或花费资金,那么即使形成错误认知,也无法造成太大影响。

如果代理会读取不受信任的内容,而您目前还无法记录来源,请在不使用长期记忆的情况下运行代理。无状态代理会在会话结束时忘记这次攻击。这会实质性降低可用性,但在存储尚未隔离且写入路径尚未明确之前,这仍是正确的权衡。

FAQ

内存投毒只是换了个更长名称的提示注入吗?

不是。入口相同,但存续时间不同。提示注入只存在于一个上下文窗口中,因此结束会话即可清除它。内存投毒会将恶意文本写入持久存储,因此下一次会话会将其检索为既定事实;重启也不会改变结果。OWASP 因此将两者分开:提示注入属于 LLM Top 10,而持久内存损坏属于 Agentic Applications Top 10 中的 ASI06,即 Memory and Context Poisoning。

不访问数据库,也能投毒代理的内存吗?

可以,这正是 MINJA 论文的结论。攻击者只发送查询并读取答案,不访问内存库;写入操作由代理自身的内存步骤执行。在不同配置下,该论文报告的平均注入成功率为 98.2%,平均攻击成功率为 76.8%。只要不受信任方的文本可以到达提取步骤,任何此类接口都是写入路径,包括支持邮箱或抓取的网页。

为每个租户分配独立集合,能解决多租户数据泄露吗?

只有在每条代码路径都应用过滤器时,它才能解决读取侧问题;但这正是薄弱环节:遗漏过滤器时,系统会返回其他租户的行,而不是报错,因此你不会得到任何提示。它无法解决写入侧问题:某个租户的文本可能被提取到记录中,并以全局记录形式或错误的 id 存储。使用独立凭据的独立存储会安全失败,因为错误的连接字符串完全不会返回任何行。

代理内存应保留多长时间?

应短于直觉上认为合适的时间。TTL 决定了投毒行最多能被检索多长时间,因此 30 day 的过期时间会将暴露期限制为 30 day 的会话。对于模型提取的任何内容,应使用较短的默认期限,并要求人员审核记录后再将其设为永久内容。将 expires_at 存储在记录本身中,这样过期任务只需执行一个 DELETE,而不必依靠脚本猜测。