Skip to content
ratio's blog
Go back

博客发布 Harness:一个 Agent 和人的协作实验

1. 博客只是个借口

我最近搭了个个人博客。技术栈是 Astro + Cloudflare Tunnel + Docker,审批发布流是 Telegram Bot + 内联按钮 + 倒计时 cron。

但我写这篇文章不是为了炫耀这些。博客只是一个具体的载体——一个足够小、足够个人的场景,小到可以让我在里面做一个完整的实验。

这个实验的问题是:人和 Agent 能不能搭一套协作流程,谁也离不开谁。


2. 架构速览

先快速过一遍基础设施,后面不再展开:

关键设计决策:不搞独立前端。没有 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.mjsnpm 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,两者混用时边界在哪——这个话题值得单独写一篇。


Share this post:

Next Post
Hello Astro