乐于分享
好东西不私藏

搭了个AI公司:OpenClaw+Vercel+Supabase

搭了个AI公司:OpenClaw+Vercel+Supabase

如果你最近在玩 AI Agent,大概率已经把 OpenClaw 跑起来了。Claude 能用工具、能浏览网页、能跑定时任务,你给它派 cron job,它每天准时”上班”。

但两周前我发现一个尴尬的事:我的 5 个 Agent 能聊天、能出报告、能写内容,但系统本身还是我在运营。Agent 产出的东西堆在输出层,我得手动去捞、手动去发、手动标记”做完了”。

这不是”自主运营”,这是”Agent 干活,我当监工”。

先说结论。花了 2 周把 AgentForge 从”能产出”改成”能闭环”。核心不是加功能,是补上 Proposal → Mission → Step → Event → Reaction → 新 Proposal 这个循环。

下面讲我踩的坑和最终的架构取舍——有些地方和主流做法不一样,但跑了一个月确实更稳。

01 闭环到底是什么

很多人觉得”Agent 能调工具”就是闭环了。不是。

我定义的闭环是这个循环必须永远跑下去:

Agent 提一个想法 (Proposal)     ↓ 被批准变成任务 (Mission)     ↓ 拆解成具体步骤 (Step)     ↓ Worker 认领并执行     ↓ 执行完产生事件 (Event)     ↓ 事件触发新想法 → 回到起点

这个环永远在转。这就是闭环。

关键不是每一步都能跑,关键是每一步都能自动触发下一步。任何一个环节断了,系统就退回”Agent 干活,人当监工”。

整个系统的骨架就是 4 张表:

含义
ops_mission_proposals
“我想做”
ops_missions
“批准你做”
ops_mission_steps
“正在做”
ops_agent_events
“做完了”

为什么是 4 张表而不是 1 张?因为职责不同。混在一起你没法做配额检查——你不知道哪些是已批准的、哪些还在排队。

闭环不是功能,是分水岭。能跑和能自己跑下去,差一个完整的反馈回路。

02 坑一:执行面只能有一个

这个坑最蠢,但也最容易被忽略。

我的 VPS 上有 OpenClaw worker 在认领任务。同时 Vercel 上有个 heartbeat cron 也在跑 mission-worker,也在试图认领同一个 step。两边查同一张表,抢同一个任务,各自独立执行——纯竞态条件。

偶尔一个 step 会被两边同时打上冲突的状态标签。日志看不出问题,因为两边都”成功”了,但数据库里的状态是脏的。

修复:砍掉 Vercel 的执行能力。VPS 是唯一执行面,Vercel 只做轻量控制——评估触发器、处理反应队列、清理卡死任务。

// Heartbeat 只做 6 件事,绝不碰执行 1. evaluateTriggers()      — 检查触发规则 2. processReactionQueue()  — 处理反应队列 3. processInitiatives()    — 处理主动提案 4. promoteInsights()       — 晋升洞察为永久记忆 5. recoverStaleSteps()     — 恢复卡死步骤 6. dailyBriefing()         — 每日简报

附带好处:省了 Vercel Pro 的钱。Heartbeat 不用 Vercel cron 了,VPS 上一行 crontab 每 5 分钟调一次:

*/5 * * * * curl -s -H "Authorization: Bearer $CRON_SECRET" https://your-domain.com/api/ops/heartbeat

每一步独立 try-catch——一个挂了不影响其他的。这个坑的教训:执行面双写不是双保险,是竞态条件

03 坑二:提案入口不统一,触发器变孤儿

我写了触发器:推文火了自动分析、任务失败自动诊断、内容发布自动评审。

测试时发现一个诡异的现象:触发器正确检测到条件了,也创建了提案,但提案一直挂在 pending,永远没变成任务。

翻代码发现:触发器直接往 proposals 表插数据,但正常审批流程是:插入提案 → 评估自动审批 → 如果通过则创建任务+步骤。触发器跳过了后两步。

这种 bug 最阴的地方在于:日志没有报错。提案创建成功了,触发器以为自己完成了任务,但下游永远没人接。

修复:抽一个统一的提案入口管道 proposal-service.ts。不管提案从哪来——Agent 主动提的、触发器产生的、反应矩阵产生的——全部走同一个函数:

// lib/ops/proposal-service.ts — 整个系统的枢纽 1. 检查这个 Agent 今天是否已超提案上限 2. 检查 Cap Gate(配额满了?→ 立即拒绝,不创建任何 Step) 3. 插入提案记录 4. 评估是否自动过审(低风险任务自动通过) 5. 如果通过 → 创建 Mission + Steps 6. 写入事件(前端能看到)

