☰
ContextLeak防护指南:LLM Agent工具调用中的上下文泄漏与安全边界
2026/9/30 11:07:48 网站建设 项目流程

最近讨论 LLM Agent 安全时,ContextLeak 是一个越来越常被提到的风险类型:恶意工具描述可能导致 Agent 把运行时上下文带进工具调用参数,最终被工具背后的外部服务接收。它不一定需要模型本身存在严重缺陷,而是 Agent 在工具调用过程中的信任边界没有划清楚。对正在开发 Agent 应用的人来说,这不是一个可以等到上线后再处理的边缘问题,而是一个需要在设计阶段就要纳入考虑的安全检查项。下面内容不讨论如何复现攻击,而是从防御和工程化落地的角度,拆解 ContextLeak 的成因、验证方法、隔离设计、上线排查和长期缓解方向。

如果你正在接触 RAG、工具调用、多步骤 Agent、插件市场或企业内部助手,这篇文章会相对实用。读完后你能拿起自己项目的工具清单做一轮自查:哪些工具描述来自不可信来源?哪些字段可能把内部状态写入参数?外部工具返回内容重新进入模型后,有没有二次污染的可能?这几件事看清了,ContextLeak 的攻防逻辑也就清楚了。

1. 先弄清 ContextLeak 说的是哪一层风险

ContextLeak 从名字看像某个具体的模型漏洞编号,实际上它描述的是一整类 Agent 安全问题。核心对象不是模型本身,而是 LLM Agent 的工具调用链路。一个 Agent 为了完成任务,会按照系统提示和用户输入来决定是否调用某个工具,以及把哪些参数传给工具。这个决策过程依赖模型对上下文的综合理解,而工具名称、工具描述、参数 schema 同样会进入模型上下文。

如果某个工具描述是由第三方插件、开源仓库、自动生成脚本或用户可控配置拼接而来,那么它就不再是单纯的“功能说明书”,而是可能带有额外指令的文本。模型读到这些文本后,如果把它误判成需要遵循的指令,就可能出现两类异常:一类是改变任务路径,让 Agent 去调用不该调用的工具;另一类是把模型当前看到的运行时上下文写入工具参数,导致这些信息被发送到工具接收方。后者就是 ContextLeak 关注的重点。

运行时上下文这个词看起来抽象,落到实际场景里却很具体。

1.1 工具描述为什么会成为提示注入入口

要理解这个问题,需要先理解 LLM 和传统程序的一个本质差异:传统程序里,代码和数据有严格的边界;而在 LLM Agent 里,系统提示、用户消息、工具描述、工具返回结果、检索到的文档片段,全部以 token 文本的形式出现在同一个上下文窗口内。模型在生成文本或决定工具调用时,只能靠语义去判断哪些是指令、哪些是数据。

架构上,系统提示词通常被放在最高优先级,但它本质上也是上下文里的文本。工具描述被设计为一种特殊的上下文区域,目的是告诉模型“这个工具什么时候可以用、参数是什么”。问题恰恰出在这里:模型不会像程序那样把工具描述当作不可执行的结构化元数据,而会把它当成可供参考的文本。于是,一旦工具描述中被塞入与功能无关的指令型内容,模型的注意力就可能被带偏。

这就是 ContextLeak 这类攻击的入口,能力强大的 Agent 更容易受这种问题影响,因为能力越强,它会越积极地参考上下文里的各种说明,包括不可信说明。它不会天然区分“这是框架自动生成的工具元数据”和“这是来自第三方客户端的攻击输入”。开发者和安全人员必须假设工具描述本身是不可信的,并在这个前提下设计隔离机制。

1.2 “运行时上下文”一旦被带出,意味着什么

先把风险等级讲清楚。运行时上下文泄漏不等于普通的文本拼接错误,它可能包含会话内部信息。最常见的场景是:一个用户问某个问题时,Agent 为了调用工具,需要把用户问题、业务 ID、权限状态或者历史摘要作为参数传给工具。正常情况下,这些参数是完成任务所需的最小集合。但如果外部不可信内容影响了模型,它可能把与任务无关的额外上下文也一并放进参数里,例如系统提示中的隐藏规则、同一会话里之前轮次的原对话框、内部环境变量、数据库查询结果、文档检索片段等。

这些信息一旦进入工具参数,就不再受模型进程保护。工具背后的接收方可能是外部 HTTP 服务、日志系统、分析平台或第三方 API,无论接收方是否有意收集,数据都已经离开可信区域。如果接收方是恶意构造的端点,这些内容就可能被记录、聚合、重放或用于后续攻击。

