AB
AiBoss站
教程

Lovable

教程

从零搭建一个 Lovable 式 AI 应用生成器:沙箱、Agent 循环与实时预览

用 Next.js、Node 工作进程、代理网关和云端沙箱,搭一个「用自然语言描述应用、AI 写代码并自我修复、实时预览、版本回滚、一键发布」的生成器。本文拆解整体架构、沙箱选型与网络隔离、内存快照加速启动、Agent 工具循环、事件流式推送、热重载预览网关、Git 版本管理、沙箱休眠唤醒与发布流程。

Lovable 这类工具第一次用会让人觉得像变魔术:在聊天框里描述一个应用,几秒后就能看到一个能跑、能分享、能下载的成品。但真正值得弄清楚的是它背后的机制——AI 写的代码总得有人安装、构建、运行,而且这些代码在运行前几乎没人读过,它可能报错、可能很慢,也可能做出不该做的事。本文要做的不是复刻一个 Lovable,而是把它的逻辑拆开,从零搭一个「Lovable 式」的 AI 应用生成器:用户用自然语言描述应用,AI Agent 在隔离的云端沙箱里写代码、自己检查、自己修错,然后把实时预览呈现出来;用户可以继续对话迭代、回滚到任意版本、一键发布。如果你正在评估或自建这类产品,可以参考本站的 Lovable 工具页了解它的定位,再往下看具体实现。

整体架构:三个进程加几层基础设施

在写代码之前,先把各部分的职责划清楚。整个系统由三个进程和若干基础设施组成,有一条原则必须从一开始就守住:Web 应用永远不自己运行 AI Agent,Agent 也永远不在沙箱里运行。这条边界决定了后面所有的设计。

一次完整的请求流程是这样的:

  1. 提交提示词。用户在界面输入提示词,Web 应用(Next.js)把它存下来,在 Postgres 里创建一条「run」记录,往队列里放一个任务,然后立刻返回。用户不需要挂着 HTTP 请求等 Agent 干完活。
  2. 运行 Agent。一个独立的工作进程取走任务,先确认该项目有可用的沙箱,然后启动 Agent 循环。由大模型决定下一步做什么,它发出的每一次工具调用(写文件、执行命令、安装依赖等)都真实发生在沙箱内部。每一步同时作为事件写入数据库,浏览器就是靠这些事件拿到实时进度。
  3. 展示预览。生成的应用在沙箱里跑自己的 Vite 开发服务器。另有一个轻量网关服务,把 http://<project-id>.preview.localhost:4000 代理到那个开发服务器,包括 Vite 热重载所用的 WebSocket。所以 Agent 一改文件,预览就自动更新。
  4. 保存与发布。Agent 完成后,工作进程把改动提交为一个新版本。用户点击发布时,应用被构建,静态文件上传到 S3,这样即使沙箱处于休眠状态,已发布的站点依然可访问。

按层来划分更清楚:

r>
层技术选型职责
界面与 APINext.js用户交互、提示词接收、状态查询
Agent 层Node.js + pg-boss消费队列任务、驱动 Agent 循环
预览层Node.js 代理把子域名请求转发到沙箱内的开发服务器
状态与队列Postgres项目、run、事件、版本记录
执行层云端沙箱隔离运行生成的应用
存储层S3(本地用 MinIO)存放构建产物
推理层任意大模型决定 Agent 的下一步动作

为什么必须要有沙箱

这个产品的本质,用一句话说就是「运行没人审阅过的代码」。Agent 写代码,npm install 拉进来的包,其安装脚本几乎可以执行任何操作,接着开发服务器会把这些代码全部跑起来。这种东西不应该跑在你自己的服务器上。

所以每个项目都要有自己独立的沙箱,本质上是一台云端的小型虚拟机。挑选沙箱服务时,真正硬性的需求只有两条:

  • 挂起与恢复。大多数项目大部分时间都是闲置的。需要能把它们休眠,再在几秒内唤醒,并且内存状态保持完整。
  • 文件、命令与终端。Agent 需要读写文件、执行命令,同时最好也能给用户一个真实的 shell。

