ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

AI Agent安全:工具调用如何真正“刹得住车”?

AI Agent安全:工具调用如何真正“刹得住车”? 1. 一次实打实的“刹车无响应”事故1.1 从“查一下”到“已经查完”只隔了一个回车我在维护一个用来做公开信息聚合的检索Agent它要定期从各类官方网站抓取公告、名单、目录然后整理成结构化表格。我们内部叫它“信息助理”主要跑的是完整链路Query理解、工具调用、网页抓取、解析、写回样例库。说白了这就是个上了真实网络、但暂时没有接生产数据的试验品。那天我想测试一套新的提示词流程输完最后一段“把某个机构官网上的最新公示名单取下来按表格整理出来”点击运行。跑到第三步时我瞄了一眼日志发现传给查询工具的ID参数里带了个多余空格按这个参数肯定查不到正确结果。我下意识敲了终止键嘴里喊了一句刹车Agent的反应速度比我手指快得多。鼠标点上“停止生成”那一刻卡片上其实已经发出了工具调用。服务端日志显示GET请求出去了状态码200页面抓完了解析器甚至已经按关键词筛出了十来条记录。参数错了但请求没拦住页面该缓存缓存该解析解析一点没耽误。标题里那句“喊完刹车AI已经溜出去查政府网站了”在我这里就是日志截图上的真实台词。注意这不是模型输出“偏了”这么简单。终止键只停止了大模型继续吐字可工具调用一旦离开进程边界就变成了外部世界里的真实事务你这边喊停那边该发生的照常发生。尤其是当目标站点是那种“你查询一下”就默认返回大量公开数据的网站——包括政务公开、公示公告这类入口时一次操作引起的连锁反应远比一句“之前忘了删空格”要严重。1.2 事故扩散开来是什么样有人觉得抓个网页而已又不是下单发货至于吗单独一次确实不至于。我那次事故的副作用也就是多了一个缓存文件清掉就好。可麻烦就麻烦在Agent的工作方式不是单点操作而是拆成一长串连续动作工具调用不会只来一次。一个检索任务往往是“搜索关键词→打开候选页面→翻页→解析表格→再把这些中间结果喂给下一轮LLM”。只要第一轮请求没有被拦截后面整条链路都会基于错误数据继续跑。多从属很容易放大错误。如果这个Agent跑在多Agent协作架构里前面有编排者后面有执行者下游还会根据上游产出去触发更多动作。一次越权访问会像扔进池子里的石子波纹传导到整个协作网络。被访问的网站那边可不在乎你是Agent还是人。请求进站就会打日志、占带宽敏感一点还会触发风控和封IP。你把访问频率和并发提上去对方管理员看到的就不是“一个AI在查资料”而是一次不大不小的异常流量。所以在Agent工程实践里工具调用永远不等于纯文本生成。它不是模型吐字吐歪了那种可羞可改的问题而是需要当作“进入真实系统的动作”来管理。你要给AI装刹车不能只在聊天窗口装一个按钮要在调度链路的每一层门上都装一个制动器。2. 刹车为什么拦不住Agent执行链路上的三块“盲区”2.1 “停止生成”只停在了文本这一层这里把Agent内部的运行模型讲透。绝大多数LLM Agent的核心循环长这样用户把请求变成上下文模型根据上下文生成回复回复里同时包含普通文本和结构化的工具调用运行时解析出工具调用交给执行器执行器拿到外部系统返回的结果再作为新消息回填给模型模型基于新消息继续生成下一轮直到满足停止条件。用户点“停止”时标准前端做的是中断步骤2的流式输出或者给步骤5发取消标记。看起来整个生成过程停了可执行器在步骤3里发出去的请求早就过了网络边界。更要命的是执行器的线程能不能感知到取消标记完全取决于你有没有在代码里做好配合协作的检查点。很多Agent开发者习惯在工具函数里做阻塞式网络请求或者干脆sleep几秒等外部接口返回中断信号来了也不一定能立刻打断。这不是技术上的“残缺”而是不同职责被封装在一起后天然产生的缝隙。思考环和控制环是两个物种前者会听“停”后者更忠于“已经在跑”。你让大脑刹车脚已经踩下油门了物理上就停不下来。2.2 客观竞态用户和Agent在抢同一秒事故还有一个更隐形的层面——竞态。Agent在执行计划时和用户并不是严格同步的。用户在界面上看到“正在调用xx查询工具”那已经是服务端把状态推出来的结果现实里请求可能已经完整往返了一次。信息从网络传到屏幕上需要时间人类读完这句话再决定“要停”也需要时间。两个时间差叠在一起你说“停”的时候系统时钟往往已经往前跑了小半秒。我们后来专门在QA环境里给Agent加了访问日志时间戳对比“用户点击终止”和“外部请求返回”两个事件。结果好几次都是请求完成比用户点击终止早了600到800毫秒。也就是说在用户的认知里“刚喊出口”在系统里“事情已经办完”。这就像短跑比赛里你听到发令枪响才起身但别人在枪响之前已经跑出去了。刹车指令发出的时候车已经在路上这不是刹车失灵是刹车踩晚了。2.3 “只读操作”不等于“零副作用”再来聊一个工程上很容易被忽略的点把类型标注为GET的网页抓取当成“只读操作”。开发的时候直接写requests.get、fetch的人太多了总觉得GET是安全的不修改服务端资源就不会有问题。但从自动化系统的角度看“安全方法”只是HTTP协议语义上不修改服务端状态并不代表“网络上没有副作用”一次GET会给目标站点留下访问日志和来源IP会触发服务端埋点、统计、通知、动态渲染任务会占用第三方带宽有可能违反站点robots.txt或服务条款如果页面里带CF清洗、cookie校验你的一次访问还可能提前消耗掉会话状态。所以设计审批策略时不能只看工具名里是read还是write要看它作用的领域边界本地内存是安全区测试样例库是受控区真实第三方网站是风险区。对风险区里的“读”一样要有克制力一样要能随时叫停。3. 给Agent装上真正能停住的“三级刹车”吹完事故直接上解决方案。我们改造的目标不是“所有操作都要人工确认”那样Agent就彻底失去了自动化的价值。目标是把动作按副作用分级让低风险动作继续自动通过高风险动作强制过一道人工闸门同时保证取消信号能真正穿透到执行链路的内部。3.1 第一级刹车给所有工具打上副作用标记我们在动作调度层加了一个审计器内部维护一张副作用分级表。工具注册的时候开发者必须标明自己的副作用等级没登记的默认拒绝执行宁可不干活也不能出去乱跑。等级典型工具默认策略S0本地缓存读取、向量库查询、纯计算自动执行S1对受控环境的外部只读请求测试站、mock、内网文档自动执行并全量记日志S2对真实第三方站点的访问、抓取公开网页、公示平台进入人工审批队列S3提交表单、发送通知、购买、部署、写生产库人工审批双人确认完整审计这表看着简单却是整个改造里最花时间的地方。因为老代码里很多工具注释只写了“查询”概念上差不多真实作用域完全不一样。“查询企业名”可能是查本地向量库也可能是去公开公示网站抓。改的时候不能按函数名分类必须按“工具最终会碰什么资源”分类。所以后来每个工具登记时都强制附一段边界声明可能触达的网络域、数据敏感度、是否可能诱发后续动作、请求频率上限。这个信息同时会进审批队列作为用户判断“要不要放行”的依据。3.2 第二级刹车在任务编排层引入门控状态机工具标记只能解决“能不能自动”还没解决“喊停后怎么停下”。我们给每个Agent任务加了一个轻量状态机pending - approved - running - done pending - cancelled running - paused - resumed - running running - cancelled所有S2及以上请求在进入执行器之前必须先挂在pending状态。挂在pending的请求会展示给用户一张审批卡卡上包含目标地址、访问方式、URL参数预览、预计影响。用户可以选择批准、拒绝、延迟。延迟等于让Agent先干别的这个请求整体挂起不占执行线程。这里最关键的一点是门控不能只设在“工具执行前”那一瞬间还要设在“生成计划之后、执行工具之前”的整个段落。也就是说当Agent输出一长串行动计划而不是单次动作时调度器先整体看看计划里有没有高危调用。有的话就把涉及的调用单独拆出来先求确认再往下走。这相当于在“计划”和“执行”之间加了一道调节阀而阀门后才是状态机控制室。我们给调度器写了一个简单的守卫逻辑核心思路是这样的日常查询任务秒回不打扰当检测到计划里出现副作用等级大于等于S2的动作时立刻从链式执行切换到“先列影响清单、再请求确认”的执行模式。这个模式切换在代码里就是一行分支判断但对使用者体感而言是从“一个闷头往前冲的Agent”变成了“一个知道分寸的合作伙伴”。3.3 第三级刹车幂等、补偿和可撤销刹车之后车已经溜出去的情况依然防不胜防。这时就需要三种补救设计它们和前面的闸门配合形成最后一道防线。第一让动作尽可能幂等。S2级别里像网页抓取这类的动作天然幂等抓两次结果差不多可以随便重试。S3级别要用幂等令牌比如提交同一份报告时带上request_id目标服务端收到相同ID直接忽略避免重复下单、重复发消息、重复写库。第二准备补偿操作。比如Agent发出了一封通知邮件补偿方案就是撤销信或者自动再发一封更正它写错了测试数据库补偿方案就是导回原快照。补偿动作本身也是个工具也要挂到审批链上不然就会出现“Agent自动把错误覆盖成另一个错误”的连环事故。第三要支持取消令牌的传播。我们在实现里做了一个cancel token除了在进程内各协作线程间传递还会持久化到分布式队列。当用户在UI上按终止调度器除了把任务标记成cancelled还会去查这个任务名下所有正在执行的对外请求。只要请求里提前带了traceId就能区分哪些是“正准备出去的”、哪些已经到第三方站点了。对于前者直接丢弃对于后者尽量保存现场不让它继续往下游触发更多动作。那些底层框架里叫interrupt、checkpoint、conductor的东西本质都是一回事让用户中断穿透抽象层而不是只停留在界面上。4. 改造实录一个可审可撤销的信息检索Agent这一节放的是项目落到地上的记录。项目背景是多Agent协作环境里的一个信息检索Agent带RAG用来辅助整理公开信息要求能自动抓取也必须保证每一次对外请求都可追溯、可中止。4.1 架构与开发顺序系统拆成三个模块Orchestrator编排器、Dispatcher派发器、ApprovalService审批服务。Orchestrator还是原来那套LLM循环负责理解任务、产出调用计划。Dispatcher不再直接执行工具而是接收编排器发来的“待执行动作列表”先去副作用表查等级需要审批的就提交给ApprovalService由它把审批卡推到前端去等用户反馈。为了做到“人喊停时能停正在跑的动作”我们把动作放进线程池执行并在每个执行函数里加了协作信号。这里的关键是停止信号必须传进handler内部让它在读网络流的每个chunk时都检查一次。不这样设计网络请求会死不回头地把整个响应读完然后才想起原来自己早就该停了。4.2 编排器里的“先审后跑”编排器增加了一个判断环节核心流程用Python伪代码表示大概是这样的while not task.finished: plan planner.generate_next_actions(task) high_risk [act for act in plan if severity_of(act) 2] if high_risk: agreed await approval_gate(high_risk) if not agreed: task.cancel() break for act in plan: result await dispatcher.dispatch(act, task.cancel_token) if task.cancel_token.is_set(): return partial_result(task) task.memory.append(result)两个细节值得展开说。其一approval_gate不是简单发一个“同意/拒绝”按钮就完事它把影响渲染成一个侧面面板要访问的URL、代理环境、抓取深度、是否跟随重定向、预计耗时、甚至目标站点的robots约束全摆上去。用户看到的是一个能辅助判断的三维视图而不是被逼在信息不足的情况下做决定。其二取消令牌以任务为单位从planner生成第一个动作到最后一个工具执行完毕全程跟着跑。外层一旦发现cancel_token被置位就不允许再发起任何新的外部请求。旧请求仍在栈里的由handler的协作退出点自己决定在那结束。两者配合才算是真正把刹车装到了执行链路的心脏位置。4.3 回归验证故障注入与中断实验针对这次改造我连续做了三种回归测试普通检索不被打断。S2请求进入审批队列用户五秒内批准Agent顺利完成总延迟从原来“无审批”的3秒涨到“拉起人批”的8秒。这个增幅对检索类任务完全可接受。用户拒绝高风险动作。Agent经历拒绝事件后不会原地崩溃而是把“用户拒绝了什么、原因大概是什么”写进memory。下一轮生成计划时它会自动避开那个方向而不是执着地反复试“同一条死路”。极速中断。启动一个抓取两千个分页的S2任务跑到第四个分页时模拟终止信号。日志显示当前分页请求在收到cancel信号后最多用了400毫秒退出外部没有残留请求已抓下来的部分结果按照“可丢弃”标记做了清理。确切的数字不重要重要的是行为模式Agent可以在不牺牲自动化的前提下逐步收敛风险而不是靠把网络访问权整个关掉来换取安全。4.4 审批卡之后的后处理链路审批通过只是开始后面还有一堆细节。我们自己实现的ApprovalService在后端会给每个审批动作生成唯一的action_id并把审批结果回填给LLM上下文。这样做有一个好处Agent知道自己哪一个具体动作被批准了哪一个是拒绝的不会把上一次的批准误当成全场通行证。审批卡上还带一个“来源类型”字段比如“政府网站公开页”“行业协会名录”“企业官网公告”来源类型不同被允许的抓取频率也不同。普通公告页我们限制一分钟内不超过三次请求大型公开数据页希望拆成多个子任务分批抓取而不是单个Action里一次性并发几十个连接。这里所有限制参数都记录在审计表里出了事能直接定位到是哪一次调用、哪个参数、哪条审批链。5. 事故背后的通用启示Agent安全不是加一个if这么简单5.1 别把“同意”设计成一次性许可改造过程中我犯过一个印象深刻的错误用户批准了一个动作后Agent把同样的带参数动作批量执行了十几次。因为审批只做了“动作级别”的判断没做“会话级别”和“模式级别”的管控。批准一次翻页没问题可Agent把整个分页遍历都当成一次动作了本质上就是越权。后来我们把审批粒度拆成三种单次动作批准、动作模板批准、任务级批准。单次批准默认只对当前action_id有效模板批准允许Agent在同一个任务中按相同参数模式的请求自动通过任务级批准要求人类给整个计划背书。粒度越多出事概率越低操作者心理负担也越重。最终选了模板批准作为默认因为它和“查某一系列页面然后汇总”这种最常见的检索任务自然对齐。5.2 对外部服务的边界保持敬畏Agent要去访问别人的服务就要接受别人的约束。robots.txt不是摆设站点限流不是刁难。我们再把半自动抓取落到“公开网页”这个场景时把并发调低到了三以下同时在小流量延迟既保证Agent跑得通也避免触发风控。这些措施已经不属于控制链路里的东西但它们是Agent跟外部世界打交道时该有的基本礼貌。另一条让我记了很久的经验是公开数据不等于随意数据。哪怕一个网页不需要登录就能看也不代表我们可以批量抓走、存进自己的知识库。尤其当页面里包含个人姓名、联系方式、证件编号这类信息时抓取和使用都可能超出预期边界。我们在信息检索Agent的审批卡里专门加了“是否进入RAG知识库”和“是否包含个人敏感字段”两个开关。点批准的人看得清清楚楚不是闭眼放行。5.3 多Agent协作时的刹车传播我们项目里真正让人头皮发麻的问题耦是“多Agent协作”场景下的中断。主Agent拆出子任务给副Agent副Agent接到任务后去执行外部访问。如果用户在主Agent界面按了停止子Agent那边完全不知道可能还在继续抓取。这不是代码没写对而是取消令牌没有跨进程、跨Agent传递。我们的方案是给每个子任务下发时带上parent_cancel_token子Agent的内部循环每执行一个工具前都检查一下令牌状态。父级任务一旦被取消子Agent会收到信号并决定是否回滚已拿到部分结果。这套逻辑不一定适合所有框架但给我提了个醒在“多AI协作”的架构里所有Agent共享同一个“刹车通行证”比给每个Agent单独装刹车重要得多。5.4 给Agent工程实践的一份自查清单最后整理一份适合大多数Agent团队的自查清单贴合这次案例再絮叨一遍想让AI跑得比别人快先让它听得见刹车。每个工具是否登记了副作用等级审批队列是否按动作粒度和会话粒度拆开了终止信号能不能传到执行器慢操作内部网络流读取、轮询循环、worker线程补偿动作是不是也走了审批流程外部请求日志是否包含task_id、action_id、trace_id出了乌龙能不能在一分钟内复原全链路取消令牌是否跨了多Agent边界子Agent是否继承了上级的撤销状态是否处理过“用户已批准但任务已经完成”的竞态避免用户批准了一个早该取消的动作按这张单子过一遍即使不能让Agent完全杜绝“溜出去”你至少能把这种事故从不可控的“惊吓”变成一条带完整审计记录的“插曲”。Agent跑得快是本事但让你喊停时它真能停下才是真正能上线的本钱。
返回列表