1. 博客只是个借口
我最近搭了个个人博客。技术栈是 Astro + Cloudflare Tunnel + Docker,审批发布流是 Telegram Bot + 内联按钮 + 倒计时 cron。
但我写这篇文章不是为了炫耀这些。博客只是一个具体的载体——一个足够小、足够个人的场景,小到可以让我在里面做一个完整的实验。
这个实验的问题是:人和 Agent 能不能搭一套协作流程,谁也离不开谁。
2. 架构速览
先快速过一遍基础设施,后面不再展开:
- Astro:静态站点生成器,选了它是因为 JS 生态对 Agent 友好
- Cloudflare Tunnel:Mac Mini 在家里没有公网 IP,CF Tunnel 零运维解决了外部访问
- Docker 双环境:
blog-prod和blog-dev两个独立容器,dev 有 Basic Auth - DeepSeek v4 + OpenClaw:Agent 侧,运行在同一个 Mac Mini 上
- Telegram Bot:协作界面,也是审批控制台
关键设计决策:不搞独立前端。没有 dashboard、没有管理后台、没有任何”为了管理而管理”的 UI。所有协作都发生在 Telegram 对话框里,因为那是我已经在用的地方。
3. Telegram 按钮:协作的入口
整个审批流的核心是两个按钮:✅ 发布,❌ 取消。简单得不像一个”系统”。但这背后有一些选择值得说说。
为什么选 Telegram
没做 Web 后台,因为整个流程本来就在 Telegram 里——写草稿、改文章、发预览都是通过 OpenClaw 在这个对话框完成的。审批如果另开一个入口,等于多了一种需要维护的界面。
还有一个更实际的原因:我每天打开 Telegram,不需要多记一个地址。Agent 发消息过来,看见就处理。
Telegram Bot API 本身就很成熟——内联按钮、消息编辑、callback,这些能力直接拿来用,不需要从零搭通知系统。
按下就是决定
没有”确认弹窗”。点 ✅ 后直接进入五分钟倒计时,点 ❌ 就直接取消。Agent 不能替我做决定。但替我做决定的反面不是替我问”你确定吗”,而是我说发,它就发。
一条消息走完全程
同一条 Telegram 消息从生到死都在自己身上变:
📤 预览待审批(带 ✅/❌ 按钮 + 预览链接)
→ ✅ 已批准,⏳ 5:00 倒计时(带 ❌ 取消按钮)
→ ⏳ 还剩 2:30(带 ❌ 取消按钮)
→ ⏳ 还剩 0:30(带 ❌ 取消按钮)
→ ✅ 已发布 + 文章链接(自动置顶)
或者:
→ ❌ 已取消(标题和标签保留,链接移除)
每一步都通过 editMessageText 更新同一条消息。不需要额外的 log 系统,Telegram 的消息编辑历史本身就是审计日志——什么时候批的、什么时候发的、有没有取消过,全在一条消息里。
取消后消息不会被删——标题、标签、字数都在,只是没了链接。过几天翻回来还能知道哪篇被取消过。
两个按钮够了
这一步只做一个决定:发还是不发。
文章写得好不好、要不要再改一版——这些应该在流程的上一阶段解决。到了”批不批准”这一步,就只有 ✅ 和 ❌。减少选项就是在减少纠结。
倒计时
批了之后不立刻上线,等五分钟。这五分钟内随时可以 ❌ 取消。
五分钟是拍脑袋定的。作用就是万一刚才手抖点错了,还有机会。
倒计时中间更新两次消息(2:30 和 0:30),让时间感还在。到点自动 build + 清缓存 + 上线。
把判断塞进系统
按钮做的事本质上很简单:把人的判断转换成 Agent 能处理的东西。
Agent 写文章、建预览站、发包、倒计时、上线都行。但有一件事它做不了——判断这篇文章该不该发。这件事只有人能做。
按 ✅,这个判断变成 pub_hello-astro 进入 Agent。按 ❌,变成 cancel_hello-astro。Agent 拿到信号后各干各的,不需要再问。人和 Agent 各做自己擅长的事,按钮是中间的接口。
一个教训
Agent 花了很长时间绕弯——调 Bot API、试脚本、试参数——最后发现 presentation 格式干这个最合适。
Agent 什么时候能主动用已有工具而不是一直自己造?prompt 不够清楚?上下文里缺了文档?暂时没有答案,后续探究。
但这个教训值得记住。以后每遇到一次”绕弯才找到现成方案”,都应该记录下来,因为这是协作效率的关键瓶颈。
回过头看,blog_publish.py 这个脚本已经迭代了好几版——审批门同步问题、state 不同步、SSL 重试、取消后消息保留。每次踩坑,修 bug 的过程就是把学到的教训编码进脚本里。脚本在进化,进化的驱动力是人在 loop 里发现的问题。
4. Harness 的四个锚点
按钮是界面,界面下面是四个机制在撑着这套流程。
编译时 gate
approval-gate.mjs 在 npm run build 之前检查一个叫 .approved-posts 的文件。里面没有的文章,即使 draft: false 也发不出去。
编译时检查比运行时可靠。运行时可能被跳过、网络挂了、状态没同步——编译时不过这道门就不让你 build。不过 .approved-posts 和 .blog_state.json 是两个独立文件,一致性靠脚本手动维护——这个坑留到后面说。
运行时 gate
编译时 gate 只管”能不能 build”,运行时 gate 管”还要不要 build”。
审批后的五分钟倒计时就是运行时 gate。状态从 approved 变成 published 之前,随时可以点 ❌ 撤回。三个 cron job 接力跑——T+2:30 更新消息、T+4:30 再更新、T+5:00 触发 build。中间任何一个时间点取消,cron 直接作废。
状态追踪
每个 slug 从生到死的状态都存在 .blog_state.json 里,一个 JSON 文件,简单直接:
{
"hello-astro": {
"msg_id": "19086",
"status": "published",
"url": "https://blog.ratio-dd.com/posts/hello-astro/"
}
}
msg_id 是关键——Telegram callback 里只有 slug,要通过 state 文件把 slug 映射回消息 ID,才能编辑它。没有这个文件,回调拿到了也不知道改哪条消息。
环境隔离
dev 和 prod 是两个独立的 Docker 容器,dev 有 Basic Auth。两个环境共享同一套源码但挂载不同的构建产物目录。重启 dev 不影响 prod,dev 崩了也不影响外面看到的东西。
主意是我提的,但全程我只提供了主意——Agent 自己把容器拉起来、配好 nginx、挂了 Tunnel。
5. 人做什么,Agent 做什么
总编和作者
在这个流程里,我的角色像总编,Agent 像作者。
Agent 负责产出:写文章、建预览站、发消息、倒计时、打包上线。人负责判断:看预览,决定发不发,点 ✅ 或 ❌。
这不是”Agent 听话”。这是各做各擅长的事。作者不会自己签版——版是总编签的。
这不叫 harness
审批流、编译时 gate、dev/prod 环境隔离——这些事本身不新鲜。任何正经上线的产品都有 code review、有 staging、有审批。这不是”给 Agent 专门发明的 harness”,这是生产系统的基本功。
但 LLM 的介入让这些平常的机制变得不平常。
LLM 是概率生成。一个人类作者偶尔会写砸一篇,但不会”偶尔”写出一篇方向完全歪的文章。LLM 会。概率生成的波动比你预期的要大。所以同样的门控在 LLM 驱动的系统里通过率更低、触发频率更高——不是门变了,是走过门的东西变了。
把错误假设进系统设计
这个思路和设计云服务很像。你建一个服务的时候,不会假设”服务器不会挂”——你会假设它一定会挂,然后做多地备份、做健康检查、做自动切换。
LLM 驱动的系统也应该是这个思路。不要把可靠性寄托在”这次输出没问题”上。假设它会出错,然后在这个前提下建保护层。
审批门、倒计时 cancel、编译时 gate——这些不是”更好”的 feature,是同一个前提下的不同防线。Agent 产出的东西可能有问题,这些防线负责抓住。
Harness 不只是容错。它还管协作的边界、决策的接口、状态的可见性。但容错是底层逻辑——先保证不出事,再谈效率。
6. 它还不完美
诚实说几个还在疼的点。
两个状态源 —— 架构上的疏漏。.approved-posts 和 .blog_state.json 各有各的用途,但 design 的时候没想过一致性。cmd_approve 要手动写两处,忘了一次就反咬。本质上是一开始没把状态当一等公民来设计,后面打补丁打的。
state 不同步。cmd_publish 在一次 SSL 错误后崩溃,build 成功了但 _set_state 没跑到。后来加了重试和异常兜底,勉强修了。根源是脚本里的 Telegram API 调用没做健壮的错误处理。
代理没配好。脚本里的 HTTP 请求不走 Clash 代理,间歇 SSL 连接失败。
倒计时 cron 要手建。每次 approve 后 Agent 要手动 cron add 三个 job。理想状态是 cmd_approve 自己就能建 cron,但现在还没有——一次改一处的事。
除去第一个,后面这些说到底是实现没做干净。Harness 的骨架是对的,但肉还没长全。
一个 harness 不是设计一次就完事的。每次踩坑都是把教训写进系统。现在的它不完美,缺的东西还不少,但我大概知道缺的是哪几块。
7. 两个概念
审批按钮的体验让我想到了两个概念。分开说。
human in the loop
人站在循环里,做一个决策节点。
流程是 Agent 驱动的大部分步骤(写→建站→发包→推送),到了某个点停下,等人给信号,然后继续(发布→清缓存→置顶)。人在这个循环里的位置是精确的、受限的——你只在这个节点做判断,不能跳进其他步骤指手画脚。
博客的审批按钮就是这个模式的一次实践。
human create the loop
人设计好循环的规则,然后退到循环外,让它自己跑。
blog_publish.py 就是 create the loop 的产物。我定义了状态机(预览→批准→倒计时→发布)、定义了两个 gate(编译时 + 运行时)、定义了按钮的回调格式。定义完之后,除非出 bug,这个循环不需要我再插手。新的文章进来,走一样的管道。
它们是什么关系
in the loop 管”这一次”——这一篇发不发。create the loop 管”每一次”——以后所有文章走什么流程。你设计好管道,然后自己站到管道里做一次判断。两种模式在同一个系统里交替出现。
什么时候该 in the loop,什么时候该 create the loop,两者混用时边界在哪——这个话题值得单独写一篇。