我用 OpenClaw + Vercel + Supabase 搭建了一家 AI 公司——两周后,他们自己把它运转起来

6 个 AI 代理、1 台 VPS、1 个 Supabase 数据库——从「代理能聊天」到「代理能自主运营网站」,我用了两周。本文会把这中间究竟缺了什么、如何补齐,以及一套你可以直接带走复用的架构讲清楚。
起点:你已经有 OpenClaw 了。然后呢?
如果你最近在玩 AI 代理,大概率已经把 OpenClaw 搭起来了。
它解决了一个大问题:让 Claude 能用工具、浏览网页、操作文件,并运行定时任务。你可以给代理分配 cron 作业——每天发推、每小时情报扫描、定期生成调研报告。
我也是从这里开始的。
我的项目叫 VoxYZ Agent World——6 个 AI 代理在一间像素风办公室里,自主运营一个网站。技术栈很简单:
OpenClaw(在 VPS 上):代理的「大脑」——负责圆桌讨论、cron 定时任务、深度研究
Next.js + Vercel:网站前端 + API 层
Supabase:所有状态的单一事实来源(提案、任务、事件、记忆)
六个角色,各司其职:Minion 负责决策,Sage 分析策略,Scout 收集情报,Quill 撰写内容,Xalt 管理社媒,Observer 做质量检查。
OpenClaw 的 cron 任务让他们每天都能「准时上班」。圆桌让他们讨论、投票、达成共识。
但这只是「能说」,不是「能干」。
代理产出的所有东西——推文草稿、分析报告、内容文章——都停留在 OpenClaw 的输出层。没有任何东西把它变成真正的执行,也没有任何机制在执行完成后告诉系统「已完成」。
在「代理能产出输出」和「代理能端到端跑完整流程」之间,缺的是一个完整的 execute → feedback → re-trigger 闭环。这正是本文要讲的。
一个闭环应该长什么样
先把「闭环」定义清楚,免得一上来就造错。
一个真正能无人值守的代理系统,需要这条循环持续运行:
代理提出一个想法(Proposal)
↓
自动审批检查(Auto-Approve)
↓
创建任务 + 步骤(Mission + Steps)
↓
执行者领取并执行(Worker)
↓
发出事件(Event)
↓
触发新的反应(Trigger / Reaction)
↓
回到第一步
听起来很直观?但在实践里,我踩了三个坑——每一个都让系统「看起来在跑,其实在原地打转」。
坑 1:两处同时抢活儿
我的 VPS 上有 OpenClaw worker 在领取并执行任务。与此同时,Vercel 上还有一个 heartbeat cron 在跑 mission-worker,也在试图领取同一批任务。
两边都查同一张表、抓同一个 step、各自执行。没有协调,纯粹的竞态条件。有时同一个 step 会被两边打上互相冲突的状态。
修复:砍掉一个。VPS 作为唯一执行者。Vercel 只跑轻量的控制平面(评估触发器、处理反应队列、清理卡死任务)。
改动很小——把 heartbeat 路由里的 runMissionWorker 调用删掉:
// Heartbeat now does only 4 things
const triggerResult = await evaluateTriggers(sb, 4_000);
const reactionResult = await processReactionQueue(sb, 3_000);
const learningResult = await promoteInsights(sb);
const staleResult = await recoverStaleSteps(sb);
额外收益:省下了 Vercel Pro 的费用。Heartbeat 不再需要 Vercel 的 cron——VPS 上 crontab 一行就够了:
*/5 * * * * curl -s -H "Authorization: Bearer $KEY" https://yoursite.com/api/ops/heartbeat
坑 2:触发了,但没人接
我写了 4 个触发器:推文爆了就自动分析、任务失败就自动诊断、内容发布就自动审稿、洞见成熟就自动晋升。
测试时我发现:触发器确实能正确检测条件并创建 proposal。但 proposal 会永远停在 pending——不会变成 mission,也不会生成可执行的 steps。
原因是:触发器直接往 ops_mission_proposals 表里插数据,但正常的审批流是:插入 proposal → 评估 auto-approve → 若通过,创建 mission + steps。触发器跳过了最后两步。
修复:抽出一个共享函数 createProposalAndMaybeAutoApprove。任何创建 proposal 的路径——API、触发器、反应——都必须走同一个函数入口。
// proposal-service.ts — the single entry point for all proposal creation
export async function createProposalAndMaybeAutoApprove(
sb: SupabaseClient,
input: ProposalServiceInput, // includes source: 'api' | 'trigger' | 'reaction'
): Promise
// 1. Check daily limit
// 2. Check Cap Gates (explained below)
// 3. Insert proposal
// 4. Emit event
// 5. Evaluate auto-approve
// 6. If approved → create mission + steps
// 7. Return result
}
改完之后,触发器只返回一个 proposal 模板。由评估器去调用服务:
// trigger-evaluator.ts
if (outcome.fired && outcome.proposal) {
await createProposalAndMaybeAutoApprove(sb, {
...outcome.proposal,
source: 'trigger',
});
}
一函数统天下。以后任何新增检查逻辑(限流、黑名单、新的上限)——只改一个文件就行。
坑 3:配额满了,队列还在长
最阴险的 bug:表面上看一切正常,日志里也没报错,但数据库里排队的 steps 越堆越多。
原因是:推文配额已经满了,但 proposal 仍然在被批准,mission 仍然在生成,queued steps 也仍然在产生。VPS worker 看到配额满了就直接跳过——既不领取,也不标记失败。第二天,又来一批。
修复:Cap Gates——在 proposal 入口处就拒绝。不要让它在一开始就生成 queued steps。
// The gate system inside proposal-service.ts
const STEP_KIND_GATES: Record
write_content: checkWriteContentGate, // Check daily content cap
post_tweet: checkPostTweetGate, // Check tweet quota
deploy: checkDeployGate, // Check deploy policy
};
每种 step kind 都有自己的 gate。推文配额满了?proposal 立刻被拒绝,理由清清楚楚,同时发出 warning event。没有 queued step = 不会堆积。
下面是 post_tweet 的 gate:
async function checkPostTweetGate(sb: SupabaseClient) {
const autopost = await getOpsPolicyJson(sb, 'x_autopost', {});
if (autopost.enabled === false) return { ok: false, reason: 'x_autopost disabled' };
const quota = await getOpsPolicyJson(sb, 'x_daily_quota', {});
const limit = Number(quota.limit ?? 10);
const { count } = await sb
.from('ops_tweet_drafts')
.select('id', { count: 'exact', head: true })
.eq('status', 'posted')
.gte('posted_at', startOfTodayUtcIso());
if ((count ?? 0) >= limit) return { ok: false, reason: Daily tweet quota reached (${count}/${limit}) };
return { ok: true };
}
关键原则:在 gate 处拒绝,不要在队列里堆。被拒绝的 proposals 会被记录下来(用于审计),而不是悄悄丢掉。
让它「活起来」:Triggers + Reaction Matrix
三个坑修完之后,闭环就能跑了。但系统此时只是「无错误的流水线」,还不是「会响应的团队」。
触发器(Triggers)
内置 4 条规则——每条检测一个条件,并返回一个 proposal 模板:
| 条件 | 动作 | 冷却时间 |
|---|---|---|
| 推文互动率 > 5% | Growth 分析它为什么爆了 | 2 小时 |
| 任务失败 | Sage 诊断根因 | 1 小时 |
| 新内容发布 | Observer 做质量审查 | 2 小时 |
| 洞见获得多个 upvote | 自动晋升为永久记忆 | 4 小时 |
触发器只负责检测——它们不会直接写数据库,而是把 proposal 模板交给 proposal service。所有 cap gates 和 auto-approve 逻辑都会自动生效。
冷却时间很重要。没有它的话,一条爆款推文会在每个 heartbeat 周期(每 5 分钟)都触发一次分析。
反应矩阵(Reaction Matrix)
最有意思的部分——代理之间的自发互动。
在 ops_policy 表里存一个 reaction_matrix:
{
"patterns": [
{ "source": "twitter-alt", "tags": ["tweet","posted"], "target": "growth",
"type": "analyze", "probability": 0.3, "cooldown": 120 },
{ "source": "*", "tags": ["mission:failed"], "target": "brain",
"type": "diagnose", "probability": 1.0, "cooldown": 60 }
]
}
Xalt 发了一条推文 → 有 30% 的概率 Growth 会去分析它的表现。任何 mission 失败 → 有 100% 的概率 Sage 会来诊断。
概率不是 bug,而是特性。100% 的确定性 = 机器人;加一点随机性 = 更像真实团队:有时有人回应,有时没人接。
自愈(Self-Healing):系统一定会卡住
VPS 重启、网络抖动、API 超时——steps 会卡在 running 状态,但实际上没人处理。
heartbeat 里包含 recoverStaleSteps:
// 30 minutes with no progress → mark failed → check if mission should be finalized
const STALE_THRESHOLD_MS = 30 * 60 * 1000;
const { data: stale } = await sb
.from('ops_mission_steps')
.select('id, mission_id')
.eq('status', 'running')
.lt('reserved_at', staleThreshold);
for (const step of stale) {
await sb.from('ops_mission_steps').update({
status: 'failed',
last_error: 'Stale: no progress for 30 minutes',
}).eq('id', step.id);
await maybeFinalizeMissionIfDone(sb, step.mission_id);
}
maybeFinalizeMissionIfDone 会检查 mission 里的所有 steps——只要有一个 failed,整个 mission 就失败;全部 completed 才算成功。不再出现「某一步成功了,于是整个 mission 被标成 success」这种问题。
完整架构
三层清晰分工:
OpenClaw(VPS):思考 + 执行(大脑 + 双手)
Vercel:审批 + 监控(控制平面)
Supabase:所有状态(共享皮层)
你可以直接带走的东西
如果你用的是 OpenClaw + Vercel + Supabase,这是一份最小可用闭环(MVP closed-loop)清单:
- 数据库表(Supabase)
至少需要这些:
| 表 | 用途 |
|---|---|
| ops_mission_proposals | 存储 proposals(pending/accepted/rejected) |
| ops_missions | 存储 missions(approved/running/succeeded/failed) |
| ops_mission_steps | 存储执行步骤(queued/running/succeeded/failed) |
| ops_agent_events | 存储事件流(所有代理动作) |
| ops_policy | 存储策略(auto_approve、x_daily_quota 等,以 JSON 形式) |
| ops_trigger_rules | 存储触发器规则 |
| ops_agent_reactions | 存储反应队列 |
| ops_action_runs | 存储执行日志 |
- Proposal Service(一个文件)
把 proposal 创建 + cap gates + auto-approve + mission 创建都放在同一个函数里。所有来源(API、触发器、反应)都调用它。它是整个闭环的枢纽。
- 策略驱动配置(ops_policy 表)
不要把限制写死在代码里。所有行为开关都放在 ops_policy 表里:
// auto_approve: which step kinds are allowed to auto-pass
{ "enabled": true, "allowed_step_kinds": ["draft_tweet","crawl","analyze","write_content"] }
// x_daily_quota: daily tweet cap
{ "limit": 8 }
// worker_policy: whether Vercel executes steps (set false = VPS only)
{ "enabled": false }
不需要重新部署代码,随时都能调整策略。
- Heartbeat(一个 API 路由 + 一行 Crontab)
Vercel 上一个 /api/ops/heartbeat 路由。VPS 上一行 crontab 每 5 分钟调用它。路由里运行:触发器评估、反应队列处理、洞见晋升、卡死任务清理。
- VPS Worker 合约
每种 step kind 映射到一个 worker。完成某个 step 后,worker 调用 maybeFinalizeMissionIfDone 检查是否该收尾整个 mission。绝不要因为某一个 step 完成了就把 mission 标记为 succeeded。
两周时间线
| 阶段 | 时间 | 完成内容 |
|---|---|---|
| 基础设施 | 已有 | OpenClaw VPS + Vercel + Supabase(已搭好) |
| Proposals + Approval | 3 天 | Proposals API + auto-approve + policy table |
| Execution Engine | 2 天 | mission-worker + 8 个 step executor |
| Triggers + Reactions | 2 天 | 4 种 trigger + reaction matrix |
| Loop Unification | 1 天 | proposal-service + cap gates + 修复三个坑 |
| Affect System + Visuals | 2 天 | Affect 重写 + idle 行为 + 像素办公室整合 |
| Seed + Go Live | 半天 | Migrations + seed policies + crontab |
不算已有的基础设施,核心闭环(propose → execute → feedback → re-trigger)大约一周就能接好。
最后的一点想法(Final Thoughts)
这 6 个代理现在每天都在自主运营 voxyz.space。我仍在每天优化系统——调策略、扩触发规则、改进代理协作方式。
它离完美还很远——代理间协作还很基础,「自由意志」也主要是靠基于概率的非确定性在模拟。但系统确实能跑,确实不需要人盯着。
下一篇文章我会写代理如何「争论」与「说服」彼此——圆桌投票与 Sage 的记忆整合,如何把 6 个独立的 Claude 实例变成某种接近团队认知的东西。
如果你也在用 OpenClaw 搭代理系统,我很愿意交流笔记。做独立开发时,每一次对话都能帮你少踩一个坑。






链接: http://x.com/i/article/2019906747750658049