English
← 返回 Agent Rule

在一个 sun-port 后运行多个 Hermes 网关 Profile ✓ 已验证

2026-08-18 · 15 分钟 · Hermes · Docker · sun-port · Git · PostgreSQL

一个网关、一个 Agent、一个消息平台。这是一套 Hermes 安装开始时的形态,对单个机器人来说已经足够。但真正的舰队是横向扩张的:一个负责内部运营的飞书 Agent、一个面向客户的微信 Agent、一个服务工程团队的Slack Agent——每个都拥有不同的 skills、不同的 cron 任务、不同的 memories,而且你绝不希望它们互相串味。答案不是三台 VPS,而是Profile 隔离:多个 Hermes 网关跑在同一台主机上,每个都扎根于自己的 HERMES_HOME,用 Docker 容器化,统一路由在一个 sun-port 之后,并用 Git 做版本管理。本教程从零搭建这样一支舰队。

本文中的每一条命令都运行在真实的 Hermes 网关基础设施上——多个 HERMES_HOME Profile、Docker 容器、一张 sun-port 路由表,以及一个纳入 Git 版本管理的配置目录——✓ 已验证 徽章意味着真实的执行,而非从 README 里复制粘贴。

1. 为什么每个平台一个网关还不够

一个 Hermes Profile 不只是一个登录态——它是一个自包含的工作空间。每个 Profile 都带着自己的:

两个逻辑上独立的 Agent 一旦共享同一个 Profile,就会共享以上全部——而一个出错的 cron 任务或一条泄漏的 memory,就足以制造一团你理不清的乱麻。Profile 隔离就是解药:给每个 Agent 自己的 HERMES_HOME,让它们并排运行、彼此永远看不到对方的状态。

2. Hermes 的 Profile 隔离是如何工作的

Hermes 从 HERMES_HOME 环境变量读取它的主目录。默认是 ~/.hermes;一个 Profile 位于 ~/.hermes/profiles/<NAME>。把网关进程指向某个 Profile 目录,它就会加载那个 Profile 的 .envconfig.yamlgateway.pidgateway_state.json——与默认 Profile 的完全分开。

$ ls ~/.hermes
config.yaml  .env  skills/  cron/  memories/  plugins/  gateway.pid  gateway_state.json

$ ls ~/.hermes/profiles/finance
.env  config.yaml  skills/  cron/  memories/  gateway.pid  gateway_state.json

两个 Profile、两个 gateway.pid 文件、两个 gateway_state.json 文件——互不冲突,因为每个进程都通过 HERMES_HOME 被“锁”进了自己的目录树。

唯一不会替你隔离的东西:进程环境。你 shell 里导出的变量——尤其是像 FEISHU_APP_ID 这样的消息平台凭据——会被你启动的每一个网关继承,与 Profile 无关。这是打垮大多数多 Profile 配置的陷阱,第 6 节就是专门讲如何封死它的。

3. 前置条件

$ docker --version
Docker version 27.3.1, build ce12230
$ git --version
git version 2.46.0
$ ls ~/.hermes/profiles
finance  support  sales

你需要先有一套能正常工作的单 Profile Hermes 安装(见我们的 Ubuntu 教程),以及一个运行中的 sun-port(见 反向代理教程)。这里的模式也与我们早前讲过的 Docker 部署模式天然契合。

4. 创建第二个 Profile

从目录骨架开始。一个 Profile 至少需要一个 .env 和一个 config.yaml;skills、cron 和 memories 初始为空,会随着 Agent 的工作逐渐填满:

$ mkdir -p ~/.hermes/profiles/finance/{skills,cron,memories,logs}
$ touch ~/.hermes/profiles/finance/.env
$ touch ~/.hermes/profiles/finance/config.yaml

如果你已经有一个想复制的 Profile(比如一个很适合当作销售 Agent 起点的客服 Agent),就复制目录、然后再裁剪——绝不要盲目克隆,否则会带过来你本不想要的 memories 和 cron:

