企业级 AI 网关的插件系统,核心是把”模型接入”与”网关主流程”解耦:定义统一模型插件接口,用 SPI 与 Spring 工厂机制做插件发现,再用策略工厂按提供商标识路由到具体实现,新增一家模型厂商只写一个插件类、改一行配置,网关主体代码一行不动。我在企业级 AI 网关项目里用这套方案接入了 OpenAI、DeepSeek、通义、Ollama 四类模型源,下文从接口设计、发现机制、生命周期三块拆开讲。
一、插件要解决的核心矛盾
网关接大模型,第一道坎是供应商 API 千差万别:有的走 OpenAI 兼容协议,有的自定义鉴权头,有的模型名不统一,连 temperature、maxTokens 这类采样参数都各叫各的。把这些差异直接写进调用层,网关每接一家模型就要改一遍业务代码,路由、鉴权、限流、计量全部跟着返工。
插件系统把变化隔离在一条稳定边界之外:
| 插件要素 | 网关侧约定 | 插件侧实现 |
|---|---|---|
| 接口 | 只认统一 ChatModel 抽象 | 各家适配器实现它 |
| 发现 | 启动时扫描并注册 | 提供 SPI 描述文件或工厂配置 |
| 路由 | 按 provider 标识匹配 | 插件声明自己支持的标识 |
| 生命周期 | 容器统一管理 | 插件只管自己的初始化与销毁 |
边界定清楚后,网关的鉴权、限流、监控都在统一抽象之上工作,模型能力差异被收敛进插件内部。
二、接口与抽象层的设计
2.1 模型插件接口
接口要覆盖三件事:声明能力、构造模型客户端、对外暴露元信息。下面是我在项目中收敛后的定义:
public interface AiModelPlugin {
// 声明支持的提供商标识,如 "deepseek"、"openai"
boolean supports(String provider);
// 用网关下发的配置构建模型客户端
ChatModel buildChatModel(ModelConfig config);
// 插件元信息,供管理后台与能力矩阵展示
PluginMeta meta();
}
ChatModel 由 Spring AI 提供,把 Prompt 收敛为输入、ChatResponse 收敛为输出,call 与 stream 两种调用面统一。适配层融合了策略、构造器与适配三种模式:插件是策略,各自构造客户端;Spring AI 的 Builder 组装请求参数;不同供应商协议的差异在适配器内消化。
2.2 为什么不把逻辑全写在一个类里
网关早期版本把模型切换写成 if-else,后来接第三家模型时,代码开始出现”模型名带版本号””异常类型互相污染”这类问题。改为插件后,每类模型一个类,出错能精确归因到对应插件,回归测试也能按插件独立跑。
三、插件发现机制:从 SPI 到 Spring 工厂
插件要被容器认出来,得有一个注册路径。我同时保留了 JDK SPI 与 Spring 工厂两条路,线上默认走 Spring 工厂。
3.1 JDK SPI(ServiceLoader)
接口实现类全名写进 META-INF/services/ 下以接口全名为文件名的文件里,运行时用 ServiceLoader 扫描加载。优点是无框架依赖,缺点是每个接口对应一个文件,插件一多文件就散。
3.2 Spring 工厂机制
Spring Boot 在 META-INF/spring.factories 里登记实现类,SpringFactoriesLoader 读取后实例化。项目落地时用的是这样一份配置:
com.gateway.plugin.AiModelPlugin=\
com.gateway.plugin.impl.DeepSeekPlugin,\
com.gateway.plugin.impl.OpenAiPlugin,\
com.gateway.plugin.impl.DashScopePlugin,\
com.gateway.plugin.impl.OllamaPlugin
Spring Boot 3.x 已转向 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 新目录格式,插件项目也同步迁移,旧文件只作兼容保留。两者核心思路一致:用约定位置的文件承载扩展点,让核心模块在编译期不知道插件存在。
3.3 热插拔:加载外部 JAR 的边界
需要运行时动态加载插件 JAR 时,用 URLClassLoader 指向插件目录,配合 Class.forName(className, true, loader) 按配置清单实例化。这套方案能实现不停机上线新模型,代价是类加载器隔离复杂:插件依赖的库版本与网关冲突时会出现 NoSuchMethodError,且网关内每个插件类加载器各持一份实例,元数据缓存要按 loader 维度管理。网关大多数场景静态部署就够,我建议先做编译期插件,热加载留在确有灰度上线需求的阶段再上。
四、策略工厂与动态路由
插件发现之后,调用侧需要一个统一的取用入口。网关里放了一个策略工厂,构造时把所有 AiModelPlugin 注入成 List:
@Component
@RequiredArgsConstructor
public class ModelPluginFactory {
private final List<AiModelPlugin> plugins;
public AiModelPlugin resolve(String provider) {
return plugins.stream()
.filter(p -> p.supports(provider))
.findFirst()
.orElseThrow(() -> new UnsupportedModelException(provider));
}
}
Spring 会把所有实现类按声明顺序注入,新增插件只需实现接口并注册为 Bean,工厂无需改动。请求带哪个提供商标识,网关就路由到哪个插件。这套”List 注入 + 谓词匹配”的写法,比在工厂里维护 switch 分支干净得多。
4.1 能力矩阵与降级
多模型接入的隐性风险是”接口统一、能力不统一”:文本聊天能切模型,不代表工具调用、结构化输出也能切。网关维护了一张能力矩阵,记录每个模型支持的上下文长度、是否支持 Function Calling、是否支持图片输入,路由前先过滤掉能力不足的候选。降级只处理明确定义的临时故障,如连接超时、HTTP 5xx、429,配置错误与鉴权失败直接返回,不做静默切换。
五、插件生命周期与配置管理
插件的配置集中在 application.yml,由 @ConfigurationProperties 绑定,API Key 一律从环境变量读取,不进代码库:
gateway:
models:
- name: deepseek-chat
provider: deepseek
api-key: ${DEEPSEEK_API_KEY}
options:
temperature: 0.7
max-tokens: 4096
- name: qwen-plus
provider: dashscope
base-url: https://dashscope.aliyuncs.com/compatible-mode/v1
api-key: ${DASHSCOPE_API_KEY}
插件启动阶段做三件事:解析配置、构造客户端、向注册表登记元信息。销毁阶段由 Spring 容器接管,插件实现 DisposableBean 关闭连接池。若要让某类插件在缺少对应 API Key 时静默禁用,用 @ConditionalOnProperty 控制装配条件,而不是在运行时抛异常。
六、落地时的三个易错点
第一,插件类别做太重。网关插件只负责”模型适配”,不该承担业务规则,业务层代码一进插件,插件就退化成模块,失去热插拔意义。第二,忽略能力边界。能切换模型不等于能力等价,必须按”模型名 + 接入协议 + 版本”绑定能力矩阵,并写进测试持续验证。第三,配置错误伪装成系统故障。模型名拼错、Key 过期这类错误在降级逻辑里被吞掉,线上排障会非常痛苦,这类异常要原样抛出并告警。
常见问题(FAQ)
Q1:插件必须做成独立 JAR 才叫插件吗?
不必。同一工程内按包隔离、按接口扩展也算插件化,热插拔才需要独立部署。
Q2:SPI 和 Spring 工厂怎么选?
插件不需要容器管理选 SPI;需要依赖注入、生命周期托管就选 Spring 工厂。
Q3:新增一家模型厂商要改多少代码?
写一个实现类加一行注册配置,网关路由、鉴权、限流代码零改动。