我认为最值得警醒的一点是:很多团队给 Agent 增加工具调用能力时,只检查了“这个工具能不能完成任务”,却没有检查“为了完成任务,模型会把多少额外上下文交给工具”。实际上,后者才是 ContextLeak 关心的核心问题。知道这个风险后,设计和调用的透明性就成了比单次效果更重要的指标。

2. 开发 Agent 前,把信任边界画清楚

要做到安全,第一步不是写防注入的提示词,而是把 Agent 应用里的信任边界画出来。边界画不清楚,后续所有防御手段都容易漏。

2.1 工具描述、工具入参、外部返回内容都按“不可信内容”管理

很多开发者习惯性地认为:“工具描述是开发者自己写的,不是用户输入,所以它是可信的。”这个假设在绝大多数简单 Demo 里成立,一旦系统进入真实环境,立刻会出问题。

工具描述可能来自内部开发人员,但更常见的是来自这些地方:第三方插件市场的自动元数据、前端团队根据后端接口生成的 OpenAPI 描述、开源社区仓库中的 tool schema、由提示词工程工具自动总结出的工具说明。任何一条链路被污染,最终进入模型上下文的工具描述都可能包含非预期文本。因此,我建议把工具描述、工具返回内容、外部网页检索结果都当作不可信的输入来治理,而不是当作可信任的内部代码。

另外,不要把“过滤用户输入”当成唯一的防线。用户输入只是入口之一,工具返回内容被拼入上下文后同样可能影响后续工具调用。举例来说,一个 Agent 调用搜索工具后,把搜索返回的文本直接作为上下文再送给模型,如果这份文本来自外部站点,当中包含了非业务指令,模型可能在下一次工具调用时做出错误决策。这本质上是同一个漏洞的不同触发面。

下面用表格做一个快速边界梳理:

数据源是否可信防护重点
开发者手动维护的系统提示基本可信,仍需版本审查不动态拼接不可信文本
用户聊天消息不可信不直接拼入高权限指令区
工具描述与参数 schema默认不可信最小化、白名单、定期审计
工具从外部返回的内容不可信截断长度、字段白名单、抽取值
Agent 运行时内部状态仅内部可信不进入工具参数,除非通过安全中间层

2.2 最小验证:在隔离环境检查外部内容是否会改变工具参数

画完边界后,可以用一个小实验验证:当前 Agent 是否会把不可信描述当成指令,从而影响工具参数。

这里要注意,实验必须在隔离环境里做,不能连生产数据,也不能把任何真实用户信息和内部系统状态放进测试输入。我自己实践时,会准备一个只跑本地日志的测试工具,作用只有一个:接收 Agent 工具调用参数并打印到本地日志文件。

测试思路如下:

  1. 准备一个正常工具,工具描述只包含一句话,说明用途。
  2. 在另一个版本的描述中,混入一段与工具功能无关、明显不属于正常说明的文字。
  3. 让 Agent 执行同一个普通测试任务,例如查询当前天气。
  4. 对比两次调用产生的工具参数、工具名称和调用路径。
  5. 如果第二次调用出现非预期字段,说明当前环境对工具描述缺少隔离能力。

需要注意,这个实验不是要去验证某个提示词能造成多大破坏,而是帮助团队建立基线:生产中任何工具描述、插件描述、外部返回内容发生变化时,都能自动触发回归测试。测试工具的输出只能发送到本地审计日志,不能发送到外部端点。

注意:不要使用真实敏感数据做这类实验。第一次测试尽量在普通开发机上完成,关闭外向请求,并确认日志文件只保存在本机。

如果实验发现一定条件下外部描述确实会影响调用,不要急着认为“模型有问题”。这个阶段真正需要解决的是架构问题:为什么模型能看到那么多不该用于工具决策的内部内容?为什么工具参数没有最小集限制?把这些源头堵住,比反复调整一条提示词可靠得多。

3. 工具描述与运行时上下文如何隔离

明白风险闭环之后,代码层面能做的事情就很具体了。我在多个 Agent 项目里验证过,隔离效果最好的不是某一句“系统提示词”,而是组合设计:描述最小化、数据引用化、字段白名单化。

3.1 描述最小化:只写“什么时候用、输入是什么、输出是什么”

一个工具描述越短、越结构化,模型误解和执行异常指令的空间就越小。

