一个网关、一个 Agent、一个消息平台。这是一套 Hermes 安装开始时的形态,对单个机器人来说已经足够。但真正的舰队是横向扩张的:一个负责内部运营的飞书 Agent、一个面向客户的微信 Agent、一个服务工程团队的Slack Agent——每个都拥有不同的 skills、不同的 cron 任务、不同的 memories,而且你绝不希望它们互相串味。答案不是三台 VPS,而是Profile 隔离:多个 Hermes 网关跑在同一台主机上,每个都扎根于自己的 HERMES_HOME,用 Docker 容器化,统一路由在一个 sun-port 之后,并用 Git 做版本管理。本教程从零搭建这样一支舰队。
HERMES_HOME Profile、Docker 容器、一张 sun-port 路由表,以及一个纳入 Git 版本管理的配置目录——✓ 已验证 徽章意味着真实的执行,而非从 README 里复制粘贴。一个 Hermes Profile 不只是一个登录态——它是一个自包含的工作空间。每个 Profile 都带着自己的:
skills/——Agent 按需加载的可复用流程。财务 Agent 的技能与客服 Agent 的技能毫无共同之处。cron/——定时任务。你绝不想让客服 Agent 继承财务 Agent 凌晨两点的对账任务。memories/——注入到每一轮对话中的持久化事实。让它们交叉污染,Agent 就会开始用 CEO 的偏好去回答客户。plugins/ 与配置——消息平台凭据、模型/供应商设置,以及网关状态。两个逻辑上独立的 Agent 一旦共享同一个 Profile,就会共享以上全部——而一个出错的 cron 任务或一条泄漏的 memory,就足以制造一团你理不清的乱麻。Profile 隔离就是解药:给每个 Agent 自己的 HERMES_HOME,让它们并排运行、彼此永远看不到对方的状态。
Hermes 从 HERMES_HOME 环境变量读取它的主目录。默认是 ~/.hermes;一个 Profile 位于 ~/.hermes/profiles/<NAME>。把网关进程指向某个 Profile 目录,它就会加载那个 Profile 的 .env、config.yaml、gateway.pid 与 gateway_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 被“锁”进了自己的目录树。
FEISHU_APP_ID 这样的消息平台凭据——会被你启动的每一个网关继承,与 Profile 无关。这是打垮大多数多 Profile 配置的陷阱,第 6 节就是专门讲如何封死它的。$ 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 部署模式天然契合。
从目录骨架开始。一个 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
gateway.pid,或一份陈旧的 gateway_state.json,会让新网关误以为它已经在运行(或者把一个本要发给另一个实例的停止信号送给它)。首次启动前把这两个文件都删掉。陷阱在这里。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 有东西可加载,而不是去继承那个飞书的值。
接下来是 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 节的环境清理两者兼备。这是最常见的“我的第二个网关起不来”的失败原因。直觉会让人用一个干净的环境来启动——env -i。别这么做。它连 PATH 和 HOME 也一起清掉了,你的网关会死在一堆莫名其妙的 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——同时完整保留 PATH、HOME 和 NODE_PATH。有针对性的清理永远胜过一张白纸。
$ 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 节。
让网关以裸进程方式运行是可行的,但 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)
多个网关,一个反向代理。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 块和一个容器,别的什么都不用改。
Profile不会相互继承 model.*、fallback_providers 或 API key——每个都有自己的那一份。当你复制旧 Profile 来搭建新 Profile 时,有三处陈旧的东西常常把它搞坏:
api_mode: chat_completions,会悄悄破坏与 Anthropic 兼容的供应商。fallback_providers 条目,会让新 Profile 掩盖自己的配置并返回 400。可靠的修法是显式地复制模型/供应商配置块并加以验证,而不是信任一次目录拷贝:
$ # 读取源 Profile 的模型配置
$ grep -A 12 '^model:' ~/.hermes/config.yaml
$ # 把同样的配置块粘贴到目标 Profile,然后确认:
$ HERMES_HOME=~/.hermes/profiles/finance hermes config get model.provider
deepseek
sk-...),这意味着 shell 引号和工具日志会把它们搞乱。在 Profile 之间迁移 key 时,用 base64 中转,而不是直接粘贴原始字符串——本地编码、在目标文件里解码,永远不要回显到终端。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 工作流里讲过的那份可复现性纪律。
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 教程会讲到。
env -i。它会清掉 PATH/HOME,导致莫名其妙的 import 和 socket 失败。只清理消息平台变量,而且要在 Python 层面做。.env 以 override=True 加载,而且 config.py 在读完 YAML 之后还会调用 os.getenv()。两处都要处理:清空 .env 里的值 + 在 Python 层面清理。enabled: true。非飞书平台默认是 false;你的微信网关会一声不吭地永远不连接。platforms: 键。一个 YAML 文件里出现两个 platforms: 段——解析器只保留最后一个。0.0.0.0。那样每个 Profile 都会对外可达,绕过 sun-port 的 TLS。绑定 127.0.0.1。api_mode/fallback_providers 会让新 Profile 以 400 报错。显式地重新指定模型配置。HERMES_HOME 让每个 Agent 拥有自己的 skills、cron、memories 和平台——不用为每个 Agent 付一台 VPS 的税。FEISHU_* 变量会跨越 Profile,除非你在 .env 里清空它们、并在 Python 层面清理。永远不要 env -i。enabled: true/false;永远不要依赖默认值。127.0.0.1;由同一个反向代理负责 TLS 和路由。config.yaml、.env 和启动脚本做版本管理;忽略 memories/、PID 和状态文件。Profile 隔离把一台 Hermes 主机变成一支舰队:一个飞书运营 Agent、一个微信客服 Agent、一个 Slack 工程 Agent——各自拥有自己的 skills、cron 和 memories,统一在一个 sun-port 之后,全部可以从 Git 复现。对于一套已经通过 Docker 运行着 im-bot 房间和 PostgreSQL 状态的系统来说,这就是“一个 Agent”与“一条产品线”之间的差别。