本地跑着几个常驻服务:一个本地模型服务(Ollama)、一个本地 Agent 网关、一两个自建的小接口。白天用得好好的,第二天早上打开电脑,端口连不上了。或者更隐蔽——端口还通,但内存已经涨到几个 G,响应从几十毫秒变成十几秒,最后被系统杀掉。
我自己这台机器上,那个本地网关从 9 月 30 号起就没重启过,跑了大半个月。它不会主动告诉你"我快不行了",只会等你用到它的时候,给你一个转圈圈。
别指望一个 nohup ... & 就等于"常驻"。 常驻 = 启动 + 探活 + 留痕 + 自愈,四件事缺一件都是裸奔。
我把这四件事塞进一个 200 行出头的 Python 脚本(零第三方依赖,只用标准库),每 30 分钟跑一次体检,连续 3 次不健康才动手重启,重启后有 10 分钟冷却期。适用于 macOS 上任何"用端口对外提供服务"的本地进程;不适用于必须人工确认才能重启的服务(比如正在跑长任务、或有状态的写入进程)。
多数人的判断标准只有一条——端口通不通。不够。端口通不代表能响应,能响应不代表不慢,不慢不代表内存没泄漏。我用了 4 个判据:
判据 | 怎么测 | 抓的是什么问题 |
|---|---|---|
端口存活 |
| 进程没了 |
HTTP 响应码 + 耗时 |
| 假死、超时 |
常驻内存 RSS |
| 内存泄漏 / 膨胀 |
日志尾部异常数 | 读最后 200 行,匹配 ERROR/Traceback | 反复报错但还没死 |
第三条最值钱。WorkBuddy 5.8.3(2026-10-19)专门修了"长期运行后内存膨胀导致卡死"的问题【实证,来源:WorkBuddy 官方更新日志】,说明这类问题不是个例。
{
"fail_window": 3,
"cooldown_sec": 600,
"services": [
{
"name": "local-model",
"host": "127.0.0.1", "port": 11434,
"http_path": "/api/tags",
"rss_limit_mb": 8000,
"http_ms_limit": 3000,
"start_cmd": "nohup ollama serve > /tmp/local-model.log 2>&1 &",
"stop_wait": 3
},
{
"name": "local-gateway",
"host": "127.0.0.1", "port": 18789,
"http_path": "/",
"rss_limit_mb": 2000,
"http_ms_limit": 5000,
"start_cmd": "cd ~/myagent && nohup npm run start > /tmp/gateway.log 2>&1 &",
"log": "/tmp/gateway.log"
}
]
}上面端口、路径、启动命令均为示例,请替换成你自己机器上的实际值。
完整脚本我放在文末附件 agent_watchdog.py,这里只贴三个关键函数:
def spawn(cmd):
"""启动常驻进程:必须脱离父子进程管道,否则调用方会一直等到超时。"""
subprocess.Popen(cmd, shell=True, start_new_session=True,
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
stdin=subprocess.DEVNULL, close_fds=True)
return 0
def port_open(host, port, timeout=2.0):
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.settimeout(timeout)
try:
return s.connect_ex((host, port)) == 0
finally:
s.close()
def rss_mb_of_pid(pid):
rc, out, _ = run("ps -o rss= -p %s" % pid) # macOS: rss 单位是 KB
return round(int(out.split()[0]) / 1024, 1) if rc == 0 and out else None这是整个脚本最容易被忽略、也最容易出事的一段:
rec["fail_streak"] = 0 if ok else rec.get("fail_streak", 0) + 1
if args.repair and not r["ok"] and rec["fail_streak"] >= window and svc.get("start_cmd"):
cd, left = in_cooldown(args.state, r["name"], cooldown)
if cd:
r["action"] = "skip: cooldown %ss left" % left
else:
...一次体检失败就重启,等于给自己埋雷:服务本来在正常冷启动,你把它杀了又拉起来,循环往复。必须"连续 N 次失败 + 冷却期"两道闸。
脚本不加 --repair 就只体检、只打印、只写留痕,绝不杀进程也绝不拉进程。这条是敢把它挂进定时任务的前提——你永远可以先跑一周只观察,看看告警是不是在误报。
# 只体检(安全,可放心挂定时任务)
python3 agent_watchdog.py --config watchdog.json --check
# 体检 + 自愈(确认告警无误报后再开)
python3 agent_watchdog.py --config watchdog.json --repair退出码 = 0 表示全部健康,非 0 表示有服务异常,可以直接被上层调度拿去判断。
在 WorkBuddy 里建一个自动化任务,指令写死成一句话(实测这种"固定命令 + 固定配置"的任务最省积分,因为它不需要每次重新理解上下文):
每隔 30 分钟,执行:cd ~/myagent && python3 agent_watchdog.py --config watchdog.json --repair
如果输出里出现 DOWN 或 rss 超过 limit,把这一行原样发给我,不要改写。crontab 在 macOS 上常因为「完全磁盘访问权限」静默失败,排查起来很烦。用 LaunchAgent 更稳:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key><string>com.example.agentwatchdog</string>
<key>ProgramArguments</key>
<array>
<string>/usr/bin/python3</string>
<string>/Users/你自己的路径/agent_watchdog.py</string>
<string>--config</string><string>/Users/你自己的路径/watchdog.json</string>
<string>--repair</string>
</array>
<key>StartInterval</key><integer>1800</integer>
<key>RunAtLoad</key><true/>
<key>StandardOutPath</key><string>/tmp/agentwatchdog.out</string>
<key>StandardErrorPath</key><string>/tmp/agentwatchdog.err</string>
</dict>
</plist>cp com.example.agentwatchdog.plist ~/Library/LaunchAgents/
launchctl load ~/Library/LaunchAgents/com.example.agentwatchdog.plist
launchctl list | grep agentwatchdog # 看第一列是不是 0(0=正常退出)以下全部是本机真实执行输出,环境 macOS(Apple Silicon)+ Python 3.13.12。
① 三服务只读体检(本机真实服务):
SERVICE PORT PID RSS(MB) HTTP MS VERDICT
------------------------------------------------------------------------------
local-model up 1954 22.0 ok 18 OK
local-gateway up 1782 128.5 ok 28 OK
not-running DOWN - - - - port 19091 closed其中 local-gateway 这个进程自 9-30 起连续运行 20 天,RSS 128.5 MB,HTTP 28 ms。
② 内存阈值告警(把 limit 故意设成 10 MB):
ollama up 1954 22.3 ok 14 rss 22.3MB > limit 10MB
退出码=1端口和 HTTP 都是好的,只有内存判据抓到了——这正是单靠端口探活漏掉的那一类。
③ 崩溃自愈闭环(临时假服务,不碰真实进程):
阶段 | 结果 |
|---|---|
健康 |
|
|
|
第 1 轮 |
|
第 2 轮 |
|
复检 |
|
④ 脚本耗时:3 个服务体检,连续 3 次 real 0.14 / 0.14 / 0.14 秒。开销可以忽略,挂 5 分钟一次也不心疼。
⑤ 积分消耗:【待本人实测补充】——挂到 WorkBuddy 自动化任务后,单次运行的积分消耗我还没记账,跑满一周后补。
nohup xxx & 会挂死 subprocess.run:我第一版用 run(shell=True, capture_output=True) 跑启动命令,直接 TIMEOUT after 15s——后台进程继承了 stdout/stderr 管道,run() 要等管道关闭才返回。改成 Popen + start_new_session=True + 三个 DEVNULL 后,15 秒超时 → 4 毫秒返回。这是本次最大的一个坑。--check 一周,拿到真实分布再定阈值。--check 告警,不配 start_cmd。None 并在表格里显示 n/a,而不是当成 0 错误——否则你会以为服务很健康。ps -o rss= 单位是 KB,直接当 MB 用会差 1024 倍。另外 xargs -r 在 BSD xargs 上不支持,清理脚本里我改成了 PID=$(...); [ -n "$PID" ] && kill -9 $PID。不适用的场景:需要人工输入密码/扫码才能启动的服务;启动依赖 GUI 会话的进程(LaunchAgent 在 SSH 登录时跑不起来);以及任何你没法承受"误杀"代价的进程。
常驻不是 nohup 一下就完事。补上「探活 + 阈值 + 抖动防护 + 留痕」这四块,一个 200 行的脚本就能把本地 Agent 网关的可用性从"看运气"拉到"可控"。先只观察一周,确认无误报再开自愈——顺序反了会出事。
缺的图:5 张全部缺。其中截图位 2 最关键的——用 python3 agent_watchdog.py --demo 在本机重跑一遍,把「健康 → kill → 第1轮不动手 → 第2轮自愈 → 复检」这五段终端输出截下来,一篇文章的图片说服力就够了。
#WorkBuddy# 让本地 Agent 网关 7×24 不倒:一个 200 行的体检+自愈看门狗(附完整脚本)草稿/assets/agent_watchdog.py(完整可运行脚本)原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。