这里说的短,不是把描述写到无法理解,而是去掉那些与“触发条件、输入参数、输出结果”无关的冗余内容。比如一个天气查询工具,理想描述可以是这样:

  • 触发条件:用户询问某地天气。
  • 必需参数:城市名称。
  • 可选参数:日期。
  • 输出:温度、天气现象、风力。

另一种写法就容易引入风险:把内部服务地址、缓存策略、权限校验细节、历史接口调整说明全部写进描述。这些内容模型不需要知道,写了反而提高上下文被污染的机率。如果调试时确实需要这些信息,应该放在独立运维文档中,不要进入模型上下文。

另外一个原则是避免动态拼接。如果工具描述中包含根据用户输入或第三方数据生成的内容,那就等于给不可信文本开了一个入口。凡是工具描述需要变化,都应走代码配置和版本管理,而不是运行时字符串拼接。

3.2 数据引用化:能传 ID 就不传原始上下文

Agent 工具调用的本质是让某个处理器去执行任务,而很多处理器并不需要拿到原始对话全文。即使模型已经读到了完整对话,也不意味着它应该把这些内容全部透传给工具。

假设你在开发一个客服 Agent,工具处理器需要读取订单详情。合理做法是让 Agent 只传订单号或用户 ID,由后端处理器在可信环境里根据 ID 去数据库查询。这样即使工具参数被外部端点记录,对方能看到的也只是订单号,而不是客服系统内部的完整会话上下文和检索片段。

如果某个工具确实需要用户问题上下文,也应该先把问题进行脱敏和裁剪。可以设计一个中间对象,只包含“问题类型、关键实体、业务 ID、当前会话标识”,原始对话永远留在内部存储中。外部工具如果必须感知上下文,可以通过会话 ID 去调用需要鉴权的内部接口,而不是直接把整段对话复制进参数。

这条规则可以适用于很多场景:

  • 文档问答工具:传文档 ID,不传文档全文。
  • 订单查询工具:传订单号,不传用户历史订单列表。
  • 搜索工具:传检索关键词,不传搜索结果的原始 HTML。
  • 数据库工具:传 SQL 模板 ID 和参数,不传整段 SQL 生成过程。

很多人担心“传 ID 之后,模型回答会变差吗”。实测中,只要后端处理器返回的是加工过的结构化结果,模型回答质量通常不会下降,因为模型本来就不擅长处理冗长的内部数据,也不应该在执行路径上背负这些上下文。

3.3 不要把内部说明留在 schema 备注里

参数 schema 同样是需要清理的地方。在 OpenAPI、JSON Schema 或函数定义里,描述字段通常会被框架直接送入模型上下文。有些团队为了便于前后端对接,会在字段描述里写内部字段名、枚举来源、权限层级、甚至数据库列名。这些信息对模型执行任务没有帮助,反而会暴露内部实现。

安全检查时,可以逐个字段问三个问题:

  1. 模型需要知道这个字段的含义才能调用工具吗?
  2. 这个说明是否包含内部实现细节?
  3. 如果这段说明被第三方读取,会造成额外信息泄漏吗?

第三个问题容易被忽略。工具描述最终会写入前端或网关日志,开发阶段的调试日志经常会把完整 schema 打印出来。把内部字段名留在 schema 描述里,相当于把内部结构写在了一本可能被分享的接口手册上。内部信息应该保存在后端处理器中,由可信代码负责转换,而不是暴露给模型。

4. 在工具调用前后增加防护层

只靠模型自身判断“哪些能传、哪些不能传”是不稳定的。要在模型和外部工具之间增加一个防护层,这个层不受大模型语义影响,只做确定性校验。

4.1 调用前:白名单、类型校验、字段来源校验

我建议工具调用入口统一经过一段 validator,无论后端是本地方法还是远程 API。validator 至少要检查以下几项:

  • 工具名是否在允许列表内;
  • 参数是否是一个合法的 JSON 对象;
  • 参数总长度是否超过阈值;
  • 是否出现禁止透传字段;
  • 是否需要人工审批。

下面是一段演示性的校验逻辑,不是完整生产代码,但能反映检查思路:

