IT相关 · 2026年9月2日

给 Hermes Agent 装个高度适配 Web 界面:Hermes WebUI 手动部署记录

最近在给一台服务器做 AI 助手相关的 Web 部署时,遇到一个让我反复纠结、也确实踩了坑的点:给 Hermes Agent 装一个可用的 Web 前端,到底该用哪个项目、怎么装。

这台机器上本来已经有一套 Open WebUI 跑着,通过 OpenAI 兼容接口连着 Hermes Agent 的网关(8642 端口),日常对话用起来没问题。但我发现 Open WebUI 本质上是一个「通用」的模型聊天界面,它把 Hermes Agent 当作一个普通模型来连,并不真正理解 Agent 的工具调用、记忆、会话等独有逻辑。于是决定再部署一个专门适配 Hermes 的 Web 前端 Hermes WebUI,实测支持中文界面,让 Agent 的能力能更完整地呈现出来。

2026-09-18 更新:重写第 6 节反代配置,新增第 7–9 节。

前置条件:先确认 8642 网关已经通

这一步是整套部署的前提,也是最容易卡住的地方。Hermes WebUI 本身不提供 Agent 运行时,它只是前端,最终要连到 Hermes Agent 的网关(默认 8642 端口)。如果你还没做过网关配置,直接连会连不上——前端会一直卡在连接状态,或者提示无法访问后端。

所以动手之前,请先确认下面三件事都成立:

  1. Hermes Agent 的内置 API Server(网关)已经开启,配置写入了对应的 config;
  2. 配置改完后已经重启生效(只改配置不重启,端口不会起来);
  3. 用 curl 或 ss -lntp 验证 8642 端口确实在监听、能正常响应。

如果你还没有配过网关,建议先跟着 《Hermes Agent 搭建 Web 前端:Open WebUI 连接内置 API Server 全攻略》 的「第一步:配置 Hermes 内置 API Server」把网关配好、重启生效并验证 8642 端口通了,再回来看这篇。

反过来,如果你的环境里 8642 网关已经在跑(比如已经有 Open WebUI 在用),那这一步可以直接跳过,进入下面的部署步骤。

先厘清三个容易混淆的点

在动手之前,我先花时间把几个容易混淆的概念搞清楚,避免装错东西:

  1. Hermes WebUI 不是 Open WebUI。虽然都叫 WebUI,但 Hermes WebUI 是专为 Hermes Agent 设计的前端,它调用 Agent 的会话、工具、记忆等接口;而 Open WebUI 是通用的模型对话界面,两者的定位和连接方式都不同。
  2. Hermes WebUI 不是 Hermes Agent 的子命令。Hermes Agent 官方主项目自带的 Web 表面是 hermes dashboard(管理面板)。而 Hermes WebUI 是一个独立的社区项目(仓库 nesquena/hermes-webui),需要单独 clone 安装,跟 Agent 核心是两套代码。
  3. Hermes WebUI 的部署形态决定是否要连网关。它有两种形态:单进程(WebUI 和 Agent 同一进程,进程内调用,不需要配网关)和两进程/两容器(WebUI 独立,通过 HTTP 连 Agent 网关 8642)。因为我这台机器上 8642 网关已经开了(供 Open WebUI 使用),所以我走的是后一种:WebUI 独立运行,复用现有的 8642 网关。这一点和 Open WebUI 类似,但后端连的是完整的 Agent 运行时(有状态、有工具、有记忆),而不是无状态的模型 API——这是本质区别。

部署步骤

因为对安装脚本(install.sh / start.sh)的健壮性有顾虑,我没有直接用脚本,而是手动分步部署,每一步都可见可控:

1. 克隆项目

git clone --depth 1 https://github.com/nesquena/hermes-webui.git /root/hermes-webui

2. 创建虚拟环境并安装依赖

cd /root/hermes-webui
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt

在继续之前,先说明一下这个项目是怎么启动的。仓库根目录下有两个相关的 Python 文件:server.py 和 bootstrap.py。server.py 是 Web 服务本体,但直接 python server.py 启动时不会读取项目根目录下的 .env 文件;而 bootstrap.py 会先加载 .env 里的环境变量(连接网关的地址、API 密钥、Agent 目录等)再启动服务。所以接下来的部署统一用 bootstrap.py 作为启动入口——第 3 步配置的 .env 正是它会读取的文件。

