最近把 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 命令,这套东西配起来才是真正费劲的地方。
一、环境要求
我的环境大概是这么个组合:
| 组件 | 版本 | 说明 |
|---|---|---|
| Python | 3.11.x | 用 uv 管理,pyproject.toml 指定 >=3.11,<3.14 |
| Node.js | v26.8.1 | 用 fnm 管理,桌面端 UI 需要 |
| Git | 自带 | Windows 桌面 runtime 需要 |
| uv | 最新版 | Python 包管理器 |
| fnm | 最新版 | Node.js 版本管理 |
注意 Python 版本有硬性要求,太新太旧都不行。我这边用 uv 管 Python,fnm 管 Node,两条线分开走,互不干扰。
二、项目安装
2.1 创建虚拟环境
| |
注意:uv sync 这里大概率会翻车。原因在 pyproject.toml 里的 exclude-newer = "14 days" 这条规则。
exclude-newer是 uv 的"依赖冷却期"机制,用来做供应链安全——它会把指定时间窗口内新发布的包全部忽略掉。问题是有些 PyPI 包没带上传日期,被 uv 当成"过期"过滤掉,解析就挂了。
这个规则本身是个好设计,但在实际项目里偶尔会误伤。解决办法很简单,绕开 uv 直接走 pip:
| |
不想每次手动改的话,也可以把冷却期临时改短,或者用 exclude-newer-package 针对单个包豁免。不过对我这种只想快速跑起来的人来说,pip install -e ".[dev]" 是最省事的。
2.2 安装 Node.js 依赖
| |
Node 这块就没什么坑了,装完就完事。
三、桌面客户端 Runtime 配置
Hermes Studio 桌面客户端需要一个独立的 runtime 目录来运行 CLI 命令,结构大概是这样的:
| |
这里我把所有本机真实路径都用占位符替代了(%HERMES_REPO% 指仓库路径,%USERPROFILE% 指用户主目录),免得以后自己翻记录的时候路径对不上。
3.1 复制运行时组件
把项目 .venv 里的东西复制到 runtime 目录,核心是这几样:
.venv\Scripts\python.exe→python\python.exe.venv\Scripts\hermes.cmd→python\Scripts\hermes.cmd.venv\Scripts\hermes.exe→python\Scripts\hermes.exerun_agent.py、cli.py→python\run_agent.py、python\cli.py- fnm 的
node.exe→node\node.exe - Git 的
git.exe+ 依赖 DLL →git\cmd\git.exe
3.2 创建 runtime 元数据文件
runtime-manifest.json 和 active-version.json 是 Studio 判断 runtime 就绪的钥匙,格式如下:
| |
| |
Studio 的 runtime-manager.js 会逐个检查必需文件(python.exe、hermes.cmd、node.exe、run_agent.py、cli.py、git.exe 等)是否齐全,缺一个就报 runtime error。
四、关键问题与修复
这一节才是这次折腾的主菜,三个坑都挺有代表性。
4.1 坑一:ModuleNotFoundError: No module named ‘hermes_cli’
配置完 runtime,一跑就报:
| |
原因:.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:
| |
这里用到的机制是 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 直接跑源码,绕开编译二进制:
| |
受影响的是 runtime 目录下的 hermes.cmd、hermes-agent.cmd、hermes-acp.cmd,以及 %USERPROFILE%\bin 下那几个 .bat。改完之后,源码改动的确即时生效,不再有"改了没反应"的困惑。
这是很多 Python CLI 项目在 Windows 上共同的坑:
pip install -e生成的.exe是入口打包器,不是源码软链。以后改了源码不生效,第一反应先查是不是走在了编译二进制上。
五、系统环境变量配置
为了让 hermes、hermes-agent、hermes-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 就对了。
六、验证清单
配完后逐项过一遍,确认没漏:
| |
七、版本升级流程
hermes-agent 仓库发新版时(比如 0.21.0 → 0.21.1),升级 runtime 其实很轻量:
| |
由于 runtime 通过 sitecustomize.py 指向仓库路径,代码自动更新,基本不用重新复制 runtime 文件,只要把 active-version.json 里的版本号更新一下就行。
八、WAF 403 排查实录
这节是压轴的,也是最烧脑的一个坑。Studio 通过一个托管在 PaaS 平台(Render 免费托管)上的自定义 OpenAI 兼容代理访问模型时,出现了一个非常诡异的现象:
- 纯多轮聊天一切正常;
- 一旦 agent 调用工具(terminal 输出、读文件等结果回填进消息),下一次请求就大概率 403;
- 但同一端点、同一 API Key 用 OpenAI SDK 手工测试又完全正常。
一开始我按"请求指纹"的假设去查,走了不少弯路。依次排除过三类嫌疑:
- OpenAI SDK 的
x-stainless-*系列 headers(lang / package-version / os / arch 等)——在 httpx 请求层全部剥离后 403 依旧; - SDK 默认的
User-Agent: OpenAI/Python ...——对 custom_providers 路由写死 UA 后 403 依旧; - 第三方插件、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 就行,不用再动源码。
九、维护注意事项
几条容易再踩的,先记下来:
- sitecustomize.py:如果 uv Python 升级或 site-packages 位置变化,需要重新创建;
- 编译二进制陷阱:
.venv/Scripts/hermes.exe是编译快照,不拾取源码修改。改源码后务必走python.exe -m,别用hermes.exe; - uv sync 问题:
exclude-newer卡住时,用pip install -e ".[dev]"临时绕过; - MCP SDK 依赖:MCP server 需要
mcp==2.0.0+pywin32,装完还得跑pywin32_postinstall.py -install把 DLL 放对位置;Desktop Python 的sitecustomize.py要额外加win32、win32/lib、Pythonwin路径(因为.pth文件不被处理); - 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 指纹上死磕。
