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、模型映射或网络故障,而是上游的内容过滤对长上下文不友好。网关检测到失败后切换到另一个账号,因此最终任务仍能完成。

这个案例说明:

  1. 最小请求只能证明基础可用;
  2. Agent Provider 还要测试长上下文;
  3. 短对话可用的中转,不一定适合作为 Agent 主力;
  4. 自动 failover 很重要,但日志必须能看出最终使用了哪个账号。

主模型与辅助模型可以分流

Hermes 不只有主聊天模型,还可能有:

  • 视觉分析;
  • 网页提取;
  • 上下文压缩;
  • 后台摘要;
  • 子 Agent。

把所有任务都压到最昂贵或最敏感的主 Provider 上并不合理。更稳妥的策略是:

主工程任务 → 高质量主模型
上下文压缩 → 稳定、长上下文模型
视觉任务 → 专门视觉模型
网页提取 → 速度优先模型

但分流同样需要验证,不应为了省钱使用明显不可靠的模型。

日常巡检清单

我会定期检查:

  • /models 是否仍包含配置模型;
  • 最小 Chat Completions 请求;
  • Responses API 请求;
  • 流式响应是否会中断;
  • 大请求是否触发 WAF 或敏感词过滤;
  • 网关实际命中的上游账号;
  • fallback 是否真的被调用成功;
  • 模型名是否仍保持透明。

小结

一个可靠的 Agent 路由体系,不是 Provider 越多越好,而是每一层职责清晰:

Hermes:选择适合任务的真实模型
网关:管理账户、并发和故障切换
上游:提供经过验证的模型能力

最终原则可以浓缩成四句话:

  1. 使用真实模型名,不做隐藏跨模型映射;
  2. 不相信模型列表,必须做真实请求;
  3. Fallback 只选经过验证的高质量模型;
  4. 短请求成功后,还要验证 Agent 的长上下文场景。

做好这些,模型服务偶尔波动时,Agent 才不会在关键任务中突然失去工作能力。

安全输入:示例须在交互 TTY 使用,拒绝无 TTY 与所有重定向;先替换并核对精确 HTTPS 网关地址与费用。模型列表不代表 Responses/chat 真实生成成功。

官方参考:https://hermes-agent.nousresearch.com/docs/integrations/providers 。custom_providers 是仍兼容的旧格式;backup-b 是未定义的概念模板,需单独配置验证,不可直接投入生产。未改用户 Hermes 配置或调用付费模型。

安全程序只替换 url 的 HTTPS 地址,保留占位主机拒绝规则;Responses/chat 验证只输出布尔结果,不公开完整响应体。模型名也必须按实际账户配置核对。

全部文章 · 返回 OneMJJ 首页

OneMJJ · VPS、自托管与 AI 工具与历史维护笔记。少踩坑,多留传家宝。

部分工具由第三方运营;本站命令仅供复制,不会自动执行。运行前请核对环境、阅读源码并备份重要数据。本站可能展示 Google AdSense 广告,详见隐私政策。