3. 配置环境变量(连接现有网关)

cat > /root/hermes-webui/.env <<'EOF'
HERMES_HOME=/root/.hermes
HERMES_GATEWAY_URL=http://127.0.0.1:8642
HERMES_API_SERVER_KEY=<你的 8642 API 密钥>
HERMES_WEBUI_AGENT_DIR=/usr/local/lib/hermes-agent
HERMES_WEBUI_HOST=127.0.0.1
HERMES_WEBUI_PORT=8787
EOF

注意:HERMES_WEBUI_AGENT_DIR 要指向服务器上 Hermes Agent 的实际安装目录(这里示例是 /usr/local/lib/hermes-agent),否则 WebUI 找不到 Agent 目录,工具调用等功能不可用。

若采用单进程形态(WebUI 与 Agent 同进程、进程内调用 AIAgent),HERMES_GATEWAY_URL 和 HERMES_API_SERVER_KEY 可省略,详见第 9 节。

顺带把上面几个变量的含义说清楚,这也是连不通时最该先检查的地方:

  • HERMES_GATEWAY_URL:就是前置条件里验证过的那个网关地址,默认 http://127.0.0.1:8642。它必须和网关实际监听的地址、端口一致,否则 WebUI 起来也连不上后端。
  • HERMES_API_SERVER_KEY:连接网关用的 API 密钥,要和网关侧配置的密钥完全相同。密钥不对时,前端通常表现为能打开页面但无法发起会话,看服务端日志更容易定位。
  • HERMES_WEBUI_HOST / HERMES_WEBUI_PORT:WebUI 自己监听的地址和端口,跟网关端口是两回事,不要填混。

4. 前台验证一次

cd /root/hermes-webui
HERMES_WEBUI_FOREGROUND=1 python bootstrap.py

确认启动日志正常、Agent 目录识别正确、端口监听 127.0.0.1:8787 后,Ctrl-C 停掉。

5. systemd 托管(开机自启 + 崩溃重启)

cat > /etc/systemd/system/hermes-webui.service <<'EOF'
[Unit]
Description=Hermes WebUI
After=network.target

[Service]
Type=simple
WorkingDirectory=/root/hermes-webui
Environment=HERMES_WEBUI_FOREGROUND=1
ExecStart=/root/hermes-webui/.venv/bin/python /root/hermes-webui/bootstrap.py
Restart=on-failure
RestartSec=3

[Install]
WantedBy=multi-user.target
EOF

systemctl daemon-reload
systemctl enable --now hermes-webui
systemctl status hermes-webui

6. 宝塔面板 nginx 反向代理(2026-09-18 更新)

