惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

博客园 - Franky
N
Netflix TechBlog - Medium
Google Online Security Blog
Google Online Security Blog
月光博客
月光博客
量子位
酷 壳 – CoolShell
酷 壳 – CoolShell
V
V2EX
腾讯CDC
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
博客园 - 聂微东
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
M
MIT News - Artificial intelligence
Vercel News
Vercel News
The GitHub Blog
The GitHub Blog
Hugging Face - Blog
Hugging Face - Blog
博客园 - 【当耐特】
Apple Machine Learning Research
Apple Machine Learning Research
aimingoo的专栏
aimingoo的专栏
博客园 - 三生石上(FineUI控件)
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
MongoDB | Blog
MongoDB | Blog
H
Help Net Security
The Cloudflare Blog
Blog — PlanetScale
Blog — PlanetScale
F
Full Disclosure
G
Google Developers Blog
罗磊的独立博客
Jina AI
Jina AI
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
Y
Y Combinator Blog
H
Hackread – Cybersecurity News, Data Breaches, AI and More
J
Java Code Geeks
A
About on SuperTechFans
IT之家
IT之家
大猫的无限游戏
大猫的无限游戏
S
SegmentFault 最新的问题
有赞技术团队
有赞技术团队
GbyAI
GbyAI
雷峰网
雷峰网
T
The Blog of Author Tim Ferriss
The Register - Security
The Register - Security
U
Unit 42
D
Docker
Martin Fowler
Martin Fowler
L
LINUX DO - 热门话题
NISL@THU
NISL@THU
阮一峰的网络日志
阮一峰的网络日志
C
Cybersecurity and Infrastructure Security Agency CISA
博客园_首页
Google DeepMind News
Google DeepMind News

青萍叙事

AI漫剧之配音篇 用 Hermes Agent 搭了一条内容生产线 2026精选5款热门AI配音软件 用 Hermes 打造 LLM Wiki 知识库:Karpathy 方法实战指南 用 Hermes 打造个人知识管理系统 给 Hermes 装上本地记忆:Hindsight 自部署完整记录 给 Hermes Agent 装上慧眼:GLM 视觉 MCP Server 配置指南 AI 配音多音字踩坑记 DeepSeek 5月六连崩,是时候重视 LLM API 故障转移了 做内容为什么最好用真人配音 青萍AI语音开放真人配音师入驻 给 Hermes 装上真记忆:Hindsight 上手指南 Hermes 多 Agent 管理指南 邀请有礼:把好用的 AI 工具分享出去,和朋友一起拿积分 Hermes Agent 基础配置指南:从零搭建你的 AI 管家 推荐一个免费在线音频编辑器,像剪映一样好用 我把小说变成了有声故事,而且每个人物都有自己的声音 FunASR 语音识别:让声音变成可编辑的文字 FFmpeg 音频格式转换实用指南 青萍AI语音多人对话模式:5分钟快速生成播客 青萍创作者平台伙伴共创计划正式启动 免费开源的 Screen Studio 平替!OpenScreen 让你的产品演示瞬间专业起来 语音合成入门:SSML 标记语言快速上手 还在为配音头疼?我宣布:这个免费工具彻底治愈了我 青萍AI语音:用 AI 重新定义你的声音 OpenClaw 记忆对比:向量化 VS Markdown,到底该怎么选? 建站一周年:从 NotionNext 迁移到 Hexo 安知鱼
Hermes Gateway 飞书连接断开排查
青萍叙事 · 2026-06-10 · via 青萍叙事
HermesHermes-Agent运维macOS

前言

昨天手痒,给 Hermes Gateway 换了个新模型。
结果飞书那边突然就收不到消息了。

第一反应是:换模型搞坏了?
赶紧查 Gateway 日志,看到了这么一行:

1
RuntimeError: Executor shutdown has been called

飞书的 WebSocket 连接也断了,Gateway 看起来还在跑,但实际上已经是个空壳。

排查:先看 Gateway 日志

1
2
3
4
5
2026-06-09 14:23:45 INFO  Gateway started
2026-06-09 14:23:46 INFO Feishu WebSocket connected
...
2026-06-09 15:34:12 ERROR RuntimeError: Executor shutdown has been called
2026-06-09 15:34:12 ERROR Feishu WebSocket connection lost

Gateway 14:23 启动,15:34 报错,刚好运行了 1 小时 11 分钟

这个时间点很微妙,不是刚启动就挂,而是运行了一段时间后才出问题。
换模型的操作发生在 15:30 左右,看起来像是换模型触发了问题,但实际上根因不在这里。

根因:Python asyncio executor 的生命周期陷阱

Gateway 是 Python 写的,核心用的是 asyncio event loop。
问题出在 run_in_executor 这个调用上。

Python asyncio 的 loop.run_in_executor() 会把同步代码丢到线程池里执行。
默认情况下,它用的是 event loop 自带的 ThreadPoolExecutor
这个 executor 的生命周期管理有个坑:

如果 executor 的引用计数归零,Python 的垃圾回收器会把它清理掉,调用 shutdown()

在长时间运行的服务里,这种情况可能悄无声息地发生:

1
2
3
4
5
6

async def call_llm(prompt):
loop = asyncio.get_event_loop()

result = await loop.run_in_executor(None, sync_llm_call, prompt)
return result

