要把 Nous Research 的 Hermes Agent 跑到家里的 Mac Mini M4 上,长期常驻。需求很具体:大脑用 DeepSeek V4 Pro,入口要微信和 Telegram,浏览器复用系统已装的 Chrome(不想让 Playwright 再下一个 Chromium),而且以后要改它的源码。
一开始是在 DigitalOcean 上用 Docker 隔离测的,跑通后发现两件事决定了最终方案必须是 Mac 本机裸装:一是 computer-use(cua-driver)是 macOS 专属,Linux 容器里根本起不来;二是要改源码,预编译模式不方便。于是整条链路迁到 Mac Mini,用 developer 模式装。
下面是完整记录,最值得看的是中间那个把人绕了半天的 401。
Developer 模式安装
Hermes 的 install.sh 是预编译路径,不支持 --dev。developer 安装走仓库自带的 setup-hermes.sh:
git clone https://github.com/NousResearch/hermes-agent.git ~/hermes-agentcd ~/hermes-agent./setup-hermes.sh这个脚本做的事:检测平台 → uv venv venv --python 3.11 → 可编辑安装 uv pip install -e ".[all]" → 把 hermes symlink 到 ~/.local/bin → 同步 bundled skills。venv 目录名是 venv(在项目根,不是 .venv),-e 模式下改源码直接生效,这正是要的。
装完 hermes --version 正常,74 个 skills 同步完成。但很快踩了第一个坑。
坑一:uv sync --extra all 静默回退,漏装 telegram 依赖
配 Telegram 时报:
ModuleNotFoundError: No module named 'telegram'setup-hermes.sh 默认 uv sync --extra all --locked,理论上 all 应该包含 messaging(telegram 的 python-telegram-bot 在这个 extra 里)。但 all 聚合了一些重型 extra(比如 matrix 的 mautrix[encryption]、discord[voice]),其中某个在 macOS 上编译失败,触发脚本的 fallback 链,最后装了个不含 messaging 的子集——而退出码还是 0,所以当时没察觉。
单独补装就好:
cd ~/hermes-agent && source venv/bin/activateuv pip install -e ".[messaging]"+ python-telegram-bot==22.6+ discord-py==2.7.1+ slack-bolt==1.27.0...教训:--extra all 成功退出不代表 all 都装上了,关键平台的依赖要单独验证 python -c "import telegram"。
接 DeepSeek V4 Pro:custom provider
Hermes v0.16 的内置 provider 列表里没有 deepseek(有 openrouter / anthropic / gemini / zai / kimi / minimax …… 和 custom)。DeepSeek 完全 OpenAI 兼容,所以走 custom:
hermes config set model.provider customhermes config set model.base_url https://api.deepseek.com/v1hermes config set model.default deepseek-v4-prohermes config set model.max_tokens 8192hermes config set model.context_length 1000000 # V4 Pro 原生 1M contextcontext_length 必须手动设——custom 端点 Hermes 探测不到上下文窗口,不设会过早压缩历史。
key 从本地 .env 注入到 mini 的 ~/.hermes/.env(全程用 shell substitution,值不落进终端回显)。然后 hermes status 显示得很正常:
Model: deepseek-v4-proProvider: Custom endpointDeepSeek ✓ sk-1...e86d看着一切就绪。然后测试就翻车了。
坑二:那个把人绕半天的 401
现象:no final response
hermes -z "Reply with exactly one word: PONG"hermes -z: no final response was produced; treating the run as failed.错误方向:以为是 reasoning 模型 max_tokens 被吃光
DeepSeek V4 Pro 是推理模型,会先吐 reasoning_content 再吐 content。第一直觉是 Hermes 给的 max_tokens 太小,被 reasoning 吃光、content 没机会产出。直接 curl 验证:
curl -s https://api.deepseek.com/v1/chat/completions \ -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \ -d '{"model":"deepseek-v4-pro","messages":[{"role":"user","content":"say PONG"}],"max_tokens":10}'{"choices":[{"message":{"content":"","reasoning_content":"We are asked..."},"finish_reason":"length"}], "usage":{"completion_tokens_details":{"reasoning_tokens":10}}}max_tokens=10 时 content 确实空(全被 reasoning 占了)。但把 max_tokens 提到 400,content 就是 "PONG"、finish_reason=stop——模型完全正常。于是设 max_tokens=8192 重测,还是 no final response。所以根本不是 max_tokens。
关键:错误被 devnull 吞了
为什么报错这么没信息量?翻源码 hermes_cli/oneshot.py:
with redirect_stdout(devnull), redirect_stderr(devnull): response = _run_agent(prompt, ...)...if not (response or "").strip(): real_stderr.write("hermes -z: no final response was produced...")-z(oneshot)模式把 agent 的 stdout/stderr 全 redirect 到 /dev/null(设计是防 cron/SSH 下崩溃刷屏),所以 agent 内部真正的报错全被吞了。换交互模式跑就能看到:
hermes chat -q "Reply with exactly: PONG"⚠️ API call failed: AuthenticationError [HTTP 401] Error: HTTP 401: Your api key: ****ired is invalidHTTP 401。但我 curl 用 .env 里同一个 key 是 200 成功的,而且报错的 key 结尾是 ired,不是我那个结尾 e86d 的 key。
排查:key 来源全是干净的
挨个查 key 可能被污染的地方,全部排除:
~/.hermes/.env:OPENROUTER_API_KEY/OPENAI_API_KEY各一行,长度 35(DeepSeek key 正确长度),无重复、无尾随\r- 登录环境(
zsh -ic env)和.zshrc/.zprofile:没有任何*_API_KEY ~/.hermes/auth.json:不存在config.yaml:没有未注释的api_key
也就是说所有 key 来源都是那个正确的、curl 能用的 key。但 Hermes 发出去的是另一个结尾 ired 的 key。
根因:status 认对 key,请求发错 key
Hermes 自己写了 request dump,用脚本(脱敏)看它实际发的请求头:
request.url: https://api.deepseek.com/v1/chat/completionsrequest.headers.Authorization: len=22 tail='ired'request.body.model: deepseek-v4-proAuthorization 头只有 22 字符。正确的应该是 Bearer + 35 = 42。也就是说:hermes status 的配置层读到了正确的 key,但 custom provider 在构造实际 HTTP 请求时没把这个 key 传给 OpenAI SDK,SDK 退回到了某个内部 fallback 值。
修复:显式设 config.yaml 的 model.api_key
config.yaml 的 model.api_key 优先级最高(注释里写 “set here instead of .env”)。显式设上:
hermes config set model.api_key <key>hermes chat -q "Reply with exactly: PONG" PONG Duration: 3s通了。这个 bug 在 developer 模式下以后可以直接去改 custom provider 的 key 传递逻辑——这也是当初选 developer 模式的价值之一。
两个教训值得单独记:一是 -z 把错误 redirect 到 devnull,调 agent 问题要用 hermes chat -q 才看得到底层异常;二是「配置层显示的 key」和「请求实际用的 key」可能不是一回事,dump 请求头是最终的判据。
Telegram:gateway + pairing
依赖补齐、LLM 通了之后,Telegram 很顺。token 写进 .env 的 TELEGRAM_BOT_TOKEN,装成 launchd 常驻服务:
hermes gateway install # 生成 ~/Library/LaunchAgents/ai.hermes.gateway.plist[Telegram] Connected to Telegram (polling mode)第一次给 bot 发消息,它回了个 pairing code——Hermes 内置配对机制,未授权用户发消息会拿到一次性码,owner 批准即可:
hermes pairing approve telegram <CODE>之后日志确认端到端打通:
inbound message: platform=telegram msg='hi'response ready: time=5.9s api_calls=1 response=38 chars5.9 秒是 DeepSeek V4 Pro 的 reasoning + content 输出时间,正常。
微信:iLink 扫码,1v1 绑定
微信这块要先搞清楚 Hermes 的两个适配器:weixin.py 是个人微信,走腾讯 2026 年官方开放的 iLink Bot API(域名 ilinkai.weixin.qq.com,纯 HTTP long-poll,35 秒 hold);wecom.py 是企业微信,走 WebSocket。两者都不需要公网回调,这对家里没公网的机器很关键。
个人微信用 iLink,凭据靠扫码自动获取,不用预先申请:
hermes gateway setup # 选 Weixin → 终端显示二维码 → 微信扫二维码有 35 秒时效,而 Mac Mini 是远程机,所以这一步直接屏幕共享到 Mini 本地操作最稳。扫完凭据自动写入 ~/.hermes/.env(WEIXIN_ACCOUNT_ID / WEIXIN_TOKEN),重启 gateway:
[Weixin] Connected account=xxxxxxxx base=https://ilinkai.weixin.qq.comGateway running with 2 platform(s)发条「你好」,日志确认:
inbound message: platform=weixin msg='你好'response ready: time=5.3s api_calls=1 response=14 chars关于 iLink 的安全模型iLink bot 是1v1 绑定的:接入后是一个独立的
xxx@im.bot身份,只有扫码绑定的那个微信号(你自己)能 DM 它,别人加不进来也发不了。所以配置里WEIXIN_ALLOW_ALL_USERS=true并不构成风险——它天然就只服务绑定者。另外 iLink 协议只能「回复」不能「主动发起」,对方必须先发消息。
浏览器:复用系统 Chrome,不下 Chromium
Hermes 的 browser 工具默认要么用 Playwright 的 Chromium,要么走 Browserbase 云。要复用系统 Chrome,正确做法是 CDP(Chrome DevTools Protocol),不是网上传的 AGENT_BROWSER_EXECUTABLE_PATH(这个变量在源码里根本不存在)。
源码 tools/browser_cdp_tool.py 里 CDP endpoint 的解析顺序是 BROWSER_CDP_URL 环境变量 → config.yaml 的 browser.cdp_url。所以启动一个开了 remote-debugging 的系统 Chrome,再把 cdp_url 指过去即可:
open -na "Google Chrome" --args \ --remote-debugging-port=9222 \ --user-data-dir="$HOME/.hermes/chrome-debug" \ --no-first-run --no-default-browser-check--user-data-dir 必须是独立目录,否则如果系统已经在用普通模式跑 Chrome,新进程会复用旧进程,9222 不会监听。验证:
curl -s http://127.0.0.1:9222/json/version# {"Browser":"Chrome/149.0.7827.54", ... "webSocketDebuggerUrl":"ws://127.0.0.1:9222/..."}配上 cdp_url 后测试 agent 浏览:
hermes config set browser.cdp_url http://127.0.0.1:9222hermes chat -q "navigate to https://example.com and report the <h1>" -t browser🌐 navigate example.com 2.2sThe main <h1> is: Example Domainagent 通过 CDP 连上了系统 Chrome,全程没下 Chromium。
收尾:常驻化
两件让它真正「常驻」的事:
Chrome 开机自启——把上面的启动命令包成 LaunchAgent(KeepAlive + RunAtLoad),Mini 重启后 9222 自动恢复:
launchctl load -w ~/Library/LaunchAgents/com.user.chrome-cdp.plist让聊天里也能用 browser——gateway 各平台的 toolset 里要含 browser。Telegram 默认的 hermes-telegram toolset 本身就含 browser;微信默认 hermes-weixin 不确定,显式加上:
platform_toolsets: weixin: [hermes-weixin, browser]经验总结
- developer 模式(
setup-hermes.sh+ editable install)值得,尤其当你预期要改源码、或踩到像 custom provider key 传递这种需要改代码才能根治的 bug。 -z模式会吞错误,调 agent 行为问题一律换hermes chat -q看底层异常。- 「配置显示的 key」≠「请求实际发的 key」,怀疑认证问题时直接看 request dump 的
Authorization头长度和尾部。 --extra all不等于全装上,关键平台依赖(telegram 的messaging)要单独 import 验证。- DeepSeek V4 Pro 是推理模型,custom 接入要给够
max_tokens、手动设context_length。 - 家里没公网也能接微信:个人微信走 iLink long-poll、企业微信走 WebSocket,都不需要回调地址。