全文约 1760 字,读完大约 3 分钟。
Hermes 源码解读五:Provider 抽象为什么不能只写 if/else
Agent 框架接模型,最容易低估复杂度。
一开始看起来很简单:
OpenAI 一个 client。
Anthropic 一个 client。
OpenRouter 换个 base_url。
本地模型再加个 if。
写着写着,if provider == ... 越来越多。
真正麻烦的不是“多支持几个模型”,而是每个 provider 背后都有自己的 API mode、凭证来源、base_url 规则、模型列表、fallback、reasoning 字段、工具调用格式和错误恢复语义。
我看 Hermes 的 provider 相关源码时,一个判断很明显:
Hermes 不想把 provider 差异散落在主循环里,它用 ProviderProfile 把差异前移。
这也是这篇最值得拆的地方。
ProviderProfile 是能力描述,不只是配置

核心文件在 providers/base.py 和 providers/__init__.py。
ProviderProfile 里描述的不只是 provider 名字。
它会承载:
| 字段 | 解决的问题 |
|---|---|
name / aliases | 一个 provider 可能有多个用户可见名称 |
base_url | OpenAI-compatible endpoint 或原生 API 入口 |
api_mode | chat_completions、anthropic_messages、codex_responses 等 |
env_vars | 凭证从哪些环境变量或配置项读取 |
default_model | 用户没显式指定时如何落点 |
fallback_models | live fetch 失败时模型列表如何兜底 |
headers / extra_body | provider 特有请求差异 |
如果这些都写成主流程里的 if/else,后果很直接:
provider 越多,主流程越脏。
更麻烦的是,CLI、Gateway、Cron、ACP、辅助模型调用都要各写一遍。
Hermes 的做法是:provider 自己注册 profile,运行时 resolver 只按 profile 解析。
plugin provider
↓ register_provider(profile)
providers registry
↓ get_provider_profile()
runtime_provider
↓ resolve base_url / api_key / api_mode
AIAgent这就是抽象的价值。
不是为了“面向对象好看”,而是为了让差异只出现在应该出现的地方。
插件式 provider:新增能力不改 resolver 主干
看 plugins/model-providers/openrouter/__init__.py、plugins/model-providers/anthropic/__init__.py 这类文件,会发现 provider 是以插件形式注册的。
比如 Anthropic profile 会声明 api_mode="anthropic_messages"。
OpenRouter profile 会声明自己的 base_url、env var、fallback model。
providers/__init__.py 负责 lazy discovery。第一次 get_provider_profile() 时,才发现并加载这些插件。
这带来一个很重要的扩展边界:
新增 provider,不应该改主循环。
理想情况下,新增 provider 是新增一个插件目录,写好 profile 和必要适配逻辑。
主 resolver 只认统一结构。
这和后端系统里的支付渠道、消息渠道、存储后端很像。你不会希望订单主流程里写满 if alipay、if stripe、if paypal。
Agent provider 也是一样。
api_mode 比 provider 名更重要
很多人会把 provider 理解成一个“模型供应商”。
但在 Hermes 里,真正影响运行时的是 api_mode。
同一个 provider 可能有不同传输语义:
| api_mode | 意味着什么 |
|---|---|
chat_completions | OpenAI-compatible Chat Completions 风格 |
anthropic_messages | Anthropic 原生 Messages API |
codex_responses | OpenAI Codex Responses 路径 |
bedrock_converse | AWS Bedrock Converse 语义 |
codex_app_server | Codex app server 运行路径 |
这不是命名细节。
api_mode 决定了:
- • messages 怎么转换;
- • tools 怎么传;
- • reasoning 怎么保留;
- • streaming 怎么解析;
- • 错误怎么分类;
- • provider 返回内容怎么投影回 Hermes 内部消息格式。
如果只按 provider 名做分支,很快会遇到一个问题:
某个 provider 下既有 OpenAI-compatible 模型,又有 Anthropic-like 模型。
或者某个 endpoint 名义上兼容 OpenAI,但模型实际需要特殊字段。
Hermes 在 hermes_cli/runtime_provider.py 里大量处理的,就是“不要让过期 api_mode、错误凭证、错误 base_url 泄漏到当前 provider”。
这不是过度设计。
这是多 provider 系统跑久以后必然出现的问题。
凭证隔离是 provider 抽象的底线

Provider 抽象里还有一个很实用的点:凭证不能乱串。
文档里专门提到 OpenRouter 和 custom OpenAI-compatible base_url 的关系。
如果用户同时有 OPENROUTER_API_KEY 和 OPENAI_API_KEY,系统不能因为都是 OpenAI-compatible,就把 OpenRouter key 发给一个自定义 endpoint。
这属于安全和隐私边界。
Hermes 的 runtime resolver 会区分:
真实 custom endpoint
≠
OpenRouter fallback path这个边界在企业系统里很常见。
“协议兼容”不等于“凭证可共享”。
如果你做内部 Agent 平台,把不同供应商、代理网关、本地模型服务都接进来,凭证隔离必须在 resolver 层解决,而不是靠业务调用方自觉。
fallback 也不该是简单换模型
Hermes 支持 fallback provider chain。
表面看是模型调用失败后换一个 provider/model。
但源码里真正麻烦的是:fallback 触发以后,不只是改 model 字符串。
它要重新解析 provider client,重新确定 base_url、api_key、api_mode,还要针对 Anthropic、Codex 这类非普通 chat completions 路径做不同 client 构造。
换句话说:
fallback 不是“字符串替换”,而是一次运行时 provider 重解析。
这点很容易被低估。
如果你只在异常时把 model = fallback_model,下一轮请求可能还拿着旧 client、旧 header、旧 api_mode。
结果会更糟:系统看起来 fallback 了,实际请求仍然是错的。
Hermes 把 fallback 放进 provider runtime 逻辑里,至少方向是对的。
这套设计给自建 Agent 的启发
如果你要做一个长期可维护的 Agent 平台,provider 层最好早一点抽出来。
不要等到支持五六个供应商后再补。
最少也要把这几件事分开:
| 问题 | 不建议 | 更稳的做法 |
|---|---|---|
| provider 信息 | 散在 if/else | 统一 profile |
| API 格式 | provider 名硬编码 | api_mode 明确表达 |
| 凭证解析 | 调用处自己找 env | runtime resolver 统一处理 |
| fallback | 只换 model 字符串 | 重新解析 provider runtime |
| 新供应商 | 改主循环 | 插件注册 profile |
我的判断是:
多模型 Agent 的复杂度,不在“能不能调通 API”,而在 provider 差异能不能被稳定收敛。
调通一个模型是 demo。
让 CLI、Gateway、Cron、ACP、辅助任务都能用同一套 provider 解析逻辑,才是运行时。
Hermes 这部分代码很重,runtime_provider.py 也不算短。
但这份“重”是有原因的。
它在替整个系统承接 provider 混乱,而不是把混乱推给每个调用点。
如果你正在做 Agent 工程,看到 provider 抽象时别只问“支持多少模型”。
更应该问:
它怎么隔离凭证?
它怎么表达 API 差异?
它怎么处理 fallback?
它怎么让新 provider 不污染主循环?
它怎么让辅助任务和主对话共享解析逻辑?这些问题回答清楚,多模型才真正可维护。
再看 Gateway 时,这一点会更明显:模型只是运行时的一部分。真正的 Agent 系统,还要让不同入口、不同平台、不同 session 都能复用同一个核心。

如果你也在搭自己的 Agent 平台,可以先收藏这篇。Provider 抽象一开始写烂,后面每接一个模型都会还债。
夜雨聆风