生产环境选型时要考察的维度远不止这些,但以上是必须满足的底线。可选的方案很多,E2B、Daytona、Modal,甚至自建 Firecracker 都可以。关键是把所有沙箱相关代码收进一个独立包(例如 packages/sandbox),这样换供应商时只需要改这一处,应用其余部分根本不需要知道底层用的是谁。

准备工作

开始之前,先确认本地环境具备以下条件:

  • Node.js 22 或更高版本
  • pnpm
  • Docker(本地测试时用来跑 Postgres 和 MinIO)
  • 沙箱服务商的 API Key
  • 一个大模型的 API Key(Anthropic 或 OpenAI 均可)

然后克隆仓库并安装依赖:

git clone <repository-url>
cd lovable-build-tensorlake-aws
pnpm install

接着创建环境变量文件并填入密钥:

cp .env.example .env
# 填入沙箱与大模型的密钥,并用下面的命令生成 AUTH_SECRET
openssl rand -base64 32

再启动本地基础设施、建表、配置存储桶:

# 启动 Postgres 和 MinIO
pnpm infra:up

# 创建数据库表
pnpm db:migrate

# 创建存储桶并收紧权限
pnpm s3:setup

之后构建所有新项目共用的基础快照,这一步大约需要 45 秒:

pnpm sandbox:build-base

最后把全部服务跑起来:

# Web 在 :3000,预览网关在 :4000
pnpm dev

打开本地地址、登录、描述一个应用即可。应用支持 GitHub 登录,但也提供了一个简单的开发登录方式,方便你在创建 GitHub OAuth 应用之前先试用。开发登录在生产环境中始终是关闭的。

操作步骤

第一步:用内存快照把沙箱启动压到几秒

每个生成的应用都从同一个模板开始:Vite、React、TypeScript、Tailwind,加上几个常用库。最直白的做法是每建一个新项目就重来一遍:创建全新沙箱、上传模板、跑 npm install、启动开发服务器。这能work,但实测从开始到预览可访问需要 33.4 秒,其中约 23 秒纯粹是 npm install 在单 vCPU 上跑。

解决办法是:这些事只做一次,把结果保存为内存快照。内存快照会捕获文件、内存和正在运行的进程,恢复时开发服务器已经在跑了,不需要重新启动任何东西。

export async function buildBaseSnapshot(log: Logger) {
  // 冷路径,只执行一次:创建、上传模板、npm install、git init、
  // 验证能构建、启动开发服务器并预热 Vite 缓存。
  const { ps } = await coldCreateFromTemplate({
    name: `base-${Date.now()}`,
    log,
    verify: true,
  });

  try {
    // 内存检查点:文件 + 内存 + 运行中的进程。
    const snapshotId = await ps.checkpoint();
    writeBaseSnapshot({ snapshotId, createdAt: new Date().toISOString() });
  } finally {
    await ps.terminate();
  }
}

有了基础快照,为新项目创建沙箱就只剩「恢复」这一步,然后立刻收紧权限:

static async createFromSnapshot(opts: { snapshotId: string; name: string; log: Logger }) {
  const sb = await Sandbox.create({
    snapshotId: opts.snapshotId,
    name: opts.name,
    timeoutSecs: 600,
  });

  const ps = new ProjectSandbox(sb, opts.log);

  await sb.update({
    exposedPorts: [5173], // Vite 开发服务器,通过代理访问
    allowUnauthenticatedAccess: false, // 端口 URL 永远不公开
    network: {
      allowInternetAccess: true,
      allowOut: ["registry.npmjs.org"], // 只放行 npm
      denyOut: [],
    },
  });

  return ps;
}

