
AI代理AI Agent落地时最刺激的永远是“技能Skill调用”那一下。我在某Agent平台做安全支撑时几乎每天都要回答同一个问题模型被诱导了怎么办技能越权了怎么办恶意输入注入了怎么办我的答案始终落在两件事上策略引擎做前置裁决沙箱隔离做兜底隔离。这篇文章不聊花哨的算法只讲我们如何把“技能安全执行”拆成可以落地的设计和代码以及在真实环境里踩过哪些坑。如果你正在做Agent产品、正在设计技能执行框架或者只是好奇大模型应用怎么在权限上刹住车接下来的内容值得看完。1. 为什么AI代理的安全问题不能用老办法解决1.1 技能执行链上新增的三个风险面AI代理与传统API服务最大的不同是它把“意图”和“动作”之间的链条拉长且自动化了。传统API每个接口有固定参数、固定权限调用方是明确的人或服务Agent则是由模型基于自然语言生成一个动作序列再分发到不同技能模块执行。这中间产生了三个传统接口层不会面对的风险面。第一个风险面是意图与动作的解耦。模型理解的“用户意图”和最终执行的动作可能被提示注入攻击改写。比如用户要求“帮我查一下销售数据”模型本来会生成调用内部报表接口的动作但如果某个外部内容在对话中被注入了恶意指令模型可能把动作改成“把报表发到某个外部地址”。这意味着我们不能只校验“输入文本”必须校验“即将执行的动作本身”。第二个风险面是上下文跨越。Agent通常在同一个会话里连续调用多个工具前一个调用的输出会成为后一个调用的输入。用户很难感知一次对话里实际发生了多少次跨权限调用。传统系统靠“用户身份接口权限”就能隔离但在Agent里用户身份不变可实际操作多个系统权限边界变得很模糊。第三个风险面是自动执行闭环。为了把产品体验做顺很多团队默认让Agent自动执行低风险操作只有高风险操作才转人工。可是“低风险”和“高风险”的划分如果只在写代码时定死等新增技能时往往跟不上节奏结果会漏掉一部分需要管控的动作。这个坑我们第一次上线时就踩过当时只覆盖了几个核心技能后面每加一个技能都要重新审视权限模型成本极高。所以我的结论是AI代理的安全设计必须重点解决“动作层面的授权”问题而不是再去强化单点接入的鉴权。单账号的权限体系覆盖不了多步动作链必须有一个集中式的裁决层替每一个动作做决策。1.2 安全目标拆解不做什么比做什么更重要设计伊始我和团队列了一份“绝不做什么”清单这份清单反而比功能列表更有用。我们约定绝不允许模型直接执行带副作用的动作哪怕用户语气再着急删除、批量发送、修改配置这类操作也必须经过审批绝不把安全规则写死在业务代码里规则要能热更新要能被审计要能被非开发人员评审绝不追求“完全自主”AI代理的体验可以流畅但高风险动作必须保留人工兜底。在此基础上把目标拆成三个可测量的指标阻断率、误杀率、逃逸率。阻断率指明显恶意或越权的动作被拦截的比例误杀率指被误拦截的正常请求比例逃逸率指已发生违规但未被发现的比例。这三个指标互相制约调策略的时候如果只盯着阻断率误杀率会飙高只追求低误杀逃逸风险又变大。我们后来用策略引擎配合灰度流量就是为了同时管理这三个指标。还有一件事要提前想清楚技能类型差异很大。我们内部把技能分成三个风险等级只读信息型、受控操作型、高风险变更型。只读型技能即使是访问敏感数据重点也在数据脱敏和字段级权限受控操作型技能比如发送邮件、创建工单重心在目标白名单和频率限制高风险变更型技能比如删除文件、批量更新数据库优先级永远是“先审批后执行”。这个三级分类贯穿了后面所有策略和沙箱设计一开始不把分类定清楚后面规则会越来越乱。2. 策略引擎的选型与整体设计2.1 为什么要把安全决策从代码里抽出来最早我们确实把安全检查写在每个技能模块里结果不到两个月就撑不住了。新增一个技能要改代码、发版本、重新走测试而且每个模块的安全判断标准不统一有的判断参数是否包含特殊字符有的只判断数量级有的干脆只检查文件扩展名。这种嵌入式写法的最大问题是安全规则和业务逻辑绑定在一起业务一变安全就碎。策略引擎的思路是把“做什么决定”这件事从具体业务代码里抽出来变成一个独立服务或独立模块。它接收的是“一次动作请求”的结构化描述输出的是“允许、拒绝、需要人工审批、需要额外验证”的决策。业务代码不关心规则怎么定义只负责执行决策。这种解耦带来的直接好处有三个策略变更可以不经发版热更新安全团队可以单独审计策略不用去翻散落各处的代码决策链路有统一的地方记录出问题追查路径清晰。我们花了一个迭代周期做重构把原来散落在技能模块里的几十个安全检查全部迁移到策略引擎虽然当时看起来多花了一点时间但后续每次新增技能、每次应对新的攻击手法都轻松了很多。2.2 策略语言从DSL到结构化配置在设计策略语言时我们踩过一个不小的坑一开始想用自研DSL做一个类似“IF 技能 send_email AND 收件人 NOT IN 白名单 THEN 需要审批”的语法。原型跑通后团队里没人愿意维护写DSL解析器的只有一个人其他人看一眼就头疼。后来我们果断换成了结构化配置也就是用JSON和YAML描述策略再用一个简单的规则引擎来解释。一个策略条目长这样- id: rule-high-risk-action-001 version: 5 name: 高成本动作必须人工审批 priority: 90 when: all: - field: skill.name op: in value: [file.delete, db.truncate, batch.send_email] - field: env.is_production op: eq value: true then: decision: require_human_approval reason: 生产环境高危操作强制双人审批 ttl_minutes: 15这里有几个关键设计点。字段全部来自结构化的“动作请求上下文”而不是原始对话文本。技能名、目标资源、数据敏感级别、用户角色、租户、环境标签、模型置信度这些都是字段。只允许在特定字段上做匹配不允许在策略代码里写任意逻辑这样可审计性才有保障。操作符尽量简单。我们只保留了eq、neq、in、not_in、exists、regex、cidr_match、time_range这几类够用且不会把人绕晕。支持regex是因为有些技能要按路径白名单匹配比如只允许写工作目录下的文件路径规则必须能做模式匹配。决策字段不只有allow和deny还引入了require_human_approval和require_secondary_check。前者是转人工审批流后者是发起一次额外验证请求比如让用户在App端确认一次动态码。很多动作谈不上“禁止”但机械地允许又不放心提供中间态能大幅降低误杀率。每条策略必须有条目id和版本号。决策日志里必须记录命中的策略id和版本否则策略一改回溯就全乱了。这一点我们做过血的教训早期没有版本字段规则更新后出了疑似逃逸事件根本说不清当时到底执行的是哪一套规则。2.3 决策流程一次裁决的完整路径策略引擎的裁决流程我习惯叫“五步裁决”从请求进入引擎到返回结果路径是固定的。第一步构建动作请求上下文。Agent侧在决定调用某个技能前会把动作意图、参数、目标资源、当前用户、所属租户、环境信息、模型标识、trace_id打包成一个标准结构发给策略引擎。这里特别强调一点参数必须先做一次规范化文件路径要转成绝对路径邮箱要用统一格式URL要解析出scheme、host、port避免策略匹配时被格式差异糊弄过去。攻击者经常利用相对路径、URL编码、大小写变体来绕过规则规范化这步就是第一道过滤网。第二步拉取相关策略。引擎先根据租户、环境、技能名三个维度缩小候选策略集而不是把所有策略拉出来一条条跑。候选集通常压到几十条以内再做精确匹配。这一步如果没做索引规则量一上来性能只会越来越差。第三步逐条评估。按照优先级从高到低执行命中的决策类型如果是deny直接短路返回如果命中的是require_human_approval则继续评估后续低优先级规则因为低优先级规则里可能有allow白名单允许某些内部IP或特定用户跳过审批。这个“继续评估”的细节很关键它保证了白名单和兜底策略可以并存。第四步合并决策。合并规则很简单deny大于一切require_hman_approval优先于allowallow直接返回都没有命中则返回默认deny。注意默认动作是deny也就是“白名单默认拒绝”双模式。没有命中任何策略的请求默认拒绝避免新增技能时因为策略缺失而裸奔。第五步记录决策日志并返回。决策日志里要包含请求id、策略版本快照、命中规则、决策结果、耗时、触发原因。这些数据后面审计会用到。我们曾经因为这一步偷懒日志里没记请求参数结果事故回溯时只能看到“拒绝”或“允许”完全还原不了攻击手法的上下文后来才补上。2.4 性能优化三层缓存与异步审计刚开始我们把策略引擎做成每次都实时查询规则库QPS一上来就爆炸。优化思路分三层。第一层是规则索引。把策略表的id、优先级、适用范围字段做成内存索引启动时加载全量规则变更时通过订阅消息增量更新。决策过程几乎不打数据库。我们实测下来一次决策本地执行耗时中位数在0.5毫秒左右全链路加网络传输在8毫秒左右对Agent场景完全够用。第二层是决策缓存。相同用户、相同技能、相同参数特征对参数做hash的请求在TTL窗口内直接复用决策结果。这个缓存对非敏感只读技能特别有效命中率能到60%左右。但要注意高风险技能不能开缓存或者TTL要极短一句话的功夫就可能让一个delete操作溜过去。缓存键的设计也值得注意必须把租户、用户、技能名、参数hash、环境标签都放进去少一个都可能串权限。第三层是异步审计。决策日志不阻塞主线流程先写入本地环形队列由后台任务批量刷到审计存储。审计存储必须保留原始请求和决策结果一旦发现逃逸事件可以回溯当时的完整决策链。性能优化不要牺牲审计完整性这是底线。3. 沙箱隔离兜底的最后一道墙3.1 为什么有了策略引擎还要沙箱策略引擎即使再完善也只是“理论上”拦截风险。现实的残酷在于规则永远比漏洞慢一步。某种提示注入可能构造出策略引擎认不出来的动作描述某个技能的代码本身可能有漏洞被参数触发后执行了模型根本没打算执行的操作某个依赖库的已知漏洞可能在爬取一个URL时被远程利用。这些情况光靠前置拦截是堵不住的必须假设“技能代码会被突破”然后让被突破的代码无路可走。沙箱想解决的问题不是“允许做什么”而是“万一跑了不该跑的代码它能访问到什么”。我们内部有一句话策略引擎决定让谁进门沙箱决定进门的人能碰到哪些东西。把沙箱放在技能执行的最底层相当于给策略决策加了一个保险丝。3.2 隔离方案选型三条路径对比做沙箱隔离我评估过三类方案各有取舍。第一类是进程级隔离基于操作系统提供的namespace和seccomp机制把子进程限制在独立的mount、pid、network namespace里再配合只读文件系统和系统调用白名单。优点是资源开销小、启动快适合高频低风险技能缺点是隔离强度有限进程逃逸时风险较大不适合跑可信度低的代码。第二类是容器级隔离在进程隔离基础上叠加容器的镜像和资源限制体系把整个Agent技能放到容器里跑通过只读根文件系统、无特权容器、禁用设备挂载等加固项来收紧。优点是隔离性较好部署运维工具链成熟适合大多数业务技能缺点是开销比进程级高一些启动速度和镜像管理需要额外处理。第三类是语言级虚拟机隔离比如用某种沙箱化的运行时来执行技能代码把外部系统调用翻译成受限操作。优点是隔离边界清晰很多危险系统调用直接不支持性能也可以接受缺点是只能跑特定语言生态的技能对依赖动态库、系统调用的技能不友好。我们最终选择的是“轻量容器seccomp”的混合路线常规技能跑在容器里特殊高吞吐技能跑在进程级环境里两类沙箱都接入统一的策略引擎。选这个方案是因为它在隔离强度、性能、团队认知成本之间平衡得最好。三种方案的快速对比大致如下方案隔离强度性能开销部署复杂度适用场景进程级隔离中低低高频、低风险技能容器级隔离较高中中大多数业务技能语言级虚拟机高中高高可信度低、需要最强隔离的技能3.3 最小化沙箱环境的落地清单要让沙箱真正起到兜底作用光有容器技术还不够关键在于“最小化”原则。我把它总结成一份清单照着做基本不会出大问题。网络必须是白名单模式。容器默认没有网络技能需要联网时由沙箱网关在容器外做一层域名和IP白名单检查只允许访问标记为信任的API域名。不让容器直接出网是防止数据外泄最重要的一条。文件系统做成只读并挂载临时工作目录。技能的代码和依赖放在只读层里容器启动后获得一个临时可写目录大小限制在几百MB以内进程结束后自动销毁。技能要写文件只能写进临时目录路径不能超过预设长度文件名要做过滤。系统调用过滤用最小白名单。基础白名单只有read、write、close、fstat、mmap、brk、exit、epoll_wait这些遇上需要额外系统调用的技能单独给这个技能所属的策略组增加白名单。不要让所有技能共享一套宽泛的白名单否则等于没设。资源限制按CPU和内存双上限控制。内存上限设置成技能正常峰值的两倍左右太紧了会导致初始化失败太松了又起不到防失控作用。CPU配额和超时时间配合使用执行超过时限直接杀进程。一个实际的沙箱配置模板大致长这样sandbox: image: agent-skill-base:2024.03 read_only_rootfs: true tmpfs: - /workspace: size512m,mode0700 network: disabled seccomp_profile: default-strict resource: cpu: 0.5 memory: 1GiB timeout: 30s capabilities: drop: [ALL]3.4 沙箱内外的通信边界沙箱里执行代码不难难的是怎么把它“能做的事情”限制在业务真正需要的范围内。我们的做法是沙箱进程内部不开放通用HTTP客户端只开放一个Agent执行SDK提供的受限调用接口。SDK内置了带白名单校验的HTTP封装、受限文件读写、格式化输出等能力。技能作者写代码时只能调用这些能力不能直接发起任意请求。这样一来沙箱的网络和文件系统都能被策略层控制住而不是让技能代码自己到处乱飞。一个容易被忽略的细节是输出序列化。沙箱执行完技能后返回给Agent的不仅是结果文本还可能包含大对象、临时文件引用、异常堆栈。如果对这些输出不设限攻击者可能通过技能把沙箱里的敏感数据打包返回。所以我们在沙箱出口再加一层输出校验结果大小上限、可序列化类型白名单、禁止返回文件路径、异常信息做脱敏处理。这个“出口校验”和入站校验一样重要很多团队只盯着入口忽视了出站。4. 策略引擎与沙箱的联动设计4.1 一次技能调用的完整生命周期把策略引擎和沙箱放在一起后一次技能调用的完整生命周期大致如下Agent收到用户请求后先做意图识别和技能选择。如果即将调用的技能带有副作用Agent侧会把动作请求上下文发给策略引擎。策略引擎裁决返回allow、deny或require_human_approval。allow则请求进入沙箱调度器deny则请求直接终结需要审批则进入待审批队列。沙箱调度器根据技能名称选择对应的镜像和资源模板从预热好的沙箱池里取一个空闲实例注入会话上下文。沙箱实例内的技能代码开始执行期间可以调用受限SDK。沙箱出口做输出校验后结果返回到Agent层。一次会话里如果还要继续调用下一个技能就重复上面的裁决流程。这里要特别强调决策和沙箱分配的绑定关系不能断。策略引擎给出的决策结果要带一个决策上下文ID沙箱调度器执行时把这个ID也带上审计系统通过这个ID把“决策-执行-输出”三个阶段日志串在一起。没有这一环事后排查逃逸事件时根本对不上号。4.2 审批流里的上下文保留require_human_approval这个中间态实际落地时比想象中复杂。核心问题在于Agent的会话上下文是有状态的如果请求进入审批队列而Agent那边上下文很快失效审批通过后再继续执行技能可能拿不到必要的中间数据。我们为此做了一个“上下文快照”机制。进入审批队列前Agent侧把当前会话的关键上下文——用户历史消息、已执行的决策结果、技能参数、临时文件列表——序列化保存一份带一个审批票据ID。审批通过后Agent按票据ID恢复上下文重新走一次策略裁决。这次因为有了审批票据直接命中allow再进入沙箱执行。这个机制看起来多绕了一圈但安全性上得到了保证。审批通过不意味着动作无限期有效恢复后二次裁决时如果环境里的敏感标记发生了变化比如数据分级从低风险变成了高风险仍然可以拦截住。这也是“审批只是过程不是豁免”的原则。如果贪图方便把审批通过直接当作后续所有动作的白名单很快就会被滥用。4.3 审计系统出了事怎么还原现场安全设计如果缺了审计等于没有闭环。我们最后一次大事故排查就是靠完整的审计日志才在几小时内定位到具体是哪一个技能的哪一个参数触发了违规。审计日志的每条记录都包含五个关键字段trace_id关联整个链路决策ID把策略引擎一侧的日志和沙箱执行日志串联起来策略版本快照记录当时生效的每一条策略的状态因为规则会热更新没有版本快照就说不清当时到底执行的是哪套规则执行摘要记录沙箱内部的关键系统调用次数、网络连接目标、文件写入列表、超时时间输入输出指纹对技能入参和返回结果做哈希匹配上哈希就知道这次执行结果是否被篡改过。还有一个经常被忽略的点审计日志不仅要记录成功的命令还要记录被拦截的请求。很多团队只关注“出了什么事”不关心“差点出什么事”。被拦截的请求恰恰能反映策略的覆盖情况和攻击趋势我们会定期分析拦截样本库用来迭代策略规则和调整沙箱白名单。反馈闭环越强安全系统的进化速度越快。5. 落地过程中的坑与排查实录5.1 策略过严导致技能大面积误杀系统刚上线第一周技能误杀率冲到35%运营同事直接炸锅。排查后发现是策略配置过粗只要技能参数列表包含“环境”字段我们就要求人工审批结果很多正常只读查询因为带了环境标识就走审批体验极差。这个教训很直接策略字段的粒度必须和技能的实际行为对齐。只读技能不应该和高风险变更技能走同一套审批策略。我们把技能分成只读、受控操作、高风险变更三个风险等级不同等级默认策略不同。只读技能只做敏感数据字段检查高风险技能才强制审批。调整后误杀率降到2%以内而阻断率基本没有变化。5.2 沙箱内存限制过紧技能起不来某个进行PDF解析的技能在沙箱里反复启动失败打开日志才发现是内存限制给少了。这个技能要加载一个比较重的解析库启动时峰值内存比我们预估高了30%。我们最初按一般技能的两倍峰值设定限制结果偏紧。后来改成按技能分别标注资源画像每次发布技能前先在沙箱里跑一遍基准测试记录CPU、内存、启动时间、网络请求数再按基准的2.5倍设定实际上限。这个“按技能设资源”的做法虽然增加了上线工作量但显著减少了运行时问题。至于内存不足导致的安全风险则通过沙箱自身的隔离能力来控制不会因为资源给多了就放开限制。5.3 一个文件描述符引发的逃逸排查某次安全扫描发现一个沙箱进程的日志里出现了异常的文件路径访问。顺着审计日志一查发现技能代码里有一行“读取配置文件”的逻辑没有直接用SDK的受限文件接口而是偷偷用了一种绕过方式拿到了宿主机上某个路径的文件描述符访问能力。这个案例很值得复盘。问题根源不是沙箱技术没生效而是技能代码本身绕过了我们规定的SDK接口。我们的整改措施是沙箱内禁止加载原生的网络和文件操作模块在SDK层面做符号级的白名单同时对沙箱进程做定期扫描发现调用白名单之外模块的直接隔离下线。这也是为什么我们后来特别强调“出口校验”——绕过SDK的代码就算拿到了数据也要能被堵在沙箱内。5.4 策略引擎性能瓶颈正则解析拖垮QPS有一阵子策略引擎的P99延迟从8毫秒直接飙升到300毫秒定位后才发现是某个新加的策略用了一个特别复杂的正则表达式参与评估的次数又多直接把CPU打满了。优化方案有两条一是给规则加“粗筛索引”比如按技能名先排除80%无关规则再走精确正则匹配二是取消对用户输入长文本做高开销正则改为分词和抽样匹配或者把正则拆成多个简单规则组合。经过这两项处理P99延迟降回10毫秒以内。现在我们对新策略有一个硬性要求上线前必须做性能自检单条规则在最坏输入下的评估耗时不得超过100微秒。5.5 调整策略时要盯三张图别让规则静默失效最后分享一个运维层面的经验。每次调整策略或沙箱参数我会同时盯三张图拦截量趋势、误杀率趋势、逃逸事件趋势。任何参数变更都要先在影子环境跑一轮对比这三张图确认没有新的风险波动再全量生效。安全这件事最怕“静默变化”——新的策略规则上线了违规拦截量反而突然少了一大截这时候大概率是规则出了漏洞而不是攻击真的变少了。我个人在这些项目里最深的一个体会是AI代理的安全不是某个单一组件能搞定的策略引擎和沙箱隔离必须配合起来一个管“怎么进门”一个管“进门后能碰到什么”。而两者之间那条审计链路才是让安全问题可以被讨论、被复盘、被迭代的基础。后来者如果能少踩几个同样的坑就不亏了。