Featured image of post Hermes Agent 桌面端 Runtime 配置实战:踩坑与修复全记录

Hermes Agent 桌面端 Runtime 配置实战:踩坑与修复全记录

从零配置 hermes-agent 桌面客户端(Hermes Studio)Runtime 的完整过程,记录 ModuleNotFoundError、编译二进制陷阱、WAF 403 等关键问题的排查与解决。

最近把 hermes-agent(Nous Research 出的那个跑在终端里的 agent)装到 Windows 上,又折腾了一套桌面客户端 Hermes Studio 的 runtime 配置。整个过程不算短,中间踩了几个挺典型的坑——模块导不进去、编译二进制不拾取源码、还有一次挺绕的 WAF 403。整理成一篇记录,给同样在 Windows 上折腾 hermes 的朋友做个参考。

先交代一下背景。Hermes Agent 是个模型无关、能调工具的 AI agent,可以本地跑也可以扔 VPS 上;Hermes Studio 则是它的桌面端 + Web 控制台,负责跑 agent 对话、管理模型和 profile、看日志之类的。桌面端跟纯 CLI 不一样的是,它需要一套独立的 runtime 目录来调用 CLI 命令,这套东西配起来才是真正费劲的地方。

一、环境要求

我的环境大概是这么个组合:

组件版本说明
Python3.11.x用 uv 管理,pyproject.toml 指定 >=3.11,<3.14
Node.jsv26.8.1用 fnm 管理,桌面端 UI 需要
Git自带Windows 桌面 runtime 需要
uv最新版Python 包管理器
fnm最新版Node.js 版本管理

注意 Python 版本有硬性要求,太新太旧都不行。我这边用 uv 管 Python,fnm 管 Node,两条线分开走,互不干扰。

二、项目安装

2.1 创建虚拟环境

1
2
# 如果 .venv 不存在,uv 会自动创建
uv sync

注意uv sync 这里大概率会翻车。原因在 pyproject.toml 里的 exclude-newer = "14 days" 这条规则。

exclude-newer 是 uv 的"依赖冷却期"机制,用来做供应链安全——它会把指定时间窗口内新发布的包全部忽略掉。问题是有些 PyPI 包没带上传日期,被 uv 当成"过期"过滤掉,解析就挂了。

这个规则本身是个好设计,但在实际项目里偶尔会误伤。解决办法很简单,绕开 uv 直接走 pip:

1
2
source .venv/bin/activate  # 或 .venv\Scripts\activate
pip install -e ".[dev]"

不想每次手动改的话,也可以把冷却期临时改短,或者用 exclude-newer-package 针对单个包豁免。不过对我这种只想快速跑起来的人来说,pip install -e ".[dev]" 是最省事的。

2.2 安装 Node.js 依赖

1
2
fnm use 26.8.1
npm install

Node 这块就没什么坑了,装完就完事。

三、桌面客户端 Runtime 配置

Hermes Studio 桌面客户端需要一个独立的 runtime 目录来运行 CLI 命令,结构大概是这样的:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
~/.hermes-web-ui/desktop-runtime/hermes/<version>/<platform>/
├── python/
│   ├── python.exe          # Python 解释器
│   ├── Scripts/
│   │   ├── hermes.cmd      # 启动批处理文件
│   │   └── hermes.exe      # Hermes CLI 编译二进制
│   ├── run_agent.py        # Agent 入口
│   ├── cli.py              # CLI 入口
│   └── venv/               # (可选) venv 环境
├── node/
│   └── node.exe            # Node.js 运行时
├── git/
│   └── cmd/
│       └── git.exe         # Git 命令行工具
├── runtime-manifest.json   # Runtime 元数据
└── active-version.json     # 当前激活版本信息

这里我把所有本机真实路径都用占位符替代了(%HERMES_REPO% 指仓库路径,%USERPROFILE% 指用户主目录),免得以后自己翻记录的时候路径对不上。

3.1 复制运行时组件

把项目 .venv 里的东西复制到 runtime 目录,核心是这几样:

  • .venv\Scripts\python.exepython\python.exe
  • .venv\Scripts\hermes.cmdpython\Scripts\hermes.cmd
  • .venv\Scripts\hermes.exepython\Scripts\hermes.exe
  • run_agent.pycli.pypython\run_agent.pypython\cli.py
  • fnm 的 node.exenode\node.exe
  • Git 的 git.exe + 依赖 DLL → git\cmd\git.exe

3.2 创建 runtime 元数据文件

runtime-manifest.jsonactive-version.json 是 Studio 判断 runtime 就绪的钥匙,格式如下:

1
2
3
4
{
  "schema": 1,
  "platform": "win-x64"
}
1
2
3
4
5
6
7
8
9
{
  "schema": 1,
  "desktopAppVersion": "0.20.6",
  "hermesRuntimeVersion": "0.21.1",
  "runtimeDirectory": "%USERPROFILE%\\.hermes-web-ui\\desktop-runtime\\hermes\\0.20.6\\win-x64",
  "platform": "win-x64",
  "updatedAt": "2026-09-09T00:00:00.000Z",
  "runtimeValidationFailures": []
}