这段配置里有几个要点值得单独说明:

  • exposedPorts 让开发服务器可以通过服务商的代理访问,但前提是持有 API Key,后面网关会用到这一点。
  • network 块是一份白名单。一旦 allowOut 里有了条目,其余出站流量全部被阻断。可以用 example.com、github.com 以及云元数据 IP 做验证,它们都会被拦下,而 npm 依然正常工作。
  • 沙箱环境里完全没有任何密钥:没有大模型 Key、没有数据库连接串、没有 AWS 凭证。即使生成的应用或提示词注入试图窃取信息,里面也没有东西可偷。

实测数据(计时到预览真正加载完成):

路径耗时
冷启动(创建、安装、启动开发服务器)33.4 秒
从内存快照恢复4.0 秒
唤醒休眠中的沙箱2.4 秒

大致是 8 倍的差距。

第二步:设计 Agent 循环与工具集

Agent 循环是整个系统的大脑。每次用户发来提示词,跑的就是它。思路本身很简单:给大模型一个目标加一组工具,让它调用,把结果喂回去,如此反复直到完成。

Agent 可用的工具如下:

  • list_files、read_file、write_file、edit_file、delete_file:文件操作
  • run_command:执行快速检查,例如 npx tsc --noEmit
  • install_packages:安装 npm 包
  • get_dev_server_logs、get_browser_errors:用于调试
  • finish:Agent 认为完成时调用

每个工具就是一个小文件,包含一个 Zod schema 和一个 run 函数。以 edit_file 为例:

export const editFile = defineTool({
  name: "edit_file",
  description: "Replace one exact snippet in a file. `search` must match exactly and occur exactly once.",
  schema: z.object({
    path: z.string(),
    search: z.string().min(1),
    replace: z.string(),
  }),
  async run({ path, search, replace }, { sandbox }) {
    // 在沙箱内读取文件、校验 search 唯一匹配、写回替换结果
  },
});

把 search 限定为「必须精确匹配且只出现一次」是有意为之:这样模型就不能含糊地「大概改一下」,一旦匹配失败或匹配到多处,工具会直接报错,迫使模型重新读取文件、给出更精确的片段。这比让模型整文件重写要安全得多,也更容易在版本对比中看出改了什么。

第三步:让 Agent 检查自己的工作

Agent 循环里最关键的一环不是「写」,而是「验」。工具集里专门留了 run_command、get_dev_server_logs 和 get_browser_errors,目的就是让模型在宣布完成之前先自己跑一遍检查:

  • 用 npx tsc --noEmit 做类型检查,把编译错误在提交前消掉。
  • 读取开发服务器日志,捕捉启动失败、模块解析错误、运行时异常。
  • 读取浏览器端错误,捕捉类型检查发现不了的前端运行时问题。

这些检查的结果会作为工具返回值回到模型上下文里,模型据此决定是继续修还是调用 finish。也就是说,自我修复不是额外加的功能,而是循环本身的一部分:报错信息进入上下文,模型产生新的工具调用,改动再次落到沙箱里,如此往复。

第四步:把进度实时推给浏览器

Agent 的每一步都会作为事件写入数据库。浏览器不是靠长连接直接盯着工作进程,而是订阅这些事件。这样做的好处是:即使页面刷新,已经发生的事件依然在数据库里,重新连上就能补齐进度,不会丢。

事件流大致覆盖这些节点:run 开始、模型思考、工具调用发起、工具调用返回、文件写入、命令输出、错误、run 结束。前端按顺序渲染,用户看到的就是「Agent 正在做什么」的实时时间线。

第五步:搭预览网关,把热重载透出去

生成的应用在沙箱里跑自己的 Vite 开发服务器,监听 5173 端口。网关服务的职责是把 http://<project-id>.preview.localhost:4000 代理到对应沙箱的开发服务器,并且要一并代理 Vite 热重载所用的 WebSocket。少了 WebSocket,页面就只能手动刷新,体验会差一大截。

因为沙箱端口设置了 allowUnauthenticatedAccess: false,网关在转发时必须带上服务商的 API Key,端口 URL 本身不会暴露在公网上。子域名里的 <project-id> 用来定位是哪个项目的沙箱。

第六步:用 Git 做版本管理,但别把令牌放进沙箱