以后任何检查逻辑(限流、黑名单、新的 cap),改这一个文件就够。这个坑的教训:所有创建提案的路径必须走同一个入口,否则触发器创建的提案会变成孤儿,永远没人执行,而且日志不会告诉你。

触发器只负责”检测条件 + 返回提案模板”,不负责创建提案。创建走统一入口,所有 cap gate 和审批逻辑自动生效。

04 坑三:配额满了还在塞,队列悄悄堆积

这个 bug 最阴。表面看一切正常——日志没报错,任务在跑,Agent 在干活。但数据库里 queued step 越堆越多。

原因:推文配额满了。但提案还在被审批通过,还在生成任务,还在生成 queued step。VPS worker 看到配额满了直接跳过——不认领、不标记失败、不通知任何人。第二天又来一批,第三天又来一批。

什么是 Cap Gate?想象你的公司有条规定——每天最多发 8 条推文。如果你不在”提交请求”这一步检查配额,请求照样被批准、任务照样排队、执行器检查发现”今天已经发了 8 条”然后跳过——但任务还在队列里堆着。等你发现时已经堆了一堆。

修复:Cap Gates——在提案入口就拦住,别让它生成 queued step:

step_kind
门控检查
作用
write_content
每日内容上限
防止内容泛滥
post_tweet
推文日配额
每天最多 N 条
deploy
部署策略开关
一键开关部署权限

核心原则:在入口处拦截,不要让任务堆在队列里。被拒绝的提案要留记录(审计追踪),不能静默丢弃。

在 gate 拒掉,别在队列里堆。被拒要有记录,不能默默丢掉——否则就是定时炸弹。

05 Worker 怎么认领:CAS 原子认领

3 个坑修完,闭环能跑了。但还有个底层问题:多个 Worker 并发时,怎么保证一个 step 只被一个 Worker 认领?

主流方案是上消息队列(Redis/RabbitMQ)。但我选了更轻的:PostgreSQL 自己的行级锁 + CAS(Compare-And-Swap)

// Worker 循环 while (true) {   1. 检查自己是否被启用(ops_policy 控制)   2. 从队列取一个 status='queued' 的 Step   3. 原子认领(CAS):      UPDATE SET status='running', reserved_at=now()      WHERE status='queued' — 失败者拿到零行,继续找下一个   4. 路由到对应的 Executor 执行   5. 标记成功或失败   6. 如果 Mission 所有 Step 完成 → 终结 Mission   7. sleep → 继续循环 }

原子认领听起来复杂,但就是一个数据库技巧。想象两个人同时伸手拿最后一个甜甜圈——数据库确保只有一只手拿到。不需要单独的锁服务,PostgreSQL 原生处理。

不需要 Redis,不需要 RabbitMQ。每多一个中间件,就多一个故障点、多一个运维成本。AgentForge 的 VPS 月成本 $8,能不引入新依赖就不引入。

06 让系统活过来:反应矩阵

闭环能跑了,但系统只是个”不出错的流水线”,不是”有反应的团队”。缺的是自发互动

触发器从条件产生工作。但 Agent 之间呢?Agent A 做了件事,Agent B 怎么决定要不要响应?

这就是反应矩阵——定义 Agent 之间”谁做了什么,谁可能跟着做什么”的规则系统。规则存在 ops_policy 表里:

