7.4. 自定义 Provider 接入自有模型只需实现抽象接口

自定义 Provider 接入自有模型只需实现抽象接口

你在上一章构建的多智能体协作体系,已经把消息传递、工具链和记忆网络编织成了一张精密的网——但有一个前提一直没有说破:这张网上流转的所有知识,都还运行在你指定的那几个公开模型上。如果你的企业安全策略明确要求“所有推理必须在内部网络完成,数据不出机房”,或者你已经用企业独有的领域数据微调出了一个效果吊打通用模型的自研大模型,你该怎么办?难道要推翻重来,为私有模型重新设计一整套调用框架吗?

不需要。Hermes Agent 从第一天起就为这种场景留好了入口。截至 2026 年 6 月,这个开源项目的 Provider 子系统已经演进为一个极简的抽象层,你只需要实现 3~5 个方法,就能让部署在内网任意推理后端(vLLM、TGI、Ray Serve 甚至自研服务)上的模型变成 Agent 的“大脑”。这一章我们就来完成这个动作:把一个运行在 vLLM 上的自托管模型接入 Hermes,让已经建好的所有 Agent 都能无缝切换到这个私有推理端点。


你需要什么

  • 环境:Python 3.10+,已通过 pip install hermes-agent 或从源码安装了 Hermes Agent(本章基于 hermes-agent==0.13.0 编写)。
  • 基础工具:一个可用的文本编辑器或 IDE。
  • 目标服务:一台可以通过 HTTP/HTTPS 访问的 vLLM 推理服务器,上面加载着你选择的模型(例如 NousResearch/Meta-Llama-3.1-8B-Instruct),能够接收 OpenAI 兼容的 /chat/completions 请求。
  • 预计时间:15 分钟阅读 + 编码,5 分钟验证。

如果你手边还没有运行中的 vLLM 服务,可以暂时用 Ollama 的本地模型做测试,本章给出的代码适配任何 OpenAI 兼容 API,原理完全一致。


最终成果

完成本章后,你将得到一个文件 my_providers/vllm_provider.py,其中包含一个 VLLMProvider 类。把它注册到 Hermes 的配置中后,Agent 将不再访问任何公有云 API,而是直接与你的私有推理服务对话。你还会掌握声明模型能力边界的方法(最大上下文长度、是否支持工具调用等),从而避免后续因配置错误导致的莫名其妙报错。

为什么要做这件事?因为控制模型接入点,就等于控制了数据流向和推理成本。Provider 抽象就是那道门,打开它,你把模型部署在哪里,Agent 的知识就产生在哪里。


步骤 1:读懂 AbstractProvider —— 你必须实现什么

Hermes 在 hermes.provider.base 模块中定义了一个抽象基类 AbstractProvider。从当前调研到的源码结构和社区文档来看,它要求任何自定义 Provider 至少提供以下方法:

方法 签名(简化) 必须实现? 说明
complete self, prompt: str, **kwargs -> str 必须 文本补全任务(老式 LLM 调用方式)
chat self, messages: List[Dict], **kwargs -> dict 必须 多轮对话任务,返回符合 Hermes 规范的消息字典
embed self, texts: List[str], **kwargs -> List[List[float]] 可选 生成嵌入向量,若你的模型不支持可留空并抛出 NotImplementedError
get_model_info self -> ModelInfo 必须 返回模型能力声明,包括模型名称、最大上下文长度、是否支持工具调用等

你可能注意到了:Hermes 并没有要求你实现 async 版本的方法。这是因为它内部通过线程池统一处理同步与异步的切换,你的 Provider 只要提供同步方法即可(这也是官方推荐的降低复杂度做法)。

踩坑经验
早期测试中,有人直接在 chat 方法内发起同步 HTTP 请求,但未设置合理的超时时间,导致 Agent 执行工具链时卡死。请务必在 requests.post()http.client 调用中显式设置 timeout——哪怕你部署的是本地推理服务,也不排除偶尔出现超长推理的情况。代码示例中我会带上这个参数。

理解了接口契约后,我们直接动手。


步骤 2:实现 VLLMProvider —— 把私有推理端点“搬”进 Hermes

假设你的 vLLM 服务已启动,并且可以使用 OpenAI 兼容的请求格式去访问它的 /v1/chat/completions 端点,服务地址为 http://10.0.0.12:8000。我们新建文件 my_providers/vllm_provider.py,内容如下:

