1. 项目背景与核心定位
1.1 这个项目到底解决什么问题
先说结论:Zorv AI 对话框 Python 执行,本质上是给 AI 对话框装了一个"代码运行沙箱"。用户在对话框里写一段 Python 代码,系统直接执行并返回结果,而不是只给一段代码让用户自己复制到本地跑。
我最初接触这个需求时,第一反应是"这不就是 Jupyter 换个壳吗"。但真正动手才发现,事情远没有这么简单。AI 对话框的最大特点是会话上下文——你在对话框里让 AI 写一段代码,AI 不仅要执行这段代码,还得记住之前聊过的内容,知道上一次运行定义了哪些变量、生成了什么结果。这种"有状态"的交互,比单纯的代码执行工具复杂一个量级。
举个例子:用户先问"帮我读取一下这个 Excel 文件的前 5 行",AI 执行完返回结果;接着用户说"把第 3 列的数据做一下归一化"。这时候系统必须知道"那个 Excel 文件"指的是什么、前 5 行数据存在于哪个变量里。这不是简单的命令逐条执行,而是要求代码执行引擎具备持久化和状态管理能力。
另一个待解决的问题是并发隔离。多位用户同时在对话框里执行 Python 代码,代码之间绝对不能互相影响。如果用户在代码里写了一个 while True 死循环,或者不小心把内存打满,其他用户的会话不能被拖垮。这些都是代码执行引擎在架构层面必须考虑的事。
1.2 适合谁读这篇文档
如果你属于以下任何一类人,这篇文章都有参考价值:
- AI 应用开发者,正在做 AI 对话框的代码解释器功能,想了解执行层的架构设计。
- 后端工程师,需要为自己产品接入代码执行能力,想知道沙箱怎么做、资源怎么控制。
- 技术负责人,在评估"对话式编程"这类功能的可行性和实现成本。
- 对技术架构感兴趣的读者,想搞明白为什么有些 AI 编程工具能做到"写代码 + 跑代码 + 看结果"一条龙。
我会从架构分层、核心模块设计、实际踩坑记录三个层面展开,把我在这个项目中遇到的问题和解决方案完整讲一遍。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 整体架构与关键设计决策
2.1 三层架构:对话层、执行层、隔离层
整个系统我拆成了三层:
| 层级 | 职责 | 核心组件 | 关键技术点 |
|---|---|---|---|
| 对话层 | 维护用户会话状态、管理消息上下文 | 会话管理器、消息队列 | 上下文窗口、会话持久化 |
| 执行层 | 接收代码、执行、返回结果 | 代码执行引擎、运行时管理 | 状态保留、超时控制、输入输出捕获 |
| 隔离层 | 保证代码在安全沙箱运行 | 容器编排、资源限制 | 容器隔离、内存/CPU 控制、网络策略 |
对话层面向用户,执行层和隔离层对用户透明。用户感知不到沙箱的存在,他只知道在对话框里写代码、点运行、看结果。这个设计最核心的思路是把对话上下文与代码执行状态解耦。
我踩过最大的坑就是把两者耦合在一起。早期版本里,每个会话对应一个 Python 进程,进程里跑的变量就是会话状态。听起来没问题,但论坛上有用户开着 200 个会话,系统直接崩了——200 个 Python 进程同时跑,内存轻松打满。后来改成三层架构,会话状态存数据库,执行状态由执行层的运行时管理,隔离层按需拉起容器,问题才解决。
2.2 为什么选择容器作为沙箱方案
代码执行沙箱有几种常见方案,我逐个对比过后才确定用容器:
方案一:子进程方案。直接在服务端用 subprocess 拉起一个 Python 进程执行代码。优点是简单、启动快,缺点是隔离性差。子进程可以读宿主机文件,可以发起网络请求,一旦代码里有恶意操作(比如读取 /etc/passwd),后果很严重。
方案二:Docker 容器方案。每次执行代码拉起一个临时容器,用完销毁。隔离性好,但启动耗时较长,而且频繁创建销毁容器对宿主机压力不小。
方案三:容器池化方案。预启动一批容器放在池子里,请求来了直接分配一个,用完回收。这个方案兼顾了启动速度和隔离性,但需要自己写调度逻辑。
我最终选择的是方案三。实际项目中,我用 Docker 预创建了一个执行容器池,池容量根据历史请求量动态伸缩。高峰期扩到 10 个容器,低峰期收缩到 2 个。每个容器只挂载必要的临时文件目录,网络策略默认禁止外部访问,只有内网域名白名单可以通过。
有个细节值得提:容器镜像一定要做裁剪,不要装多余的工具包。官方 Python 镜像默认带了一堆东西,攻击面太大。我用的镜像精简到只剩 Python 运行时、pip、以及预先装好的常用依赖库,删掉了 curl、wget 这类网络工具,哪怕容器被攻破,攻击者也没工具可用。
2.3 状态管理:让 AI 记住上次的代码结果
这是整个项目里最考验设计能力的地方。用户希望 AI 能像 Jupyter 一样记住变量状态,但又不能真的为每个用户开一个常驻进程。
我的做法是检查点快照机制。每次代码执行完成后,对运行时做一次序列化快照,把执行环境的关键状态(变量值、已导入的模块、当前工作目录)保存到对象存储。下次用户发新指令时,先加载快照,再在新的运行时环境中恢复状态,然后执行新代码。
听起来简单,实现起来坑很多。Python 的 pickle 可以序列化大多数对象,但遇到 lambda 函数、生成器、打开的文件句柄就完全白搭。我遇到过用户用 open() 打开了一个文件,然后下一次提问时试图对这个文件做操作,结果恢复状态后发现文件句柄已经失效。
最后的解决方案是半持久化:只持久化关键变量和基本类型,文件操作和 IO 对象不做恢复。这种场景下,系统会明确提示用户"文件句柄无法跨会话保留,请重新打开文件"。宁可少做一点,也不能给出错误结果误导用户。
3. 核心细节解析与实操要点
3.1 输入处理:用户代码二次加工的那些坑
用户提交的代码远没有想象中规范。有的用户只写一行 print("hello"),有的用户贴了一整段带有 Markdown 标
