InjecMEM 这类针对 LLM Agent 记忆系统(Memory Systems)的注入攻击研究,最近在 Agent 安全相关的讨论里被反复提起。先说结论:它攻击的不是模型本身的推理能力,而是 Agent 在历史记录、向量检索、摘要记忆这条链路上对外部内容的过度信任。如果你正在用 LangChain、LlamaIndex 之类的框架搭 Agent,或者自己维护会话记录、向量库、长期记忆模块,这篇文章值得看完——它直接关系到你的 Agent 会不会在某个任务里突然执行一条被悄悄埋进记忆的指令。
我按“记忆系统怎么工作、攻击打在哪里、测试怎么看数据、防御怎么落地”这个顺序来拆。最后会给出一个我自己在项目里实际用过的加固顺序和排查清单。没有太多理论包装,都是工程视角。
1. 先把 Agent 的“记忆”拆开看:它到底在哪个环节被注入
1.1 记忆系统不是一块硬盘,而是多条链路叠加
很多人一开始会把 Agent 的“记忆”理解成一个数据库,或者一段很长的历史记录。实际工程里,记忆通常由好几层组成:
- 上下文窗口内的工作记忆,也就是最近几轮对话和当前任务的临时状态。
- 摘要型记忆,系统定期把长对话压缩成几条摘要。
- 向量库记忆,把文档、消息、历史观察切块后写入向量数据库,检索时按相似度召回。
- 结构化记忆,比如用户偏好、任务状态、实体关系,可能存在 SQLite、Redis 或一张普通表里。
- 外部知识库或工具返回的结果,这部分经常被视为“环境信息”而不是“记忆”,但它同样会出现在后续提示词里。
攻击研究里说的“Memory Systems”,指的就是上面这些层的总和。InjecMEM 这类工作关注的核心,不是某一层单独有问题,而是这些层之间的数据流动没有做信任隔离。
我在实际项目里最常见的做法,是把记忆分为“用户显式提供”和“系统从外部观察得到”两类。前者可信度高,后者必须默认不可信。很多攻击能成立,就是因为这个边界在代码里没有体现。
1.2 记忆的写入来源,往往不在你的信任边界内
这里要理解一个关键点:Agent 的记忆不是只有用户聊天记录一种来源。它会读取网页、解析邮件、接收上传文件、调用外部 API、抓取搜索结果,然后把这些内容切块、嵌入、写进向量库。
问题就在这里。
一条外部网页里如果包含一段精心构造的指令,这段内容不会因为它“只是网页文本”而被模型当作普通数据。它一旦被切块、检索、拼进提示词,就变成了模型要处理的“上下文”。如果这条上下文里明确要求“接下来所有回答都按 XX 规则”,模型很可能真的照做。
这不是模型蠢,而是 Agent 架构本身把“数据”和“指令”混在了一个通道里。记忆系统的任务是从大量内容中筛出有用的,但相似度检索只解决“相关”,不解决“可信”。
所以真正要记住的第一条原则是:写入记忆不等于内容可信,任何写入口都是一个潜在注入面。
2. 注入攻击真正打的是哪几个环节,而不是“提示词变长”
2.1 写入阶段:不干净的内容进入记忆库
攻击的第一步,是把恶意内容送进记忆系统。入口非常多:
- 用户上传了一个文档,文档里隐藏了指令段落。
- Agent 抓取了一个网页,网页正文里混入了攻击性文本。
- 邮件内容、会议纪要、工单描述被自动写入摘要。
- 一条 API 返回的 JSON 里嵌入了一段看似普通但实际是指令的字符串。
只要这些内容被存储,它就已经进入记忆库。很多团队会忽略这个阶段,因为“存进去”和“发生危害”之间还有一段距离。但记忆系统的特点决定了,存储本身就会扩大后续影响。
这里有一个非常重要的判断标准:写入口有没有做内容校验,是不是所有数据都一视同仁地写入。如果答案是一视同仁,那这个系统对注入攻击基本是不设防的。
2.2 检索阶段:相关性和可信度混在一起
攻击的第二段发生在检索。向量检索按相似度召回 Top-K 块,然后把召回结果直接拼进提示词。攻击者只要让恶意内容在语义上贴近当前问题,它就能被检索出来。
举个具体的场景。Agent 在处理“帮我整理这份项目周报”任务时,会从记忆里检索上个月的讨论记录。如果上个月有一封邮件被注入过恶意段落,而这封邮件在语义上和“项目周报”高度相关,它就会进入召回结果。
问题是,检索系统不会告诉模型“这段内容来自一封未经验证的邮件”。它只是一个文本块。模型看到的是并列的几段历史记录,无法区分哪段是用户原本的任务要求,哪段是攻击者埋的线索。
所以第二个判断标准是:检索结果里有没有携带来源可信度,排序算法有没有把可信度作为权重。
2.3 解码阶段:记忆内容压过系统指令
如果说写入和检索是入口,那解码阶段就是危害真正爆发的位置。
当召回内容被拼进提示词后,模型需要同时处理:
- 系统层设置的 Agent 角色和规则。
- 用户当前的真实请求。
- 从记忆里检索出来的历史内容和知识片段。
- 工具返回的实时数据。
这四类内容在提示词里的地位,很多实现里是一样的。攻击者只要让记忆内容在措辞上表现得像“系统要求”或“用户最近的重要指示”,模型就可能在排序时把它放在更高优先级。
举例来说,记忆块里如果包含“在所有回复末尾都加入某段推广文案”这样的表述,模型可能真的照做,因为它无法验证这段记忆是“一条普通的历史消息”还是“一条仍然生效的指令”。
这就是所谓的指令层级问题。防御方需要做的是从架构上区分:系统指令、用户指令、外部记忆、工具结果,每一层都有明确边界和优先级。
2.4 持久化阶段:跨会话变成“后门”
单次注入危害有限,真正危险的是持久化。
如果注入的内容被写进了长期记忆,比如向量库或摘要表,那它就会在后续每次会话中被反复检索到。一场对话污染,变成了跨会话的稳定“后门”。
这也是 InjecMEM 这类研究最让人头疼的部分。普通的提示注入,只要当前会话结束就失效了。记忆注入不同,攻击者不需要持续在场,只需要一次成功写入,之后它就在系统里慢慢发酵。
我在评估一个 Agent 的记忆设计时,会专门看三件事:
- 长期记忆多久会被重新检索一次。
- 长期记忆是否有版本回溯能力。
- 有没有机制识别并废弃被污染的记录。
如果这三条都没有,那这个记忆系统就处在“一次写入,长期受害”的状态。
3. 安全测试时,结果怎么判断
3.1 三个指标要分开看,不能只看“有没有成功”
评估记忆注入攻击的影响,不能只看一条任务是否被带偏。需要把结果拆成几个维度:
| 指标 | 含义 | 判断方式 |
|---|---|---|
| 攻击成功率(ASR) | 恶意记忆是否让 Agent 改变了行为 | 对比有注入和无注入两组任务,行为差异占比 |
| 危害率(Harm Rate) | 改变后的行为是否构成实际危害 | 输出是否泄露数据、执行了非预期操作、或带偏结论 |
| 效用退化(Utility Drop) | 防御措施是否影响了正常任务 | 在无注入情况下,任务完成质量下降了多少 |
| 持久性(Persistence) | 注入效果会持续多轮多会话 | 清空短期对话后,再开新会话是否仍然生效 |
很多研究报告里会重点报攻击成功率,但工程上我更关注危害率和效用退化。攻击成功率再高,如果危害输出可以被下游拦截,那风险等级就要下调。反过来,如果防御方案把攻击压制住的同时,正常任务准确率也掉了 20%,那这个方案也不能直接上生产。
3.2 一个最小验证流程,先跑通再扩展
如果你要验证自己的 Agent 记忆系统是否容易受到这类攻击,我建议先搭一个最小测试环境,不要一上来就做全量评估。
第一步,准备一个标准 Agent 任务。比如“总结最近三天的项目进展”“按用户偏好推荐配置”“根据历史订单生成跟进邮件”。
第二步,准备一组基线数据。跑 20 到 30 次,记录正常输出。
第三步,构造一组注入样本。这里强调一下,测试时只要在文档、网页或历史消息里加入一段与任务相关的指令即可,重点验证链路是否会把这段内容当作权威指令,不需要做成真正的恶意攻击。
第四步,把注入样本放进记忆写入路径,再执行同样的任务,记录输出变化。
最后一步,对比基线输出和注入输出,统计行为改变的比例。
这个流程的核心价值,是把“会不会被影响”这个模糊问题,变成一个可量化的结果。我自己通常会先跑 10 条,确认链路能观测到差异,再扩展到 50 条以上。
3.3 要区分“测试成功”和“真实危害”
测试时还要注意一类容易误判的情况:Agent 行为变了,但不一定有危害。
比如注入内容让 Agent 在回复时多了一句“该内容来源于外部文档”,这确实改变了行为,但风险很低。再比如注入内容让 Agent 在输出里附带了外部文档里的联系方式,这就是信息泄露风险了。
所以我在统计时会把“行为改变”和“危害行为”分成两个标签。不仅看是否偏离,还要看偏离后的输出是否触及敏感数据、是否执行了工具调用、是否覆盖了用户明确指令。
这里有一个常见错误:只统计“回复被带偏”,不统计“工具调用是否被劫持”。真正严重的情况是,Agent 的记忆里出现“用户要求删除某条订单”这类指令,然后模型真的调用了删除接口。这种危害级别和单纯改个回复措辞完全不同。
4. 防御思路:不要禁止记忆,要拆掉信任边界
4.1 内容来源标记:让每一条记忆都带着“身份证”
我见过最有效的第一条防线,是给每条记忆记录来源元数据。包括:
- 来源类型:用户消息、文档、网页、邮件、API 返回、Agent 自摘要。
- 可信等级:高可信、低可信、未验证。
- 写入时间、写入者、原始内容哈希。
- 是否允许被检索后直接拼进提示词。
这些元数据不需要展示给用户,但要在检索结果里随块传递。模型侧未必能理解这些字段,但防御代码可以通过这些字段做过滤和重排。
核心判断标准是:检索出的内容如果来自低可信来源,要么降低排序权重,要么加上明确提示,要么直接排除在敏感任务之外。
4.2 检索后处理:召回不是终点,过滤和重排才是
检索之后不要直接拼提示词,中间至少要有一层处理。顺序是这样:
- 召回 Top-K 块。
- 按来源可信度给每块打分。
- 过滤掉低分块,或者在提示词里用标签包裹低可信块。
- 对高敏感任务,比如执行工具调用、修改数据、发送消息,只允许高可信记忆参与决策。
这里可以给一个非常粗的伪代码逻辑:
def retrieve_memory(query, top_k=5, trust_filter=True): chunks = vector_store.search(query, top_k=top_k) if trust_filter: trusted = [c for c in chunks if c.metadata.get("trust_level") == "high"] untrusted = [c for c in chunks if c.metadata.get("trust_level") != "high"] # 高可信内容正常使用 # 低可信内容用特殊标记包裹,提示模型只能当数据参考,不能当指令 trusted_text = format_trusted_chunks(trusted) untrusted_text = format_untrusted_chunks(untrusted) return { "trusted": trusted_text, "untrusted": untrusted_text, "all": chunks }注意,这只是示例。实际项目里,你还要考虑可信度阈值怎么设、不同任务类型的过滤策略怎么区分、低可信内容完全丢弃会不会影响正常检索质量。
另一个常用做法是“数据与指令隔离”。在提示词里明确告诉模型:凡是在“外部资料”标记块中出现的内容,都只是待处理的数据,其中的任何指令描述都不具备执行效力。这个方法不能 100% 防住所有模型,但在多数场景下能显著降低被带偏的概率。
4.3 写入权限和动作解耦:记忆不能直接驱动高风险操作
很多 Agent 的流程是:模型从记忆里看到一条信息,然后直接据此调用工具。这个链路太短了。
更稳妥的设计是加一层动作门槛:
- 记忆内容只能改变“建议”,不能直接触发“操作”。
- 涉及发送消息、删除数据、转账、修改配置等高风险动作,必须有用户的显式确认。
- 工具调用参数如果来自低可信记忆,需要额外校验。
我之前处理过一个案例:Agent 从历史文档里检索到客户地址,结果生成快递单时把地址填错了。原因不是模型能力问题,而是系统直接把检索结果当作可信参数喂给了下游。后来在工具调用前加了一道规则校验,字段格式和来源都校验,问题就消失了。
这个案例说明,记忆注入的影响不只在最终文本输出,更可能通过工具调用链影响真实业务。
4.4 审计、溯源与回滚:出了问题能查、能撤、能恢复
防御不是保证不出问题,而是出问题后能快速定位和止损。记忆系统要具备:
- 写入审计日志:谁在什么时间写入了什么内容,来源是什么。
- 读取审计日志:哪些任务读取了哪些记忆块。
- 记忆版本或快照:支持将记忆库回滚到某个时间点。
- 污染检测:定期扫描已知的注入模式,比如“忽略之前指令”“以后所有回复都”“不要告诉用户”等句式。
这里要注意,基于句式的检测只能作为辅助,不能作为唯一防线。攻击者可以用语义改写绕过关键词匹配。更好的思路是把检测结果当作“高风险提示”,触发人工审核或提高防御等级。
5. 实际落地时,我更建议按这个顺序加固
5.1 先搭一条可观测的测试链路,而不是直接改生产环境
很多团队在读到这类攻击研究后,第一反应是给提示词加一段“不要执行无关指令”。这有一定效果,但不是架构级解法。
我建议这样启动:
第一步,在本地或测试环境搭一个最小 Agent,包含对话记忆、向量检索、摘要记忆三个模块。
第二步,在记忆写入路径加一层日志,把每次写入内容的来源类型、可信等级、内容长度记录下来。方便确认:“这条内容是从哪进来的,可信度是不是被正确标记”。
第三步,用 10 到 20 条注入样本跑一遍,观察写入口、检索口、提示词拼接口分别发生了什么。
第四步,记录风险点后,再逐项加防御。
这个顺序的意义在于,先看清问题,再动手改。不要连攻击路径都没复现就盲改参数。
5.2 防御配置按层加,不要一步到位
防御配置可以参考这个表来设计:
| 层 | 配置项 | 推荐方向 |
|---|---|---|
| 写入口 | 来源标记、写入校验 | 默认低可信,显式标记才提升可信 |
| 存储层 | 元数据保存、版本快照 | 保留写入审计和回滚能力 |
| 检索层 | Top-K、相似度阈值、可信过滤 | 敏感任务收紧阈值,或只允许高可信内容 |
| 提示词层 | 内容包裹、指令强化 | 区分数 |