Hermes Agent 崩溃分析:asyncio 清理异常引发网关连锁重启
Hermes Agent 崩溃分析:asyncio 清理异常引发网关连锁重启
一次 Hermes 网关在线跑了约 48 小时后,CLI 进程在关闭清理阶段触发异常,连带网关被 SIGTERM 反复重启,形成「重启打断会话 → 用户重连 → 又被打断」的恶性循环,CLI 不可用约 12 分钟。这篇拆解了完整的崩溃链路和根因。
背景
| 项目 | 信息 |
|---|---|
| 版本 | Hermes Agent v0.14.0 (2026.5.16) |
| 环境 | macOS, Apple M4 Mac mini, Python 3.11.15 |
| 故障时段 | 2026-05-20 22:50 ~ 23:02(北京时间) |
崩溃时间线
| 时间 | 事件 |
|---|---|
| 05-18 14:25 | 网关启动(PID 70093),此后稳定运行约 48.5 小时 |
| 05-20 22:50:36 | 用户发起 CLI 会话,触发标题生成等辅助任务 |
| 05-20 22:51:59 | 🔴 CLI 进程崩溃 — asyncio.run() 关闭期间触发 KeyboardInterrupt |
| 05-20 22:52:00 | 级联错误:16 个 async generator 关闭失败 + 15 个 pending Task 被强制销毁 |
| 05-20 22:52:11 | 用户第一次尝试重连(会话 225211),输入"你好" |
| 05-20 22:53:40 | API 调用开始 |
| 05-20 22:55:01 | 🔴 网关收到 SIGTERM,关闭(systemd Restart=on-failure 重启) |
| 05-20 22:55:02 | 网关重启(新 PID 37043) |
| 05-20 22:55:13 | 会话 225211 的 API 流被中断(stream_interrupt_abort) |
| 05-20 22:55:38 | 重试再次被中断(收到 SIGHUP),会话彻底失败 |
| 05-20 22:56:42 | 用户第二次尝试重连(会话 225642),输入"你好" |
| 05-20 22:58:54 | 会话 225642 被中断(interrupted_during_api_call),收到 SIGTERM |
| 05-20 23:01:46 | 🔴 网关再次收到 SIGTERM,关闭 |
| 05-20 23:01:57 | 网关重启(当前实例),此后稳定运行 |
| 05-20 23:02:44 | 用户第三次尝试(当前会话 230244),成功恢复 ✅ |
根因分析
主因:CLI 进程 asyncio 清理异常
File "cli.py", line 10098, in _signal_handler
from tools.voice_mode import play_audio_file
KeyboardInterrupt
RuntimeError: aclose(): asynchronous generator is already runningCLI 进程在关闭时,_signal_handler 尝试导入 voice_mode 模块,触发了 KeyboardInterrupt。由于当时有 16 个 prompt_toolkit 的 in_terminal 异步生成器正在运行,asyncio 的清理机制无法正常关闭它们,导致:
- 16 个
async_generator抛出RuntimeError: aclose(): asynchronous generator is already running - 15 个
prompt_toolkit渲染 Task 被强制销毁(Task was destroyed but it is pending)
这表明 signal handler 中的模块导入与正在运行的异步事件循环存在冲突。
网关连锁崩溃
CLI 崩溃后,网关经历了两次 SIGTERM 重启循环:
- 每次重启都会杀死正在等待 API 响应的 CLI 会话
- 被杀的会话触发
stream_interrupt_abort - systemd 的
Restart=on-failure策略自动重启网关 - 用户重连后又被下一次重启打断 → 恶性循环
附带问题:Telegram 连接不稳定
- 大量
Telegram polling conflict错误(集中爆发约 2 小时) httpx.ConnectTimeout— 连接 Telegram API 超时- 原因:代理端口不稳定,或存在多实例冲突
影响范围
| 影响项 | 详情 |
|---|---|
| CLI 不可用时长 | 约 12 分钟(22:50 ~ 23:02) |
| 失败的会话 | 2 个(225211、225642) |
| 网关重启次数 | 2 次 |
| 崩溃前正常运行时间 | ~48.5 小时 |
| Telegram 消息 | 崩溃期间无法收发 |
建议修复
1. Signal Handler 安全性(高优先级)
cli.py 的 _signal_handler 在信号处理函数中执行了模块导入,这在异步上下文中是不安全的。建议:
- 将
from tools.voice_mode import play_audio_file改为延迟加载或在启动时预导入 - Signal handler 中只做标记/标志位设置,不做实际 I/O 或导入
2. 网关优雅关闭(中优先级)
网关在收到 SIGTERM 时应等待活跃的 API 请求完成(或设置合理的超时),而不是立即中断正在流式传输的响应。
3. systemd 重启策略(低优先级)
考虑为 Restart=on-failure 添加重启延迟(如 RestartSec=3),避免快速连续重启导致的恶性循环。
4. Telegram 连接稳定性(低优先级)
- 确认代理端口的稳定性
- 检查是否有多个 Hermes 实例同时使用同一个 Telegram Bot Token
总结
这次崩溃是一次典型的「单点异常放大」:CLI 信号处理函数里的模块导入触发了 asyncio 清理失败,本应是进程级的小问题,但叠加网关的 Restart=on-failure 自动重启策略,把每次重启都变成对正在进行的 API 会话的打断,形成了重启-打断-重连-再打断的循环。根子在 signal handler 不该做导入,但放大器是缺乏优雅关闭和重启节流。
排查这类问题时,先把完整时间线拉出来(每条日志的时间戳 + PID + 信号),就能看清因果链:哪个事件触发了哪个,哪一步是连锁、哪一步是独立。本例里 CLI 的清理异常和网关的 SIGTERM 循环是两个独立机制在时间上叠加,单独看都不致命,叠在一起才造成 12 分钟不可用。
