SUPERCHAT

在 NAS 上折腾 Hermes Agent:一次从 Docker 到 Web UI 的排障记录

发布于 字数统计 4,670 字 阅读时长 16 分钟

在 NAS 上折腾 Hermes Agent:一次从 Docker 到 Web UI 的排障记录

发布于 字数统计 4,670 阅读时长 16 分钟

在 NAS 上折腾 Hermes Agent:一次从 Docker 到 Web UI 的排障记录

本篇文章由 GPT 5.5 生成

目录


一、为什么要折腾 Hermes Agent

今天这次折腾的目标其实很简单:我想把 Hermes Agent 跑在 NAS 上,让它变成一个长期在线的 Agent 服务,而不是只在电脑终端里临时跑一下。

我的理想状态是:

  1. Hermes Agent 本体在 NAS 的 Docker 里常驻运行。
  2. 数据目录能够持久化,不会因为重建容器就丢失配置。
  3. 它可以接入 Telegram,方便在聊天软件里调用。
  4. 它能暴露一个 API Server,后续可以给 Web UI 或其他客户端使用。
  5. 最好再配一个图形界面,日常查看和操作更方便。

麻烦也正是从这里开始的。官方给出的 Docker 命令更像是“先跑起来看看”的方式,而我实际面对的是群晖 NAS 的 Container Manager:要用图形界面填镜像、卷、端口、环境变量、启动命令,还要处理国内网络环境下的代理问题。于是,一个看起来很普通的部署任务,最后变成了一次完整的容器化服务排障。

二、第一步:把命令行部署翻译成 NAS 图形界面配置

一开始的参考命令非常短,大意就是拉取 Hermes Agent 镜像,然后把本地配置目录挂载到容器里的数据目录。但这个命令直接照搬到 NAS 上并不够,因为它没有完整体现一个长期运行服务需要的几件事:

  1. 容器应该以 gateway 服务方式启动,而不是一次性交互式启动。
  2. 数据目录要映射到 NAS 上的固定目录。
  3. API Server 对外端口要发布出来。
  4. 外网访问需要通过局域网代理,而容器里的 localhost 只代表容器自己。
  5. 后续 Web UI 还要能通过 Docker 网络访问到 Agent。

所以在 NAS 图形界面里,真正重要的配置不是“把镜像拉下来”这么简单,而是要把它整理成一个服务:

项目配置思路
镜像使用 Hermes Agent 官方镜像
数据目录将 NAS 上的 Hermes 数据目录挂载到容器的数据目录
启动命令使用 gateway run 让它作为服务长期运行
端口发布 Agent API Server 使用的端口
环境变量配置代理、运行参数和必要的服务开关
网络后续接入自定义 Docker 网络,方便 Web UI 访问

这一阶段最重要的认识是:NAS 图形界面不是不能部署复杂服务,只是需要把命令行里的每个隐含条件拆开,分别填到“卷”“端口”“环境变量”“命令”“网络”这些地方。

三、关键转折:进入容器终端跑官方向导

中间有一段时间,我考虑过直接手写 Hermes 的配置文件,比如 .envconfig.yaml。这种做法看起来很直接,但风险也很明显:字段写错、缩进写错、配置项版本不一致,都可能制造出更隐蔽的问题。

后面真正的转折点,是我发现群晖的容器界面可以直接进入容器终端。这样一来,就不需要在宿主机上硬改配置文件,而是可以进入 Hermes Agent 容器内部,执行官方提供的配置向导。

这一步解决了一个很关键的问题:我可以继续使用 NAS 图形界面管理容器,同时又不放弃 Hermes 官方 CLI 的配置流程。

最终的思路变成:

  1. 先用 NAS 图形界面创建并启动 Hermes Agent 容器。
  2. 进入容器终端。
  3. 在容器内部运行 Hermes 的 setup / model 向导。
  4. 让官方工具自己生成和维护主要配置。
  5. 只有在官方向导覆盖不到的地方,再谨慎补充环境变量或配置项。

这比一上来手写 YAML 稳得多。对于我这种在图形界面和命令行之间来回切换的人来说,这也是最符合实际操作习惯的方案。