import json TOOL_CALL_ALLOWLIST = {"get_weather", "get_order_status", "search_doc"} MAX_TOOL_ARG_BYTES = 512 BLOCKED_PARAM_FIELDS = {"internal_ctx", "system_prompt", "chat_history"} def validate_tool_call(tool_call): if not isinstance(tool_call, dict): return False, "tool_call must be dict" tool_name = tool_call.get("tool_name", "") if tool_name not in TOOL_CALL_ALLOWLIST: return False, f"tool_name not in allowlist: {tool_name}" params = tool_call.get("params", {}) if not isinstance(params, dict): return False, "params must be dict" raw_size = len(json.dumps(params, ensure_ascii=False).encode("utf-8")) if raw_size > MAX_TOOL_ARG_BYTES: return False, f"params too large: {raw_size} bytes" for blocked_field in BLOCKED_PARAM_FIELDS: if blocked_field in params: return False, f"blocked field appears in params: {blocked_field}" return True, ""

这段代码至少能实现两层作用:第一层把不在白名单里的工具请求全部拦下;第二层在参数结构层面挡住明显的违规透传。实际项目中字段来源校验更复杂,比如“用户在主对话里输入的内容”可以传到 get_order_status,但“开发者预埋在系统提示里的内部标识”不能传。这需要业务侧定义参数来源规则,再映射成代码。

4.2 调用后:返回内容进入模型前要“净化”

很多团队只在输入和调用前做防护,忽略了工具返回内容。工具返回内容是模型继续推理时的重要上下文,如果它本身包含不可信文本,就会在后续调用中重新形成注入面。

一个很典型的场景是爬虫工具或网页转文本工具。它会抓取一个网页并把正文返回给 Agent。网页内容里通常包含很多导航文字、推广文案、隐藏说明。如果 Agent 把整段网页内容当作事实依据,并继续调用其他工具,那么外部内容就有了第一次间接影响工具参数的机会。

处理思路不是把所有外部内容都删掉,而是让返回内容“净化”后再进入模型。常见的做法包括:

  • 只抽取结构化字段,不返回原始全文;
  • 对返回文本设置最大长度;
  • 对返回内容增加一个标记,明确它来自外部数据源而非指令;
  • 对可疑内容做二次转义,降低被当成指令的可能。

即使这样,也要明白:任何净化技术都不能保证 100% 消除风险。更稳妥的设计是让外部工具返回内容不能触发新的工具调用,系统只有在用户明确动作或业务条件满足时才允许下一步调用。

4.3 对外传输:审计日志和接收方确认

Agent 产生的工具调用,在发送到外部服务前,最好有审计日志。这样一旦发生异常,你可以回溯到具体是哪个模型决策、哪一段上下文、哪一次工具描述变更导致的问题。

日志里至少记录:

  • 调用时间;
  • 工具名称;
  • 参数脱敏后的摘要;
  • 参数是否包含任何内部字段标记;
  • 外部端点地址;
  • 模型请求 ID;
  • 触发该调用的用户会话标识。

需要警惕的是,审计日志本身也可能成为敏感信息存储点。不要把完整参数原样写入日志,尤其是在本地开发时。建议默认只记录参数结构和长度,只有在明确需要排查时才打开详细记录,并且设置自动过期清理。

如果工具必须向第三方外部端点发送请求,还要确认接收方是谁。一个可行的做法是在网关层配置外部端点白名单,所有 Agent 工具调用不得直接请求任意 URL,而是通过统一出口发送,由统一出口校验域名和路径。

注意:不要在日志、监控和调试文件里存放真实上下文。模拟排查时用假数据,生产环境里使用脱敏字段。

5. 上线后的排查:从现象反推风险点

不是所有 ContextLeak 风险都能在设计阶段彻底消除。系统上线后,需要靠日志、调用记录和监控来发现异常。

5.1 排查顺序

如果你的 Agent 工具调用表现异常,比如出现奇怪的参数、连续触发多个工具、把无关内容写入字段,我建议按照下面顺序排查:

  1. 先确认工具调用日志里出现了什么参数;记录参数的实际内容,但不要在文档里原样输出敏感内容。
  2. 再看参数来源,是用户输入、系统提示、工具返回内容,还是其中多项的组合。
  3. 检查参数是否包含不应出现的内部标记,例如系统提示里的某个特殊标识符、内部文档编号、以及你没有允许传给工具的字段名。
  4. 回到模型请求上下文,确认这次调用前,模型是否读取过外部可控工具描述或外部返回内容。
  5. 检查工具描述最近是否发生变化,是否由不可信流程生成。
  6. 最后检查工具调用出口是否走统一网关,外部端点是否在白名单内。

这套顺序的核心逻辑是:先看最容易被观察到的工具参数,再反推模型是从哪段上下文读取了这些信息。很多人一遇到 Agent 调用异常,就认为“模型犯傻”,实际上问题往往出在某个外部返回了一段结构奇怪的内容,或者工具描述被错误拼接。