Agent 每完成一轮工作,工作进程就把改动提交为一个新版本。版本能力带来两个直接好处:用户可以回滚到任意历史版本;发布时也有明确的「当前版本」可以构建。

这里有一条安全约束必须遵守:不要把 Git 令牌放进沙箱。沙箱里跑的是未经审阅的代码,任何凭证放进去都等于交出去。提交动作应该由沙箱之外的工作进程完成,沙箱只负责产生文件改动。

第七步:休眠、唤醒与共享沙箱

大多数项目大部分时间闲置,让它们一直跑着既浪费也不必要。做法是让空闲沙箱进入休眠,需要时再唤醒。前面测过,唤醒一个休眠沙箱大约 2.4 秒,用户基本感觉不到中断。

共享方面,因为每个项目有独立的子域名预览地址,把链接发给别人就能看到当前状态。但要注意:预览地址指向的是沙箱里的开发服务器,沙箱休眠时它就不通了。真正需要长期可访问的,是发布后的产物。

第八步:发布应用

用户点击发布时,应用被构建,静态文件上传到 S3(本地开发用 MinIO)。这样即使沙箱处于休眠状态,已发布的站点依然能正常访问。发布和预览是两条独立的路径:预览服务于「正在迭代」,发布服务于「已经定稿」。

一个完整示例

把上面的步骤串起来,从零到能看到预览的完整流程如下。

1. 准备环境并安装依赖

git clone <repository-url>
cd lovable-build-tensorlake-aws
pnpm install

2. 配置密钥

cp .env.example .env
openssl rand -base64 32
# 把生成的 AUTH_SECRET 以及沙箱、大模型的 API Key 填入 .env

3. 启动基础设施并初始化

pnpm infra:up
pnpm db:migrate
pnpm s3:setup

4. 构建基础快照(约 45 秒,只需一次)

pnpm sandbox:build-base

5. 启动全部服务

pnpm dev
# Web: http://localhost:3000
# 预览网关: http://localhost:4000

6. 在浏览器中打开 Web 地址,登录,输入一句自然语言描述,例如「做一个带搜索和分页的待办列表」。此时后台发生的事是:提示词被保存、Postgres 里创建 run、队列收到任务、工作进程取走任务、从基础快照恢复出一个沙箱(约 4 秒)、Agent 循环开始调用工具、每一步写入事件、浏览器实时渲染进度、Vite 开发服务器在沙箱内启动、网关把子域名代理过去、预览自动出现。

7. 继续对话迭代,Agent 会基于当前文件状态继续修改,预览随热重载自动更新。

8. 回滚或发布:需要时切回任意历史版本;确认无误后点击发布,产物构建并上传到 S3。

注意事项

  • 出站网络是白名单机制。一旦 allowOut 里写了条目,其余出站流量全部被阻断。只放行 registry.npmjs.org 时,npm 可用,其他外部地址不可达。需要访问别的服务时必须显式加进白名单。
  • 沙箱内不放任何密钥。没有大模型 Key、没有数据库连接串、没有云存储凭证。这是防止提示词注入和恶意依赖的最后一道防线,不要为了方便破坏它。
  • Git 令牌不要进沙箱。提交动作放在沙箱之外的工作进程里完成。
  • 端口不公开。allowUnauthenticatedAccess: false 意味着端口 URL 不能直接访问,必须经过带 API Key 的网关。
  • 预览依赖沙箱存活。沙箱休眠时预览地址不可用;需要长期可访问的内容应当走发布流程,把静态产物放到对象存储。
  • 开发登录仅用于本地。它让你在配置 GitHub OAuth 之前就能试用,但在生产环境中始终关闭。
  • 换沙箱供应商只需改一个包。所有沙箱代码集中在 packages/sandbox,应用其余部分不感知具体供应商。
  • 基础快照需要重建。模板依赖或基础镜像变化后,需要重新执行基础快照构建,否则新项目仍会从旧快照恢复。
  • 价格、配额、沙箱超时上限、存储容量等易变信息,请以各服务商官网当前公布的信息为准。