把 AI Agent 塞进一个进程里,第一天就会撞上两堵墙
Agent 沙盒系列第一篇。让 AI 帮你连续跑两小时的活,为什么"能跑通"和"能上线"之间差着一道隔离?用一个会被系统强制杀掉的小服务做基线,把问题看清楚。术语都有解释,不熟悉 Kubernetes 也能读。
设想一个很常见的需求:让 AI 帮你把三十个网站的数据抓下来、清洗、整理成一份报告。这个活不像聊天,问一句答一句就结束——它要连续跑两个小时,中间会下载文件、占内存、还会执行 AI 自己临时写出来的代码。
在自己电脑上跑通它,一个下午就够了。把它变成一个多人同时用的服务,难的地方才开始。
这是「从零构建 Agent 沙盒」系列的第一篇,一共六篇。每篇只解决上一篇暴露出来的一个问题,代码都在 agent-sandbox-oss 里,可以自己跑一遍。文章里的术语第一次出现时都会解释,不需要你熟悉 Kubernetes。
先说清楚这些代码是什么:教学骨架。真实的抓取被替换成了“睡一会儿”,集群跑在笔记本上。它们不是拿来上生产的,价值在于——每一步翻的车,都是真实项目里会翻的车。
最自然的写法,和它藏着的假设
要做这么一个服务,最自然的写法是:一个 Web 接口收任务,收到就在后台开始干,任务进度记在内存里。
IN_MEMORY_DB = {} # 任务进度就存在这里
async def run_data_research_task(task_id, target_pages, simulate_leak=False):
for i in range(target_pages):
await asyncio.sleep(0.5) # 假装在抓一个网页
IN_MEMORY_DB[task_id]["pages_crawled"] += 1 # 记一笔:又抓完一页
部署也是最自然的:一份程序、一个容器,给它 128MB 内存的上限。
容器可以理解成一台一次性的小服务器:程序和它需要的环境打成一个包,随时能启动、能销毁。内存上限是给它划的红线——超过就会被系统强制关掉。
这个写法没有任何“错误”。它能跑、能返回任务号、能查进度。问题在于它默默做了一个假设:所有任务都乖。
真实世界里不成立。三十个网站里总有一个页面特别大,AI 生成的代码也总有一次会写出死循环。下面这两件事,就是这个假设破掉时发生的。
第一堵墙:一个任务出事,所有人陪葬
代码里留了个开关,模拟“抓到一个巨大的网页”——每抓一页就往内存里塞 20MB。第七页的时候,内存越过 128MB 那条红线,系统直接把整个程序关掉了。
在 Kubernetes(一套管容器的系统,下文简称 K8s)里,这个过程你能直接看到:
kubectl get pod -l app=agent -w
# NAME READY STATUS RESTARTS
# agent-monolith-xxx 1/1 Running 0
# agent-monolith-xxx 0/1 OOMKilled 0 ← 内存超标,被强制杀掉
# agent-monolith-xxx 1/1 Running 1 ← 几秒后自动重启,看起来没事了
OOMKilled 就是“内存用超了,被干掉了”。看到它自动重启,第一反应通常是松口气:系统自愈了嘛。
但这时候去查另一个同时在跑的任务——那个一直老老实实抓小网页的任务:
curl localhost:8000/status/<另一个任务的 id>
# {"detail":"Task not found. Data lost due to Pod crash!"}
# 任务找不到了:进程崩溃,数据已丢失
它也没了。跑了一小时四十分钟的进度,一起归零。
因为被杀掉的不是“那个出问题的任务”,而是装着所有任务的那个程序。 打个比方:十个人合租一间房共用一个厨房,其中一个人把锅烧糊了触发消防喷淋——喷的是整间屋子,所有人的饭一起报废。
还有更难受的一点:因为进度只记在内存里,重启之后连“刚才跑到第几页了”都查不出来。K8s 的自动重启保护的是服务能不能被访问,不是任务做到哪了。它把门重新打开了,没把你的两小时找回来。
我把这叫作单体幻觉:RESTARTS: 1 这个数字看着人畜无害,代价却是当时所有在跑的任务。
第二堵墙:Agent 执行的是 AI 现场写的代码
这是 Agent 和普通后端服务最不一样的地方。普通服务执行的是你自己写好、审过、测过的代码;Agent 会执行大模型几秒钟前刚生成的代码。
而这个程序能碰到什么,容器里一清二楚:
cat /var/run/secrets/kubernetes.io/serviceaccount/token
这条命令读的是系统挂给这个程序的访问凭证——相当于一把钥匙。同一个程序里,意味着共享同一套文件、同一批环境变量、同一把钥匙。于是任务 A 生成的代码,能读到任务 B 抓下来的数据,也能读到这把钥匙。
这一条不是“写代码时小心一点”能解决的。只要在同一个程序里,边界就不存在——因为操作系统压根没提供“同一个进程内部还能互相防着”这种东西。
这一步定下来的事
这个最朴素的版本值得亲手跑一次,因为它一次证伪了两个很自然的想法:
- “内存不够就多给点”——给到 1GB,只是把翻车推迟到第几十页,连坐关系一点没变;
- “加个错误捕获不就行了”——内存超标是操作系统内核动的手,程序连喊一声的机会都没有。
所以路只剩一条:每个任务给一个自己的容器。听上去理所当然。
代价在下一篇:按需去创建一个容器,用户要在那儿干等 45 到 90 秒。
一句话版本:同一个进程里没有边界。隔离不是性能优化,是这类服务能不能对外提供的前提。
代码:agent-sandbox-oss/lab1,跑起来只要一个本地集群加一条命令。
- Kubernetes
- AI Agent
- 云原生
- 架构演进
- 故障演练