# my_providers/vllm_provider.py
import requests
from typing import List, Dict, Optional
from hermes.provider.base import AbstractProvider, ModelInfo

class VLLMProvider(AbstractProvider):
    """适配自建 vLLM 推理服务的 Provider。"""

    def __init__(self, endpoint: str = "http://10.0.0.12:8000/v1",
                 model_name: str = "custom-model",
                 api_key: str = "not-needed"):
        self.endpoint = endpoint.rstrip("/")
        self.model_name = model_name
        self.api_key = api_key          # vLLM 默认不需要鉴权,可填任意值
        self._session = requests.Session()
        self._session.headers.update({
            "Authorization": f"Bearer {self.api_key}",
            "Content-Type": "application/json"
        })

    def complete(self, prompt: str, **kwargs) -> str:
        # 如果你的模型只服务于 chat 接口,可以直接透传
        return self.chat([{"role": "user", "content": prompt}], **kwargs)["choices"][0]["message"]["content"]

    def chat(self, messages: List[Dict], **kwargs) -> dict:
        url = f"{self.endpoint}/chat/completions"
        payload = {
            "model": self.model_name,
            "messages": messages,
            "max_tokens": kwargs.get("max_tokens", 1024),
            "temperature": kwargs.get("temperature", 0.7),
            "stream": False           # Hermes 默认不使用流式
        }
        try:
            resp = self._session.post(url, json=payload, timeout=60)
            resp.raise_for_status()
            return resp.json()
        except requests.exceptions.Timeout:
            raise TimeoutError(f"推理服务 {url} 在 60s 内未响应,请检查 vLLM 负载或网络")
        except requests.exceptions.RequestException as e:
            raise ConnectionError(f"无法连接到 vLLM 服务 {url}: {e}")

    def embed(self, texts: List[str], **kwargs) -> List[List[float]]:
        # 自托管模型可能没有专用 embedding 接口,抛出异常即可
        raise NotImplementedError("当前 VLLMProvider 不支持 embedding,请使用单独部署的嵌入模型")

    def get_model_info(self) -> ModelInfo:
        # 返回模型硬能力,非常重要!
        return ModelInfo(
            name=self.model_name,
            max_context_length=8192,          # 请根据实际模型修改
            supports_tools=True,              # 若模型经过工具调用微调则为 True
            embedding_dim=0                   # 因为不支持 embed,置0
        )

关键行解读

  • self._session:复用 HTTP 连接池,避免每次请求都重新建立 TCP 连接。
  • complete 方法直接委托给 chat 方法:现代 LLM 几乎都使用聊天接口,这种做法可以避免维护两套几乎相同的逻辑。
  • get_model_info 返回的 ModelInfo 对象决定了 Agent 如何对待这个模型。如果你实际模型的最大上下文是 32k,这里却填了 4096,Agent 会在发起长对话时主动截断历史,导致上下文丢失;反之填大了,模型会报错或产生幻觉。 务必据实填写。
  • timeout=60:对于通过 HTTP 访问的本地推理任务,60 秒是一个比较平衡的值,既不会过早断开慢推理,也不会让 Agent 永久挂起。

预期结果

保存该文件后,你可以通过 python -c "from my_providers.vllm_provider import VLLMProvider; p=VLLMProvider(); print(p.get_model_info())" 先行测试,确认 ModelInfo 输出与预期一致,且没有语法错误。


步骤 3:注册 Provider 并声明模型能力

仅仅写完类还不够,Hermes 需要知道在什么条件下使用它。找到你的主配置文件(通常是 hermes_config.yaml 或在代码中通过 AgentBuilder 注入),做两件事:

A. 在配置文件中指定自定义 Provider 路径

如果你使用 YAML 配置,可以像这样填写:

provider:
  type: custom
  module: my_providers.vllm_provider
  class: VLLMProvider
  init_params:                 # 传递给 __init__ 的额外参数(可选)
    endpoint: "http://10.0.0.12:8000/v1"
    model_name: "Meta-Llama-3.1-8B-Instruct"

如果不使用配置文件,你也可以直接在 Python 脚本里硬编码注册:

from hermes import AgentBuilder
from my_providers.vllm_provider import VLLMProvider

