乐于分享
好东西不私藏

Hermes 源码解读五:Provider 抽象为什么不能只写 if/else

Hermes 源码解读五:Provider 抽象为什么不能只写 if/else

全文约 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.pyproviders/__init__.py

ProviderProfile 里描述的不只是 provider 名字。

它会承载:

字段解决的问题
name / aliases一个 provider 可能有多个用户可见名称
base_urlOpenAI-compatible endpoint 或原生 API 入口
api_modechat_completions、anthropic_messages、codex_responses 等
env_vars凭证从哪些环境变量或配置项读取
default_model用户没显式指定时如何落点
fallback_modelslive fetch 失败时模型列表如何兜底
headers / extra_bodyprovider 特有请求差异

如果这些都写成主流程里的 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__.pyplugins/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 alipayif stripeif paypal

Agent provider 也是一样。

api_mode 比 provider 名更重要

很多人会把 provider 理解成一个“模型供应商”。

但在 Hermes 里,真正影响运行时的是 api_mode

同一个 provider 可能有不同传输语义:

api_mode意味着什么
chat_completionsOpenAI-compatible Chat Completions 风格
anthropic_messagesAnthropic 原生 Messages API
codex_responsesOpenAI Codex Responses 路径
bedrock_converseAWS Bedrock Converse 语义
codex_app_serverCodex 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_KEYOPENAI_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 明确表达
凭证解析调用处自己找 envruntime 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 抽象一开始写烂,后面每接一个模型都会还债。