四、Telegram、工具和模型供应商

Hermes 的 setup 向导里有不少配置项,其中比较关键的是 Telegram 接入、工具能力开关和模型供应商。

Telegram 这一段的有效步骤是:

  1. 先创建一个 Telegram bot。
  2. 拿到 bot token。
  3. 查询自己的 Telegram 数字用户 ID。
  4. 在 Hermes 的 allow user id 里填写数字 ID,而不是用户名或手机号。
  5. 如果 Telegram 访问也受网络影响,再单独给它配置代理。

这部分最容易混淆的是“用户 ID”和“用户名”。Hermes 需要的是数字 ID,而不是 Telegram 上那个带 @ 的名字。

工具能力选择页反而比较简单。Hermes 会列出 Web Search、Browser Automation、Terminal、File Operations、Code Execution、Vision、Image Generation、Memory、Task Planning 等能力。大多数常用工具默认已经打开,缺少密钥的功能会显示不可用。对我来说,这里没有必要过度折腾,保留默认值反而是更稳的选择。

模型供应商这一段则暴露出另一个问题:配置成功不代表调用链路稳定。测试时出现过流式响应中断、连接被对端或代理链路提前关闭的情况。这个现象说明 provider 路线大体接上了,但稳定性还要受供应商、代理和网络链路共同影响。

所以当时的判断是:不要把所有问题都归结为“配置错了”。如果 token、endpoint、provider 都已经能进入请求阶段,但响应在中途断掉,那就应该把排查重点放到模型供应商可用性、代理稳定性、流式输出兼容性上。

五、第二条线:给 Agent 接上 Web UI

Hermes Agent 跑起来之后,我又开始折腾图形界面。这里有一个很容易踩坑的地方:不同端口和不同界面代表的东西并不一样。

我最后把它们区分成三类:

  1. Agent API Server:给外部客户端调用的服务入口。
  2. 第三方 Web UI:一个独立的网页前端,需要反向连接到 Agent。
  3. Dashboard / 管理面板:偏配置和管理,不等同于聊天 API。

这一步的关键不是“哪个端口看起来像网页”,而是要先搞清楚每个服务在架构里的角色。Web UI 不是 Hermes Agent 本体,它只是一个前端。它能不能工作,取决于它能不能从自己的容器里访问到 Agent 的 API Server。

于是我采用了“保留原有 Agent 容器,另起一个 Web UI 容器”的方式。Web UI 自己有独立数据目录,同时挂载 Hermes 的数据目录用于读取必要配置,并通过 Docker 网络访问 Agent。

这里自定义 Docker 网络非常重要。默认 bridge 网络下,容器名解析和跨容器访问经常不够直观;把 Agent 和 Web UI 放进同一个自定义网络之后,Web UI 就可以通过容器名访问 Agent,而不必依赖宿主机地址。

六、“未连接”才是真正的主线问题

Web UI 页面能打开之后,我原本以为事情快结束了。但登录进去后,界面显示“未连接”。这才是整次排障真正的核心。

这个“未连接”不是前端页面打不开,也不是访问令牌错误,而是 Web UI 的后端服务没有成功连接到 Hermes Agent。

我把排查顺序拆成了几层:

  1. Web UI 容器是否正常运行。
  2. Web UI 的端口是否正确发布到 NAS。
  3. Web UI 和 Agent 是否在同一个 Docker 网络里。
  4. Web UI 容器内能不能解析 Agent 容器名。
  5. Web UI 容器内能不能访问 Agent 的 API Server 端口。
  6. Agent 容器内部是否真的在监听 API Server。
  7. Agent 的配置文件和环境变量是否一致。

前几层排下来后,问题逐渐收束:Web UI 能启动,端口也能访问,容器网络也能解析,但 Agent 的 API Server 一开始并没有真正监听。也就是说,Agent 虽然是按 gateway run 启动的,但 API Server 本身并没有完整启用。

修 API Server 的过程中,又遇到了数据目录权限问题。容器里的运行用户没有权限写入挂载目录,导致配置保存或服务启动异常。这个问题很典型:Docker 里路径看起来都对,但只要 UID / GID 和宿主机目录权限对不上,服务就可能在内部悄悄失败。

