ARTICLE DETAIL

资讯详情

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

AI Agent安全执行代码:E2B沙箱隔离实践与工具封装指南

AI Agent安全执行代码:E2B沙箱隔离实践与工具封装指南 1. AI Agent执行代码的普遍困境与E2B的核心思路做过AI Agent工程化的人大概率都撞上过同一个问题模型负责产出代码可这代码得有个地方真正跑起来。我最初用OpenAI的Code Interpreter时觉得这体验真顺但一旦到了自己搭平台、做企业内部工具、或者给Agent接自定义数据源执行环境就得自己扛。当时的处境是这样的数据分析、文件转换、报表绘图、甚至网页内容提取都依赖一段段临时代码。Agent的完整闭环是观察任务-生成计划-执行动作-观察结果-调整计划而执行动作里运行代码往往是占比最高、变数也最大的环节。代码能不能安全地跑直接决定这个闭环能不能转起来。我最早的处理方式很朴素本地写一个execute_python函数拿subprocess去跑。配合一张禁用函数清单把shutil.rmtree、os.remove、eval、exec这些全列入黑名单。听起来还行实际一测就露馅了。禁了shutil.rmtree模型可以用os.walk配合os.unlink循环删禁掉了直接的exec模型可以通过写一段带编译逻辑的脚本绕过。更隐患的是提示词注入本身无法完全避免——Agent在读取网页、文档甚至Excel单元格内容时可能被隐藏指令影响而生成恶意代码。如果执行环境是我自己的开发机等于让陌生人进了家门还给他递上了钥匙。我也想过Docker方案用docker-py动态创建容器来隔离。隔离效果确实比subprocess好一个量级但也有新麻烦镜像拉取、容器启动延迟、Docker daemon的权限管控、镜像仓库的维护成本这些对普通Agent应用来说太重了。你要的是一个用完即走的执行环境而不是一套容器编排系统。后来我调研到E2B这个项目。它的核心思路很直接不要试图审查代码可不可信而是默认所有AI生成代码都不可信把一个隔离的Linux环境microVM变成可编程的沙箱执行完拿走结果、销毁环境。带着这个思路我把当时手头三个Agent项目里的执行模块全部切换到了E2B捣腾了快两个月写下了这篇分享。先放一张当时我整理的方案对比信息密度很高值得存方案隔离强度启动速度Agent接入成本适合场景本机subprocess直接跑几乎为零即时最低只执行完全可信的内部脚本正则/沙箱库如deno中快中受限程度高的Node场景Docker容器中上秒级到十秒级中服务编排、集成测试E2B沙箱microVM高秒级配合模板低Agent动态执行不可信代码E2B给我的第一印象是SDK写得像个正经产品不是玩具。它提供Python和TypeScript两种SDK简单几个调用就能创建沙箱、跑命令、读写文件、下载产物。但在讲接入之前我想先把它的沙箱模型讲透。2. E2B的沙箱模型一个用完即走的Linux环境2.1 从最低限度的代码看沙箱生命周期先看一段最简单、最完整的E2B代码from e2b import Sandbox # 创建沙箱默认模板 sbx Sandbox() # 执行系统命令 out sbx.commands.run(python3 --version) print(out.stdout) # 关闭沙箱 sbx.close()这段代码背后发生了三件事创建一个隔离的Linux环境在这个环境里执行命令并收集输出销毁环境。理解这三件事的顺序就理解了E2B的整个使用模型。Sandbox就是那个隔离环境。它不是进程级的隔离而是虚拟化级别的隔离——沙箱跑在一个独立的microVM里有自己的文件系统、进程树、网络栈跟你的宿主机几乎没有共享面。这比在容器里跑或者在本机起个进程要硬核得多安全边界由虚拟化兜底。sbx.commands.run()是同步执行等命令跑完返回输出。它把stdout、stderr以及退出码都带回来了。如果只是执行一段短命令这个接口够用。还有sbx.process.start()这种异步接口用于启动后台进程或长驻服务后面讲多步任务时再展开。sbx.close()是回收动作很多人写代码时会忘。忘了的后果是云上资源一直被占用接着就是账单和限额报警。后面我会讲自动化处理方式但至少你该记住每个创建的沙箱最终都必须关闭。2.2 文件系统与命令执行配合起来的实际例子既然Agent要跑代码一个高频场景就是生成数据文件然后执行分析脚本处理它。我用一个订单数据汇总的例子说明from e2b import Sandbox ANALYZE_SCRIPT import pandas as pd df pd.read_csv(/workspace/orders.csv) summary df.groupby(category)[amount].sum() print(summary.to_string()) sbx Sandbox() # 写入数据文件 sbx.files.write(/workspace/orders.csv, category,amount\nA,100\nB,200\nA,300\nB,400) # 写入要执行的分析脚本 sbx.files.write(/workspace/analyze.py, ANALYZE_SCRIPT) # 执行 out sbx.commands.run(python3 /workspace/analyze.py) print(out.stdout) sbx.close()这里有个值得注意的细节我习惯先写脚本文件再执行而不是直接把长代码塞进python3 -c ...。原因很现实——长代码塞进命令行shell转义、换行、引号嵌套迟早会坑你一次而写成文件再执行逻辑清晰、报错时也方便把文件导出来排查。这个习惯在接入Agent之后尤其管用因为模型生成的代码动辄几十行经不起命令行转义的折腾。文件的读写权限也遵照Linux直觉写入路径需要确保父目录存在所以在写/workspace/下的文件时最好先有个mkdir -p /workspace的动作或确认模板里已有该目录。默认沙箱环境里自带一些常用目录但没有magic——你不创建、不写入那就是空的。2.3 沙箱的临时性与持久化边界E2B沙箱默认是临时的。你创建的沙箱一旦close()所有文件、安装过的包、跑过的进程全部灰飞烟灭。这个特性对执行不可信代码恰恰是优点任务结束、环境消失恶意代码没有落脚点。但如果你希望代理间共享中间产物或者一个Agent任务需要分多步跑、每步之间保留变量那你得主动管理方案一整个任务期间复用同一个Sandbox实例不close。方案二把重要产物通过下载接口保存到本地再写进下一个沙箱。这两个方案没有谁绝对好取决于你对隔离的诉求有多强。我自己的习惯是不确定任务边界时按每个任务一个沙箱处理宁可多创建几次也不要让两个任务的文件互相污染。后面第5章会专门展开多智能体场景下的共享与隔离取舍。3. 把E2B封装成Agent可调用的Tool完整示例3.1 定义一个清晰的Function SchemaE2B本身不关心你用哪家的模型它只是一个可编程沙箱。真正的接入工作是把E2B调用包成一个LLM能理解的工具函数。我用OpenAI Function Calling的格式来定义这个工具{ type: function, function: { name: execute_code, description: 在隔离沙箱中执行Python代码返回程序输出。沙箱已预装pandas、requests、matplotlib等常用库。代码中需要使用print打印结果。, parameters: { type: object, properties: { code: { type: string, description: 要执行的完整Python代码 }, timeout_seconds: { type: number, description: 超时时间默认60, default: 60 } }, required: [code] } } }写描述时有个讲究一定告诉模型输出结果要用print打印。模型训练数据里见过很多只计算不输出的代码风格如果你不强调它可能跑出一个无输出的结果然后你对着stdout发愣它还一脸无辜。实测下来把这句话写进description能显著减少这类无效执行。3.2 实现执行器的关键细节工具函数我封装成下面这样from e2b import Sandbox from e2b.exceptions import TimeoutException def execute_code_in_sandbox(code: str, timeout_seconds: int 60) - str: sbx Sandbox(timeouttimeout_seconds 30) try: # 清理并重建工作目录避免上次运行残留 sbx.commands.run(rm -rf /tmp/task mkdir -p /tmp/task) # 把代码写入沙箱 sbx.files.write(/tmp/task/main.py, code) # 执行实际超时留出余量 exec_timeout min(timeout_seconds, 90) out sbx.commands.run( python3 /tmp/task/main.py, timeoutexec_timeout, ) if out.error: return f[stderr]\n{out.error} return out.stdout except TimeoutException as e: return f[执行超时{timeout_seconds}秒]{e} finally: sbx.close()几个细节值得展开为什么沙箱timeout要比命令timeout多30秒因为命令执行和沙箱销毁是两段独立资源消耗。如果命令本身就要60秒沙箱生命周期却只有60秒那最终很可能在命令刚跑完、正要收集输出时沙箱被回收了。多出30秒的余量保证整个流程从容收尾。为什么要先rm -rf /tmp/task同一个Agent对话中工具可能被调用多次。如果上轮代码在/tmp/task里留下了残留文件下一轮代码可能会无意识地读到它导致结果不可预期。对模型来说这种隐形状态是最难debug的——它不知道上一轮留下了什么。为什么用out.error判断而不是只看返回码E2B的CommandOutput对象里stdout和stderr是分开的。把stderr原样返回给模型模型才能根据报错信息自己修正代码。这是让Agent具备自我修复能力的关键一环。3.3 接入Function Calling主循环有了工具函数接下来把它接进Agent主循环。伪代码级的完整流程如下import json import openai client openai.OpenAI() messages [ {role: system, content: 你是数据分析Agent。需要运行代码时调用execute_code工具返回值是代码输出。}, {role: user, content: 读取这段数据统计各分类总金额categoryA,amount100;...} ] for _ in range(10): resp client.chat.completions.create( modelgpt-4o, messagesmessages, tools[EXECUTE_CODE_TOOL], # 就是上面的JSON ) msg resp.choices[0].message if not msg.tool_calls: # 模型最终回答 print(msg.content) break # 这一步很关键带着tool_calls的消息要完整放回上下文 messages.append(msg.model_dump()) for tool_call in msg.tool_calls: args json.loads(tool_call.function.arguments) name tool_call.function.name if name execute_code: result execute_code_in_sandbox(**args) else: result 无效工具名 messages.append({ role: tool, tool_call_id: tool_call.id, content: result, })这个循环我实测跑了很久稳定。需要注意一个版本相关的小问题OpenAI Python SDK不同版本对msg.model_dump()和tool_calls的序列化行为有差异如果你用的是较新版本直接messages.append(msg)有时也work但model_dump()最保险——它会把工具调用请求完整保留模型才能基于执行结果继续思考。3.4 为什么不让模型直接传多条命令或shell脚本有不少人问过我既然沙箱是完整Linux环境为什么不在工具参数里提供一个shell_commands让模型直接传shell脚本我更推荐只暴露执行Python代码这一种形态。原因有两个。一是模型写Python比写Shell熟练得多输出稳定性更高二是Python代码可以塞进文件带上print出错时栈信息清晰模型更容易根据反馈自我修正。Shell脚本的报错信息五花八门模型看着都头大。如果你需要执行Shell命令不要新开工具直接在Python代码里用subprocess.run包一层就行import subprocess result subprocess.run([ls, -la], capture_outputTrue, textTrue) print(result.stdout)这样既保留了执行通道又统一了反馈格式Agent的决策链路更简单。4. 从测试到生产五个最常踩的坑4.1 超时不是随便设的——timeout语义要搞清E2B里有两层timeout第一次用的人很容易混沙箱创建时的timeout参数是沙箱整体生命周期上限到点自动销毁。sbx.commands.run(cmd, timeout...)里的timeout是单条命令的执行上限到点杀掉这条命令。这两者必须协调。如果命令timeout设成120秒沙箱timeout也设成120秒在极端情况下第118秒命令还在跑沙箱就面临销毁最终拿不到完整输出。我的经验公式命令超时 30到60秒缓冲 沙箱生命周期。同时单条命令超时最好不要超过3分钟因为大部分Agent任务里的代码真正常超过3分钟的往往是死循环或效率极低的操作。让模型试着写更高效的实现比无脑等待更值。另外超时抛出的是TimeoutException你要显式捕获并返回给模型。否则异常冒泡Agent的整个对话流程就断了。4.2 不可信代码的网络策略默认禁网是常态有了沙箱不代表就能放养。尤其当你执行的是来源不可信的代码比如Agent从网页提取的信息里夹带的代码片段网络访问权必须收敛。E2B为沙箱网络访问提供了配置能力可以设置允许或阻断的域名。我的策略很简单大多数数据分析场景根本不需要外网数据文件由你写进沙箱执行的代码也不该联网。那就直接禁网或仅放行必要域名。这个策略也能防住一类偷数据行为如果沙箱里有敏感数据文件而执行的代码能自由联网它完全可以把文件内容POST到任意服务器。禁网之后这条路就断了。注意这里的网络配置参数在不同版本的E2B SDK里写法会变使用前翻一下当前版本的官方文档最稳妥。原则先记住——默认禁网按需放行。4.3 大文件读取Artifacts才是正解沙箱里跑数据分析经常会导出图表、CSV、Excel。E2B的sbx.files.read()接口有大小限制我记得默认是10MB级别存疑时不要愣头青直接读大文件。超过限制的文件用Artifacts下载机制处理# 同步或异步调用下载接口把大文件拉回本地 # 具体接口名称和参数在不同SDK版本中存在差异以官方文档为准 sbx.artifacts.download(/workspace/plot.png, ./output/plot.png)我踩过的坑是最开始把所有文件都当普通文本读取结果一个十几MB的CSV直接报错我还在怀疑是不是网络问题。后来才意识到E2B对小文件交互式读写和大文件下载设计了两条不同通路业务代码要选对。实践中我建议在工具函数里加一条明确约定沙箱内执行结果如果涉及生成文件尽量把文件路径写入stdout。这样模型知道哪些文件需要下载给用户你再根据输出里的路径去拉Artifacts。4.4 沙箱泄漏auto_dispose与总量控制沙箱资源泄漏是我见过最普遍的隐形炸弹。模型可能连续调用十次工具如果你的代码每次创建沙箱而忘记关闭后台就会攒下十个僵尸沙箱持续计费。三条硬性经验所有创建沙箱的代码必须用try/finally或with风格确保close()。沙箱创建时设置auto_dispose参数比如300秒就算异常导致close()没执行沙箱也会在一定时间后自动销毁。在平台侧记录并发沙箱总量超过阈值时拒绝新任务。别等账单出来再心疼。有一次我在本地调试一晚上崩溃了三次第二天一看后台多了六个沙箱。从那以后auto_dispose成了我的默认参数。4.5 模板预热把秒级启动变成常态每次从零创建沙箱都要经历基础镜像加载和初始化一个裸沙箱几秒到十几秒启动是常见情况。这个延迟在单次调用里还能忍但在Agent多次调用工具的场景下累积起来就非常影响体验。E2B的Template机制就是干这个的把装好依赖、配好目录、做好系统设置的环境保存成模板之后每次创建沙箱都基于这个模板启动速度会快很多。我个人的模板制作路径是先在沙箱里把常用库装一遍确认环境工作正常再把这个沙箱保存为模板而不是直接在本地Docker里搭完上传。原因很简单——沙箱里验证过的环境才真正匹配运行时行为。实测下来预装依赖的模板不仅启动更快还能减少Agent为了装一个包现场pip install然后等待一分钟的低效操作。我甚至会在模板里提前把/workspace/_output这类目录建好省去每次手动mkdir。5. 从单Agent到多智能体沙箱复用与隔离的取舍5.1 共享工作区多Agent协作的常见需求聊到多智能体Multi-Agent很多人默认每个Agent一个独立沙箱是对的。但实际做项目时会发现一个数据分析流水线拆成三个Agent之后它们经常需要处理同一份数据、连续生成中间产物。我的做法是在协作型任务里让多个Agent共享同一个沙箱实例约定统一的共享目录。举个例子规划Agent负责拆分任务把步骤写进/workspace/tasks.json。执行Agent读取tasks.json逐个步骤跑代码。质检Agent扫描/workspace/output/*.csv检查数据完整性。这种情况下如果每个Agent各起一个沙箱数据就被人为割裂了——规划Agent写的计划文件执行Agent根本看不见。共享沙箱配合固定目录约定才是真正贴合业务的架构。共享沙箱的代价是缓存污染。一个Agent留下的临时文件可能影响另一个Agent的判断。所以共享场景下一定要约定目录规范目录用途清理策略/workspace/input/原始输入数据只读不清理/workspace/task/当前任务临时文件每次任务开始前清空/workspace/output/最终产物任务结束后下载并清理5.2 严格隔离多租户和敏感数据的底线共享沙箱虽然爽但碰到多租户场景就必须回到每任务一沙箱的隔离模式。用户A的数据分析任务和用户B的任务如果共用沙箱会发生数据串联。尤其涉及密钥、隐私数据时私密性不是优先级问题而是底线问题。我在一个内部工具里就是这么处理的按用户ID任务ID维度创建独立沙箱任务结束即销毁。数据落地和沙箱生命周期完全绑定用户A永远不可能在文件系统层面碰到用户B的数据。这种场景下沙箱的创建并发量会明显上升需要关注两个点并发上限E2B配额限制和你的付费套餐有关超过上限会创建失败代码里要有重试和排队。成本控制每个沙箱都对应真实云资源消耗。设计上要给每个任务设定资源上限比如单沙箱最大运行时长、单任务最多创建多少个沙箱防止一个失控的Agent把成本打爆。5.3 一个多Agent协作沙箱的目录规范示例最后给一个可以直接抄的规范。这个规范我在项目里迭代了四五次目前比较稳定/workspace/ ├── input/ # 不可变输入数据 ├── task/ # 每轮任务隔离目录 ├── output/ # 最终产物 ├── agents/ # 各Agent专属工作区 │ ├── planner/ │ ├── analyzer/ │ └── reviewer/ └── logs/ # 每步执行的日志配套三条约束凡是写入input/的操作视为违规审查时会直接标红。每轮工具调用前执行器清理task/目录不转储上一轮内容。最终结果只允许出现在output/统一由Artifacts下载机制拉回本地。有了这套规范多Agent协作的文件流是透明的任何一个中间文件都可以追溯到是哪个Agent、哪一步生成的。对于多智能体协助开发这类更复杂的场景这套逻辑同样适用——只是把代码执行换成了代码评审执行编译测试执行安全边界和安全协议完全可以复用。我在实际项目中感受最深的一点是执行环境的安全设计不能只靠某一个工具的隔离强度还得看你自己的业务编排有没有遵守清晰的边界纪律。E2B提供了很好的技术底座但真正让Agent安全落地的东西是你对沙箱生命周期、网络策略、目录规范、超时机制这套组合拳的执行力。如果你也想做从0到1搭建AI Agent我建议在动手写第一行业务逻辑之前先花半天时间把执行沙箱这一层打通。这半天省下来的是后面几个月无穷无尽的模型乱跑代码排查时间。
返回列表