Studio 的 runtime-manager.js 会逐个检查必需文件(python.exehermes.cmdnode.exerun_agent.pycli.pygit.exe 等)是否齐全,缺一个就报 runtime error。

四、关键问题与修复

这一节才是这次折腾的主菜,三个坑都挺有代表性。

4.1 坑一:ModuleNotFoundError: No module named ‘hermes_cli’

配置完 runtime,一跑就报:

1
ModuleNotFoundError: No module named 'hermes_cli'

原因.venv\Scripts\python.exe 本质上是个 venv 启动器。当它从 runtime 目录(而不是项目目录)被调用时,sys.path 里只有 uv 管理的 Python 默认路径,项目源码和 venv 的 site-packages 都不在——于是什么都导入不了。

修复:在 uv 基础 Python 的 site-packages 里放一个 sitecustomize.py,让 Python 启动时自动把项目路径和 venv 的 site-packages 塞进 sys.path

1
2
3
import sys
sys.path.insert(0, r"%HERMES_REPO%")
sys.path.insert(0, r"%HERMES_REPO%\.venv\Lib\site-packages")

这里用到的机制是 Python 的 sitecustomize.py——每个 Python 进程启动时,site 模块会自动 import 它,所以放这里等于给所有走这个解释器的命令统一打补丁。一个文件解决 hermes_cli 和后面的 yaml 两个问题。

4.2 坑二:ModuleNotFoundError: No module named ‘yaml’

和上一个同根同源——runtime Python 访问不到 venv 里的第三方包(PyYAML、pydantic 这些)。sitecustomize.py 里加了 .venv\Lib\site-packages 路径后,这个坑自动填平了。

4.3 坑三:编译二进制不拾取源码修改

这个坑最隐蔽。.venv\Scripts\hermes.exe 是个编译后的二进制(108KB),在 pip install -e ".[dev]" 时它把当时的源码打包进去了。你改了 agent/auxiliary_client.py 之类的源文件,hermes.exe 还是跑编译时的旧代码——源码改了,行为不变,排查半天才发现问题在这。

修复:把所有 .cmd/.bat 包装器改成用 python.exe -m 直接跑源码,绕开编译二进制:

1
2
3
@echo off
set PYTHONPATH=%HERMES_REPO%
%HERMES_REPO%\.venv\Scripts\python.exe -m hermes_cli.main %*

受影响的是 runtime 目录下的 hermes.cmdhermes-agent.cmdhermes-acp.cmd,以及 %USERPROFILE%\bin 下那几个 .bat。改完之后,源码改动的确即时生效,不再有"改了没反应"的困惑。

这是很多 Python CLI 项目在 Windows 上共同的坑:pip install -e 生成的 .exe 是入口打包器,不是源码软链。以后改了源码不生效,第一反应先查是不是走在了编译二进制上。

五、系统环境变量配置

为了让 hermeshermes-agenthermes-acp 命令在任意终端都能直接敲,我建了一组包装脚本放到 %USERPROFILE%\bin\,并把这个目录加进了 PATH。

文件内容
%USERPROFILE%\bin\hermes.bat@echo off + python.exe -m hermes_cli.main %*
%USERPROFILE%\bin\hermes-agent.bat@echo off + python.exe -m hermes_cli.main %*
%USERPROFILE%\bin\hermes-acp.bat@echo off + python.exe -m hermes_cli.acp %*

验证方式很直接,任意目录敲 hermes --version,能输出 Hermes Agent v0.21.x 就对了。

六、验证清单

配完后逐项过一遍,确认没漏:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# 1. Python 模块导入
python -c "import hermes_cli; print('hermes_cli OK')"
python -c "import yaml; print('yaml OK')"

# 2. hermes CLI 版本
python\Scripts\hermes.cmd --version
# 期望输出: Hermes Agent v0.21.x

# 3. logs list 命令
python -m hermes_cli.main logs list
# 期望输出日志文件列表,无报错

# 4. runtime 文件完整性
# 确认 python.exe / hermes.cmd / run_agent.py / node.exe / git.exe 等文件均存在

七、版本升级流程

hermes-agent 仓库发新版时(比如 0.21.0 → 0.21.1),升级 runtime 其实很轻量:

1
2
3
git fetch origin
git checkout v<new-version>
pip install -e ".[dev]" --no-deps --no-build-isolation

由于 runtime 通过 sitecustomize.py 指向仓库路径,代码自动更新,基本不用重新复制 runtime 文件,只要把 active-version.json 里的版本号更新一下就行。

八、WAF 403 排查实录