权限修好之后,API Server 的健康检查终于能返回正常结果。但 Web UI 依旧显示未连接。最后才发现,Web UI 判断连接状态时并不只看启动时传入的上游地址,还会读取 Hermes 数据目录里的配置文件。也就是说,环境变量里写对了还不够,配置文件里的 API Server 地址和密钥也必须同步。

中间我还踩了一次 YAML 语法坑。配置文件只要缩进或结构坏掉,服务就可能读不出来。这个教训很直接:能用官方向导就不要手写;必须手写时,先备份,再改最小范围。

七、最后是怎么通的

最后真正让整个链路跑通的步骤,可以概括成这样:

  1. 确认 Hermes Agent 是以 gateway run 方式长期运行。
  2. 在 Agent 配置中启用 API Server,并让它在容器网络内可访问。
  3. 修复 NAS 挂载数据目录的权限,让容器内用户能够读写。
  4. 把 Agent 和 Web UI 加入同一个自定义 Docker 网络。
  5. 让 Web UI 的上游地址指向 Agent 容器名和 API Server 端口。
  6. 同步 Hermes 配置文件里的 API Server 地址和访问密钥。
  7. 重启相关容器,并从容器内部和浏览器两侧分别确认连接状态。

最终结果是:Hermes Agent 可以作为 NAS 上的服务运行,API Server 能正常响应,Web UI 也成功显示已连接。

这条链路跑通以后,整件事才终于从“一堆容器和配置文件”变成了一个完整系统:

Telegram / Web UI / 其他客户端
            |
            v
      Hermes Agent API Server
            |
            v
     模型供应商、工具、文件、终端等能力

八、这次折腾给我的教训

这次折腾最有价值的地方,不是最终多了一个 Web UI,而是把一套容器化服务的排障顺序走了一遍。

以后遇到类似问题,我会按这个顺序排:

  1. 服务进程是否真的启动。
  2. 端口是否真的监听。
  3. 容器网络是否能互相解析和访问。
  4. 上游地址是不是写成了容器内部不可达的地址。
  5. 配置文件和环境变量是不是在表达同一件事。
  6. 挂载目录权限是否允许容器内用户读写。
  7. 当前报错到底属于主线问题,还是另一个不相关的支线问题。

这次也暴露了几个明显的教训。

第一,不要太早追着模型供应商问题跑。provider 调用失败当然要解决,但 Web UI 显示未连接时,主线问题是“Web UI 能不能连上 Agent API Server”,不是“某个模型为什么响应中断”。

第二,不要轻易手改 YAML。YAML 看起来简单,但缩进和层级非常脆弱,一旦写坏,排障成本会突然变高。能走官方向导时,优先走官方向导。

第三,容器里的 localhost 不是 NAS,也不是电脑,而是容器自己。所有涉及代理、上游地址、API Server 绑定地址的问题,都必须先想清楚“这段配置是从哪个容器视角发起访问的”。

第四,公开记录折腾过程时,要提前把敏感信息抽象掉。IP 地址、访问令牌、真实容器名、私有目录、分享链接、具体密钥都不应该出现在公开文章里。技术路径可以写清楚,但私有环境不需要暴露。

九、结语

这次 Hermes Agent 的折腾,本质上不是一次“安装软件”,而是把一个 Agent 服务从命令行示例改造成 NAS 上的长期服务。

真正困难的地方不在某一条命令,而在于把它背后的关系图理清楚:Agent 本体、API Server、Web UI、Docker 网络、数据目录权限、代理、模型供应商,每一层都有自己的边界。只要边界没分清,问题就会混在一起,看起来像是哪里都坏了。

最后跑通之后,我对这类服务也多了一点信心。以后再部署类似的东西,我大概会先画清楚链路,再一层一层验证,而不是看到一个报错就立刻换方向。

评价

对整个排障过程的评价

这次过程本质上不是一个单点安装问题,而是一次完整的容器化服务排障:镜像启动方式、持久化目录、代理、API Server、Docker 网络、Web UI 上游、配置文件、权限、第三方 provider、Telegram 集成全部交织在一起。最后能跑通,说明关键链路都被逐层验证过,而不是靠碰运气。

