Hermes Agent 实战 01|架构总览:用一个 Agent 管一整个机房原创
# 架构总览:用一个 Agent 管一整个机房
这是《Hermes Agent 实战》系列的第一篇。整个系列不讲概念、不抄文档,全部来自我自己这套跑了几个月、积累了上万次真实会话的部署——它管着我手上十多台机器、一个数据库巡检团队、一个量化交易监控、一台 WAF、还有一整套自建 AI 基础设施。
# 1. 先说结论:它现在长什么样
我的主力机是一台 CasaOS 小主机,上面跑着 一个 Hermes Agent runtime(~/.hermes)。就这一个 runtime,对外却分裂成 12 个互相隔离的「数字员工」——在 Hermes 里它们叫 Profile。每个 Profile 有自己的人格、自己的后端机器、自己的会话历史、自己的技能集和记忆。
| Profile | 它负责什么 | 后端 |
|---|---|---|
default | 跨 Profile 超级管理员,SSH 进所有机器办事 | 本地 + SSH |
casa | CasaOS 主机:容器健康、Cloudflare Tunnel、OpenWebUI | 本地 |
superdba / sqlserver | 数据库 DBA:慢 SQL、索引碎片、P0 故障复盘 | SSH |
freqtrade | 量化交易监控:持仓、盈亏、网格减仓 | 本地容器 |
safeline | 雷池 WAF 服务器运维 | SSH |
cloudflare | Tunnel / DNS / 域名解析 | 本地 |
<INTERNAL_BRAND> / fn-windows | 飞牛私有云、Windows 机器(走 SSH 后端) | SSH |
aws-us | 海外 GPU 机器上的 AI 网关与推理服务 | SSH |
oneapi | API 网关与模型路由 | SSH |
这些 Profile 加起来,到写这篇文章时已经沉淀了 上万次会话、近十万条消息。这个系列要讲的,就是这上万次会话里踩过的坑、定下来的规矩、和最后跑顺了的那套方法。
# 2. Hermes Agent 是什么(只讲你需要知道的部分)
Hermes Agent 是 Nous Research 开源的一个 Agent 运行时。抛开宣传词,对运维来说,你只要记住三件事:
- 它的核心就是一个同步的「对话-工具」循环:调用大模型 → 如果模型要调工具就执行 → 把结果塞回上下文 → 再调模型,直到任务完成。消息走 OpenAI 格式。
- 它的能力来自工具(tools)和技能(skills):终端、文件、Web、定时任务、记忆、委派子 agent……工具是底层能力,技能是把能力打包成「怎么做某件事」的说明书。
- 它能接到任何聊天入口:命令行、Telegram、Discord、网页 Dashboard、VS Code。同一个 Agent,多个面孔。
# 2.1 一段源码把循环讲清楚
hermes-agent/run_agent.py 里的 run_conversation() 就是整个 agent 的全部奥义——一个同步 while 循环,OpenAI 消息格式:
# ~/.hermes/hermes-agent/run_agent.py(伪码,行为等价)
def run_conversation(messages, tools, *, max_iterations=90):
for i in range(max_iterations): # 默认 90 轮硬上限
resp = client.chat(messages, tools=tools)
if not resp.tool_calls: # 模型不再要工具 → 自然结束
return resp.content
for call in resp.tool_calls:
result = handle_function_call(call) # tools/* 派发执行
messages.append({"role": "tool", "tool_call_id": call.id,
"content": result}) # 结果塞回上下文
raise MaxIterationsError(max_iterations)
2
3
4
5
6
7
8
9
10
11
四个值得记的副作用:
- 工具由
tools/registry.py自动发现——任何一个tools/*.py里写@registry.register(...)的处理函数就被纳入候选集;候选集再被toolsets.py里的_HERMES_CORE_TOOLS收敛成「这个 agent 能用什么」。这是后面第 05 篇「技能会失控增殖」的根。 - Profile 解析在
_hermes_home = get_hermes_home()这行模块级缓存完成——一个进程一辈子只认一个 Profile,这就是第 02 篇那个 SSH 泄漏 Bug 的根因:进程没「重置」过。 - 消息格式就是 OpenAI
messages——意味着任何兼容 OpenAI 的下游(OpenWebUI / 第三方客户端)都能直接调它,这就是第 09 篇「Profile = 一个模型」的依据。 - Python 模块在 import 时缓存——所以你改了
tools/*.py,必须重启对应网关进程才生效。光改文件不重启 = 没改。
模型这层我用的是自建的 newapi 网关(kimi-k2.5 打底,deepseek 兜底),后面有一整篇专门讲模型路由,这里先不展开。
# 3. 一个 runtime,五种「面孔」
同一套 Hermes,我用五种形态在用它,各管一摊。每一张脸都是同一个可执行入口——hermes 这个 console-script,只是命令行参数不同:
# ~/.hermes/hermes-agent/ 的可执行入口(pip install -e . 之后全部进 PATH)
hermes # hermes_cli/cli.py → 交互式 CLI(我排障用得最多)
hermes gateway <p> # gateway/run.py → 把 Profile 接到 IM(长驻)
hermes --tui # ui-tui/ → Ink 写的终端 UI
hermes-acp # acp_adapter/ → 接 VS Code/Zed/JetBrains
hermes-webui # 单独仓库 → 系统级 systemd 服务(见 3.1)
2
3
4
5
6
| 形态 | 入口 | 跑法 | 谁在管 |
|---|---|---|---|
| CLI | hermes | 前台 | 我手动 |
| Gateway | hermes gateway <p> | 后台 | 部分 user 级 systemd,部分裸进程 |
| WebUI | hermes-webui | 后台 | /etc/systemd/system/hermes-webui.service(系统级,与 gateway 分离) |
| TUI | hermes --tui | 前台 | 我手动 |
| ACP | hermes-acp | 后台 | IDE 拉起 |
注意:不是每个 Profile 都有 systemd 服务——只有少数几个装了 user 级 systemd unit,其余就是裸进程。这个区别后面会咬人(见第 03 篇)。
# 3.1 启动顺序铁律(这条我用三次惨痛教训换来)
# 改了源码或配置 → 对应服务必须重启(Python 模块内存缓存)
systemctl --user restart 'hermes-gateway-*' # 先网关
sudo systemctl restart hermes-webui # 后 WebUI(它启动时连 Gateway)
# WebUI 是系统级服务(独立仓库 /etc/systemd/system/),别和 user 级搞混
sudo systemctl status hermes-webui
2
3
4
5
6
一句话记住依赖关系:Gateway 要先于 WebUI 启动(WebUI 启动时会去连 Gateway);改了源码或配置,对应服务必须重启——Python 把模块缓存在内存里,光改文件不重启等于没改。
# 4. 数据都放在哪:~/.hermes 布局
整套东西的状态全在 ~/.hermes 下,没有隐藏的魔法:
~/.hermes/
├── config.yaml # 全局配置:模型、agent、gateway、安全、cron…
├── .env # 密钥
├── active_profile # 当前激活的 Profile 名(一行文本,粘性状态)
├── state.db # 会话/消息库(SQLite + FTS5 全文检索)
├── profiles/<name>/ # 每个 Profile 自己的 config / state.db / skills / logs
├── skills/ # 技能库
├── memories/ # 持久记忆
├── cron/ # 定时任务
└── logs/ # agent.log / errors.log / gateway.log
2
3
4
5
6
7
8
9
10
会话不是存在一堆 JSON 文件里,而是 SQLite 的 sessions / messages 两张表,带 FTS5 全文索引——这也是为什么我能把几个月、上万次对话一次性捞出来做成这个系列(怎么捞的,系列最后一篇讲)。每个 Profile 还会各自再有一份独立的 state.db,互不串扰。
# 4.1 真正看一眼 state.db 长什么样
-- ~/.hermes/state.db(精简到业务必需字段;FTS5 虚拟表同名镜像)
CREATE TABLE sessions (
id TEXT PRIMARY KEY,
title TEXT,
started_at REAL, -- unix epoch
message_count INTEGER,
tool_call_count INTEGER
);
CREATE TABLE messages (
session_id TEXT,
role TEXT, -- user / assistant / tool
content TEXT,
tool_name TEXT,
timestamp REAL
);
CREATE VIRTUAL TABLE messages_fts USING fts5(content, content='messages');
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
可现场跑的命令(直接验证你的库是不是这个结构):
# 1. 全库会话计数
sqlite3 ~/.hermes/state.db \
"SELECT COUNT(*) FROM sessions;"
# 2. 全文搜:捞出所有提到「checkpoint」的会话 id
sqlite3 ~/.hermes/state.db <<'SQL'
SELECT s.id, s.title, s.started_at
FROM sessions s
JOIN messages m ON m.session_id = s.id
JOIN messages_fts f ON f.rowid = m.rowid
WHERE messages_fts MATCH 'checkpoint'
GROUP BY s.id
LIMIT 10;
SQL
# 3. 一个 Profile 的库同样适用
sqlite3 ~/.hermes/profiles/superdba/state.db \
"SELECT title, message_count FROM sessions ORDER BY started_at DESC LIMIT 5;"
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Hermes 自己也是这么读的——hermes_state.py 的 SessionDB.search_fts(query) 走的就是这条路径。hermes logs --session <id> 走的是 messages WHERE session_id=? ORDER BY timestamp。
# 4.2 关键路径必须走 get_hermes_home(),别硬编码
源码里所有 Profile-aware 的路径都从一个函数出:
# hermes-agent/hermes_constants.py
HERMES_HOME = Path(os.environ.get("HERMES_HOME", Path.home() / ".hermes"))
PROFILES_DIR = HERMES_HOME / "profiles"
def get_hermes_home() -> Path:
"""Profile-aware: the *current* profile's HERMES_HOME, not the root."""
# 1) env 覆盖 2) `active_profile` 文件 3) 默认根
2
3
4
5
6
7
写自己的插件/工具时,永远走 get_hermes_home(),不要 Path.home() / ".hermes"——后者永远指向根,跨 Profile 时必错。这也是为什么这个系列里几乎每个工具示例第一行都是 get_hermes_home()。
# 5. 最关键的一个设计:Profile 隔离 + 超管穿透
整套部署的灵魂在两点,理解了它,后面所有篇章才看得懂:
- 隔离:每个 Profile 是一个独立沙盒——独立人格、独立后端、独立会话库、独立技能。
sqlserver这个 DBA 不会知道freqtrade在干嘛,也连不上它的机器。这是安全边界。 - 穿透:唯独
default是超级管理员,它通过~/.ssh/config里的别名(<INTERNAL_BRAND> / fn-windows / aws-us / oneapi…)SSH 进每一台机器办事,本地的事就用本地终端。但它不能「切换」成别的 Profile——delegate_task委派子 agent 也跨不了 Profile 边界。
这个「隔离 + 穿透」的组合,恰恰是 Bug 的高发区。比如切换 Profile 时,上一个 Profile 的 SSH 后端连接会「泄漏」到下一个 Profile 的会话里——我曾经在 superdba 的会话里,发现它的命令跑到了 safeline 的机器上。这个跨 Profile SSH 泄漏我修过、提了 PR、还在每次升级时盯着它别被冲掉——单独一篇讲(第 10 篇)。
# 5.1 十分钟克隆一个新 Profile
新建 Profile 最稳的路是「克隆一个最像的、改三处」:
# 进入交互式 CLI
hermes
> /profile create fn-windows --clone <INTERNAL_BRAND> # 从 <INTERNAL_BRAND> 克隆一份
> /profile list # 看到 fn-windows 已建好
> /exit
# 改三处:终端后端 / 密钥 / 人格
$EDITOR ~/.hermes/profiles/fn-windows/config.yaml # 1. terminal.backend: ssh
$EDITOR ~/.hermes/profiles/fn-windows/.env # 2. 这个 Profile 自己的密钥
$EDITOR ~/.hermes/profiles/fn-windows/SOUL.md # 3. 它的人格(可选)
# 立即验证:基础对话是否正常
hermes --p<PASSWORD> fn-windows # 选一个无副作用的任务跑一下
sqlite3 ~/.hermes/profiles/fn-windows/state.db \
"SELECT COUNT(*) FROM sessions;" # >0 = 这个 Profile 已能独立落库
2
3
4
5
6
7
8
9
10
11
12
13
14
15
最后一条 sqlite3 是立即验证——Profile 一旦开始用会话,就一定会有 state.db 写入。没创建 = 路径不对;创建了但 sessions 为 0 = 没有对话成功落地。下一章第 02 篇把这条「Profile 怎么搭」完整展开讲。
# 6. 这个系列接下来讲什么
这上万次会话里,真正值钱的不是「成功跑通」的部分,而是它怎么失败、为什么失败、最后怎么治住的。后面 11 篇都围绕真实事故和真实方法展开:
| 篇 | 主题 | 一句话钩子 |
|---|---|---|
| 02 | 多 Profile 与超管模型 | 一个 Agent 怎么安全地管十几台机器 |
| 03 | Gateway 运维 | systemd 还是裸进程?一个 Telegram token 撞车搞了我半天 |
| 04 | 模型路由 | 哪些模型真支持 thinking、辅助模型怎么配、401/503 根因 |
| 05 | 技能工程 | 技能怎么写、怎么去重、怎么每周自动审计 |
| 06 | 让 Agent 自己上班 | cron 驱动的无人值守巡检 |
| 07 | 数据库实战 | 慢 SQL、索引碎片、一次放大 42 倍的 P0 复盘 |
| 08 | 量化交易助手 | 持仓盈亏与网格减仓分析 |
| 09 | 接入 OpenWebUI | 把 Hermes 暴露成一个「模型」 |
| 10 | 升级与给上游提 PR | 升级不丢本地补丁;那个 SSH 泄漏 Bug 的完整复盘 |
| 11 | 踩坑合集 | checkpoint 膨胀吃满磁盘、Profile 目录莫名混乱… |
| 12 | 工具链外延 | Claude Code / opencode / ACP / 自动更新 webhook |
举个后面会细讲的真实例子,先感受下「真实」是什么味道——有一次我只说了三个字:
记录这个
Agent 没动手,反而去加载了一堆笔记类技能,然后弹出一串「你要存成 Memory 还是文件还是 Obsidian?」的选择题,最后什么都没记下来。这是一个我后来反复治理的反模式:上下文已经很清楚时,Agent 应该自己推断默认值直接干,而不是把模糊当成借口反过来盘问用户。这种「澄清瘫痪」,以及一堆「网络层失败了却假装成功」的幻觉,才是真正要写进笔记的东西。
# 7. 适用读者与声明
- 本系列假设你对 Linux、Docker、SSH、SQL 有基本概念,但不要求你用过 Hermes Agent。
- 所有内容来自我个人自建的部署,不是 Hermes 官方文档;命令和路径以我这套为准,你的环境可能不同。
- 文中所有 IP、密钥、令牌、内网域名、业务名均已脱敏处理。
# 8. 第一次部署?跑一遍这份自检
如果你打算自己起一套,下面是一份「装完应该是什么样」的可执行清单——任何一项不对,部署就没真正完成:
# 1) 五个入口都能起(CLI 至少能开,TUI 装好 node 后 npm run dev)
hermes --version # 应输出 commit hash + 版本
hermes gateway --help # 应列出子命令
systemctl --user list-units 'hermes-gateway*' # 至少有你要长期跑的 Profile
sudo systemctl status hermes-webui # 系统级服务 active
# 2) Profile 隔离确实生效
ls ~/.hermes/profiles/ # 看到 N 个 Profile 目录
for p in ~/.hermes/profiles/*/; do
echo "== $(basename $p) =="
sqlite3 "$p/state.db" "SELECT COUNT(*) FROM sessions;" 2>/dev/null \
|| echo " (no sessions yet)"
done
# 3) state.db 全文搜索可用
sqlite3 ~/.hermes/state.db \
"SELECT name FROM sqlite_master WHERE type='table' AND name='messages_fts';"
# 必须输出 messages_fts
# 4) FTS 真的能搜到东西(替换成你 Profile 里出现过的关键词)
sqlite3 ~/.hermes/state.db <<'SQL'
SELECT COUNT(*) FROM messages_fts WHERE messages_fts MATCH 'gateway';
SQL
# 5) 至少有一个 Profile 接到了 IM(或者你自己 CLI 跑了一次对话)
hermes logs --level WARNING --limit 20 # 没有堆红色 = 健康
# 6) Profile 切换不会泄漏(手工验证一次)
# a) 在 superdba 里跑:echo "I-am-supperdba" > /tmp/marker && ssh <INTERNAL_BRAND> 'cat /tmp/marker'
# b) 切到 safeline 跑同一条命令,应该拿不到这个 marker(或 SSH 重连)
# 失败 = 撞上了第 10 篇那个 SSH 泄漏 Bug 的旧版本
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
任意一条不过,回到第 02 篇(Profile 怎么搭)、第 03 篇(Gateway 怎么跑)、第 10 篇(升级后补丁还在吗)对应章节逐个排查。别跳过这条直接「能跑就行」——上面的六条覆盖了至少四个我真栽过的坑。
下一篇,我们把镜头拉近到那个最危险也最有用的 default 超级管理员,看一个 Agent 到底怎么做到「安全地」管十几台机器。