在宝塔面板建好域名站点(域名这里用 *.example.com 占位)后,按宝塔惯例,把一段只含 location 块的反代配置放进 /www/server/panel/vhost/nginx/proxy/<域名>/ 目录下的 .conf 文件(文件名带 md5 前缀,由宝塔通配符 include proxy/<域名>/*.conf; 自动引用),只做反向代理,不需要配 server / ssl(由宝塔面板统一管理):

location ^~ / {
    proxy_pass http://127.0.0.1:8787;
    proxy_http_version 1.1;
    proxy_set_header Host              $host;
    proxy_set_header X-Real-IP         $remote_addr;
    proxy_set_header X-Forwarded-For   $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
    # 实时流是 SSE(不是 WebSocket),这两行是为其它升级请求留的余量
    proxy_set_header Upgrade           $http_upgrade;
    proxy_set_header Connection        $connection_upgrade;
    # SSE 流:关缓冲更稳(本机实测不加也正常,见下文「坑二」)
    proxy_buffering off;
    proxy_cache off;
    # 长连接超时(Agent 工具调用可能耗时较长)
    proxy_read_timeout  3600s;
    proxy_send_timeout  3600s;
}

改完后 nginx -t 校验语法、nginx -s reload 重载。$connection_upgrade 这个变量需要 map 定义,宝塔自带模板里已经有了;如果你手写 conf 只抄了这一段、没抄 map,就把它换成字面量 "upgrade"。如果你不打算支持任何 WebSocket,把 Upgrade / Connection 这两行直接删掉也没影响。

7. 实测纠错:三个网上抄不到的坑(2026-09-18 新增)

上面那段配置我是照着别人的文章抄的,实际跑起来之后发现有三处不对。下面都是我在这台机器上真实验证出来的结果,不是推测。

坑一:实时流是 SSE,不是 WebSocket

很多文章(包括我一开始抄的那篇)都写「Hermes WebUI 用 WebSocket 做实时聊天」,所以反代必须配 Upgrade / Connection 头。我去源码里核对了一遍:实际走的是 Server-Sent Events,路径是 /api/chat/stream,响应头是 Content-Type: text/event-stream。

怎么验证的:直接抓那个流看原始输出,能拿到标准的 SSE 帧——

curl -sN -b cookie.txt "https://<域名>/api/chat/stream?stream_id=<流ID>"

返回长这样:

id: 041c81a0b42e4e36aa0615f8a53fd778:1
event: context_status
data: {"session_id": "dd6f3bd17c5f", ...}

id: 041c81a0b42e4e36aa0615f8a53fd778:2
event: token
data: {"text": "1"}

是 event: token 这种一行一事件的格式,不是 WebSocket 帧。所以那两个 Upgrade 头对这个流一点用都没有,配不配都一样。留着无害(万一以后加了 WebSocket 功能能用上),但别指望它能解决「流不吐字」的问题。

坑二:以为要加 proxy_buffering off,实测不用

接着上一个坑——既然确认了是 SSE,我第一反应是「nginx 默认开着 proxy_buffering,一定会把 SSE 攒成一大块再吐」,于是准备去加 proxy_buffering off;。

但动手前先测了一遍,结果是不加也没问题。测法很简单:发一个要求写长回答的请求,然后逐 token 打时间戳,看 token 是「一个一个到」还是「最后一次性到」。

curl -sN -b cookie.txt "https://<域名>/api/chat/stream?stream_id=<流ID>" | python3 -u -c "
import sys, time
t0 = time.time()
first = last = None
n = 0
for line in sys.stdin:
    if line.startswith('event: token'):
        t = time.time() - t0
        if first is None:
            first = t
        last = t
        n += 1
print('tokens=%d  first=%.2fs  last=%.2fs  spread=%.2fs' % (n, first, last, last - first))
"

实测结果:tokens=294 first=9.49s last=11.49s spread=1.99s。294 个 token 分布在 2 秒里陆续到达,说明是实时逐字吐出来的,没有被 nginx 缓冲住。

为什么没被缓冲?因为 nginx 对 text/event-stream 这类响应有特殊处理,加上 WebUI 端本身 每 5 秒会发一次心跳,心跳会不断把 TCP 缓冲区顶出去(源码里 _SSE_HEARTBEAT_INTERVAL_SECONDS = 5)。所以这一段我最终没有改——配置里那两行 proxy_buffering off; 是写给「万一你的环境确实出现攒字」的情况,属于预防性冗余,不是必需品。如果你的流正常逐字输出,可以不加。

坑三:proxy_read_timeout 3600s 也不是必须的

原配置把超时设到 3600 秒,理由是「Agent 工具调用可能耗时较长」。这个理由本身没错,但要注意它针对的不是这个 SSE 流。

因为上面说的心跳每 5 秒就发一次数据,nginx 的 proxy_read_timeout(默认 60s)是「两次读之间」的间隔计时,只要有新数据就一直重置。5 秒 < 60 秒,所以默认值就够用,长任务不会因为读超时被断开。

那 3600s 是给谁用的?是给普通的非流式 HTTP 请求——比如某些同步接口、文件上传、或者你在设置里关掉了流式输出。这类请求从发起到返回中间可能一个字都没有,全靠这个超时兜着,设长一点更保险。所以这一行建议保留,只是别把它当成「维持 SSE 不断线」的手段。

8. 反代验证清单(2026-09-18 新增)

配完之后按顺序过一遍,每一项都要看到真实输出,不要只看「页面能打开」就收工:

  1. 本地直连:curl -s -o /dev/null -w "%{http_code}" http://127.0.0.1:8787/login 应为 200。这一步能把「WebUI 自己有问题」和「反代有问题」区分开。
  2. 公网跳转:curl -s -o /dev/null -w "%{http_code} %{redirect_url}" https://<域名>/ 应为 302 且跳到 /login。
  3. 登录打通:POST /api/auth/login 带密码,应返回 {"ok": true};确认响应里种下了会话 cookie。
  4. 鉴权生效:不带 cookie 请求 /api/sessions 应返回 {"error":"Authentication required"},带 cookie 才返回数据。这一步能证明反代没有把认证绕过去。
  5. 真实模型对话:走一遍 /api/session/new → /api/chat/start,再从 /api/chat/stream 读到 assistant 回复。这是唯一能证明「链路 + Agent 运行时 + 模型」三者都通的测试,前面几项都替代不了。
  6. 静态资源:把首页引用的 static/*.js、*.css 逐个 curl 一遍,确认全是 200。我这边测了 20 个文件,全 200。
  7. 重启存活:systemctl restart 之后本地和公网再各测一次。

补充一个容易误判的点:我注意到宝塔生成的 vhost 里有一条「禁止访问的敏感文件」正则,会拦截 .env.*、README.md、requirements.txt 这类路径返回 404。这是好事,但要确认你部署的静态资源路径没撞上这些规则——否则会出现「后端明明有文件,公网却 404」的怪现象。

9. 两种部署形态下反代的区别(2026-09-18 新增)

这一条很重要,因为网上教程大多只写一种形态,照抄到另一种上就会一脸懵。

  • 两进程形态(本文前面的步骤):WebUI 独立进程,通过 HTTP 连 8642 网关。反代只需要指着 WebUI 自己的端口(8787),HERMES_GATEWAY_URL / HERMES_API_SERVER_KEY 是 WebUI 到网关这一段的事,跟 nginx 无关。
  • 单进程形态:WebUI 和 Agent 在同一个进程里(进程内调用 AIAgent),根本不需要 8642 网关,HERMES_GATEWAY_URL / HERMES_API_SERVER_KEY 这两个变量也不用配。我这次另一台机器就是走这个形态——因为那台机器上压根没开 8642,去配网关反而是白费功夫。nginx 反代部分两种形态完全一样,都是指着 WebUI 的 8787。

怎么判断自己该走哪种?先跑 ss -lntp | grep 8642:有监听、且有别的前端(比如 Open WebUI)在用,就走两进程复用网关;没有监听、也不打算为它单独起一个,直接走单进程更省事。

验证与注意事项

  • 部署完成后,浏览器访问 https://*.example.com 会先跳转到登录页(302 到 /login),输入 Agent 的登录凭据即可进入,说明反代链路和认证都正常。
  • 密码设置:Hermes WebUI 支持中文界面。若要防止未授权访问,加密码不一定只能通过环境变量 HERMES_WEBUI_PASSWORD 配置,也可以直接在 WebUI 后台的设置界面里配置访问密码(或启用 passkey)。如果只是本机访问则可不设,但一旦通过域名暴露到公网,务必设置密码或启用更严格的身份校验。
  • 连不上后端时先查网关,不要先怀疑 WebUI:页面能打开但会话发不出去、或者提示后端不可用,绝大多数情况是 8642 网关没开、没重启生效、或 HERMES_API_SERVER_KEY 与网关不一致。按「前置条件」那三件事复查一遍(端口在不在监听、配置有没有重启、密钥对不对),通常就能解决。
  • 会话恢复告警可忽略:启动日志里偶尔会出现类似 KeyError: 'agent:main:...' 的报错,是旧的会话记录恢复失败所致,不影响正常使用。
  • 因为复用现有的 8642 网关,Open WebUI 和 Hermes WebUI 两个前端可以并存,共享同一份 /root/.hermes 状态(会话、记忆、技能)。

常用命令速查

systemctl status hermes-webui    # 查看服务状态
systemctl restart hermes-webui   # 重启服务
journalctl -u hermes-webui -f    # 实时查看日志