{   "patterns": [     {       "source": "scout",          // 谁做的事?"*"=任何人       "tags": ["crawl"],          // 事件 tags 包含什么       "target": "quill",          // 谁来响应       "type": "draft_tweet",      // 响应的 step_kind       "probability": 0.3,         // 30% 概率响应       "cooldown": 120             // 2 小时冷却     },     {       "source": "*",       "tags": ["mission_failed"],       "target": "nexus",       "type": "diagnose",       "probability": 1.0,         // 100% 响应       "cooldown": 60     }   ] }

读法:每当 Scout 的事件包含 crawl 标签,Quill 有 30% 概率被分配一个起草推文的任务,2 小时内不重复。任何任务失败 → 100% Nexus 会诊断。

probability 不是 bug,是 feature。100% 确定性 = 机器人。加点随机性 = 更像真实团队——”有时候有人响应,有时候没有”。

看一个完整的闭环例子,从触发到完成 6 分钟,成本约 $0.002:

09:00  心跳触发 proactive_scan 规则        → proposal-service 创建提案:"Scout 扫描 Hacker News"  09:01  提案自动过审(crawl 是低风险 step_kind)        → 创建 Mission + 1 个 Step (kind=crawl, status=queued)  09:02  VPS Worker 原子认领这个 Step        → 路由到 crawl 执行器        → 爬取 Hacker News,写入事件 (tags: ['crawl', 'scout'])        → 标记 Step 完成  09:02  Worker 检查反应矩阵        → 事件 tags 匹配规则:source=scout, tags=["crawl"]        → 掷骰子:30% 概率 → 通过        → 写入反应队列:target=quill, type=draft_tweet  09:05  下一次心跳消费反应队列        → proposal-service 给 Quill 创建提案        → 自动过审 → 创建 Mission + Step  09:06  Worker 认领 Quill 的 Step        → 路由到 draft_tweet 执行器        → LLM 生成推文草稿  结果:一条推文草稿准备好了,全程无人介入。

probability 不是不确定性,是拟人化。100% 确定的反应是机器人,有概率的反应才像团队。

07 自愈:系统一定会卡住

VPS 重启、网络抖动、API 超时——step 会卡在 running 状态,没人真的在处理。这种”幽灵 step”不处理,整个系统会慢慢僵死。

heartbeat 里的 recoverStaleSteps 解决这个问题:

// 30 分钟没进展 → 标记失败 → 检查任务是否应该完结 const STALE_THRESHOLD_MS = 30 * 60 * 1000;  // 找出所有卡死的 running step const { data: stale } = await sb   .from('ops_mission_steps')   .select('id, mission_id')   .eq('status', 'running')   .lt('reserved_at', staleThreshold);  // 逐个标记失败,然后检查 Mission 是否应该完结 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 的逻辑很重要:任何一个 step 失败则整个 Mission 失败,全部完成才算成功。再也不会出现”一个 step 成功了整个任务就被标记成功”的鬼情况。

还有熔断器:Worker 连续失败 3 次 → 自动禁用自己 → 触发 Telegram 告警。你会在面板上看到它坏了,而不是一周后才发现。

08 完整架构:三平面分离

最终架构是三层,职责严格分离:

平面
职责
绝不做什么
VPS(执行面)
Agent 思考 + 执行任务
不碰审批逻辑
Vercel(控制面)
审批 + 触发 + 监控
不碰执行
Supabase(事实源)
所有状态的唯一真相源
不碰业务逻辑

关键原则:每个平面只做自己擅长的事,不侵入其他平面。Vercel 不执行任务(避免坑一),Supabase 不写业务逻辑(保持纯净),VPS 不做审批(避免越权)。

策略全部存在 ops_policy 表里,key-value JSON 结构。凌晨 3 点系统出问题?把 enabled 改成 false 就行,不用重新部署。

09 和主流方案的差异

写到这有人会问:这和 LangChain / CrewAI / AutoGen 有什么区别?

核心差异:它们解决”编排”,我解决”状态”

维度
LangChain / CrewAI
AgentForge
核心问题
怎么编排多个 Agent 完成一次任务
怎么让多个 Agent 持续运行不卡死
状态管理
内存/外部存储,需自己接
PostgreSQL 状态机,原生
并发控制
需自己实现
CAS 原子认领,数据库保证
故障恢复
需自己实现
recoverStaleSteps 自愈
配额控制
需自己实现
Cap Gates 入口拦截

LangChain 适合”给一个任务,跑完返回结果”的单次场景。但 AgentForge 是 5 个 Agent 持续运行,有状态演化、有并发冲突、有配额耗尽、有故障恢复。这些是状态管理问题,不是编排问题。

不是 LangChain 不好,是场景不对。框架不是越全越好,是越合身越好。

多 Agent 的瓶颈不在编排,在状态管理。框架解决”怎么跑一次”,状态机解决”怎么跑下去”。

10 最后

这 5 个 Agent 现在每天自主运营 AgentForge。我还在每天优化——调策略、扩触发器、改 Agent 协作方式。

远谈不上完美。Agent 间的协作还基础,”自由意志”主要靠概率性非确定性模拟。但系统真的在跑,真的不需要有人盯着。

技术栈:Next.js + Supabase + VPS。月成本:$8 固定 + LLM 用量。没有 OpenAI Assistants API,没有 LangChain,没有 AutoGPT。就是 PostgreSQL + 几个 Node.js Worker + 一个规则引擎。

你不需要一上来就 5 个 Agent。从 3 个开始——一个协调者、一个执行者、一个观察者——就能跑通完整闭环。

Agent 系统的价值不在”能跑”,在”能自己跑下去”。闭环不是功能,是分水岭。

你在搭 Agent 系统吗?踩过类似的坑没?评论区聊聊,每条我都看。

星标一下,下一篇发 Roundtable 实录——5 个 Agent 怎么吵起来的,含完整对话和 Challenger 硬约束代码。