这节是压轴的,也是最烧脑的一个坑。Studio 通过一个托管在 PaaS 平台(Render 免费托管)上的自定义 OpenAI 兼容代理访问模型时,出现了一个非常诡异的现象:

  • 纯多轮聊天一切正常;
  • 一旦 agent 调用工具(terminal 输出、读文件等结果回填进消息),下一次请求就大概率 403;
  • 但同一端点、同一 API Key 用 OpenAI SDK 手工测试又完全正常。

一开始我按"请求指纹"的假设去查,走了不少弯路。依次排除过三类嫌疑:

  1. OpenAI SDK 的 x-stainless-* 系列 headers(lang / package-version / os / arch 等)——在 httpx 请求层全部剥离后 403 依旧;
  2. SDK 默认的 User-Agent: OpenAI/Python ...——对 custom_providers 路由写死 UA 后 403 依旧;
  3. 第三方插件、tools、压缩链路里"裸构造 OpenAI(...) 的路径"——做全局默认注入兜底后 403 依旧。

三步做完时,所有出站请求的指纹已经验证是干净的了(ua=... stainless=[]),但 403 依然如故。

真正根因:PaaS 平台 WAF 对大请求包直接 Blocked

最后才定位到:某些 PaaS 平台的边缘 WAF 会对大请求包、以及包含特定特征内容的请求(比如 ../ 路径序列、${jndi: 这类字符串)直接返回 403,响应体是平台的 <title>Blocked</title> 拦截页,不是正常的 JSON 错误。

这一下所有现象都解释通了:

  • 纯聊天的请求体只有几 KB、内容干净 → 正常通过;
  • 工具调用后,工具输出被回填进消息,请求体涨到几十~上百 KB,而且几乎必然包含路径等内容 → 命中 WAF 规则 → 403;
  • 拦截的是请求 body,headers 层面怎么改都无解

WAF 判断的是请求体内容(body),不是 header 指纹。工具输出一多、一长,body 里就难免出现 ../ 这类触发特征,于是被边缘 WAF 拦在门外——这类问题跟云厂商(Azure Application Gateway、Cloudflare、Fly Proxy 等)WAF 拦截合法请求的原理完全一致,都是内容规则命中,不是身份认证问题。

解法

  • 根治:换托管。把自建代理迁出有内容 WAF 的免费托管,本地/内网直接跑、自有 VPS、或换没有这类规则的平台。
  • 客户端缓解(降概率,无法根治):更激进的上下文压缩、限制单条工具输出长度——但 body 里只要仍出现触发特征,照样被拦。
  • headers 加固对 403 无解,但建议保留当通用防御,顺便把诊断日志沉淀下来,方便下次排查。

诊断落点放在了 Studio runtime 的 sitecustomize.py 里:启动指纹、httpx send 层加固(剥 x-stainless-* + 对 custom_providers host 写死 UA)、requests/urllib 保底加固,以及全量出站诊断日志。日志写到真实数据目录下的 waf_diag.log,用环境变量 HERMES_WAF_DIAG=0 可关闭。

最后收了个尾:仓库内的源码补丁全部 git checkout 还原,上游源码保持干净,所有运行时加固只保留在 uv 的 sitecustomize.py 一处。这样将来某条 custom_providers 路由需要注入 UA,只需要在该路由的 config 条目加 extra_headers 就行,不用再动源码。

九、维护注意事项

几条容易再踩的,先记下来:

  1. sitecustomize.py:如果 uv Python 升级或 site-packages 位置变化,需要重新创建;
  2. 编译二进制陷阱.venv/Scripts/hermes.exe 是编译快照,不拾取源码修改。改源码后务必走 python.exe -m,别用 hermes.exe
  3. uv sync 问题exclude-newer 卡住时,用 pip install -e ".[dev]" 临时绕过;
  4. MCP SDK 依赖:MCP server 需要 mcp==2.0.0 + pywin32,装完还得跑 pywin32_postinstall.py -install 把 DLL 放对位置;Desktop Python 的 sitecustomize.py 要额外加 win32win32/libPythonwin 路径(因为 .pth 文件不被处理);
  5. HERMES_HOME:Windows 上 Hermes 默认 HERMES_HOME%LOCALAPPDATA%\hermes,不是 ~/.hermes/。改了 ~/.hermes/config.yaml 但 CLI 读不到,得复制过去或设环境变量。

十、总结

这次配置的核心收获就三条:

  • sitecustomize.py 是 Windows 上修 Python 路径问题的万能钥匙——一个文件,让所有走这个解释器的命令都能拿到正确的 sys.path
  • python.exe -m 才是开发期的正解,别信编译二进制,那只是发布时的产物;
  • 碰到"小请求没事、大请求 403"的怪象,先怀疑 body 内容被 WAF 拦,别在 header 指纹上死磕。
本博客所有内容无特殊标注均为大卷学长原创内容,复制请保留原文出处。
Built with Hugo
Theme Stack designed by Jimmy