Skip to content
Tuplog
Go back

做一个云端 Agent:原理与实现

Edit page

用户在网页上描述需求,agent 在云端写代码、装依赖、起服务,网页上实时看到过程并直接预览结果。这篇讲它的原理,以及实现时的选型。

Cloud Agent:左边是 agent 的工作过程,右边是实时预览

输入「用 Vite + React 做一个 Todo 应用」:agent 建项目、装依赖、写代码、构建,最后启动预览,右侧直接可用。

原理

agent 是一个循环

用户需求「做个番茄钟」
→
agent 循环(跑在服务端)
模型只决定下一步做什么
tool call⇄执行结果
工具由程序真正执行
写文件 · 跑命令 · 起服务
→不再调工具
完成
模型从不执行任何东西,它只输出「我要调 write,参数是……」

「agent 跑在云端」,指的就是这个循环和工具的执行都发生在服务端。

工具要有地方执行:沙箱

工具要写文件、跑命令,就需要一台机器。但要跑的是模型生成的代码,加上它装进来的第三方依赖,都不可信;多个用户之间还要互相隔离。所以每个对话分配一台独立的沙箱作为工作空间。

agent 本身留在后端,只把工具调用转发进沙箱:

✓ agent 在外(采用)
后端agent 循环 + 🔑 API key
→工具调用
沙箱代码 · 依赖
key 不进沙箱;沙箱坏了,上下文还在后端
✗ agent 在沙箱里
沙箱agent + 🔑 API key
+ 不可信代码和依赖
沙箱里的任何代码都能读到 key

整体架构

浏览器对话 · 预览 · 文件
HTTP 发消息⇄SSE 事件流
后端
agent 循环每个对话一个
事件流编号 · 存库 · 推送
预览代理校验登录后转发
→调用模型
模型 APIDeepSeek
→工具调用
预览请求
沙箱集群
对话 A 的沙箱
对话 B 的沙箱

前端怎么同步:带编号的事件流

agent 的每个动作都变成一条事件,按顺序编号后推给浏览器,前端按顺序「播放」出来:

#1 用户消息 #2 模型输出文字 #3 调用 write #4 写入成功 #5 调用 bash #6 命令输出… #7 启动预览 #8 完成
断线重连「我收到 #4 了」→ 从 #5 接着推
刷新页面从 #1 重新推一遍,对话原样恢复
实时推送和历史回放是同一套事件,前端只需要一套渲染逻辑

用 SSE 而不是 WebSocket:数据基本是单向的(服务端推、浏览器收),用户发消息、点中断用普通 HTTP 请求就够了;SSE 自带断线重连和 Last-Event-ID 续传。

怎么持久化

事件日志存数据库
用来回放对话过程
agent 上下文存数据库
服务重启后 agent 记得做过什么
沙箱的文件和进程沙箱休眠时快照到磁盘
唤醒后原样恢复
运行中
→空闲 15 分钟
休眠不占 CPU 和内存
→再次访问
唤醒约 1.6 秒

前两类存在数据库里,所以服务端的进程内存里只放正在跑的东西,随时可以丢掉,下次用到时再从库里重建。

实现

技术栈:React + Node(Hono)+ pi + CubeSandbox + DeepSeek + Postgres(Drizzle)。

Agent:pi

agent 循环本身很简单,难的是边角:流式输出、中断、运行中插话、上下文压缩。选 pi 这个轻量的开源 agent 内核,有两个原因:

agent调用 bash 工具
→
bash 工具参数解析 · 输出截断 · 超时
→operations
默认:本机 shell
替换为:沙箱 SDKsandbox.commands.run(...)
只换最底层的执行,agent 和工具逻辑都不感知

相比之下,大框架抽象太多,这个场景用不上;厂商的 agent SDK 一般围绕自家模型设计,内置工具默认操作本机。

沙箱:CubeSandbox

第一步是定隔离级别。常见的有三种,区别在于沙箱里的程序和宿主机内核之间隔了什么:

容器(Docker)
对话 A
对话 B
共享宿主内核
宿主机
内核漏洞 → 逃逸到宿主和其他对话
gVisor
对话 A
对话 B
用户态内核 A
用户态内核 B
宿主内核(只开放少量系统调用)
宿主机
拦截系统调用,用 Go 重新实现一遍
不需要 KVM,但兼容性和 I/O 有损耗
MicroVM
对话 A
对话 B
内核 A
内核 B
KVM 硬件虚拟化
宿主机
虚拟机级隔离,启动速度接近容器

coding agent 的典型负载是 npm install、编译、起 dev server,文件 I/O 和系统调用都很密集,而且要跑的软件五花八门。综合考虑,选 MicroVM:兼容性最好,隔离最彻底。代价是机器必须支持 KVM。

第二步是选具体方案:

E2B(托管)自建 FirecrackerCubeSandbox
部署无需部署网络、镜像、调度全自己做一键安装
数据位置第三方自己的机器自己的机器
休眠 / 唤醒有自己做有
SDKE2B SDK自己写兼容 E2B SDK

选 CubeSandbox(腾讯云开源):能自己部署,自带休眠唤醒,而且兼容 E2B 的 SDK。代码只依赖 e2b 这个库,以后想换成 E2B 托管,改一个地址就行。

实测:新建沙箱 0.5~1.2 秒,唤醒约 1.6 秒,每台实际占内存 100~700 MB。代价是需要 x86 Linux + KVM,官方建议 8G 内存起步。

预览

agent 调用自定义的 preview 工具,在沙箱里后台启动服务(比如 vite --host 0.0.0.0),然后把预览地址交给前端,用 iframe 打开。沙箱的端口浏览器访问不到,所以要经过后端代理转发。问题是,转发到哪个地址上:

✗ 挂在主站路径下
:8787/preview/A/
白屏页面请求 /src/main.tsx
→ 打到主站,404
不安全和主站同源,页面里的代码能
fetch("/api/...") 读走用户数据
✓ 独立的源
:20001或子域名 5173-A.preview.xxx.com
跑在根路径项目不用改任何配置
和主站隔离浏览器同源策略拦住跨站读取
浏览器
→:20001
预览代理校验登录和对话归属
→
沙箱 Avite :5173

有域名就用子域名,只需开一个端口;没有域名时给每个预览分配一个独立端口。

除了预览,右侧还可以直接浏览沙箱里的文件。和预览不同,文件内容由后端从沙箱读出来,以纯文本的形式返回给前端,不会在浏览器里执行,所以走主站的接口就行:

文件视图:浏览沙箱里的项目结构和源码


代码:benhaooo/cloud-agent


Edit page
Share this post on:

Next Post
LeetCode JS 常用写法选择