Hermes Agent 多 Provider 实践:自建网关、真实模型名与高质量故障切换
用 OpenAI 兼容网关管理多上游模型:真实模型名、验证链路、高质量 fallback 与长上下文兼容性。
工程笔记 · 2026-07-24 · 约 6 分钟阅读 · 标签:Hermes Agent、LLM、OpenAI Compatible、自托管
安全审校:2026-10-07;保留原发布日期 2026-07-24 及历史正文。此次仅静态语法、官方参考及离线安全检查,未部署、调用模型或执行生产操作;不是实时源站同步。命令前提为 Linux/macOS/WSL2 POSIX shell,Docker 需 Compose v2 与正确项目目录。
当 AI Agent 不只是聊天,而是会运行命令、修改代码、管理服务器时,模型服务的稳定性会直接影响任务能否完成。单一 API 上游很容易因为限流、余额、模型下线或网络问题中断长任务。
我目前采用的方案是:Hermes Agent 连接一个自建 OpenAI 兼容网关,网关后面再配置多个高质量上游;同时保留少量经过验证的 Hermes 直连 Provider 作为备用。
本文不公开真实 API Key、服务器地址和账户信息,只讨论配置原则和验证方法。
为什么不直接在 Hermes 里堆很多 Key
直接配置多个 Provider 当然可行,但自建网关有几个优势:
- 统一 API 地址和鉴权;
- 账户级优先级与并发管理;
- 上游失败后自动切换;
- 统一记录请求状态与错误;
- 客户端无需感知具体账号变化;
- Hermes、Codex 和其他工具可以共用同一入口。
整体结构如下:
Hermes Agent
→ OpenAI-compatible gateway
→ upstream A
→ upstream B
→ upstream C
网关解决的是“上游账户与容错”,Hermes 负责“任务与模型选择”。两层职责不要混在一起。
第一原则:模型名必须透明
很多中转喜欢提供好记的别名,甚至把一个模型名静默映射到完全不同的模型。这对普通聊天也许无所谓,对 Agent 却很危险:
- 上下文长度不同;
- 工具调用能力不同;
- 推理风格和代码质量不同;
- 价格、速度和限额不可预测;
- 出现回归时无法定位真实上游。
因此我的规则是:公开什么模型,就用什么真实名字;映射保持 1:1。
例如上游真实提供:
gpt-5.6-sol
gpt-5.6-luna
gpt-5.6-terra
网关也只暴露这些名字:
{
"gpt-5.6-sol": "gpt-5.6-sol",
"gpt-5.6-luna": "gpt-5.6-luna",
"gpt-5.6-terra": "gpt-5.6-terra"
}
不要把 gpt-best、smart-model 之类的别名放进生产路由,更不要在名字背后跨模型家族切换。
第二原则:/models 返回 200 不等于能用
这是最常见的误判。很多平台允许任何 Key 查看模型列表,但真正调用时可能出现:
401:Key 无效;403:账号没有模型权限;429:额度或并发不足;500/503:上游兼容性或服务故障;- 返回 200,但实际模型字段被替换。
因此新增 Provider 时至少做两层测试。
1. 检查模型列表
python3 -c 'import getpass,json,sys,warnings,urllib.request,urllib.error,urllib.parse
if not sys.stdin.isatty() or not sys.stderr.isatty():
raise SystemExit("Interactive TTY required; refusing echoed input")
warnings.simplefilter("error",getpass.GetPassWarning)
class NoRedirect(urllib.request.HTTPRedirectHandler):
def redirect_request(self,req,fp,code,msg,headers,newurl):
raise urllib.error.HTTPError(req.full_url,code,"Redirect refused",headers,None)
url="https://gateway.example.com/v1/models"
if urllib.parse.urlsplit(url).hostname in ("gateway.example.com", "example.com"):
raise SystemExit("Replace the example HTTPS host with your reviewed exact gateway host first")
key=getpass.getpass("OpenAI API key (hidden): ")
if not key or any(c in key for c in "\r\n"):
raise SystemExit("Invalid key")
req=urllib.request.Request(url,headers={"Authorization":"Bearer "+key})
try:
with urllib.request.build_opener(NoRedirect()).open(req,timeout=30) as response:
print("HTTP",response.status)
data=json.load(response)
print("\n".join(m["id"] for m in data["data"]))
except urllib.error.HTTPError as error:
raise SystemExit("HTTP "+str(error.code)+"; response body hidden")
except (urllib.error.URLError,ValueError,KeyError):
raise SystemExit("Request failed; details hidden")
'
记录真实模型 ID,不凭经验猜名字。
2. 发真实完成请求
python3 -c 'import getpass,json,sys,warnings,urllib.request,urllib.error,urllib.parse
if not sys.stdin.isatty() or not sys.stderr.isatty():
raise SystemExit("Interactive TTY required; refusing echoed input")
warnings.simplefilter("error",getpass.GetPassWarning)
class NoRedirect(urllib.request.HTTPRedirectHandler):
def redirect_request(self,req,fp,code,msg,headers,newurl):
raise urllib.error.HTTPError(req.full_url,code,"Redirect refused",headers,None)
url="https://gateway.example.com/v1/chat/completions"
if urllib.parse.urlsplit(url).hostname in ("gateway.example.com", "example.com"):
raise SystemExit("Replace the example HTTPS host with your reviewed exact gateway host first")
key=getpass.getpass("OpenAI API key (hidden): ")
if not key or any(c in key for c in "\r\n"):
raise SystemExit("Invalid key")
req=urllib.request.Request(url,data=json.dumps({'"'"'model'"'"': '"'"'gpt-5.6-sol'"'"', '"'"'messages'"'"': [{'"'"'role'"'"': '"'"'user'"'"', '"'"'content'"'"': '"'"'Reply exactly: OK'"'"'}], '"'"'max_tokens'"'"': 16, '"'"'stream'"'"': False}).encode(),headers={"Authorization":"Bearer "+key,"Content-Type":"application/json"})
try:
with urllib.request.build_opener(NoRedirect()).open(req,timeout=30) as response:
print("HTTP",response.status)
data=json.load(response)
print("Expected model:",data.get("model")=="gpt-5.6-sol", "Reply OK:",data.get("choices",[{}])[0].get("message",{}).get("content")=="OK")
except urllib.error.HTTPError as error:
raise SystemExit("HTTP "+str(error.code)+"; response body hidden")
except (urllib.error.URLError,ValueError,KeyError):
raise SystemExit("Request failed; details hidden")
'
检查 HTTP 状态、输出内容和返回的模型字段。
如果要给 Codex 使用,还必须另外测试:
POST /v1/responses
Codex 默认使用 Responses API。只支持 /chat/completions 的中转,不能直接视为完整 Codex Provider。
Hermes 自定义 Provider 的组织方式
概念配置可以写成:
custom_providers:
- name: backup-a
base_url: https://gateway.example.com/v1
api_key_env: BACKUP_A_API_KEY
models:
- gpt-5.6-sol
- gpt-5.6-luna
密钥只放在 .env:
BACKUP_A_API_KEY=<API_KEY>
配置完成后,不要只看模型选择器。应启动一个全新的 Hermes 进程:
hermes chat \
--provider custom:backup-a \
--model gpt-5.6-sol \
--toolsets safe \
-q 'Reply exactly: PROVIDER_OK'
这一步同时验证 Provider 名称解析、.env 加载、模型选择和请求格式。
故障切换只保留高质量模型
Fallback 的目标是恢复任务,不是“有回复就行”。低质量模型可能在工程任务中:
- 漏掉约束;
- 错误使用工具;
- 编造运行结果;
- 在长任务中不断偏离目标。
因此 fallback 列表应短而明确,并为每个目标固定 Provider 与模型:
fallback_providers:
- provider: custom:backup-a
model: gpt-5.6-sol
- provider: custom:backup-b
model: gpt-5.5
每一项都必须事先真实调用成功。一个配置存在但模型不存在的 fallback,只会延长故障时间。
一个真实踩坑:短请求成功,长上下文失败
某个 OpenAI 兼容中转在测试时表现正常:
/models返回 200;- 几个模型的短对话都返回
OK; - Hermes 直连测试也成功。
但当 Agent 会话积累了大量日志、工具结果和系统指令后,请求体增长到数百 KB,上游开始返回:
500 sensitive_words_detected
这不是 Key、模型映射或网络故障,而是上游的内容过滤对长上下文不友好。网关检测到失败后切换到另一个账号,因此最终任务仍能完成。
这个案例说明:
- 最小请求只能证明基础可用;
- Agent Provider 还要测试长上下文;
- 短对话可用的中转,不一定适合作为 Agent 主力;
- 自动 failover 很重要,但日志必须能看出最终使用了哪个账号。
主模型与辅助模型可以分流
Hermes 不只有主聊天模型,还可能有:
- 视觉分析;
- 网页提取;
- 上下文压缩;
- 后台摘要;
- 子 Agent。
把所有任务都压到最昂贵或最敏感的主 Provider 上并不合理。更稳妥的策略是:
主工程任务 → 高质量主模型
上下文压缩 → 稳定、长上下文模型
视觉任务 → 专门视觉模型
网页提取 → 速度优先模型
但分流同样需要验证,不应为了省钱使用明显不可靠的模型。
日常巡检清单
我会定期检查:
/models是否仍包含配置模型;- 最小 Chat Completions 请求;
- Responses API 请求;
- 流式响应是否会中断;
- 大请求是否触发 WAF 或敏感词过滤;
- 网关实际命中的上游账号;
- fallback 是否真的被调用成功;
- 模型名是否仍保持透明。
小结
一个可靠的 Agent 路由体系,不是 Provider 越多越好,而是每一层职责清晰:
Hermes:选择适合任务的真实模型
网关:管理账户、并发和故障切换
上游:提供经过验证的模型能力
最终原则可以浓缩成四句话:
- 使用真实模型名,不做隐藏跨模型映射;
- 不相信模型列表,必须做真实请求;
- Fallback 只选经过验证的高质量模型;
- 短请求成功后,还要验证 Agent 的长上下文场景。
做好这些,模型服务偶尔波动时,Agent 才不会在关键任务中突然失去工作能力。
安全输入:示例须在交互 TTY 使用,拒绝无 TTY 与所有重定向;先替换并核对精确 HTTPS 网关地址与费用。模型列表不代表 Responses/chat 真实生成成功。
官方参考:https://hermes-agent.nousresearch.com/docs/integrations/providers 。custom_providers 是仍兼容的旧格式;backup-b 是未定义的概念模板,需单独配置验证,不可直接投入生产。未改用户 Hermes 配置或调用付费模型。
安全程序只替换 url 的 HTTPS 地址,保留占位主机拒绝规则;Responses/chat 验证只输出布尔结果,不公开完整响应体。模型名也必须按实际账户配置核对。