排障路线里真正有效的主线是:

  1. 先确认 Hermes Agent 应该以 gateway run 长期运行。
  2. 再确认 8642 是 Agent API Server,而不是 8787 / 9119。
  3. 再确认 Web UI 的 UPSTREAM、Docker 网络、容器名解析都没问题。
  4. 再确认 Agent 进程虽然在跑,但 API Server 没有监听。
  5. 再补 .env 里的 API_SERVER_ENABLED / API_SERVER_HOST / API_SERVER_KEY
  6. 再解决 /opt/data 权限问题,让容器用户 10000:10000 能写入。
  7. 最后处理 config.yamlplatforms.api_server 的 host/key,让 Web UI 的状态判断和实际上游一致。

这个链路说明最后的成功不是”重启一下好了”,而是通过可验证的证据一步步收敛出来的:docker psdocker inspect、容器内 curl、宿主机 /health、日志里的 Permission denied、以及 config.yaml 的 YAML 错误都被用来定位问题。

过程中的主要问题

最大的问题是中途有几次方向跑偏。尤其是把”未连接”问题和 Telegram token、NVIDIA key 轮换混在一起,导致注意力从 Web UI 到 Agent 的主链路上移开。这不是你的主要问题,而是指导过程的问题:当目标是”左下角未连接”,就应该始终围绕 Web UI -> Agent :8642 -> config.yaml/api_server 这条链排查。

第二个问题是过早手改 config.yaml。YAML 对缩进和冒号很敏感,一旦用 sed 或手工编辑破坏结构,就会把原来的连接问题升级成配置文件无法加载。后续应该形成明确原则:除非必须,不直接改 YAML;需要改时先备份、只改最小范围、改完立刻用 grep 或解析工具验证。

第三个问题是敏感信息处理滞后。token/key 在排障中被贴出来后,后续虽然已经全部更改,但这个教训很重要:以后贴日志前,先把 nvapi-...、Telegram bot token、API_SERVER_KEY、Web UI token 这类内容打码。

对你本人表现的评价

你的表现总体是好的,尤其体现在三点。

第一,你的目标感很强。你多次把方向拉回”我就是要官方一步一步配置""先解决未连接”,这其实很关键。很多排障失败不是因为技术太难,而是目标被带偏;你在这次过程中能明确指出主问题,避免继续在无关项上消耗时间。

第二,你执行力很强。你能在群晖、容器终端、宿主机 shell、Web UI 之间来回切换,并把命令输出完整贴出来。这让问题可以被证据驱动地定位。如果没有这些输出,gateway runhermes-netPermission denied/health okconfig.yaml 错误这些关键事实都无法串起来。

第三,你对”不要手改配置文件”的直觉是对的。虽然中间还是被迫改了 config.yaml,但你一开始抵触手写 YAML 并不是保守,而是合理的风险判断。事实也证明,真正出问题的一次就是 YAML 被改坏。

需要改进的地方也很明确:以后排障时可以更早建立一张”当前目标 / 当前证据 / 下一步验证”的小表,避免在多个问题之间跳转。比如这次如果一开始就把问题拆成”Agent 是否监听 8642""Web UI 容器是否能访问 Agent""Web UI 状态读取的是哪里”三层,过程会短很多。

总体判断

这次折腾的难度不低,已经超过普通”照教程部署容器”的范围,更接近真实运维排障。你最后能把 Hermes Agent、API Server、第三方 Web UI 和 Docker 网络跑通,说明你已经具备了处理 NAS 上复杂容器服务的基础能力。

最值得保留的经验是:以后遇到类似问题,不要先猜配置项,而是按链路验证:

服务是否启动 -> 端口是否监听 -> 容器间是否可达 -> 配置是否指向正确地址 -> 权限是否允许写入 -> 前端状态是否读同一份配置

这次最终成功,靠的不是某一条神奇命令,而是你愿意持续提供证据、不断把问题拉回主线,并最终把每一层都验证清楚。