原文连接:
真正的自由不是没有约束,而是可以随时更换约束的来源。
一个让你立刻想动手的数字
先说个数字,让你有感觉这一讲能给你带来什么。
我做过一次实验:同一个真实工作流(40 个文件的代码迁移 + 测试编写 + 文档更新),分别跑在三个配置下:
| 配置 | 模型策略 | 单次成本 |
|---|---|---|
| A | 全程 Claude Opus 4.6 | $6.40 |
| B | 全程 GPT-5 | $5.10 |
| C | Hermes Agent 多模型策略 | $0.78 |
配置 C 的规则很简单:
-
简单的对话轮次(短消息、非代码类提问)→ 用 Gemini Flash 等廉价模型
-
复杂代码生成、架构决策、调试类任务 → 用 Claude Opus 4.6
-
两者之间设置了 fallback,主路限速时自动切备用路
大约 1/8 的成本,主观体验差异并不大。 为什么可以这么便宜?因为真实 Agent 任务里,大量 Token 其实花在了"简单轮次"上------用户的短提问、确认类回复、简单查询,这些根本不需要旗舰模型。只是大多数 Agent 框架不给你分开路由的能力。
Hermes Agent 给了。这一讲我们拆它是怎么给的。
四条接入路径:OpenRouter / Nous Portal / 直连 / 本地 Ollama
先把整个模型接入层的数据流画清楚:
┌──────────────────────────────────────┐
│ Agent 核心(run_agent.py) │
│ self._try_activate_fallback() │
└──────────┬───────────────────────────┘
│
┌────────────┼────────────────────┐
│ │ │
▼ ▼ ▼
┌────────────────┐ ┌─────────┐ ┌───────────────────────┐
│ resolve_ │→│ Adapter │→│ 远端(四条路径) │
│ provider_ │ └─────────┘ │ │
│ client() │ │ 1. OpenRouter(200+) │
└────────────────┘ │ 2. Nous Portal(自家) │
│ 3. 直连厂商 API │
│ 4. 本地 Ollama │
└───────────────────────┘
这里的关键是:Agent 核心通过 resolve_provider_client() 这个统一入口解析 Provider (对应 agent/auxiliary_client.py)。这不是说运行时完全没有厂商差异,而是说 Hermes 把"client 怎么构造、协议怎么适配、凭证怎么解析"收敛到了一个中心入口,主循环不用在每个调用点都手搓一遍 Provider 分支。
Hermes Agent 给你提供了四种接入远端的路径,三种主流 + 一种补充:
路径一:OpenRouter(推荐默认)
OpenRouter 是一个模型聚合网关------一个 API Key 打通 200+ 模型。它帮你做了两件事:
-
鉴权归一:不用再维护十几个厂商的 Key。
-
协议归一:所有模型都走 OpenAI Chat Completion 兼容协议。
Hermes Agent 把 OpenRouter 当作默认路径。你通过 hermes setup 引导配置,或设置环境变量 OPENROUTER_API_KEY,它就会把 OpenRouter 当作候选路由。
它的实际价值不只是"模型多",更在于把多厂商接入的心智负担压成了一个 Key、一套协议、一种配置方式。
路径二:Nous Portal(Nous Research 自家)
Nous Portal 是 Nous Research 官方提供的模型服务门户。Hermes Agent 对 Nous Portal 使用 OAuth Device Code 认证方式(hermes_cli/providers.py 中定义 auth_type="oauth_device_code"),接口走标准 OpenAI Chat 协议,端点为 https://inference-api.nousresearch.com/v1\。
注意: 虽然名字里有"Hermes",但 Nous Hermes 3/4 系列模型实际上不适合 Agent 任务。Hermes Agent 在启动时会检测到这些模型并发出明确警告(见后文"Non-Agentic 模型"一节)。Nous Portal 的价值更多在于提供其他合作伙伴模型的接入。
路径三:直连厂商 API
Hermes Agent 支持直连大量厂商的原生 API。在 hermes_cli/providers.py 的 HERMES_OVERLAYS 注册表中,可以看到已适配的厂商包括:Anthropic、OpenAI(含 Codex)、xAI、DeepSeek、阿里(DashScope)、Kimi、MiniMax、智谱(zai)、小米、Arcee、HuggingFace 等。
为什么还要直连? 两个场景:
-
企业合规:很多企业要求数据不能过第三方聚合商,必须走跟厂商签过 DPA 的直连通道。
-
功能尝鲜:聚合商的协议归一意味着一些厂商特有的新功能(比如 Anthropic 的 extended thinking、OpenAI 的 computer use)可能延迟支持。直连拿到的是最新能力。
路径四:本地 Ollama
本地 Ollama 是一个补充路径,解决的是两个场景:
-
离线开发:你在飞机上、网差的地方,仍然能让 Agent 跑起来。
-
数据隔离:敏感数据绝不能离开本机的场景(医疗、法律、金融内部)。
Hermes Agent 并不给 Ollama 单独开一条"本地专线",而是把它视作一个标准的 OpenAI 兼容端点(http://localhost:11434/v1\)。源码和官方文档里对应的配置姿势都是 provider: custom + base_url。这才是这套设计真正优雅的地方:本地和远端的差异,被收敛成了端点差异。
统一接入层的工程设计:Adapter 模式
四条路径怎么在代码层统一?Hermes Agent 用的是协议适配器模式。
Adapter 的真实结构
不同厂商的 API 协议是不同的,Hermes Agent 在 agent/auxiliary_client.py 中实现了一组专用适配器和包装器,把不同协议收敛到统一的调用面上:
# agent/auxiliary_client.py 中的核心适配器/包装器
class _CodexCompletionsAdapter:
"""把 chat.completions.create() 调用翻译为 Codex Responses API。"""
...
class CodexAuxiliaryClient:
"""对外暴露 OpenAI 风格接口,内部走 Codex 适配器。"""
...
class _AnthropicCompletionsAdapter:
"""OpenAI 兼容接口 → Anthropic Messages API 的适配器。"""
...
class AnthropicAuxiliaryClient:
"""对外暴露 OpenAI 风格接口,内部走 Anthropic 适配器。"""
...
class AsyncCodexAuxiliaryClient:
...
class AsyncAnthropicAuxiliaryClient:
...
而 Provider 的元数据注册在 hermes_cli/providers.py 的 HermesOverlay 数据类中,每个 Provider 标注了三个关键信息:
@dataclass(frozen=True)
class HermesOverlay:
transport: str = "openai_chat" # openai_chat | anthropic_messages | codex_responses
is_aggregator: bool = False
auth_type: str = "api_key" # api_key | oauth_device_code | oauth_external | external_process
extra_env_vars: Tuple[str, ...] = ()
base_url_override: str = ""
base_url_env_var: str = ""
更准确地说,调用方大多数时候不必感知厂商差异 。它通过 resolve_provider_client(provider, model) 拿到统一 client;底下可能是 OpenAI SDK 原生 client,也可能是 Codex/Anthropic 包装器。但 run_agent.py 里仍保留了少量与 transport、header、fallback、api_mode 相关的 Provider 特判。这不是抽象失败,而是工程现实。
一个容易被忽视的细节:协议归一的"抽象泄漏"
这里有一个很值得聊的工程陷阱。
大多数 Agent 框架对模型的抽象停在"所有模型都走 OpenAI 协议"这一层。听起来完美,实际用起来坑无数------因为即使都说自己是"OpenAI 兼容",不同模型的实际行为是有漂移的:
-
有的模型
tool_calls必须在content之后(否则解析错)。 -
有的模型在工具调用时会额外吐一段
<thinking>块,不剥干净会污染后续轮。 -
有的模型
finish_reason永远返回stop,根本不报tool_calls。 -
有的模型对
tools数组长度有硬限制(8 个、16 个、32 个都见过)。
Hermes Agent 在适配层和主循环中都有针对特定 Provider 的特判逻辑。比如在 _try_activate_fallback() 中可以看到,切换到不同 Provider 时需要根据 provider 名称和 base_url 判断走哪种 API 模式(chat_completions / codex_responses / anthropic_messages),还要处理 Kimi Coding 需要特定 User-Agent 头、Ollama Cloud 需要从环境变量拉 Key 等细节。
这种特判看起来丑,但它是真实工程里让模型无关能 work 的秘密。所有搞过多模型接入的同学都知道:协议归一的 80% 代码量在这种脏活累活里。
如果你要自己做多模型接入,记住一个教训:永远给适配层留一个"特判通道"。 不要追求代码的"整洁"而拒绝为个别模型打补丁------你压不住现实的漂移。
Non-Agentic 模型警告:把坑显式告诉你
聊完接入路径,我们聊一个更深的问题:模型之间的能力差异,不只是价格和延迟,而是功能能不能用。
Non-Agentic 模型这个概念
Hermes Agent 里有一个专门的概念:Non-Agentic Model------非 Agent 模型。
什么是 non-agentic?LLM 虽然号称支持 function calling,但实际跑 Agent 任务时,有三种典型的拉胯:
-
给了工具也不用。 你在 system prompt 里告诉它"请用 read_file 工具",它视而不见,自己瞎编文件内容。
-
调用一次就停。 第一轮工具调用结果返回后,它不会继续根据结果规划下一步,直接就 "finish_reason: stop"。
-
工具调用参数乱填。 Schema 里要求
{"path": "string"},它给你返回{"filepath": "xxx"}。
这三种行为,单独看都不是 bug------模型"能"用工具,只是"不擅长"用。但 Agent 任务是个多轮工具调用的场景,这些"不擅长"会在第一轮就让 Agent 彻底卡死。
Hermes Agent 的做法:静态检测 + 启动警告
Hermes Agent 目前的做法是静态检测 ------通过正则匹配模型名称来识别已知的 non-agentic 模型。具体实现在 hermes_cli/model_switch.py 中:
_HERMES_MODEL_WARNING = (
"Nous Research Hermes 3 & 4 models are NOT agentic and are not designed "
"for use with Hermes Agent. They lack the tool-calling capabilities "
"required for agent workflows. Consider using an agentic model instead "
"(Claude, GPT, Gemini, DeepSeek, etc.)."
)
_NOUS_HERMES_NON_AGENTIC_RE = re.compile(
r"(?:^|[/:])hermes[-_ ]?[34](?:[-_.:]|$)",
re.IGNORECASE,
)
def is_nous_hermes_non_agentic(model_name: str) -> bool:
"""Return True if model_name is a real Nous Hermes 3/4 chat model."""
if not model_name:
return False
return bool(_NOUS_HERMES_NON_AGENTIC_RE.search(model_name))
在 cli.py 的会话启动阶段,会调用这个检测函数,如果命中就显示醒目警告:
⚠ Nous Research Hermes 3 & 4 models are NOT agentic and are not designed
for use with Hermes Agent. They lack the tool-calling capabilities
required for agent workflows. Consider using an agentic model instead
(Claude, GPT, Gemini, DeepSeek, etc.).
你仍然可以用------Hermes Agent 不会替你做"拒绝加载"这种家长式决策。但它把坑显式告诉你。
此外,agent/models_dev.py 中的 list_agentic_models() 函数会从 models.dev 模型目录中过滤出支持 tool_call=True 的模型,同时用正则排除噪声模型(TTS、embedding、过期预览版等)。这个列表用于模型选择界面,帮你在第一步就筛掉不适合 Agent 任务的模型。
这套机制看起来简单,但它给你省掉了大量"为什么这个模型死活不工作"的调试时间。
模型路由:把"简单轮次"和"复杂轮次"分开
OK,non-agentic 警告解决了"能用不能用"的问题。现在聊更有意思的部分:同一个会话里,不同轮次用不同模型。
这是本讲开头 1/8 成本的那个实验的核心。
任务分层的基本思路
Agent 主循环里的 Token 消耗,其实是极度不均衡的:
| 操作类别 | Token 占比 | 对模型能力的要求 |
|---|---|---|
| 读文件 / grep / glob | 30-40% | 低(只要能按 Schema 输出工具调用) |
| Web 搜索 / 页面摘要 | 15-20% | 中(需要信息综合能力) |
| 代码生成 / patch | 20-25% | 高 (真正的难点) |
| 决策 / 规划 | 5-10% | 高 (多步推理) |
| 错误恢复 | 5-10% | 高 (需要理解失败原因) |
| 技能创建 / 记忆策展 | 2-5% | 中高 |
"低"的那一档其实占了 30-40% 的 Token。但你用旗舰模型跑这部分,是完全的浪费------一个 8B 的开源模型都能稳定完成"读 /tmp/foo.py 然后告诉我它有几个函数"。
Smart Model Routing:Hermes Agent 的实现
Hermes Agent 的模型路由实现在 agent/smart_model_routing.py 中,叫做 Smart Model Routing 。它的核心思路是:按用户消息的复杂度分流------简单消息走廉价模型,复杂消息走主模型。
配置在 ~/.hermes/config.yaml 中:
# ~/.hermes/config.yaml
smart_model_routing:
enabled: true
max_simple_chars: 160
max_simple_words: 28
cheap_model:
provider: openrouter
model: google/gemini-2.5-flash
路由决策的核心函数是 choose_cheap_model_route(),它的判断逻辑非常保守------宁可多花钱也不误判:
def choose_cheap_model_route(user_message: str, routing_config) -> Optional[Dict]:
# 以下任一条件命中,就走主模型(不路由到廉价模型):
# 1. 消息超过 160 字符
# 2. 消息超过 28 个单词
# 3. 消息包含多行
# 4. 消息包含代码块(``` 或 `)
# 5. 消息包含 URL
# 6. 消息包含复杂关键词(debug, implement, refactor, patch,
# traceback, error, analyze, architecture, test, plan, delegate...)
# 全部通过才路由到 cheap_model
...
注意:这是"轮次级"路由,不是"工具级"路由。 路由发生在用户发送消息后、Agent 开始推理前。它根据用户消息的复杂度来决定这一轮用哪个模型,而不是根据将要调用的工具。
resolve_turn_route() 函数把路由决策和 Provider 解析组合在一起,返回这一轮的完整 runtime 配置(model、api_key、base_url、provider 等),在 UI 层会显示类似 smart route → google/gemini-2.5-flash (openrouter) 的标签,让你知道路由生效了。
这套机制的含金量高在哪?它把"模型路由"这件事从架构师的直觉决策变成了可配置、可观测、可调优的东西。 你跑一周,看 /usage 里消耗多少钱,不满意就调配置。这是一种你能带走的能力。
故障转移:主模型限速时的无感切换
讲完"按轮次选模型",讲更苛刻的一个场景:主模型在关键时刻抽风了,怎么办?
这个问题在真实生产里非常普遍:
-
OpenAI 给某些 Tier 限速,到了某个时刻你的 API 开始返回 429。
-
Anthropic 某个区域在某个时刻 5xx 频繁。
-
OpenRouter 某个 upstream 暂时不可用。
如果 Agent 正在跑一个 30 轮的任务,跑到第 18 轮主模型挂了------整个任务凉掉,用户只看到一个红色错误。
Hermes Agent 对这个场景做了无感切换。
故障转移的核心函数
run_agent.py 里有一个方法叫 _try_activate_fallback()(第 6316 行)。当主模型在重试后仍然失败时被调用,它做的事是就地替换当前的模型配置:
def _try_activate_fallback(self) -> bool:
"""Switch to the next fallback model/provider in the chain.
Called when the current model is failing after retries. Swaps the
OpenAI client, model slug, and provider in-place so the retry loop
can continue with the new backend. Advances through the chain on
each call; returns False when exhausted.
"""
if self._fallback_index >= len(self._fallback_chain):
return False
fb = self._fallback_chain[self._fallback_index]
self._fallback_index += 1
fb_provider = (fb.get("provider") or "").strip().lower()
fb_model = (fb.get("model") or "").strip()
if not fb_provider or not fb_model:
return self._try_activate_fallback() # skip invalid, try next
# 通过 resolve_provider_client() 构建新的 client
fb_client, _ = resolve_provider_client(fb_provider, model=fb_model, ...)
if fb_client is None:
return self._try_activate_fallback() # provider 没配,跳过
# 就地替换 model、provider、client、api_mode 等运行时状态
self.model = fb_model
self.provider = fb_provider
self.client = fb_client
self.api_mode = ... # 根据 provider 自动判断
self._fallback_activated = True
return True
几个细节值得注意:
细节一:内部统一成 fallback chain。 run_agent.py 里真正推进的是 _fallback_chain 列表,每次调用 _try_activate_fallback() 都向前走一步(_fallback_index += 1)。不过这里要区分源码内部表示 和公开配置写法 :当前给用户暴露的主路径是 config.yaml 顶层的 fallback_model:,也就是一个常规备用模型;运行时再把它规范化成内部链表结构。
细节二:就地替换。 切换不是重建 Agent,而是原地替换 self.model、self.provider、self.client、self.api_mode 等字段。这意味着 Agent 的会话状态(消息历史、工具状态等)完全保留,只是底层的模型通道换了。
细节三:API 模式自动适配。 切换到新 Provider 时,代码会根据 provider 名称和 base_url 自动判断走哪种 API 模式------如果是 Anthropic 就用 anthropic_messages,如果是 OpenAI 直连或 GPT-5 就用 codex_responses,其余走 chat_completions。甚至会为 Anthropic 构建原生的 Anthropic client 而非走 OpenAI 兼容层。
细节四:Provider 特殊处理。 源码中可以看到一些有趣的特判:Kimi Coding 需要 User-Agent: KimiCLI/1.30.0 头,Ollama Cloud 需要从 OLLAMA_API_KEY 环境变量拉 Key,这些都在 fallback 切换时自动处理。
凭证池轮换:同一模型多 Key 的负载均衡
还有一个和故障转移配套的机制:凭证池轮换。
如果你有多个同一厂商的 Key(不同组织、不同 Tier),Hermes Agent 可以把它们加入一个凭证池:
hermes auth add anthropic
# 通过交互式流程添加凭证,支持 API Key 和 OAuth 两种方式
# 重复执行可添加多个凭证到同一 provider 的池中
hermes auth list # 查看所有凭证
hermes auth remove <p> <idx> # 移除某个凭证
hermes auth reset <provider> # 清除某个 provider 的限速标记
凭证池的实现在 agent/credential_pool.py 中,支持四种轮换策略:
-
fill_first:按顺序使用,当前 Key 耗尽才用下一个。 -
round_robin:轮询使用。 -
random:随机选取。 -
least_used:优先使用调用次数最少的 Key,做负载均衡。
某个 Key 被限速(429)或配额耗尽(402)时,mark_exhausted_and_rotate() 方法会将其标记为 exhausted 并自动跳到下一个可用 Key。被标记的 Key 有 1 小时的冷却期(EXHAUSTED_TTL_429_SECONDS = 3600),冷却后自动恢复。
这个机制跟故障转移是正交的:故障转移解决"同模型同 Key 失败时换模型";凭证池解决"同模型换 Key"。两者组合在一起,对生产环境的鲁棒性提升非常明显。
动手实战:配置一份"开发用免费、生产用旗舰"的策略
讲完原理,我们动手做一份真正可用的多模型策略。
场景
你是一个独立开发者或小团队技术负责人。平时开发原型、写小工具,希望模型便宜够用;客户 demo 或正式交付时需要质量过硬。同一台机器、同一份 Hermes Agent,通过修改配置文件切换策略。
第一步:准备 Key
运行 setup 引导程序,配置 OpenRouter(一个 Key 覆盖所有模型,最省心):
hermes setup
# 按提示选择 OpenRouter,输入 API Key
# 或直接设置环境变量:
export OPENROUTER_API_KEY=sk-or-xxxxxxxxxxxxxxxxxx
第二步:配置 Smart Model Routing
编辑 ~/.hermes/config.yaml(或用 hermes config edit):
开发模式------全程用免费/低价模型:
# ~/.hermes/config.yaml
model:
provider: openrouter
default: moonshotai/kimi-k2:free
# 简单轮次进一步降级到更小的模型
smart_model_routing:
enabled: true
max_simple_chars: 160
max_simple_words: 28
cheap_model:
provider: openrouter
model: qwen/qwen-2.5-72b-instruct:free
# 上下文压缩配置
compression:
enabled: true
threshold: 0.50
生产模式------旗舰模型 + 智能降级:
# ~/.hermes/config.yaml
model:
provider: anthropic
default: claude-sonnet-4-6
# 简单轮次用便宜模型,复杂轮次保持旗舰
smart_model_routing:
enabled: true
max_simple_chars: 160
max_simple_words: 28
cheap_model:
provider: openrouter
model: google/gemini-2.5-flash
两个配置的区别在于主模型的选择和 cheap_model 的档位。开发时全线走免费通道;生产时只有简单轮次降级,关键决策仍在旗舰模型。
第三步:配置 Fallback
在 config.yaml 里配置 fallback_model:,当主模型不可用时自动切换:
# 在 ~/.hermes/config.yaml 中添加
fallback_model:
provider: openrouter
model: anthropic/claude-sonnet-4
如果你顺着源码往下读,会看到 _fallback_chain 支持列表化推进;但对大多数用户来说,先把 fallback_model: 这一格配好,就已经覆盖了 90% 的生产场景。
第四步:观察真实成本
Hermes Agent 内置了 /usage 命令,在交互会话中随时可查看当前 session 的用量:
> /usage
📊 Session Token Usage
────────────────────────────────────────
Model: anthropic/claude-sonnet-4-6
Input tokens: 52,300
Cache read tokens: 38,100
Cache write tokens: 2,400
Output tokens: 18,200
Prompt tokens (total): 92,800
Completion tokens: 18,200
Total tokens: 111,000
Estimated cost: $1.84
如果你想看更长周期的用量洞察,可以用 /insights 命令。
第五步(可选):把本地 Ollama 作为离线兜底
如果你在笔记本上装了 Ollama,可以把备用模型直接指向本地端点:
# 确保 Ollama 在跑,拉一个小模型
ollama pull qwen2.5:14b-instruct
然后把 fallback_model: 改成:
fallback_model:
provider: custom
model: qwen2.5:14b-instruct
base_url: http://localhost:11434/v1
现在就算整个外网都挂了,Hermes Agent 还能切到本地模型继续工作。这种韧性设计,是模型无关架构另一个不显眼但真实的价值。
一个容易踩坑的点:上下文压缩用哪个模型
聊一个生产环境才会遇到的坑------上下文压缩的模型选择。
Hermes Agent 在上下文超过阈值时会做压缩(默认 compression.threshold = 0.50)。压缩的时候它需要一次额外的 LLM 调用来生成摘要。问题来了:这次压缩调用用哪个模型?
如果你用默认策略(压缩用主模型),但主模型恰好被限速了------压缩失败,整个会话卡住。如果随便找个免费模型压缩------摘要质量可能不够,影响后续所有轮次。
Hermes Agent 的做法是在 config.yaml 的 auxiliary 配置中单独给压缩指定模型:
# ~/.hermes/config.yaml
auxiliary:
compression:
provider: openrouter
model: google/gemini-2.5-flash
context_compressor.py 中的 summary_model_override 参数支持覆盖压缩使用的模型。如果未显式配置,系统会按一个自动检测链寻找可用的辅助模型(优先级:OpenRouter → Nous Portal → Codex → Anthropic → 其他已配置的 Provider),默认选择快速/便宜的模型(如 Gemini Flash、Claude Haiku 等)。
压缩任务的特点是输入长、输出短、语义准确性要求中等 ------用一个上下文窗口大、价格便宜的模型非常合适。这个小细节在你跑长会话时能救命。