
1. 从“会说话”到“会动手”Sub-Agents到底带来了什么新问题先说个我自己的经历。早先做 Agent 项目的时候主 Agent 只能聊天、查资料最多调一下搜索接口我当时觉得这东西挺安全——毕竟它没有腿跑不远。直到有一天我给主 Agent 加了工具调用让它能写文件、能执行 Shell 命令、能调内部 API。那一刻我意识到Agent 有“手”了。有手的 Agent 能干实活但也能闯祸。后来为了让任务拆得更细、并发更高我又引入了 Sub-Agents也就是子代理让它去专项处理代码生成、数据处理、文档分析等任务。结果问题一下子变得复杂起来子代理不仅继承了主 Agent 的能力还会在无人值守的情况下自主决策。这就像你请了一个手脚麻利的助手但这位助手偶尔会自作主张你还得给他戴上一副“手套”再把他关进一间“玻璃房”里干活。这里说的“手套”就是一套围绕 Agent 和 Sub-Agents 展开的安全机制“玻璃房”就是沙箱。本文要聊的就是当 Agent 有了手安全边界怎么划沙箱怎么兜底Sub-Agents 的权限和隔离怎么做才不至于失控。内容主要面向正在做 Agent 开发、多 Agent 编排、或者准备把 Agent 接入生产环境的工程师。就算你是刚接触 Agent 框架的新手读完也能搞清楚“子代理为什么需要沙箱”以及“最小权限到底怎么落地”。1.1 从“会说话”到“会动手”能力边界变了风险模型必须跟着变聊天机器人阶段Agent 的输出只是一段文本它不能改变任何外部状态。你说错了用户看到了顶多觉得不专业。但一旦接入了工具调用、代码解释器、文件系统、数据库连接Agent 的每一个决策都会产生真实影响。它执行一条rm -rf不是不可能取决于权限配置它调一个内部支付接口也不是不可能取决于你是否给了它密钥。这个阶段Agent 的本质就从“信息处理器”变成了“操作执行器”。Sub-Agents 进一步放大了这种变化。主 Agent 负责理解任务、拆解目标子代理负责在特定领域内执行。你想想看主 Agent 做规划子代理跑代码、查数据、生成报告多 Agent 协作的效率确实高但安全责任也跟着分摊到了每个子代理身上。任何一个子代理出现越权操作、提示词注入或者资源滥用都可能让整个任务链出问题。最麻烦的是子代理的执行过程往往不是全人工盯着的它更像一个自动化流水线错误会在无人干预时快速累积。我做项目的时候最开始也天真地以为“只要模型本身足够强就不会出错”。后来被现实教育了模型的判断再准也保不齐它在极端情况下绕过了安全约束。安全不是靠模型的自觉而是靠架构层面的硬隔离。这也是我写这篇文章的初衷——把 Sub-Agents 的安全兜底机制讲清楚。1.2 Sub-Agents 带来的三层核心风险第一层风险是权限放大。主 Agent 为了完成任务通常会获得一组工具权限和凭证。如果设计偷懒直接把主 Agent 的凭证复制给所有子代理那么任何一个子代理都拥有了全部能力。这就像公司里每个人都拿着总经理的门禁卡出了事根本分不清是谁操作的。子代理可能只是为了写一个临时脚本结果顺手把生产环境的配置文件也读了出来这不是恶意只是权限边界没划好。第二层风险是上下文混淆与数据串扰。多个子代理在同一个会话上下文中并行运行它们的中间结果、工具返回值如果互相污染就可能出现“A 子代理拿到了 B 子代理的密钥”这类离奇事故。我遇到过真实案例两个子代理共用了同一个临时目录一个写文件另一个读文件结果 A 生成的中间数据被 B 当成了输入最终产出了一份完全错误的报告。这个问题的本质是状态隔离没做到位。第三层风险是资源耗尽与失控循环。Agent 具备自主决策能力后如果陷入死循环或者递归调用宿主环境的 CPU、内存、磁盘都会被拖垮。更要命的是如果子代理可以创建子子代理理论上调用层级可以无限加深。我在测试阶段就见过一个子代理因为解析失败而反复重试一次任务调了几百次外部 API费用账单惨不忍睹。这种失控不是偶发而是 Agent 系统在缺少护栏时的必然产物。2. “手套”怎么选Sub-Agents 安全机制的三种核心设计既然风险清楚了接下来就是怎么兜底的问题。我把它概括成三件事权限最小化、执行隔离、全程审计。这三件事不是选择题而是叠加关系。你可以在不同粒度上做组合但少一个都会留下明显的破绽。2.1 权限最小化不给任何子代理发“万能钥匙”权限最小化原则说起来很简单每个子代理只拥有完成自己任务所需的最小权限集合。但落地的时候绝大多数人会踩坑。最常见的错误是图省事直接复用主 Agent 的环境变量、API Key、文件系统读写权限。这么做短期看不出问题一旦某个子代理被提示词注入攻击它就能以主 Agent 的身份访问所有外部资源。我自己的习惯是给每个 Sub-Agent 单独定义角色。比如代码生成子代理只需要一个受限的代码执行环境和一个临时目录的写权限数据分析子代理只需要只读的数据源连接和输出目录的写权限。每一个权限都用一个显式的配置项描述而不是复制全局参数。这样做的好处是即使某个子代理的行为失控它能造成的影响范围也是预先定义好的、可控的。权限最小化还需要配合时效性。子代理的凭证应该有生命周期任务结束后立即失效。这里可以借鉴云厂商的临时凭证方案给子代理签发的令牌有效期只有几分钟而不是让它拿着一个长期密钥。这样即使密钥在子代理的日志里被泄露了别人捡到也用不了太久。2.2 执行隔离把每个子代理放进自己的“工作间”执行隔离是第二道防线。核心思想是子代理的执行环境之间互相不可见子代理和宿主环境之间只保留必需的通信通道。这就像工厂车间里的独立工作间每个工人在自己的房间干活工具放在自己手边如果要传递半成品通过传送带完成而不是直接进入别人的房间。在 Agent 框架层面隔离可以从三个纬度展开文件系统隔离每个子代理有自己的临时目录任务结束自动清理不能访问宿主目录和其他子代理的目录。网络隔离子代理只能访问白名单内的域名和端口对外部网络不可见。尤其当子代理要执行外部工具时网络白名单是最有效的防线。进程隔离如果子代理要执行代码必须放到独立的进程或容器里跑而不是直接在宿主进程内执行。这三个纬度里网络隔离最容易被忽略。很多人觉得“子代理只是跑一下 Python 脚本不需要网络”但脚本里往往藏着urllib、requests这些库。如果不对网络做限制一个看似人畜无害的脚本可能把宿主的元数据信息回传到外部服务器。所以我在设计 Agent 平台时凡是要执行代码的子代理一律默认断网只有明确需要调用外部 API 的子代理才单独开网络白名单。2.3 审计与追踪没有日志安全等于没做第三件事是审计。无论权限和隔离做得多好都无法保证百分之百不出事。出了事之后能不能快速定位、复盘、止损靠的就是审计日志。我给每个子代理执行的关键操作都加了一层透明的记录包括调用了哪个工具、传了什么参数、返回了什么结果、消耗了多少资源、花了多长时间。有人会觉得“加日志会影响性能”。实际上对于 Agent 这种低频、大 payload 的执行场景日志写入的开销微乎其微。真正影响性能的是高频小请求而 Agent 的调用往往以秒甚至分钟为单位多写几行日志根本不构成瓶颈。反而是万一出了安全问题如果日志不全你只能靠猜那时候付出的召回成本比写日志高得多。审计日志还要解决一个关键问题用户能不能追溯“这个子代理为什么做了这个决定”。这意味着日志里不仅要记录操作还要记录触发该操作的上文。一个简单有效的做法是在子代理的输入消息中注入一个任务 ID所有工具调用都带上这个 ID方便后续把决策链串起来。3. 沙箱怎么兜底一个可落地的 Sub-Agents 安全配置有了前面三件套的理论基础这一节进入实操环节。我会从一个常见的场景出发你有一个主 Agent它有一个子代理专门负责“本地代码生成与执行”。这个子代理需要读写文件、执行 Python 脚本、可能还需要调用一个内部工具 API。我们要做的事情是给这个子代理戴上“手套”——配置好权限、隔离、审计再把它放进“沙箱”——限制它的执行边界。3.1 场景定义一个典型的需要“戴手套”的 Sub-Agent假设我正在做一个企业级的智能文档处理平台。主 Agent 接收用户指令比如“把这份 Excel 统计一下生成报告”。它把任务拆成三步数据清洗、数据统计、生成报告。其中数据清洗和统计交给一个“数据分析 Sub-Agent”它需要使用 pandas 来处理文件然后把报告草稿交给“报告生成 Sub-Agent”后者调用一个内部模板 API 生成最终文档。这里的关键点是数据分析 Sub-Agent 要执行 Python 代码但它不应该访问数据库不应该调用外部网络不应该读取宿主环境变量。报告生成 Sub-Agent 需要调用内部 API但它不应该执行任意代码。两个子代理的职责隔离得很清楚权限范围也应该完全分开。在这个场景里如果我只用传统的“提示词约束”——在 prompt 里写“不要访问数据库不要读取环境变量”——那几乎是没用的。你可以想象一下模型在极端情况下比如提示词注入或者上下文被污染时完全可能忽略这些约束。所以必须上硬隔离。3.2 配置实操从零到一给 Sub-Agent 戴上手套下面是这套配置的四个关键步骤。第一步定义子代理角色与权限白名单。我建议用一个显式的配置结构而不是在 prompt 里零散地写权限要求。我用 YAML 比较多结构清晰也方便后期维护。配置内容大致如下sub_agents: data_analysis: description: 负责数据清洗与统计分析 tools: - name: execute_python allowed_imports: [pandas, numpy, re, json, csv] denied_imports: [os, subprocess, socket, requests, urllib] - name: file_operator allowed_paths: [/tmp/data_analysis, /tmp/input/] denied_paths: [/etc, /home, /var, /root] - name: internal_report_api endpoint: http://10.200.10.10:8080/render allowed_methods: [POST]这里有一个细节值得展开allowed_imports和denied_imports并不是写在 prompt 里让模型自觉遵守而是要在代码执行沙箱里做强制校验。也就是说在 Python 执行器的exec或compile阶段拦截掉未授权的模块导入。拦截os、subprocess这些模块是底线因为它们能直接和操作系统交互。第二步配置沙箱执行环境。我用的方案是给每个子代理单独起一个 Docker 容器镜像固定为纯 Python 运行环境不安装任何额外的系统工具。关键配置如下docker run -d \ --name sub_agent_data_analysis \ --network none \ --memory 512m \ --cpus 1.0 \ --pids-limit 128 \ --read-only \ -v /tmp/input:/tmp/input:ro \ -v /tmp/data_analysis:/tmp/data_analysis:rw \ python:3.11-slim \ sleep infinity这里逐条解释一下各参数的意义--network none禁掉容器网络。数据分析子代理不需要访问外网连内网 API 都不用调所以直接断网。这是最彻底的网络隔离方式。--memory 512m限制内存上限防止子代理写出内存溢出的脚本把宿主拖垮。--cpus 1.0限制 CPU 使用率防止死循环占满全部核。--pids-limit 128限制进程数。如果不限制子代理可以 fork 出大量进程形成进程炸弹。这个参数很多人容易漏掉我在这里特别提一下。--read-only把根文件系统设为只读容器内不能随意写文件只能写入挂载的/tmp/data_analysis目录。这套配置跑起来之后即使子代理执行了一段恶意代码它能做的也只是在/tmp/data_analysis里写点数据既访问不到宿主的敏感文件也连不上网络更不可能拖垮整个机器。这就是“沙箱兜底”的意义——不需要假设子代理一定忠诚而是假设它可能失控然后让失控的后果最小化。第三步配置调用链路的审计日志。我习惯在每个子代理的请求入口处加一个过滤器统一记录请求来源、目标工具、参数摘要和耗时。示例代码如下def log_tool_call(sub_agent_id, tool_name, args, result, duration): audit_event { event_type: tool_call, sub_agent_id: sub_agent_id, tool_name: tool_name, args_preview: str(args)[:500], # 避免记录超长参数 result_status: ok if result else error, duration_ms: duration } audit_service.ingest(audit_event)第四步设置任务级超时与重试上限。没有超时控制的子代理就像一个没有闹钟的午睡一旦睡死过去你也不知道它什么时候会醒。我给每一个子代理任务设置了三层超时单次工具调用超时比如 30 秒、单轮子代理运行超时比如 5 分钟、整条 Agent 链路超时比如 15 分钟。只要超过时限就强制终止并记录一个execution terminated事件。重试上限制成 2 次并且在每次重试前将错误日志回传给 Agent 本身让它决定是换策略还是彻底放弃。3.3 验证这套机制的三个关键测试配置好不等于安全了。我每次都会跑一组针对性测试确认不是只有“形式上的手套”而是真正能挡住风险。下面三个测试是必做的。第一个测试越权文件访问。让数据分析子代理执行os.listdir(/etc)或者直接读取/etc/passwd。预期结果是Python 执行器返回权限错误子代理收到失败反馈。如果容器配置里忘了加--read-only或者挂载目录时误把宿主根目录映射进去了这个测试会立刻暴露出问题。第二个测试资源耗尽阻断。给子代理一个死循环脚本比如while True: pass或[0]*10**10。预期结果CPU 被限制在 1 核以内内存超过 512m 后被 OOM 杀死进程数超过 128 后被pids-limit拦截。在整个测试期间宿主机器的压力应该几乎为零。这个测试我跑过很多次大多数沙箱方案都能拦住内存炸弹但pids-limit这一层如果没设置进程炸弹就能钻空子。第三个测试数据泄露防护。在容器内尝试读取环境变量print(os.environ)预期结果是只能看到镜像内置的基础环境变量宿主的数据库密码、API Key 完全不可见。这个测试的关键在于容器环境变量和宿主环境变量是天然隔离的前提是你没有在docker run的时候用-e把敏感变量传进去。这三个测试全部通过之后我才会放心地把子代理接入正式任务流。不要觉得这些测试针对的是“恶意攻击者”很多时候你不是被恶意代码偷袭的而是被自己写的脚本坑的——比如不小心在数据分析脚本里加了os.environ的调试输出结果敏感信息被你自己的日志收集系统存了一份。4. 常见问题与排查技巧实录配置过程中我踩过的坑不少这里整理成一份对照速查表再挑几个实际案例详细说说。4.1 典型故障对照表问题表现可能原因排查方式解决办法子代理无法访问内部 API网络白名单没加查沙箱日志确认网络请求是否被拦截在白名单中显式加入内部 API 域名和端口子代理写出了大量垃圾文件临时目录没做配额查看磁盘占用确认哪个目录暴涨给临时目录挂载单独的磁盘配额或定时清理子代理执行任务一直不返回缺少任务级超时查看日志确认任务卡在哪一步设置三层超时强制终止超时任务多个子代理互相读写文件共享临时目录检查各子代理的挂载路径是否重叠每个子代理分配独立的临时目录子代理打印了敏感环境变量环境变量未做隔离检查容器启动参数只通过白名单传入必要的环境变量子代理运行报“execution terminated due to error”超时或 OOM 被终止查看终止事件号判断是超时还是资源限制调整超时时间或增加资源配额4.2 几个我踩过的坑第一个坑是轻信网络隔离。最初我用 Docker 容器跑子代理只做了内存和 CPU 限制把网络忘了。结果一次测试中子代理生成的 Python 脚本通过urllib访问了外网拿回了一段带有提示词注入指令的文本后续子代理读了这个文本之后行为明显异常。这件事让我彻底明白execution 环境里“默认断网”必须写死在基线的配置里。第二个坑是只限制“做了什么”不限制“用了多少”。我最初的沙箱配置里没有pids-limit直到有一次子代理代码出了 bug在循环里不断启动子进程几分钟内主机负载飙升我才意识到进程数也是需要限制的。后来我把pids-limit调成 128再没遇到类似问题。第三个坑是审计日志不全。早期日志只记录了“调用成功与否”不记录参数摘要。有一次子代理在跑批任务时突然中断我想定位是哪一步导致的结果日志里只有一堆状态码完全还原不了现场。后来我改成记录参数摘要和耗时之后这类问题的排查时间缩短了至少一半。第四个坑是权限模型从“黑名单”做起。刚开始我采用的是“默认放开、逐个封禁”的策略结果就是不断被新的风险点打脸封了os模块又冒出subprocess封了subprocess又冒出ctypes。后来我彻底换成白名单思路默认全部封闭只放开必要模块这才真正清净了。白名单确实要花点心思设计但它带来的安全感是黑名单给不了的。4.3 上线前请把这组检查跑一遍每次给新的 Sub-Agent 接入生产任务之前我都会过一遍这份检查清单你可以直接拿去用这个子代理拥有哪些工具权限每一项权限是否都有业务必要性子代理的运行环境是否默认断网如果需要联网白名单是否精确到了域名或端口粒度是否限制了 CPU、内存、进程数磁盘写路径是否只指向独立临时目录子代理的临时凭证是否有有效期任务结束后是否立即回收是否配置了三层超时单次调用、单轮运行、整条链路关键操作是否有结构化审计日志日志里能否串起完整的调用链任务是否支持强制终止与快速回滚终止后外部副作用是否可控这组检查听起来繁琐但实际执行起来也就是半个小时的事。我宁可花这半个小时也不想在深夜被报警电话叫起来处理环境被拖垮的事故。最后再分享一个小技巧Agent 的安全方案里优先级最高的永远不是“防住每个已知风险”而是“让任何一个未知风险都能被限制在局部”。手套的意义不是保证手不脏而是让脏东西不会沾到全身。沙箱的意义也不是让子代理永不犯错而是让它犯错之后代价依然可控。带着这个思路去设计 Sub-Agents 的安全边界你会少走很多弯路。