$ cp -a ~/.hermes/profiles/support ~/.hermes/profiles/sales
$ rm -rf ~/.hermes/profiles/sales/{memories/*,cron/*}
$ rm -f ~/.hermes/profiles/sales/gateway.pid ~/.hermes/profiles/sales/gateway_state.json
清掉 PID 和状态文件。一个指向运行中进程的 gateway.pid,或一份陈旧的 gateway_state.json,会让新网关误以为它已经在运行(或者把一个本要发给另一个实例的停止信号送给它)。首次启动前把这两个文件都删掉。

5. 配置 Profile 的 .env——飞书泄漏问题

陷阱在这里。Hermes 先加载 ~/.hermes/.env(带 override=True),再加载 Profile 自己的 .env(不带 override)。所以,如果你的主 Profile 是为飞书接线的,那么在你的微信 Profile 启动时,那些变量早已进入进程环境——于是它会高高兴兴地去连接飞书。

解法是双管齐下。首先,Profile 的 .env 必须显式地清空飞书变量:

# ~/.hermes/profiles/finance/.env
FEISHU_APP_ID=
FEISHU_APP_SECRET=
TZ=Asia/Shanghai
NODE_PATH=/usr/lib/node_modules

这里空的 FEISHU_APP_ID=FEISHU_APP_SECRET= 不是摆设——它们的存在,是为了让 Profile 自己的 .env 有东西可加载,而不是去继承那个飞书的值。

6. 配置 config.yaml——显式的平台开关

接下来是 Profile 的 config.yaml。有两件事很重要:目标平台必须显式地启用(非飞书平台的默认值是 false),飞书则必须显式地禁用:

# ~/.hermes/profiles/finance/config.yaml
model:
  default: deepseek-chat
  provider: deepseek

platforms:
  weixin:
    enabled: true   # 必须显式声明!默认是 false
    token: "your-token-here"
  feishu:
    enabled: false
光有 feishu.enabled: false 还不够。网关的配置加载器是在读完 YAML 之后才去检查 os.getenv("FEISHU_APP_ID") 的,所以一个被继承的环境变量会盖过你的 false。你需要 YAML 里的开关第 7 节的环境清理两者兼备。这是最常见的“我的第二个网关起不来”的失败原因。

7. 启动脚本——清理环境,而不是清空世界

直觉会让人用一个干净的环境来启动——env -i别这么做。它连 PATHHOME 也一起清掉了,你的网关会死在一堆莫名其妙的 import 和 socket 报错里。正确做法是:在网关启动之前,只在 Python 层面清掉消息平台变量:

#!/bin/bash
# /usr/local/bin/finance-gateway
cd /root/.hermes/hermes-agent || exit 1

HERMES_HOME=/root/.hermes/profiles/finance \
GATEWAY_ALLOW_ALL_USERS=true \
TZ=Asia/Shanghai \
FEISHU_APP_ID="" \
FEISHU_APP_SECRET="" \
PYTHONUNBUFFERED=1 \
./venv/bin/python3 -c "
import os, sys
for k in list(os.environ):
    if k.startswith('FEISHU_') or k.startswith('LARK_APP_') or k.startswith('LARK_BOT_'):
        del os.environ[k]
os.environ['HERMES_HOME'] = os.path.expanduser('~/.hermes/profiles/finance')
os.environ['FEISHU_APP_ID'] = ''
os.environ['FEISHU_APP_SECRET'] = ''
sys.path.insert(0, '.')
from gateway.run import main
import asyncio
asyncio.run(main())
" 2>&1 | tee -a /root/.hermes/profiles/finance/logs/gateway.log &

它的作用是:把进程自身环境里每一个 FEISHU_/LARK_ 变量清掉,把 HERMES_HOME 重新指向 Profile,然后启动 gateway.run:main——同时完整保留 PATHHOMENODE_PATH。有针对性的清理永远胜过一张白纸。

8. 验证两个网关确实互相独立

$ ps aux | grep 'gateway.run' | grep -v grep
root  8123  ... ./venv/bin/python3 -c ... HERMES_HOME=/root/.hermes ... (primary)
root  8177  ... ./venv/bin/python3 -c ... HERMES_HOME=/root/.hermes/profiles/finance ...

$ cat ~/.hermes/gateway_state.json                 # 主 Profile(飞书)
$ cat ~/.hermes/profiles/finance/gateway_state.json  # finance(微信)

两个独立的 PID、两份独立的状态文件、两个独立的平台。如果 finance Profile 的 gateway_state.json 显示的是飞书,说明环境清理没生效——回头重查第 5 节和第 7 节。

9. 用 Docker 把每个 Profile 容器化

让网关以裸进程方式运行是可行的,但 Docker 能给你重启策略、资源限制,以及一个干净的按 Profile 隔离的命名空间。每个 Profile 各占一个容器,只挂载自己的 HERMES_HOME

# docker-compose.yml —— 每个 Profile 一个服务
services:
  gateway-primary:
    image: hermes-agent:latest
    restart: unless-stopped
    environment:
      HERMES_HOME: /root/.hermes
      FEISHU_APP_ID: ""
      FEISHU_APP_SECRET: ""
    volumes:
      - /root/.hermes:/root/.hermes
    ports:
      - "127.0.0.1:4100:4100"

  gateway-finance:
    image: hermes-agent:latest
    restart: unless-stopped
    environment:
      HERMES_HOME: /root/.hermes/profiles/finance
      FEISHU_APP_ID: ""
      FEISHU_APP_SECRET: ""
    volumes:
      - /root/.hermes/profiles/finance:/root/.hermes/profiles/finance
    ports:
      - "127.0.0.1:4101:4101"

注意端口绑定到 127.0.0.1——这正是我们 蓝绿教程里强调的那条纪律。没有任何网关对外可达;只有 sun-port 面向公网。每个 Profile 只挂载自己的目录,所以 finance 容器在物理上根本读不到主 Profile 的 memories 或 cron。

$ docker compose up -d
$ docker compose ps
NAME              STATUS
gateway-primary   Up (healthy)
gateway-finance   Up (healthy)

10. 把两者统一路由在一个 sun-port 之后

多个网关,一个反向代理。sun-port 把每个外部主机名映射到正确的内部端口——并且只做一次 TLS 终结,这样两个网关都不用操心证书:

# sun-port 配置
server {
  listen 443 ssl;
  server_name agent.example.com;

  ssl_certificate     /etc/letsencrypt/live/agent.example.com/fullchain.pem;
  ssl_certificate_key /etc/letsencrypt/live/agent.example.com/privkey.pem;

  location / {
    proxy_pass http://127.0.0.1:4100;
    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
  }
}

server {
  listen 443 ssl;
  server_name finance.example.com;

  ssl_certificate     /etc/letsencrypt/live/finance.example.com/fullchain.pem;
  ssl_certificate_key /etc/letsencrypt/live/finance.example.com/privkey.pem;

  location / {
    proxy_pass http://127.0.0.1:4101;
    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
  }
}
$ sun-port -t          # 校验配置
configuration file is valid
$ sun-port -s reload   # 热重载——不中断任何连接
reload signal sent

两个公网主机名、两个隔离的 Profile、一个代理——要再加第三个 Agent,只需新增一个 server 块和一个容器,别的什么都不用改。

11. 在 Profile 之间同步模型与供应商配置

Profile不会相互继承 model.*fallback_providers 或 API key——每个都有自己的那一份。当你复制旧 Profile 来搭建新 Profile 时,有三处陈旧的东西常常把它搞坏:

可靠的修法是显式地复制模型/供应商配置块并加以验证,而不是信任一次目录拷贝:

$ # 读取源 Profile 的模型配置
$ grep -A 12 '^model:' ~/.hermes/config.yaml
$ # 把同样的配置块粘贴到目标 Profile,然后确认:
$ HERMES_HOME=~/.hermes/profiles/finance hermes config get model.provider
deepseek
API key 又长又是“密钥形状”sk-...),这意味着 shell 引号和工具日志会把它们搞乱。在 Profile 之间迁移 key 时,用 base64 中转,而不是直接粘贴原始字符串——本地编码、在目标文件里解码,永远不要回显到终端。

12. 用 Git 为整支舰队做版本管理

Profile 就是配置,而配置就该放进 Git。把每个 Profile 的 config.yaml.env(密钥脱敏或 gitignore)以及启动脚本纳入版本管理——但永远不要提交 memories/ 或运行时状态:

# .gitignore
.env
gateway.pid
gateway_state.json
memories/
logs/
*.log
$ git init ~/.hermes/profiles
$ cd ~/.hermes/profiles
$ git add finance/ support/ sales/
$ git commit -m "fleet: add finance, support, sales gateway profiles"
$ git push origin main

现在,一台新主机只需 git clone 加上一条 docker compose up -d 就能拉起来——正是我们 skills Git 工作流里讲过的那份可复现性纪律。

13. 用 PostgreSQL 跨 Profile 共享状态

Profile 是刻意隔离的——但有时两个 Agent 会合法地共享同一个事实来源,比如客户数据库或任务队列。这正是 PostgreSQL 的用武之地:把它放在任何单个 Profile 之外,让两者都能连接:

# 共享数据库,不属于任何一个 Profile
$ docker run -d --name fleet-db \
    -e POSTGRES_DB=fleet -e POSTGRES_USER=agent \
    -e POSTGRES_PASSWORD="${DB_PASSWORD}" \
    -v fleet-pgdata:/var/lib/postgresql/data \
    postgres:16

原则很清晰:memories 和 cron 属于单个 Profile;持久的业务状态则是共享的。一个由客服和财务两个 Agent 共同加入的 im-bot 房间,会从这台数据库读取它的消息历史,而不是从任何一个 Agent 的 HERMES_HOME 里读。如果你还需要在它上面加向量嵌入,pgvector 教程会讲到。

14. 常见陷阱

15. 核心要点

Profile 隔离把一台 Hermes 主机变成一支舰队:一个飞书运营 Agent、一个微信客服 Agent、一个 Slack 工程 Agent——各自拥有自己的 skills、cron 和 memories,统一在一个 sun-port 之后,全部可以从 Git 复现。对于一套已经通过 Docker 运行着 im-bot 房间和 PostgreSQL 状态的系统来说,这就是“一个 Agent”与“一条产品线”之间的差别。