本文对Hermes的架构拆解是基于 v2026.7.30 版本的源码解析
在同一台服务器上,我们可能希望同时拥有几个用途不同的 Hermes 助手:一个负责写代码,一个负责查资料,还有一个处理日常事务。它们使用同一套 Hermes 程序,却需要不同的模型配置、身份设定和长期记忆。
如果这些助手都读写同一份配置和数据,问题很快就会出现:开发助手刚切换了模型,研究助手也跟着改变;本来只想让办公助手记住的信息,却进入了共同使用的记忆文件。
重新安装几份 Hermes 当然可以分开管理,但程序升级、配置维护也会跟着重复。Profiles 提供了另一种组织方式:共享程序,为不同助手分别保存配置和运行状态。
理解这一机制,可以沿着一条线展开:Profile 定义"这是谁的数据",HERMES_HOME 告诉程序"去哪里找数据",Gateway 再决定"这条消息由谁处理"。
一、为什么需要 Profiles:从三个助手说起
先把需求具体化。假设我们要运行三个助手:
| 助手 | 主要用途 | 希望独立维护的内容 |
|---|---|---|
| Coder | 编写代码、分析项目 | 开发模型、代码相关技能、项目记忆 |
| Researcher | 阅读资料、整理研究结果 | 研究模型、检索技能、研究偏好 |
| Assistant | 处理日常事务 | 办公技能、个人偏好、定时任务 |
它们需要分开的内容,可以分成两类。第一类是配置 :用什么模型、保存哪些密钥、采用什么身份设定。第二类是状态:积累了哪些记忆、发生过哪些对话、保存了哪些定时任务。
这里的 Memory 是助手长期保留的信息,例如用户偏好和项目约定;Session 则是一段对话及其历史。两者都会影响助手的后续行为,但生命周期和用途不同。
有了 Profiles,三个助手之间的关系可以画成这样:
#mermaid-svg-BvvMxOa7tQpOTxqC{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-BvvMxOa7tQpOTxqC .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-BvvMxOa7tQpOTxqC .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-BvvMxOa7tQpOTxqC .error-icon{fill:#552222;}#mermaid-svg-BvvMxOa7tQpOTxqC .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-BvvMxOa7tQpOTxqC .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-BvvMxOa7tQpOTxqC .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-BvvMxOa7tQpOTxqC .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-BvvMxOa7tQpOTxqC .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-BvvMxOa7tQpOTxqC .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-BvvMxOa7tQpOTxqC .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-BvvMxOa7tQpOTxqC .marker{fill:#333333;stroke:#333333;}#mermaid-svg-BvvMxOa7tQpOTxqC .marker.cross{stroke:#333333;}#mermaid-svg-BvvMxOa7tQpOTxqC svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-BvvMxOa7tQpOTxqC p{margin:0;}#mermaid-svg-BvvMxOa7tQpOTxqC .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-BvvMxOa7tQpOTxqC .cluster-label text{fill:#333;}#mermaid-svg-BvvMxOa7tQpOTxqC .cluster-label span{color:#333;}#mermaid-svg-BvvMxOa7tQpOTxqC .cluster-label span p{background-color:transparent;}#mermaid-svg-BvvMxOa7tQpOTxqC .label text,#mermaid-svg-BvvMxOa7tQpOTxqC span{fill:#333;color:#333;}#mermaid-svg-BvvMxOa7tQpOTxqC .node rect,#mermaid-svg-BvvMxOa7tQpOTxqC .node circle,#mermaid-svg-BvvMxOa7tQpOTxqC .node ellipse,#mermaid-svg-BvvMxOa7tQpOTxqC .node polygon,#mermaid-svg-BvvMxOa7tQpOTxqC .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-BvvMxOa7tQpOTxqC .rough-node .label text,#mermaid-svg-BvvMxOa7tQpOTxqC .node .label text,#mermaid-svg-BvvMxOa7tQpOTxqC .image-shape .label,#mermaid-svg-BvvMxOa7tQpOTxqC .icon-shape .label{text-anchor:middle;}#mermaid-svg-BvvMxOa7tQpOTxqC .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-BvvMxOa7tQpOTxqC .rough-node .label,#mermaid-svg-BvvMxOa7tQpOTxqC .node .label,#mermaid-svg-BvvMxOa7tQpOTxqC .image-shape .label,#mermaid-svg-BvvMxOa7tQpOTxqC .icon-shape .label{text-align:center;}#mermaid-svg-BvvMxOa7tQpOTxqC .node.clickable{cursor:pointer;}#mermaid-svg-BvvMxOa7tQpOTxqC .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-BvvMxOa7tQpOTxqC .arrowheadPath{fill:#333333;}#mermaid-svg-BvvMxOa7tQpOTxqC .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-BvvMxOa7tQpOTxqC .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-BvvMxOa7tQpOTxqC .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-BvvMxOa7tQpOTxqC .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-BvvMxOa7tQpOTxqC .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-BvvMxOa7tQpOTxqC .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-BvvMxOa7tQpOTxqC .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-BvvMxOa7tQpOTxqC .cluster text{fill:#333;}#mermaid-svg-BvvMxOa7tQpOTxqC .cluster span{color:#333;}#mermaid-svg-BvvMxOa7tQpOTxqC div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-BvvMxOa7tQpOTxqC .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-BvvMxOa7tQpOTxqC rect.text{fill:none;stroke-width:0;}#mermaid-svg-BvvMxOa7tQpOTxqC .icon-shape,#mermaid-svg-BvvMxOa7tQpOTxqC .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-BvvMxOa7tQpOTxqC .icon-shape p,#mermaid-svg-BvvMxOa7tQpOTxqC .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-BvvMxOa7tQpOTxqC .icon-shape .label rect,#mermaid-svg-BvvMxOa7tQpOTxqC .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-BvvMxOa7tQpOTxqC .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-BvvMxOa7tQpOTxqC .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-BvvMxOa7tQpOTxqC :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 同一套 Hermes 程序
Coder 的运行实例
Researcher 的运行实例
Assistant 的运行实例
coder Profile:配置、记忆、会话
researcher Profile:配置、记忆、会话
assistant Profile:配置、记忆、会话
图中的"同一套程序"表示共享安装和源码。至于运行时使用几个进程,则取决于启动方式。三个 Profile 可以分别启动,也可以由后面介绍的同一个 Gateway 管理。
程序升级时,共享的仍然是 Hermes 这套程序;助手之间的差异主要落在各自的数据目录中。这种安排让"维护软件版本"和"管理助手身份"成为两件可以分别处理的事。
二、Profile 到底是什么:一套独立的配置与状态目录
一个 Profile,可以理解为一套独立的 Hermes Home。 它为助手保存配置与持久状态,运行中的 Hermes 再读取、使用这些内容。
在默认的 Linux / WSL 目录布局下,结构大致如下。这里展示的是使用一段时间后的示意结构,部分状态文件会在相应功能运行后才出现。
text
~/.hermes/
├── config.yaml # 默认 Profile 的配置
├── .env # 默认 Profile 保存的密钥与环境设置
├── SOUL.md # 默认 Profile 的身份设定
├── state.db # 默认 Profile 的会话数据库
├── memories/
├── skills/
├── cron/
├── active_profile # CLI 保存的当前默认选择
└── profiles/
├── coder/
│ ├── config.yaml
│ ├── .env
│ ├── SOUL.md
│ ├── state.db
│ ├── memories/
│ ├── skills/
│ ├── sessions/
│ ├── cron/
│ └── logs/
├── researcher/
│ └── ...
└── assistant/
└── ...
这里容易看错的一点是:默认 Profile 就在 ~/.hermes,命名 Profile 才放在 ~/.hermes/profiles/ 下。 不需要额外寻找一个 profiles/default 目录。
Windows 原生环境的默认目录为 %LOCALAPPDATA%\hermes;在 WSL 中运行时,通常采用 Linux 的目录布局。后文统一使用 ~/.hermes 说明默认场景。
这些文件各有职责:config.yaml 保存运行配置,.env 保存该 Profile 的密钥和环境设置,SOUL.md 描述助手身份,memories/ 保存长期记忆,state.db 等文件承载会话状态。Skills 是可供助手使用的技能,Cron 则对应定时任务。
因此,仅仅修改 SOUL.md,只是改变身份设定;使用独立 Profile,还会把配置、记忆、会话等数据分开。反过来说,创建了 Profile 也不代表已经启动一个助手:目录可以一直存在,处理任务的进程可以随时启动和退出。
三、HERMES_HOME:程序如何找到当前 Profile
3.1 从目录切换理解运行机制
Hermes 需要知道这次启动该使用哪一套数据。HERMES_HOME 就是它定位运行数据的关键入口。
例如,当有效路径为下面这个目录时,程序就会围绕 Coder 的配置和状态工作:
text
HERMES_HOME=/home/dev/.hermes/profiles/coder
配置文件:/home/dev/.hermes/profiles/coder/config.yaml
密钥文件:/home/dev/.hermes/profiles/coder/.env
长期记忆:/home/dev/.hermes/profiles/coder/memories/
会话数据:/home/dev/.hermes/profiles/coder/state.db
上面展示的是有效路径与文件位置的关系。日常使用时,通过 hermes -p coder ... 选择 Profile 更直观,也能让 CLI 完成对应的解析。
关键就在于:共享的程序读取不同的根目录,自然就能得到不同的配置和历史状态。代码本身不需要为了 Coder、Researcher 各复制一份。
3.2 为什么选择 Profile 必须发生在启动早期
以启动 Coder 对话为例:
bash
hermes -p coder chat
按本文依据的版本资料,CLI 会在启动早期完成 Profile 选择,其核心流程如下:
#mermaid-svg-dk1vVQN9vCNJQISm{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-dk1vVQN9vCNJQISm .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-dk1vVQN9vCNJQISm .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-dk1vVQN9vCNJQISm .error-icon{fill:#552222;}#mermaid-svg-dk1vVQN9vCNJQISm .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-dk1vVQN9vCNJQISm .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-dk1vVQN9vCNJQISm .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-dk1vVQN9vCNJQISm .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-dk1vVQN9vCNJQISm .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-dk1vVQN9vCNJQISm .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-dk1vVQN9vCNJQISm .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-dk1vVQN9vCNJQISm .marker{fill:#333333;stroke:#333333;}#mermaid-svg-dk1vVQN9vCNJQISm .marker.cross{stroke:#333333;}#mermaid-svg-dk1vVQN9vCNJQISm svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-dk1vVQN9vCNJQISm p{margin:0;}#mermaid-svg-dk1vVQN9vCNJQISm .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-dk1vVQN9vCNJQISm .cluster-label text{fill:#333;}#mermaid-svg-dk1vVQN9vCNJQISm .cluster-label span{color:#333;}#mermaid-svg-dk1vVQN9vCNJQISm .cluster-label span p{background-color:transparent;}#mermaid-svg-dk1vVQN9vCNJQISm .label text,#mermaid-svg-dk1vVQN9vCNJQISm span{fill:#333;color:#333;}#mermaid-svg-dk1vVQN9vCNJQISm .node rect,#mermaid-svg-dk1vVQN9vCNJQISm .node circle,#mermaid-svg-dk1vVQN9vCNJQISm .node ellipse,#mermaid-svg-dk1vVQN9vCNJQISm .node polygon,#mermaid-svg-dk1vVQN9vCNJQISm .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-dk1vVQN9vCNJQISm .rough-node .label text,#mermaid-svg-dk1vVQN9vCNJQISm .node .label text,#mermaid-svg-dk1vVQN9vCNJQISm .image-shape .label,#mermaid-svg-dk1vVQN9vCNJQISm .icon-shape .label{text-anchor:middle;}#mermaid-svg-dk1vVQN9vCNJQISm .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-dk1vVQN9vCNJQISm .rough-node .label,#mermaid-svg-dk1vVQN9vCNJQISm .node .label,#mermaid-svg-dk1vVQN9vCNJQISm .image-shape .label,#mermaid-svg-dk1vVQN9vCNJQISm .icon-shape .label{text-align:center;}#mermaid-svg-dk1vVQN9vCNJQISm .node.clickable{cursor:pointer;}#mermaid-svg-dk1vVQN9vCNJQISm .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-dk1vVQN9vCNJQISm .arrowheadPath{fill:#333333;}#mermaid-svg-dk1vVQN9vCNJQISm .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-dk1vVQN9vCNJQISm .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-dk1vVQN9vCNJQISm .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-dk1vVQN9vCNJQISm .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-dk1vVQN9vCNJQISm .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-dk1vVQN9vCNJQISm .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-dk1vVQN9vCNJQISm .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-dk1vVQN9vCNJQISm .cluster text{fill:#333;}#mermaid-svg-dk1vVQN9vCNJQISm .cluster span{color:#333;}#mermaid-svg-dk1vVQN9vCNJQISm div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-dk1vVQN9vCNJQISm .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-dk1vVQN9vCNJQISm rect.text{fill:none;stroke-width:0;}#mermaid-svg-dk1vVQN9vCNJQISm .icon-shape,#mermaid-svg-dk1vVQN9vCNJQISm .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-dk1vVQN9vCNJQISm .icon-shape p,#mermaid-svg-dk1vVQN9vCNJQISm .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-dk1vVQN9vCNJQISm .icon-shape .label rect,#mermaid-svg-dk1vVQN9vCNJQISm .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-dk1vVQN9vCNJQISm .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-dk1vVQN9vCNJQISm .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-dk1vVQN9vCNJQISm :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} hermes -p coder chat
进入 hermes_cli/main.py
_apply_profile_override() 解析 Profile 参数
resolve_profile_env() 得到 coder 的目录
设置 HERMES_HOME
导入和初始化其余 Hermes 模块
通过 get_hermes_home() 获取数据目录
使用 coder 的配置、密钥和状态
启动对话
这里真正值得关注的是顺序:先确定 Profile,再初始化依赖它的模块。
Python 模块第一次被导入时,可能就会执行顶层代码。假设某个模块在此时根据 Hermes Home 计算了数据库路径,并把结果保存下来,之后再修改环境变量,就不一定能让这份已保存的路径跟着改变。
这也解释了为什么"程序启动后,再随手改一下 os.environ"不能替代完整的 Profile 切换流程。涉及配置和状态的组件,需要在正确的目录上下文中初始化和使用。
3.3 路径解析还有一层:当前执行上下文
在 hermes_constants.py 中,get_hermes_home() 的关键逻辑可以概括为:
python
# 简化示意:省略路径规范化和平台细节
def get_hermes_home():
override = get_hermes_home_override()
if override:
return Path(override)
return _hermes_home_from_env()
它体现了三层路径来源,优先级由高到低是:
- 当前执行上下文设置的 Home Override。
HERMES_HOME环境变量。- 操作系统对应的默认目录。
"执行上下文"可以先理解为当前正在处理的一段任务。普通 CLI 启动主要通过环境变量确定目录;一个进程同时服务多个 Profile 时,还需要让不同任务各自拿到正确的路径。
这时,Hermes 使用基于 Python ContextVar 的 set_hermes_home_override() 和 reset_hermes_home_override()。ContextVar 可以让不同异步执行上下文持有各自的值,避免为每条消息反复修改进程共享的环境变量。
不过,这不意味着所有 Python 全局变量都会自动隔离。相关组件仍然需要按当前上下文读取和使用 Profile 数据。后面的 Gateway 复用,就是这层机制的具体应用。
四、从创建到使用:走通一个 Profile
4.1 创建、配置,再开始对话
创建开发助手,可以从下面三步开始:
bash
# 创建 Profile
hermes profile create coder
# 为这个 Profile 完成配置
hermes -p coder setup
# 使用这个 Profile 开始对话
hermes -p coder chat
创建命令会初始化 Profile 所需的目录、密钥文件、身份设定和内置技能等内容。模型、凭据等具体配置再通过 setup 完成。创建 Researcher 时,同样使用它自己的名字执行这一组操作即可。
对应的源码入口从 hermes_cli/subcommands/profile.py 进入 hermes_cli/profiles.py,核心创建逻辑由 create_profile() 承担。阅读代码时,可以把它看成"准备数据目录"的阶段,与后续启动对话的阶段分开理解。
4.2 快捷命令与默认选择,各解决什么问题
创建 Profile 时,Hermes 还会生成快捷启动命令。在 Linux 下,通常可以在 ~/.local/bin/ 找到对应脚本。只要这个目录已加入 PATH,就可以这样使用:
bash
coder setup
coder chat
它的转发关系相当于下面的脚本,实际生成内容可能包含更多启动细节:
sh
#!/bin/sh
exec hermes -p coder "$@"
所以,coder chat 仍然调用共享的 Hermes 程序,只是替你带上了 Profile 参数。
如果希望普通的 hermes 命令默认使用 Coder,可以设置当前选择:
bash
hermes profile use coder
hermes chat
在未手动预设 HERMES_HOME 的常规 CLI 使用场景下,后续不显式指定 Profile 的调用就会采用这个选择。CLI 将名字保存到 ~/.hermes/active_profile,后续启动时再读取它。
两种用法适合不同场景:交互使用时,切换默认选择比较方便;脚本和服务配置中,显式写出 -p coder 更容易看出运行目标。profile use 改变的是后续启动的默认选择,并不会把已经运行的 Gateway 或对话进程迁移到另一个 Profile。
4.3 管理与克隆
常见管理命令可以集中记在一张表里:
| 命令 | 用途 |
|---|---|
hermes profile list |
列出 Profiles |
hermes profile show coder |
查看指定 Profile 的详情 |
hermes profile use coder |
设置后续 CLI 启动的默认选择 |
hermes profile rename coder dev |
重命名 Profile |
hermes profile export coder |
导出 Profile |
hermes profile import coder.tar.gz |
导入归档,文件名以实际导出结果为准 |
hermes profile delete coder |
删除指定的命名 Profile 及其数据 |
删除命名 Profile 时,还涉及停止其 Gateway、移除对应服务和快捷命令;默认 Profile 不能通过 profile delete 删除。由于 Profile 包含密钥和历史数据,导出、删除时应按实际需要确认处理对象。
如果已经把 Coder 配置得比较完善,希望再创建一个类似的 DevOps 助手,可以先选择 Coder,再进行克隆:
bash
hermes profile use coder
hermes profile create devops --clone
三种创建方式的差别如下:
| 创建方式 | 主要行为 |
|---|---|
| 不带克隆参数 | 初始化基础 Profile 与内置技能,适合从头配置 |
--clone |
复制配置、密钥文件、身份设定、技能和选定的记忆文件 |
--clone-all |
复制更广泛的 Profile 数据,但仍排除会话历史数据库等运行历史 |
克隆更接近"以现有助手为模板"。新 Profile 后续如何配置、保留哪些记忆,仍然需要按新用途调整。
这里有一个与版本有关的细节:原始资料记录,v2026.7.30 的说明文档与实现对克隆记忆的描述存在差异。实现中的 _CLONE_SUBDIR_FILES 包含 memories/MEMORY.md 和 memories/USER.md,因此不能把 --clone 理解为一定获得空白记忆。
如果目标是一个没有旧记忆的新助手,直接创建不带克隆参数的 Profile,意图更清楚。--clone-all 同样不能理解成完整备份,尤其不能据此假设会话历史也已保存。
五、多个 Profile 如何接入 Gateway
chat 适合直接在终端里对话。要让助手长期接收 Telegram、Discord 等平台的消息,就需要 Gateway:一个负责连接消息平台、接收消息并调度处理的服务。
Profile 决定配置和数据归属,Gateway 决定消息如何进入处理流程。两者可以按不同方式组合。
5.1 两种运行方式
默认方式是为各个 Profile 分别运行 Gateway。例如,在两个 Profile 均已完成平台配置后:
bash
hermes -p coder gateway start
hermes -p researcher gateway start
Coder 和 Researcher 各自使用对应的配置、会话数据和记忆,也可以分别配置自己的 Bot Token,即消息平台分配给机器人的访问凭据。
另一个选择是打开 gateway.multiplex_profiles。本文所依据的版本中,该选项默认关闭;启用后,一个 Gateway 进程可以服务多个 Profile,管理各自的平台适配器、凭据和运行上下文。
#mermaid-svg-wiHIANFGXt557QUZ{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-wiHIANFGXt557QUZ .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-wiHIANFGXt557QUZ .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-wiHIANFGXt557QUZ .error-icon{fill:#552222;}#mermaid-svg-wiHIANFGXt557QUZ .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-wiHIANFGXt557QUZ .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-wiHIANFGXt557QUZ .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-wiHIANFGXt557QUZ .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-wiHIANFGXt557QUZ .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-wiHIANFGXt557QUZ .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-wiHIANFGXt557QUZ .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-wiHIANFGXt557QUZ .marker{fill:#333333;stroke:#333333;}#mermaid-svg-wiHIANFGXt557QUZ .marker.cross{stroke:#333333;}#mermaid-svg-wiHIANFGXt557QUZ svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-wiHIANFGXt557QUZ p{margin:0;}#mermaid-svg-wiHIANFGXt557QUZ .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-wiHIANFGXt557QUZ .cluster-label text{fill:#333;}#mermaid-svg-wiHIANFGXt557QUZ .cluster-label span{color:#333;}#mermaid-svg-wiHIANFGXt557QUZ .cluster-label span p{background-color:transparent;}#mermaid-svg-wiHIANFGXt557QUZ .label text,#mermaid-svg-wiHIANFGXt557QUZ span{fill:#333;color:#333;}#mermaid-svg-wiHIANFGXt557QUZ .node rect,#mermaid-svg-wiHIANFGXt557QUZ .node circle,#mermaid-svg-wiHIANFGXt557QUZ .node ellipse,#mermaid-svg-wiHIANFGXt557QUZ .node polygon,#mermaid-svg-wiHIANFGXt557QUZ .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-wiHIANFGXt557QUZ .rough-node .label text,#mermaid-svg-wiHIANFGXt557QUZ .node .label text,#mermaid-svg-wiHIANFGXt557QUZ .image-shape .label,#mermaid-svg-wiHIANFGXt557QUZ .icon-shape .label{text-anchor:middle;}#mermaid-svg-wiHIANFGXt557QUZ .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-wiHIANFGXt557QUZ .rough-node .label,#mermaid-svg-wiHIANFGXt557QUZ .node .label,#mermaid-svg-wiHIANFGXt557QUZ .image-shape .label,#mermaid-svg-wiHIANFGXt557QUZ .icon-shape .label{text-align:center;}#mermaid-svg-wiHIANFGXt557QUZ .node.clickable{cursor:pointer;}#mermaid-svg-wiHIANFGXt557QUZ .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-wiHIANFGXt557QUZ .arrowheadPath{fill:#333333;}#mermaid-svg-wiHIANFGXt557QUZ .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-wiHIANFGXt557QUZ .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-wiHIANFGXt557QUZ .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-wiHIANFGXt557QUZ .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-wiHIANFGXt557QUZ .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-wiHIANFGXt557QUZ .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-wiHIANFGXt557QUZ .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-wiHIANFGXt557QUZ .cluster text{fill:#333;}#mermaid-svg-wiHIANFGXt557QUZ .cluster span{color:#333;}#mermaid-svg-wiHIANFGXt557QUZ div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-wiHIANFGXt557QUZ .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-wiHIANFGXt557QUZ rect.text{fill:none;stroke-width:0;}#mermaid-svg-wiHIANFGXt557QUZ .icon-shape,#mermaid-svg-wiHIANFGXt557QUZ .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-wiHIANFGXt557QUZ .icon-shape p,#mermaid-svg-wiHIANFGXt557QUZ .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-wiHIANFGXt557QUZ .icon-shape .label rect,#mermaid-svg-wiHIANFGXt557QUZ .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-wiHIANFGXt557QUZ .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-wiHIANFGXt557QUZ .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-wiHIANFGXt557QUZ :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 一个 Gateway 服务多个 Profile
各平台消息入口
一个 GatewayRunner 进程
coder 的运行上下文
researcher 的运行上下文
coder Profile
researcher Profile
分别运行 Gateway
开发助手的消息入口
Gateway 进程 A
coder Profile
研究助手的消息入口
Gateway 进程 B
researcher Profile
两种方式可以从运维需求上比较:
| 维度 | 分别运行 Gateway | 一个 Gateway 服务多个 Profile |
|---|---|---|
| 进程组织 | 每个 Profile 分别运行 | 多个 Profile 共享一个进程 |
| 启停维护 | 可以分别操作各 Gateway | 统一维护承担复用职责的 Gateway |
| Profile 数据 | 各自保存配置和状态 | 仍然各自保存配置和状态 |
| 选择重点 | 便于分别运维 | 减少需要维护的 Gateway 进程 |
共享进程会让这些 Profile 共同依赖该进程的可用性;分开进程则可以分别重启。这是运行方式的取舍,与 Profile 是否拥有独立目录是两个问题。
另外,分开运行时也要避免误用同一份机器人凭据。原始资料描述了重复 Bot Token 的连接保护:后启动的 Gateway 会被阻止建立对应的重复连接,避免两个进程争抢同一个机器人的消息。
5.2 用消息来源选择 Profile
开启复用后,还需要说明消息应该交给谁。profile_routes 提供了根据消息来源选择 Profile 的规则。
例如,希望同一个 Discord 服务器中的开发频道使用 Coder,研究频道使用 Researcher,可以在承担复用职责的 Gateway 所用 config.yaml 中加入以下配置。该片段只展示复用与路由部分,频道 ID 需要替换成实际值,已有的平台连接配置仍需保留。
yaml
gateway:
multiplex_profiles: true
profile_routes:
- name: development
platform: discord
guild_id: "10001"
chat_id: "20001"
profile: coder
- name: research
platform: discord
guild_id: "10001"
chat_id: "20002"
profile: researcher
这里的 Guild 可以理解为 Discord 服务器,chat_id 在这个例子中对应频道 ID,Thread 则是频道中的讨论串。
路由会根据消息来源进行匹配。按原始资料对该版本的说明,多条规则命中时优先采用更具体的规则:Thread 优先于 Channel,Channel 优先于 Guild;没有匹配时交给默认 Profile。对应的阅读入口是 gateway/profile_routing.py 中的 match_profile_route()。
5.3 找到目标之后,还要切换运行上下文
选中 coder 只是第一步。真正处理这条消息时,读取配置、访问记忆以及解析密钥,都必须落到 Coder 的上下文中。
#mermaid-svg-a2Ad98dU6Ugue1OH{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-a2Ad98dU6Ugue1OH .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-a2Ad98dU6Ugue1OH .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-a2Ad98dU6Ugue1OH .error-icon{fill:#552222;}#mermaid-svg-a2Ad98dU6Ugue1OH .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-a2Ad98dU6Ugue1OH .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-a2Ad98dU6Ugue1OH .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-a2Ad98dU6Ugue1OH .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-a2Ad98dU6Ugue1OH .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-a2Ad98dU6Ugue1OH .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-a2Ad98dU6Ugue1OH .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-a2Ad98dU6Ugue1OH .marker{fill:#333333;stroke:#333333;}#mermaid-svg-a2Ad98dU6Ugue1OH .marker.cross{stroke:#333333;}#mermaid-svg-a2Ad98dU6Ugue1OH svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-a2Ad98dU6Ugue1OH p{margin:0;}#mermaid-svg-a2Ad98dU6Ugue1OH .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-a2Ad98dU6Ugue1OH .cluster-label text{fill:#333;}#mermaid-svg-a2Ad98dU6Ugue1OH .cluster-label span{color:#333;}#mermaid-svg-a2Ad98dU6Ugue1OH .cluster-label span p{background-color:transparent;}#mermaid-svg-a2Ad98dU6Ugue1OH .label text,#mermaid-svg-a2Ad98dU6Ugue1OH span{fill:#333;color:#333;}#mermaid-svg-a2Ad98dU6Ugue1OH .node rect,#mermaid-svg-a2Ad98dU6Ugue1OH .node circle,#mermaid-svg-a2Ad98dU6Ugue1OH .node ellipse,#mermaid-svg-a2Ad98dU6Ugue1OH .node polygon,#mermaid-svg-a2Ad98dU6Ugue1OH .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-a2Ad98dU6Ugue1OH .rough-node .label text,#mermaid-svg-a2Ad98dU6Ugue1OH .node .label text,#mermaid-svg-a2Ad98dU6Ugue1OH .image-shape .label,#mermaid-svg-a2Ad98dU6Ugue1OH .icon-shape .label{text-anchor:middle;}#mermaid-svg-a2Ad98dU6Ugue1OH .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-a2Ad98dU6Ugue1OH .rough-node .label,#mermaid-svg-a2Ad98dU6Ugue1OH .node .label,#mermaid-svg-a2Ad98dU6Ugue1OH .image-shape .label,#mermaid-svg-a2Ad98dU6Ugue1OH .icon-shape .label{text-align:center;}#mermaid-svg-a2Ad98dU6Ugue1OH .node.clickable{cursor:pointer;}#mermaid-svg-a2Ad98dU6Ugue1OH .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-a2Ad98dU6Ugue1OH .arrowheadPath{fill:#333333;}#mermaid-svg-a2Ad98dU6Ugue1OH .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-a2Ad98dU6Ugue1OH .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-a2Ad98dU6Ugue1OH .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-a2Ad98dU6Ugue1OH .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-a2Ad98dU6Ugue1OH .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-a2Ad98dU6Ugue1OH .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-a2Ad98dU6Ugue1OH .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-a2Ad98dU6Ugue1OH .cluster text{fill:#333;}#mermaid-svg-a2Ad98dU6Ugue1OH .cluster span{color:#333;}#mermaid-svg-a2Ad98dU6Ugue1OH div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-a2Ad98dU6Ugue1OH .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-a2Ad98dU6Ugue1OH rect.text{fill:none;stroke-width:0;}#mermaid-svg-a2Ad98dU6Ugue1OH .icon-shape,#mermaid-svg-a2Ad98dU6Ugue1OH .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-a2Ad98dU6Ugue1OH .icon-shape p,#mermaid-svg-a2Ad98dU6Ugue1OH .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-a2Ad98dU6Ugue1OH .icon-shape .label rect,#mermaid-svg-a2Ad98dU6Ugue1OH .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-a2Ad98dU6Ugue1OH .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-a2Ad98dU6Ugue1OH .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-a2Ad98dU6Ugue1OH :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 开发频道
研究频道
无匹配
Gateway 收到消息
profile_routes 匹配结果
选择 coder
选择 researcher
选择默认 Profile
进入 _profile_runtime_scope()
设置当前上下文的 Home Override 与密钥作用域
使用目标 Profile 处理消息
处理结束,恢复先前上下文
_profile_runtime_scope() 把一次消息处理与目标 Profile 的运行环境关联起来。其中的 Home Override 对应前文的 ContextVar 机制,Secret Scope 则确定当前任务使用的密钥作用域。
这样,同一个进程中的不同任务可以分别使用 Coder 和 Researcher 的配置。任务处理结束后恢复先前上下文,也能避免把本次选择意外带到后续工作中。
这层路由解决的是"消息交给哪个助手"。它主要按平台、服务器、频道和讨论串匹配,不能直接承担企业用户身份认证或访问授权。
六、三个目录,三种职责:数据、工具身份与工作位置
理解了 Profile,还需要区分三个经常同时出现的路径概念:
| 配置或变量 | 主要职责 | 示例 |
|---|---|---|
HERMES_HOME |
定位 Hermes 的配置与状态 | /home/dev/.hermes/profiles/coder |
HOME |
为外部程序提供用户目录,很多工具据此查找配置与凭据 | /home/dev |
terminal.cwd |
指定工具执行命令时的默认工作目录 | /workspace/java-demo |
它们可以同时存在,指向三个不同的地方:Coder 从自己的 Profile 读取记忆,在 Java 项目目录执行命令,Git 则从操作系统用户目录读取配置。
6.1 Profile 分开了,外部工具身份仍可能共享
按原始资料,v2026.7.30 在宿主机执行工具子进程时,默认的配置为:
yaml
terminal:
home_mode: auto
在这一场景下,子进程保留真实操作系统用户的 HOME。如果 Coder 和 Researcher 都以同一个 Linux 用户运行,它们调用 Git、SSH、GitHub CLI 时,就可能读取同一份用户配置和凭据。
因此,Profile 的 .env 分开保存,并不代表所有外部程序的身份也自动分开。例如,两套不同的模型凭据可以分别属于两个 Profile,但 git push 仍可能使用同一份 SSH 配置。
需要按 Profile 分开这些依赖用户目录的工具配置时,可以使用:
yaml
terminal:
home_mode: profile
工具子进程的用户目录此时指向 {HERMES_HOME}/home。Coder 和 Researcher 因而可以分别初始化 Git 配置、SSH 密钥和 GitHub CLI 登录状态。
这仍然只是在改变工具查找用户配置的位置。外部程序还可能从环境变量、系统配置或其他路径获取凭据,具体行为需要结合对应工具理解。
6.2 工作目录不会自动变成访问边界
再看下面的配置:
yaml
terminal:
backend: local
cwd: /workspace/java-demo
它表达的是:使用本机执行后端,并从 /workspace/java-demo 这个工作目录开始运行命令。执行 pwd 时,通常会看到这个目录。
但本机进程仍然拥有当前操作系统用户被授予的文件权限。如果这个用户能读取另一个项目,工具进程也可能读取它。把 cwd 指向某个项目,不会自动禁止访问其他目录。
同样,把工具的 HOME 改到 Profile 里面,也不会撤销它对宿主机文件的访问权限。需要限制实际访问范围时,应结合容器、操作系统用户权限或受限制的执行环境配置,并检查挂载目录与外部服务权限。
这三层职责可以分别记住:Profile 管 Hermes 的配置与状态,工具用户目录影响配置和凭据的查找,执行环境与权限控制决定能访问什么。
七、Profile 与 Agent、用户、租户是什么关系
在个人使用中,把一个 Profile 叫作"我的开发助手"很自然。设计服务时,则需要把几个概念分清。
| 概念 | 代表什么 | 主要关心的问题 |
|---|---|---|
| Profile | 一套 Hermes 配置与持久状态 | 使用哪套配置、记忆和数据 |
| Gateway | 接收、调度消息的运行服务 | 消息从哪里进入,如何交给助手 |
| AIAgent | 执行任务的运行对象 | 当前任务如何执行 |
| Session | 一段对话及其历史 | 当前对话延续哪些上下文 |
| User | 业务用户身份 | 谁在使用服务,有哪些权限 |
| Tenant | 企业或组织的业务归属 | 数据和资源属于哪个组织 |
一个 Profile 可以服务多段 Session,也可以在不同时间创建多个 AIAgent 对象。Profile 的数据寿命可以跨越多次进程启动,而一次任务执行结束,只是运行过程结束了。
八、把整条链路连起来
从使用者的角度看,Profiles 让同一套 Hermes 程序可以维护多个长期身份。从运行机制看,这些身份主要通过各自的 Hermes Home 落在磁盘上。
启动时,CLI 先确定 Profile,把对应目录交给后续模块;处理消息时,Gateway 再根据部署方式和路由规则,将任务放入正确的运行上下文。配置、记忆和会话数据由此找到各自的归属。
理解这套机制后,几个实际选择也就容易判断:
- 需要新的长期配置与记忆时,创建 Profile;需要新的对话上下文时,考虑创建 Session。
- 手动使用可以切换默认 Profile,脚本和服务配置中宜明确写出目标 Profile。
- 需要分别运维时,可以分别启动 Gateway;需要集中管理进程时,可以考虑复用模式。
- 涉及外部工具身份、用户授权和租户权限时,继续检查
HOME、执行环境与业务权限设计。
Profiles 的价值,在于把助手的长期配置与状态从共享程序中分离出来。这为同时运行多个助手提供了清楚的组织方式,也为进一步设计消息路由和业务隔离留下了明确的位置。