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/.envauth.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 将具备以下能力:

  1. 通过一份 config.yaml 声明多个 Provider,用 .env 隔离敏感凭证
  2. 根据任务类型、成本或延迟自动选择模型,失败时自动回退
  3. 拥有一个实现了 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 输出中,openaivllm-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 参数)。

预期结果:同样的问题 "今天天气怎么样" 和 "帮我写一段并发爬虫" 分别命中 chitchatcoding 规则,调用了不同的模型。

基于成本与延迟的回退

回退策略能解决一个很现实的痛点:深夜时海外 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_policytasks 字段中限定允许回退的任务类型,避免“强任务落到弱模型”的尴尬。

一个判断 checklist:规则该写多细?

条件 建议
模型间能力差距大(如 GPT-4o vs Llama-8B) 必须限制回退任务类型
成本差别超过 10x 为高频低危任务建专用路由
所有模型能力接近(同类规格) 仅配置超时回退即可

自定义 Provider 适配

官方支持的 20 多家 Provider 覆盖了大多数场景,但总有些时候你需要接入自研模型——可能是内部微调的 LLaMA,也可能是特定硬件的推理加速服务。Hermes 的 AbstractProvider 接口为此而生。

实现 AbstractProvider 接口

我们需要实现的最少方法只有两个:chat_completionstream_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


回顾

这一章你完成了三件事,对应三个层次的需求:

  1. 配置层:用 config.yaml + .env 接入了至少两个 Provider(OpenAI + 本地端点),理解了敏感信息与环境变量的分离原则
  2. 路由层:编写了基于任务意图的路由规则,以及超时/限流触发下的自动回退策略,并知道了控制回退风险的 checklist
  3. 扩展层:手写了一个 AbstractProvider 适配器,让自研模型也能享受 Hermes 的统一路由能力

整个过程约 45 分钟,产出了一份可以直接用于中小规模生产环境的多 Provider 配置骨架。

行动清单

  1. 编辑 config.yaml,添加至少一个云端 Provider 和一个本地 Provider
  2. .env 中填入密钥,运行 hermes model list 确认状态
  3. 为你的 Agent 编写一条基于 intent 的路由规则
  4. 配置一条超时回退策略,并故意触发验证
  5. 尝试实现 AbstractProvider,将你自己的模型接入这套路由体系

在下一章《容器化与编排是生产部署的必备技能》中,我们将把这一章配好的模型路由和上一章的多终端后端一起打包成 Docker 镜像,部署到 Kubernetes 集群上——那时你会真正理解 Hermes 的“三层骨架”为什么被设计成高度解耦的形态。

本文章首发在 LearnKu.com 网站上。

上一篇 下一篇
讨论数量: 0
发起讨论 只看当前版本


暂无话题~