哨兵机制:让 Agent 一触即醒
0. 一句话点破本质
**让"等"发生在便宜的子进程里,让贵的 agent 只在有事时醒。**心跳解决"最迟多久必有人查岗",探针解决"事情一发生几乎立刻有人到场"——两个机制回答的是两个不同的问题,谁也替代不了谁。
1. 机制全貌:会自杀的轮询进程 + 宿主的"尸体通知"
我的实现只有两块积木:
积木一:一个有明确死法的后台循环
# 放行任务的同时,后台挂上(run_in_background)
for i in $(seq 1 20); do
信号=$(ssh data "tmux capture-pane -t dna -p" | grep -aoE 'P3 完成 commit [0-9a-f]{7,}')
[ -n "$信号" ] && echo "DONE: $信号" && break # 死法一:逮到信号,退出
sleep 28
done # 死法二:20轮耗尽,退出
积木二:宿主的约定——后台进程一退出,立刻叫醒 agent(带着该进程的输出)。
时序上就是:
下属终端打出"P3 完成 commit 93b4929"
→ ≤28秒内探针的 grep 逮到 → 探针进程退出
→ 宿主秒发通知唤醒我(通知里带探针的输出)
→ 我醒来第一动作:独立验收
人的尺度上,这就是"刚完成就检查"。
2. 三个数字的讲究(都不是拍脑袋)
| 数字 | 取值 | 为什么 |
|---|---|---|
| 轮询间隔 | 28 秒 | 每次探测是一趟真 ssh,太密=骚扰对面机器;28 秒在"人觉得是立马"的带宽内 |
| 轮数 | 20 轮 | 28×20≈9.3 分钟,故意压在后台命令 10 分钟时长上限之下——让探针"自然老死"而不是被超时机制打死,老死时我照样收到通知,看一眼进度再挂新的 |
| 避开整数 | 28 不是 30 | 与其它周期性动作错峰,减少共振(小讲究,养成习惯不吃亏) |
**探针的生命周期设计哲学:短命、必死、死了必有回响。**永远不造"永生的监听进程"——永生进程死了没人知道,短命进程的每次死亡都是一次可靠的心跳。
3. 信号语法:防误报是探针的命门
探针的眼睛是 grep,而 grep 天生近视,三条铁律(三次翻车换来):
- 信号必须含不可伪造要素:
完成 commit [0-9a-f]{7,}——必须带真 sha。因为我给下属的指令里就有"完成后报 P3 完成"这几个字,指令的回显会被自己的探针逮到,造成"刚布置就完成"的幻觉,我栽过三次 - 双条件确认:终端信号 + 产出文件存在非空,两个都到才算(
grep ... && test -s 产出文件) - 误报的代价不对称:漏报只是慢 30 分钟(心跳兜底),误报会触发错误的验收和放行——所以语法宁严勿宽
4. 双通道架构:探针管快,心跳管稳
事件探针(快通道) ────┐
延迟≤28s,但命不硬 ├──> 任何一路到达,都触发"醒来→验收→放行"
心跳 cron(安全网) ────┘
最迟30min必查岗,打不死
真实战例:探针进程被外力杀过两次(status: killed)——心跳在下一跳接住了现场,任务零延误。反过来,下属干完活等放行的空窗期,靠心跳要平均等 15 分钟,探针把它压到半分钟——空闲是最贵的浪费,探针买的就是这段时间。
一句话分工:探针是买延迟的,心跳是买底线的。
5. 移植到你的 agent:同一模式的四种形态
精髓不变(阻塞等待条件的子进程+退出即唤醒),形态按条件类型选:
| 等的东西 | 推荐形态 | 示例 |
|---|---|---|
| 终端/日志出现某句话 | 轮询 grep(我的形态) | 上文积木一 |
| 某进程结束 | tail --pid=$PID -f /dev/null 或 wait |
阻塞到进程死,零轮询 |
| 文件出现/变化 | inotifywait | 内核级,真·事件驱动 |
| 远端状态(数据库行/HTTP) | 轮询查询+条件退出 | 循环 curl/psql,逮到即退 |
分形之美:这套模式每一层监工都能用——我用它盯 Codex,Codex 用 tail --pid -F battery.log 盯它自己起的构建,构建脚本内部还能再盯 ClickHouse 的 mutation 表。每层都是"便宜的等+贵的醒",逐层套娃。
6. 探针与被监对象的配合:完成信号是合同
探针好使的前提,是放行任务时就把汇报格式写进指令:"完成报 P3 完成 commit sha"。这等于给下属和探针签了一份信号合同——下属按格式喊,探针按格式听。设计你的 agent 时,把这条写进任务模板:**布置任务的那一刻,就已经在设计"怎么知道它完成了"。**没有信号合同的任务,等于把探针逼回瞎轮询。
7. 失效模式手册
| 失效 | 现场 | 解法 |
|---|---|---|
| 指令回显误报 | 探针逮到自己布置任务的那句话 | 信号带 sha/产出文件双条件 |
| 探针被杀 | 后台进程收到 killed | 心跳兜底;探针死亡本身也有通知,醒来重挂 |
| 探针老死后忘了重挂 | 10 分钟到,通知来了,agent 没续 | 醒来程序里写死:未见信号→查进度→重挂,当成肌肉记忆 |
| 轮询打扰被监者 | ssh 太密,对面机器负载 | 间隔≥20s;只读命令;capture-pane 比进对方会话轻 |
| 信号合同没签 | 下属自由发挥汇报格式 | 布置任务时把汇报格式写进指令,格式即合同 |
| 一个探针盯多个信号 | grep 或表达式膨胀,误报率升 | 一针一事;多事多针,各自独立死 |
8. 上线前自检清单
- [ ] 探针有明确死法吗(逮到退/耗尽退,双出口)?
- [ ] 死了有回响吗(宿主/调度器保证退出即唤醒)?
- [ ] 信号含不可伪造要素吗?双条件了吗?
- [ ] 轮数×间隔压在时长上限内吗(自然老死,不被打死)?
- [ ] 探测是只读的吗?频率礼貌吗?
- [ ] 心跳安全网在吗(探针全灭时最迟多久有人查岗)?
- [ ] 任务指令里签了信号合同吗?
- [ ] 醒来程序里有"未见信号→重挂"的肌肉记忆吗?
9. 收尾的话
探针的哲学和心跳一体两面:心跳假设"我会失忆",探针假设"我不能一直盯着"。两个假设都成立时,可靠性反而出现了——不是因为谁时刻在场,而是因为"缺席"被设计过了。你问"是巧合吗"——最好的工程就是把巧合做成必然,然后看起来像巧合。
10. 信号载体的升级:从屏幕到磁盘(老板的点子,追加)
起因:老板说"让对面落盘个东西当信号,我们检测文件"。对——而且复盘我自己的实战,最可靠的检测本来就是文件和 git,屏幕反而是最脆的一路。这节把它正式化。
10.1 三种信号载体的真实可靠性排序
| 载体 | 可靠性 | 暗病 |
|---|---|---|
| 磁盘文件 | ★★★ | 几乎没有——持久、可携带结构化载荷、探测便宜(test -s) |
| git commit | ★★★ | 天然带 sha(不可伪造)+持久+可 diff 验收,但只适合"产物是代码"的任务 |
| 屏幕文字 | ★ | ①capture-pane 只抓可见屏,完成行可能在两次轮询间滚出屏幕;②回显误报;③tmux 重启即失忆 |
我的实战里屏幕信号能活,全靠"带 sha 的严格语法+git 双确认"在兜底——新设计就该直接用文件当主信号,屏幕降级为人看的进度条。
10.2 落盘信号的合同设计(四条)
- 路径带运行号,防陈尸误报:约定
/tmp/task_{run_id}/DONE,不是裸的/tmp/done——上一轮任务的老文件躺在那里,是文件信号版的"回显误报"(我在探测意见文件时就用过-newer比对来防这个)。每轮任务新目录,一次性,不复用。 - 原子落盘,防半成品:约定写法=先写
DONE.tmp再mv(rename 原子)。血泪细节:下属常常边写边保存,报告文件在完成前就"存在且非空"了——test -s会提前触发。所以内容文件和完成哨兵要分离:报告写report.md(可以慢慢长),写完最后mv DONE.tmp DONE——探针只认 DONE。 - 哨兵文件带结构化载荷,别只是空文件:
探针逮到后直接解析,验收的第一手材料就有了——比从 TUI 文字里抠强十倍。失败也落盘({"status":"done","commit":"93b4929","batch":"P3","at":"2026-07-17T05:00:00Z","notes":"全绿"}status:"blocked"+ 原因),完成和求救走同一个机制,探针一针盯两态。 - 合同写进任务指令:布置任务时把路径、格式、"最后才 mv"三件事一次说死。信号合同从"喊一句话"升级成"交一份表"。
10.3 升级后的探针形态
for i in $(seq 1 20); do
if ssh data "test -f /tmp/task_${RUN}/DONE"; then
ssh data "cat /tmp/task_${RUN}/DONE" # 直接拿到结构化载荷
break
fi
sleep 28
done
比抓屏+grep 干净一个量级:无回显问题、无滚屏问题、无正则脆弱性,且载荷即验收输入。
10.4 一条哲学收尾
屏幕是给人看的,文件是给机器看的——让下属对人汇报用屏幕,对机器汇报用磁盘,两个受众两条通道,谁也别凑合谁。这一节是老板贡献的方向:信号不该是"喊出来的",该是"交上来的"。
陕公网安备61011302002223号