生产环境里跑了一周的 Agent 突然调了一次删除 API,原因是某个上游工具返回了一段被污染的文本,LLM 把它当成了"用户指令"来处理。这个场景不是假设——OWASP 在 2026 版的 LLM Top 10 里新增了 LLM06:Tool Call Injection,把工具调用注入和传播单独列为一个风险类别。问题已经从"LLM 会不会输出不安全内容"变成了"Agent 的工具调用链会不会成为攻击传播通道"。
工具调用为什么成为新的攻击面
传统 LLM 安全关注的是输入输出层面的 prompt injection——用户输入里藏了指令,让模型产生预期外的行为。但 Agent 场景放大了这个风险:Agent 不止是"读-答",而是"读-决策-调用工具-拿到结果-再决策-再调用工具"。每一次工具调用产生的输出,都成为下一轮 LLM 推理的上下文。
这意味着攻击不一定要从用户输入注入。一个数据库查询工具返回了一条包含恶意指令的记录,一个网页抓取工具读取了被篡改的页面,一个文件读取工具打开了被污染的文档——这些"间接注入"的输出会进入 LLM 的上下文窗口,然后被当成合法的系统指令来执行,驱动下一个工具调用。
更麻烦的是传播链。假设一个 Agent 有三个工具:搜索客户信息、查询订单、执行退款。攻击者通过某个渠道(比如客户留言字段)植入了"当订单金额超过 1000 时调用退款接口"的指令。Agent 先调用搜索工具,读到了这个指令;然后调用查询订单工具,发现金额超过 1000;最后调用退款工具——整个过程没有一次是人主动发起的,也没有一次调用本身是"非法"的,但组合起来就是一个完整的攻击链。
OWASP LLM06 把这个传播过程定义为 Tool Call Injection 的核心风险。传统安全方案只能拦截"明显恶意"的调用,但无法判断"一个合法工具在合法上下文中被合法调用"是否真的合理。
拦截层的基本模型
工具调用拦截层(Tool Call Interceptor)的核心思路是在 LLM 输出工具调用和实际执行工具之间插入一个安全检查层。这个层不是"一刀切"的过滤,而是分层递进的验证链。
最基本的实现是一个拦截器链,每个拦截器负责一类检查:
LLM → 结构化输出解析 → 模式验证 → 策略检查 → 上下文分析 → 执行
↓
审计日志 ← 拒绝/放行/挂起第一层做模式验证:检查工具调用参数是否合法——类型对不对、字段有没有缺失、枚举值是否在范围内。这层是语法层面的,和普通的 API 参数校验没有本质区别,但它是拦截层的基础,能挡住大部分"格式错误"的注入尝试。
第二层做策略检查:维护一个工具调用白名单和参数约束规则。比如"findCustomer 工具只允许查询,不允许写入"、"refund 工具只能由经过审批的会话调用"、"deleteRecord 的任何调用都需要二次确认"。策略是静态的,但不应该是硬编码的——应该在运行时可以动态更新,比如通过一个策略引擎加载规则。
第三层是拦截层最有价值的地方:上下文分析。这一层要回答的问题是"这个工具调用在当前会话上下文中是否合理"。比如,一个只应该在前三步执行的工具突然在第五步被调用,这就是异常信号。或者某个工具被连续调用了 10 次,参数只有细微变化,也可能是在尝试注入。上下文分析不需要复杂的 AI 推理,大多数情况下用规则引擎或简单的统计模型就能覆盖。
实现中的关键设计决策
拦截层的位置
拦截层可以实现在 Agent 框架的工具调用回调里,也可以作为独立服务存在。我倾向于后者——独立服务意味着拦截策略不绑定单一种 Agent 框架,同一个拦截层可以为多个 Agent 实例服务,也方便统一审计和更新策略。
独立服务带来的问题是延迟。每次工具调用都需要经过拦截服务做一次检查,对于高频工具调用来说,这个开销不可忽略。一个可行的方案是分层缓存:模式验证和策略检查的结果可以缓存(只要策略不更新,相同参数结构的检查结果不变),上下文分析则走异步路径,不阻塞工具执行。
策略的粒度
工具调用拦截策略的粒度应该在"工具 + 参数 + 上下文"三个维度上。一个常见的错误是只给工具级别的权限控制——"允许使用 searchCustomer API"——但粒度太粗。更有效的策略是:
searchCustomer:
allowed_params: [customerId, email]
denied_params: [*]
rate_limit: 100/minute
require_approval: false
context_constraints:
- max_consecutive_calls: 5
- require_previous_tool: [authenticate, validateSession]这种细粒度的策略定义在配置上看起来繁琐,但它是可复用的。一旦定义好一组工具的策略模板,新 Agent 只需要声明自己使用哪些工具,策略引擎会自动匹配。
处理拒绝和降级
当拦截层拒绝一个工具调用时,Agent 不能直接崩溃。一个好的设计是支持三种决策:放行、拒绝、挂起。拒绝时返回一个结构化的错误信息给 Agent,让 Agent 可以尝试替代方案(比如换一个工具、修改参数重试、或者向用户请求确认)。挂起则把调用放入审核队列,等待人工确认或超时后自动拒绝。
这个模式在金融和医疗场景里尤其重要。一个 Agent 可能 99% 的工具调用都是低风险的,可以自动放行,但 1% 的高风险操作需要人工确认。如果拦截层只有"通过/拒绝"两种结果,要么风险太高,要么效率太低。
与现有安全框架的关系
工具调用拦截层不是替代已有的 Guardrails 或安全护栏,而是补齐一个缺失的层次。输入输出层面的 Guardrails 管的是"LLM 看到了什么、输出了什么",拦截层管的是"Agent 要做什么"。两者是互补关系。
在实际部署中,一个完整的 Agent 安全架构应该是四层:
输入层:对用户输入做 sanitization 和注入检测 策略层:定义 Agent 能做什么、不能用什么工具 拦截层:在运行时对每次工具调用做实时验证 审计层:记录所有工具调用的完整轨迹,包括拦截决策和原因
OWASP LLM06 的防御建议里特别强调了"跨工具数据流的严格边界"——每个工具调用应该运行在独立的上下文中,前一个工具的输出不应该自动成为下一个工具调用的"可信输入"。拦截层是实现这个边界的基础设施。
如何评估拦截层的效果
评估一套拦截层架构的有效性,不是看它拒绝了多少调用,而是看它是否在正确的位置做出了正确的决策。三个指标值得关注:
第一是误报率。拦截层把合法调用误判为高风险,会导致 Agent 效率下降、用户体验变差。如果误报率太高,团队会倾向于放宽策略,反而削弱了安全效果。每一条拦截规则上线前,应该在历史数据上做回测。
第二是漏报率。被拦截层放过的攻击性调用占比。这个指标需要持续的红队测试来校准——不是一次性验证,而是随着 Agent 能力升级和攻击手法演化,定期做对抗性测试。
第三是审计覆盖率。拦截层是否记录了每次决策的完整上下文——包括调用的原始参数、LLM 生成它的推理上下文、拦截层的决策依据。这个数据不仅用于合规审计,也是改进拦截规则的训练素材。
落地时容易踩的坑
第一个坑是"想一次覆盖所有攻击"。拦截层的能力取决于策略定义的完备性,而策略定义是一个持续迭代的过程。先覆盖最核心的几种攻击模式(参数注入、工具链滥用、高频调用异常),然后根据实际运行数据逐步扩展。追求完美覆盖反而会拖慢上线进度。
第二个坑是"把拦截层做成黑盒"。拦截层的决策必须可解释——当一次调用被拒绝时,Agent 开发者需要知道为什么被拒、是哪个规则触发的、参数里哪个字段触发了规则。没有可解释性的拦截层,在调试阶段会成为巨大的障碍。
第三个坑是"忽略拦截层自身的安全"。拦截层本身也是一个服务,如果它的配置接口、策略加载、审计日志存储存在漏洞,反而会成为新的攻击入口。拦截层应该独立于 Agent 系统部署,使用最小权限原则,并且自身的操作日志也要被审计。
下一步
如果团队已经在用 Agent 框架做生产级应用,工具调用拦截层应该是一个优先级不低的工程投入。不需要从零开始——Guardrails AI、LangChain 的 ToolMessage 验证钩子、Semantic Kernel 的 IFunctionFilter,这些现有组件已经提供了拦截层的基础能力。关键是要把这些组件串联成一个完整的、可观测的、可迭代的拦截系统,而不是零散地散布在代码里。
对于刚开始搭建 Agent 的团队,建议在 Agent 框架选型阶段就把工具调用拦截作为一个架构需求写进评估标准。选择支持拦截器模式或回调模式的框架,会大幅降低后续安全建设的成本。
#AI应用架构 #AI应用开发 #Agent应用架构 #Agent应用开发
夜雨聆风