3.2. 模型路由支持 15+ 平台网关却不需要复杂配置
模型路由支持 15+ 平台网关却不需要复杂配置
假设你刚刚完成上一章的迷你工具注册,Agent 已经能调用你写的 @tool 了。现在你盯着终端里跑起来的 Hermes,心里冒出一个很实在的问题:你的 Telegram Bot 用 Claude 跑得好好的,但 Discord 社区里那个 Bot 用户量少、预算紧,想切成 DeepSeek;而本地开发测试时又希望所有请求打到你本机的 vLLM,一分钱都不花——难道真要维护三套配置文件?
这个场景正是 Hermes 设计模型路由层的出发点。它解决的不是“能不能接多个模型”,而是“接 15 个模型和接 1 个模型差不多简单”。从当前调研资料来看,这套路由机制已经在生产环境中接入了 OpenAI、Anthropic、Ollama、vLLM、OpenRouter 等 20 余家提供商,而配置入口始终只有三个文件:config.yaml、~/.hermes/.env 和 auth.json。
本章将带你从零搭建多 Provider 环境,理解动态路由规则,并在最后手写一个自定义适配器接入自研模型。全程预计 45 分钟。
你需要什么
| 资源 | 说明 | 获取方式 |
|---|---|---|
| Hermes Agent ≥ v0.16 | 核心运行时 | pip install hermes-agent |
| OpenAI API Key | 云端模型测试 | platform.openai.com |
| 本地 vLLM 实例 | 自托管端点 | pip install vllm,默认 http://localhost:8000/v1 |
| 文本编辑器 | 编写配置和 Python 代码 | 任意 |
最终成果
完成本章后,你的 Hermes Agent 将具备以下能力:
- 通过一份
config.yaml声明多个 Provider,用.env隔离敏感凭证 - 根据任务类型、成本或延迟自动选择模型,失败时自动回退
- 拥有一个实现了
AbstractProvider接口的自定义适配器,可以接入任何自研推理服务
为什么做这个:AI Agent 的推理成本、延迟和可靠性高度依赖模型选择。一个死绑单一模型的 Agent,要么在闲时浪费算力,要么在高峰时排队超时。模型路由让你用规则而非硬编码来管理这种复杂性。
Provider 配置文件详解
统一抽象:三大件搞定所有认证
Hermes 将 Provider 的接入拆成三个层次,分别对应三类信息:
config.yaml → 声明 Provider 存在(叫什么、类型是什么、额外参数)
.env → 存放 API Key 等敏感凭证
auth.json → 存储 OAuth 流程产出的 Token(自动生成,无需手写)
这种分离的设计意味着:换一个密钥不需要改配置文件,换一个提供商不需要动网关代码。一个典型的 config.yaml 片段如下:
# config.yaml
models:
main: claude-sonnet-4-20250514 # 主模型:思考与工具调用
cheap: deepseek-chat # 廉价模型:摘要、分类等轻量任务
providers:
anthropic:
type: anthropic
api_key_env: ANTHROPIC_API_KEY # 从 .env 取密钥
deepseek:
type: openai_compatible
base_url: https://api.deepseek.com/v1
api_key_env: DEEPSEEK_API_KEY
my-local-vllm:
type: openai_compatible
base_url: http://localhost:8000/v1
api_key_env: "" # 本地服务通常无需密钥
对应的 ~/.hermes/.env 只需两行:
ANTHROPIC_API_KEY=sk-ant-xxxxx
DEEPSEEK_API_KEY=sk-xxxxxxxxxxxxx
注意:不同 Provider 的环境变量名完全独立(例如 OpenRouter 用
OPENROUTER_API_KEY,Google 用GOOGLE_API_KEY),拼错一个字母会导致静默加载失败。建议直接查阅官方 Providers 文档页面确认。
实操:接入 OpenAI 和本地 vLLM
步骤 1:检查本地 vLLM 是否在运行
curl http://localhost:8000/v1/models
# 预期输出:{"data":[{"id":"your-model-name",...}]}
步骤 2:编辑 config.yaml,添加两个 Provider
# config.yaml(新增部分)
providers:
openai:
type: openai
api_key_env: OPENAI_API_KEY
vllm-local:
type: openai_compatible
base_url: http://localhost:8000/v1
api_key_env: ""
default_model: meta-llama/Llama-3.1-8B-Instruct # 可选,指定默认模型
步骤 3:在 .env 中填入 OpenAI 密钥
echo 'OPENAI_API_KEY=sk-xxxxxxxxxxxxxxxxxxxxxxxx' >> ~/.hermes/.env
步骤 4:验证配置是否生效
hermes model list
# 你应该看到 providers 列表里出现 openai 和 vllm-local
预期结果:hermes model list 输出中,openai 和 vllm-local 的状态为 available。
踩坑经验:如果你用 Ollama 而非 vLLM,
base_url通常为http://localhost:11434/v1,且需要在 Ollama 中先ollama pull <model>。部分旧版 Ollama 的/v1/chat/completions端点实现不完整,message 角色中system可能被忽略。建议优先用 vLLM 或确认 Ollama 版本 ≥ 0.3.0。
动态模型选择与回退策略
接入多 Provider 之后,如果不写规则,Hermes 只会死板地用 main 模型处理一切。路由层真正的能力在于:同一套网关代码可以为不同消息自动选择不同的大脑。
基于任务类型的路由
Hermes 允许在 config.yaml 中根据任务的“意图标签”分配模型。一个典型的需求是:闲聊用便宜模型,逻辑推理用强模型。
# config.yaml
routing:
rules:
- intent: chitchat
model: deepseek-chat
- intent: coding
model: claude-sonnet-4-20250514
- intent: summarization
model: deepseek-chat
步骤 1:为规则定义意图标签。这些标签可以由 Agent 自身的分类器产生,也可以从前端透传。
步骤 2:发送一条测试消息,附带 intent 元数据(具体透传方式取决于你使用的终端后端,CLI 下可暂时用 --intent 参数)。
预期结果:同样的问题 "今天天气怎么样" 和 "帮我写一段并发爬虫" 分别命中 chitchat 和 coding 规则,调用了不同的模型。
基于成本与延迟的回退
回退策略能解决一个很现实的痛点:深夜时海外 API 延迟飙升,自动切回本地模型;或者主模型配额用完后无缝降级。
# config.yaml
routing:
fallback_policy:
- provider: anthropic
strategy: timeout # 超时触发
timeout_ms: 30000
fallback_to: vllm-local
- provider: anthropic
strategy: rate_limit # 限流触发
fallback_to: openai
步骤 1:在 config.yaml 中添加上述回退块。
步骤 2:故意触发一次回退。你可以临时把 Anthropic 的 timeout_ms 设成 1,让任意请求都超时。
步骤 3:观察日志输出。
预期结果:Agent 日志中出现类似 [routing] fallback triggered: anthropic -> vllm-local (reason: timeout) 的记录,且对话未中断。
踩坑经验:回退链路要注意模型的能力差。当本地模型只有 8B 参数,而你原本在跑 100K token 上下文的任务时,回退可能导致输出截断甚至格式错误。建议在
fallback_policy的tasks字段中限定允许回退的任务类型,避免“强任务落到弱模型”的尴尬。
一个判断 checklist:规则该写多细?
| 条件 | 建议 |
|---|---|
| 模型间能力差距大(如 GPT-4o vs Llama-8B) | 必须限制回退任务类型 |
| 成本差别超过 10x | 为高频低危任务建专用路由 |
| 所有模型能力接近(同类规格) | 仅配置超时回退即可 |
自定义 Provider 适配
官方支持的 20 多家 Provider 覆盖了大多数场景,但总有些时候你需要接入自研模型——可能是内部微调的 LLaMA,也可能是特定硬件的推理加速服务。Hermes 的 AbstractProvider 接口为此而生。
实现 AbstractProvider 接口
我们需要实现的最少方法只有两个:chat_completion 和 stream_completion。下面是一个最小化自研适配器示例:
# my_custom_provider.py
from hermes.providers.base import AbstractProvider
from hermes.types import Message, CompletionResponse, StreamChunk
from typing import List, AsyncIterator
import aiohttp
class MyInferenceProvider(AbstractProvider):
def __init__(self, config: dict):
# 从 config.yaml 的 providers 块传入自定义字段
self.base_url = config.get("base_url", "http://localhost:9090")
self.model_name = config.get("default_model", "my-model-v2")
self.api_key = config.get("api_key", "")
async def chat_completion(
self,
messages: List[Message],
model: str | None = None,
**kwargs
) -> CompletionResponse:
model = model or self.model_name
async with aiohttp.ClientSession() as session:
# 假设自研服务是 OpenAI 兼容的
payload = {
"model": model,
"messages": [m.dict() for m in messages],
**kwargs
}
headers = {"Authorization": f"Bearer {self.api_key}"}
async with session.post(
f"{self.base_url}/chat/completions",
json=payload,
headers=headers
) as resp:
data = await resp.json()
return CompletionResponse(
content=data["choices"][0]["message"]["content"],
model=data.get("model", model),
usage=data.get("usage", {})
)
async def stream_completion(
self,
messages: List[Message],
model: str | None = None,
**kwargs
) -> AsyncIterator[StreamChunk]:
# 流式实现类似,逐块 yield StreamChunk
model = model or self.model_name
async with aiohttp.ClientSession() as session:
payload = {
"model": model,
"messages": [m.dict() for m in messages],
"stream": True,
**kwargs
}
headers = {"Authorization": f"Bearer {self.api_key}"}
async with session.post(
f"{self.base_url}/chat/completions",
json=payload,
headers=headers
) as resp:
async for line in resp.content:
# 解析 SSE 流,略去细节
chunk = self._parse_sse_line(line)
if chunk:
yield StreamChunk(content=chunk)
步骤 1:将上述文件保存为 my_custom_provider.py,放置在 Hermes 能发现的路径下(默认 ~/.hermes/providers/)。
步骤 2:在 config.yaml 中注册它。
# config.yaml
providers:
my-inference:
type: custom
class_path: my_custom_provider.MyInferenceProvider
base_url: http://10.0.1.50:9090
default_model: internal-llama-70b
api_key: "" # 内网环境无需密钥
步骤 3:运行 hermes model list 验证注册成功。
预期结果:hermes model list 输出中出现 my-inference 且状态为 available,即可在 routing.rules 中像使用 OpenAI 一样使用它。
注意:
class_path的格式是module_path.ClassName,确保 Python 的importlib能解析。若遇到ModuleNotFoundError,检查文件是否在sys.path搜索范围内,或者将~/.hermes/providers/加入PYTHONPATH。
回顾
这一章你完成了三件事,对应三个层次的需求:
- 配置层:用
config.yaml+.env接入了至少两个 Provider(OpenAI + 本地端点),理解了敏感信息与环境变量的分离原则 - 路由层:编写了基于任务意图的路由规则,以及超时/限流触发下的自动回退策略,并知道了控制回退风险的 checklist
- 扩展层:手写了一个
AbstractProvider适配器,让自研模型也能享受 Hermes 的统一路由能力
整个过程约 45 分钟,产出了一份可以直接用于中小规模生产环境的多 Provider 配置骨架。
行动清单
- 编辑
config.yaml,添加至少一个云端 Provider 和一个本地 Provider - 在
.env中填入密钥,运行hermes model list确认状态 - 为你的 Agent 编写一条基于 intent 的路由规则
- 配置一条超时回退策略,并故意触发验证
- 尝试实现
AbstractProvider,将你自己的模型接入这套路由体系
在下一章《容器化与编排是生产部署的必备技能》中,我们将把这一章配好的模型路由和上一章的多终端后端一起打包成 Docker 镜像,部署到 Kubernetes 集群上——那时你会真正理解 Hermes 的“三层骨架”为什么被设计成高度解耦的形态。
Hermes Agent 系统设计与工程落地
关于 LearnKu