provider = VLLMProvider(
    endpoint="http://10.0.0.12:8000/v1",
    model_name="Meta-Llama-3.1-8B-Instruct"
)
agent = AgentBuilder().with_provider(provider).build()

B. 检查工具调用声明

get_model_info 中的 supports_tools=True 十分关键。Hermes 的 Agent 在执行需要工具的步骤前,会先查询该能力。如果你的模型实际上并不支持标准的工具调用(function calling)格式,却把这一项设为 True,Agent 会直接构造工具调用请求发给模型,然后收到不符合预期的回复,最终导致工具执行失败。反过来,如果你的模型确实支持原生工具调用(比如通过 tools 参数),设为 False 则会强制 Hermes 用“文本拼接”的方式模拟工具调用——结果虽然可能勉强可用,但对模型的指令理解能力要求更高。

踩坑经验
vLLM 部署的模型,即使底层是 Llama 3.1 这样支持工具调用的架构,也需要在启动时指定 --tool-call-parser llama3_json 之类的参数,才能正确解析工具调用响应。务必先在 vLLM 控制台或通过 curl 直接测试工具调用是否工作,再在 Hermes 中启用 supports_tools。别问我为什么知道,你不想花一个下午才发现是服务端解析器没开。


步骤 4:运行一次完整的端到端验证

现在,启动你的 Agent 并发出一个简单任务,观察请求是否真的被路由到私有推理服务。你可以在控制台执行以下代码(假设已经按步骤 3 构建了 agent):

# test_agent.py
response = agent.run("用中文回答我:法国的首都在哪里?")
print(f"Agent 回答: {response}")

预期结果

  1. vLLM 的服务日志中出现一次 /chat/completions 的 POST 请求记录。
  2. 程序输出正确回答。
  3. 没有出现“ConnectionError”或“TimeoutError”。

如果你在 vLLM 日志里看到请求内容,但 Agent 端报错说“Model returned invalid response”,请依次检查:

  • 返回的 JSON 结构是否与 OpenAI 完全相同(choices[0].message.content 是字符串);
  • 模型对话中存在 stop 标记没被正确截断;
  • 服务端是否返回了 "object": "chat.completion"

以上任何一点偏差都会导致 Provider 解析失败,但修改起来也很容易——你只需在 chat 方法的 resp.json() 返回前,对数据进行一次适配性转换即可。


回顾

你刚才做了什么?

  1. 理解了 AbstractProvider 的四项核心职责和必须实现的方法。
  2. 基于 vLLM 推理服务编写了一个不到 50 行的 VLLMProvider 类,处理了超时和错误。
  3. 在 Hermes 的配置系统中注册了这个自定义 Provider,并准确声明了模型的最大上下文长度和工具调用能力。
  4. 运行了一次端到端对话,确认请求链路由内部服务完成。

整个过程,如果你已有 vLLM 服务在跑,大约只需 10~15 分钟编码。现在,你构建的所有 Agent 都不再被锁定在少数几个公开模型上,你可以自由地将生产力切向任何自研、微调或加密托管的 LLM——只需要再实现一个 Provider。


行动清单

  1. 部署私有推理服务:确保 vLLM 或同等服务已就绪,并测试 /chat/completions 端点可用。
  2. 编写 Provider 类:实现 chatcompleteembed(可选)、get_model_info 四个方法,注意超时和错误处理。
  3. 声明模型能力:在 ModelInfo 中如实填写上下文长度和工具调用支持情况——数字别拍脑袋。
  4. 注册 Provider:通过 YAML 或 AgentBuilder 将 Provider 注入 Agent。
  5. 用一条简单指令测试:观察日志,确认请求发生在内网,且响应格式正常。

下一步预告

你的私有模型接入成功后,Agent 现在拥有了访问企业数据的“通行证”。但新的问题随之而来:如果 Agent 拥有工具调用权限和记忆访问能力,怎么确保它不会因用户的一句指令就删除生产数据库,或者读取其他人的会话记忆?下一章 “安全架构需要对工具调用和记忆访问施加细粒度控制” 会为你搭建一层权限模型,把“能做什么”和“不能做什么”刻进 Agent 的行为边界里。准备好,我们将为这艘舰队装上护栏。

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

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


暂无话题~