日志里的 Executor shutdown has been called 就是这么来的。
executor 被关了,所有依赖它的异步任务都会失败,飞书的 WebSocket 消息处理自然也跟着挂了。

加上 macOS 的内存压力管理(memory pressure),长时间运行的 Python 进程更容易触发这类内部状态异常。
系统可能在后台杀掉了一些不活跃的线程,导致 executor 的内部状态被破坏。

发现问题:`–replace` 的坑

最直接的办法就是重启:

1
hermes gateway start

重启后飞书消息恢复正常。
但这只是临时方案,后面还会遇到同样的问题。

这里又踩了一个坑,我发现之前的 Gateway 是用 --replace 模式启动的:

1
hermes gateway start --replace

--replace 的作用是如果已有 Gateway 在跑,先杀掉再启动新的。
听起来很方便对吧?
但在 macOS 上,如果你用了 launchd 来管理服务,--replace 会导致冲突。

Gateway 被 launchd 拉起来后,你手动 --replace 启动一个新的,launchd 不知道旧的被杀了,新旧进程就会打架。

最佳实践:用 launchd 托管 Gateway

macOS 上管理后台服务,launchd 是正道。
先看看现有的配置:

1
ls ~/Library/LaunchAgents/ai.hermes.gateway-*.plist
1
2
/Users/lu/Library/LaunchAgents/ai.hermes.gateway-default.plist
/Users/lu/Library/LaunchAgents/ai.hermes.gateway-work.plist

每个 profile 有一个对应的 plist 文件。
看看里面写了什么:

1
cat ~/Library/LaunchAgents/ai.hermes.gateway-default.plist
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?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>ai.hermes.gateway-default</string>
<key>ProgramArguments</key>
<array>
<string>/Users/lu/.hermes/bin/hermes</string>
<string>gateway</string>
<string>start</string>
<string>--replace</string>
</array>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<dict>
<key>SuccessfulExit</key>
<false/>
</dict>
</dict>
</plist>

看到了吧,ProgramArguments 里赫然写着 --replace
这就是问题所在。

批量修复 plist 配置

检查了一下,所有 profile 的 plist 都有这个问题。
手动改太慢,写个脚本批量处理:

1
2

grep -l "\-\-replace" ~/Library/LaunchAgents/ai.hermes.gateway-*.plist
1
2
/Users/lu/Library/LaunchAgents/ai.hermes.gateway-default.plist
/Users/lu/Library/LaunchAgents/ai.hermes.gateway-work.plist

批量移除 --replace

1
2
3
4
5
for plist in ~/Library/LaunchAgents/ai.hermes.gateway-*.plist; do

sed -i '' '/<string>--replace<\/string>/d' "$plist"
echo "Fixed: $plist"
done

重新加载所有 launchd 服务:

1
2
3

launchctl unload ~/Library/LaunchAgents/ai.hermes.gateway-*.plist
launchctl load ~/Library/LaunchAgents/ai.hermes.gateway-*.plist

新版 macOS 上 unload/load 已标记为 legacy,推荐用 bootout/bootstrap

1
2
launchctl bootout gui/$(id -u) ~/Library/LaunchAgents/ai.hermes.gateway-*.plist 2>/dev/null
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/ai.hermes.gateway-*.plist

验证一下:

1
launchctl list | grep hermes
1
2
ai.hermes.gateway-default    0    com.apple.xpc.launchd.oneshot.0x10000001.hermes
ai.hermes.gateway-work 0 com.apple.xpc.launchd.oneshot.0x10000002.hermes

状态码是 0,说明正常运行。

踩坑后的教训

asyncio executor 的坑要提前防。

长时间运行的 Python 服务,executor 的生命周期不能完全交给垃圾回收器。
要么显式持有 executor 的引用,要么定期检查 executor 状态。

1
2
3
4
5
6
7
8
9

class Gateway:
def __init__(self):
self._executor = ThreadPoolExecutor(max_workers=4)
self._loop = asyncio.get_event_loop()
self._loop.set_default_executor(self._executor)

async def call_in_background(self, func, *args):
return await self._loop.run_in_executor(self._executor, func, *args)

macOS 服务必须用 launchd 托管。

别用 nohup 后台跑。
别用 --replace
别用 screentmux
launchd 是 macOS 原生的服务管理器,它会帮你处理进程崩溃重启、开机自启、资源限制等问题。

关键配置点:

  • RunAtLoad=true:登录时自动启动
  • KeepAlive.SuccessfulExit=false:正常退出不重启,异常退出才重启
  • 不要加 --replace:让 launchd 管理进程生命周期

遇到连接问题先查日志。

飞书消息发不出去,原因可能很多:网络问题、token 过期、服务挂了……
别上来就重启,先看日志。
日志里的错误信息通常能直接指向问题根因。

1
2
3
4
5

tail -100 ~/.hermes/logs/gateway-default.log


log show --predicate 'processImagePath contains "hermes"' --last 1h

这次排查让我对 Python asyncio 的底层机制有了更深的理解。
asyncio 不是银弹。
executor 管理、事件循环生命周期这些细节,在开发阶段很难暴露,往往要跑到生产环境、运行一段时间后才会出问题。

好在这次只是飞书消息发不出去,不是什么致命故障。
但也提醒我,对于长时间运行的服务,监控和容错设计真的不能偷懒。