5.2 那些容易误判成“模型乱说话”的案例

我见过不少团队把 ContextLeak 误判为普通模型幻觉。举个常见的例子:一个 RAG Agent 在回答用户问题时,会先调用文档搜索工具,再调用列表处理工具。如果搜索工具返回的内容里混入了一段与问题无关的“提示说明”,Agent 可能在下一次调用列表处理工具时突然多传一个字段,导致系统报错。

开发者看到报错,第一反应是“模型选择参数不够稳定”。如果只调整提示词,问题很可能复现;因为真正的根因是搜索返回内容把不可信文本带进了上下文。把工具返回结果改成结构化字段、禁止携带任何自由文本后,问题才消失。

类似地,另一个容易误判的场景是工具参数中出现额外历史消息。团队可能会怀疑消息构造逻辑有问题。实际上可能是某个工具描述中包含了对“当前会话摘要”的调用指令。排查时可以直接检查工具调用参数是否包含原始用户消息,如果包含,说明内部对话记录进入了外部可读区域,这比性能问题严重得多。

把这些问题从“模型行为异常”中分离出来,是上线后一项持续的工作。我的建议是把“工具参数是否包含敏感字段”做成一个自动化指标,每次工具调用都检查一次,每一次触发告警都值得人工复核。

6. 结合现有实践的缓解清单

ContextLeak 没有一个能一键修复的开关,但通过组合多种工程手段,可以把风险降到可以接受的范围。下面清单是我在实际项目里会执行的分级方案。

6.1 短期可以先做的事

如果你现在正在上线一个 Agent 应用,没有时间做大规模重构,可以先做这些:

  • 关闭不必要的工具,只保留业务必需列表。
  • 把工具描述改成一段简短、结构化、无隐藏说明的文本。
  • 从参数 schema 中删除所有内部字段名和环境信息。
  • 不把完整对话历史作为工具调用参数。
  • 所有外部工具调用不再直接请求任意 URL,而是通过统一网络出口,加入域名白名单。
  • 搭建本地审计探针,记录每次工具调用的参数结构和外部端点。
  • 在隔离环境使用假数据测试工具描述变化是否会引发参数异常。

这些措施不需要改变 Agent 的整体架构,但能快速消除可见风险点。只要花一个下午梳理当前工具清单,通常就能发现三到五个可以立刻收紧的配置项。

6.2 长期要朝向的安全架构

短期缓解属于止血,长期应该把“最小权限”和“最小数据”嵌进 Agent 的基础设施。

在设计层面,我建议采用以下原则:

  • 模型上下文与业务数据分离:上下文里只保留完成任务所需的引用 ID,敏感数据不出内部服务。
  • 工具描述和 schema 纳入代码审查:工具定义的变更应该像代码变更一样走评审、测试和版本发布。
  • 外部内容不直接与模型交互:工具返回值先经过解析层,按字段提取结果,拒绝返回一段“自由文本”给模型。
  • 高风险操作强制人工确认:涉及外部发送、删除、提权、支付等场景时,Agent 不应该拥有最终决定权。
  • 工具调用路径统一收口:所有工具出口经过同一道校验,禁止 Agent 动态拼接任意 URL。

这套架构会牺牲一些“纯 Agent 自主能力”,但换来了可审计、可回滚、可定位的安全边界。我认为在企业内部系统和对外生产环境中,这种取舍是值得的。

6.3 推荐的心态与检查节奏

最后说一个心态问题:不要因为模型偶尔被外部内容影响,就认为整个 Agent 方案不可用。ContextLeak 这类风险提醒的是:既然让模型自主调用工具,就必须把工具调用当成系统边界来治理,而不能只当成模型参数调节问题。

我建议团队把安全检查放进每个迭代节奏中,而不是只在安全事故后临时处理。每次 Agent 接入新工具、插件或外部数据源时,都走一遍同样的清单:描述是否最小化?参数能传 ID 就不传原文?返回内容是否会被净化?调用出口有没有白名单?内部字段是否可能出现在任何外部日志中?

把这些动作用制度固定下来后,ContextLeak 就不再是一个让人恐慌的漏洞,而是一个可以被管理的工程风险。先跑通最小可信链路,再逐步扩大工具范围,权限始终按需分配。好的 Agent 应用不等同于“给模型最多工具的最后结果”,而是在保持能力的同时,让每一次工具调用都清楚知道自己在把什么数据送往哪里。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询