摘要
生产级 AI 后端通常不会永远只使用一个模型。不同任务对质量、延迟、价格、数据驻留和可用性的要求不同:复杂推理可能调用云端模型,普通分类可以使用低成本模型,敏感数据可以路由到本地模型;当供应商故障时,还需要有受控的降级方案。
如果业务代码直接依赖某一家模型的 SDK、模型名称和响应结构,切换供应商就会扩散到 Controller、业务服务和测试代码中。本文以 Spring AI 为基础,设计一层模型服务抽象,实现对 OpenAI、通义千问 OpenAI 兼容接口和 Ollama 本地模型的统一调用,并讨论:
- Spring AI 的
ChatModel抽象能解决什么、不能解决什么; - 如何将"供应商、模型、任务、能力"分开建模;
- 如何配置多个云端和本地模型;
- 如何实现模型路由、超时、重试和降级;
- 为什么 Embedding 模型必须独立治理;
- 如何处理流式调用、工具调用、模型能力差异和可观测性。
本文示例以 Spring AI 2.0.1 API 为参考,通义千问兼容地址、模型名称和可用能力会随地域、业务空间和产品版本变化,接入前应以当前账号的官方控制台与文档为准。
一、背景与问题
1. 为什么不能在业务代码里直接写供应商调用
早期实现可能直接在 Service 中调用某个 SDK:
java
var response = openAiClient.chat().completions().create(request);
这会让业务代码逐渐依赖:
- 某个供应商的请求对象;
- 特定模型名称;
- 特殊参数和错误码;
- 特定的流式事件结构;
- 供应商专属的工具调用格式;
- 特定的认证和重试策略。
一旦切换模型,业务层也要跟着改。更重要的是,不同任务可能适合不同模型,但代码里没有统一的路由入口。
2. "兼容 OpenAI"不代表能力完全相同
通义千问、Ollama、vLLM 和许多模型网关都提供 OpenAI 兼容接口。兼容通常意味着部分 URL、请求和响应字段相似,并不意味着所有能力完全一致。
差异可能包括:
- 支持的聊天参数不同;
- 工具调用能力和 JSON Schema 支持不同;
- 流式结束事件格式不同;
- Token 用量字段不完整;
- 多模态输入限制不同;
- 错误码、限流和重试语义不同;
- Embedding API 与 Chat API 的兼容程度不同。
因此,服务抽象应隐藏供应商协议,但不能假装所有模型能力完全相同。
3. 需要区分的几个维度
模型选择不应只是一个字符串:
text
Provider:谁提供服务
Model:调用哪个模型
Task:当前在做什么
Capability:模型支持什么
Policy:当前请求允许用什么
例如:
| 任务 | 路由策略 |
|---|---|
| 简单标题生成 | 低成本云模型 |
| 企业知识库回答 | 支持长上下文且稳定的模型 |
| 高敏感内部数据 | 通过内部网关或本地模型 |
| 工具调用 Agent | 工具协议验证通过的模型 |
| 离线批处理 | 价格优先、吞吐优先 |
4. 本文的设计目标
模型服务层需要做到:
- 业务服务依赖稳定接口,不依赖供应商 SDK;
- 管理员可配置多个模型端点;
- 每次调用都能记录模型、任务、延迟和用量;
- 可按任务选择模型;
- 仅在安全条件满足时执行重试和降级;
- 能力差异显式声明,不支持的请求提前拒绝;
- Embedding 模型和 Chat 模型分别配置、分别版本化。
二、核心概念
1. Spring AI 的 ChatModel
Spring AI 提供统一的 ChatModel 接口,主要用于发送 Prompt 并返回 ChatResponse;它也提供流式调用能力。不同供应商的模型适配器实现这一接口,使上层可以用相近的方式调用不同模型。
这层抽象可以屏蔽:
- HTTP 请求细节;
- 常见消息角色;
- 基础同步和流式调用;
- 通用响应结构;
- 一部分通用模型参数。
但它不会自动解决:
- 哪个任务应该调用哪个模型;
- 不同模型能力是否一致;
- 凭据和租户权限;
- 费用预算与限流;
- 跨供应商的错误映射;
- 安全降级和审计。
这些属于应用自己的模型服务层。
2. Provider、ModelProfile 与 ModelClient
建议将三者分开:
| 对象 | 示例 | 职责 |
|---|---|---|
| Provider | OpenAI、DashScope、Ollama | 端点、鉴权和协议适配 |
| ModelProfile | qwen-plus-prod |
某个端点下的具体模型和默认参数 |
| ModelClient | Spring AI ChatModel / ChatClient |
执行调用 |
业务调用只引用逻辑模型 ID,例如 knowledge-answer,由服务端配置映射到真实 Profile。
3. 能力描述
每个 ModelProfile 应描述能力:
text
supportsStreaming
supportsToolCalling
supportsJsonSchema
supportsVision
maxContextTokens
maxOutputTokens
inputModalities
outputModalities
路由前先检查任务需求与模型能力。不能把一个不支持工具调用的本地模型路由到依赖 Function Calling 的 Agent 流程。
4. Chat 模型与 Embedding 模型要分开
Chat 模型生成文本,Embedding 模型把文本映射成向量。它们的接口、维度、模型版本和变更成本都不同。
| 项目 | Chat Model | Embedding Model |
|---|---|---|
| 输入 | 对话消息 | 一段或多段文本 |
| 输出 | 文本、工具调用、结构化结果 | 浮点向量 |
| 常见评估 | 质量、延迟、成本 | 召回效果、维度和成本 |
| 更换影响 | 通常可切换请求路由 | 通常需要重建向量索引 |
| 版本策略 | 按任务路由 | 与索引版本绑定 |
不能只因为两个 Embedding API 都"兼容 OpenAI"就混用向量。
5. 多供应商模型组合
一个应用可以同时使用:
text
OpenAIChatModel
└─ 官方 OpenAI 或兼容端点
QwenChatModel
└─ OpenAI 兼容模式连接阿里云百炼
OllamaChatModel
└─ 本机或内网模型服务
Spring AI 允许应用中存在多个模型实现,但要显式解决 Spring Bean 注入歧义,并为每个客户端配置正确的观测和定制逻辑。
三、工作原理
1. 模型调用的分层结构
text
Controller
↓
业务服务:知道任务目标,不知道供应商
↓
ModelService:鉴权、路由、策略和观测
↓
ModelRouter:选择逻辑模型对应的 Profile
↓
Provider Adapter:Spring AI ChatClient / ChatModel
↓
OpenAI / DashScope / Ollama
2. 一次调用流程
text
1. 根据当前用户和任务构造 ModelRequest
2. 进行租户、配额和数据分级检查
3. 解析逻辑模型 ID
4. 检查模型能力是否满足任务要求
5. 选择 Provider Adapter
6. 执行调用并记录 request_id
7. 标准化响应和错误
8. 记录 Token、延迟、模型版本和成本
9. 根据业务契约返回结果
3. 模型路由
路由可以从简单映射开始:
text
knowledge-answer -> qwen-plus-prod
title-generation -> openai-mini-prod
private-summary -> ollama-private
之后再根据数据级别、模型健康度、成本和能力增加策略:
text
数据级别
↓
任务能力要求
↓
租户允许的模型
↓
模型健康度和配额
↓
成本与延迟策略
↓
最终模型 Profile
不要允许客户端直接传入任意 Base URL 或任意模型名,否则可能造成 SSRF、越权调用和费用失控。客户端最多选择管理员预先开放的逻辑模型 ID。
4. 重试与降级
重试和降级不能对所有错误都执行:
| 错误 | 建议 |
|---|---|
| 连接超时、服务不可用 | 可对幂等请求有限重试 |
| 429 限流 | 按 Retry-After 或退避策略 |
| 401 / 403 | 不重试,检查凭据和权限 |
| 400 参数不兼容 | 不重试,修正请求 |
| 内容安全拒绝 | 不应绕过拒绝转发到其他模型 |
| 响应解析失败 | 记录原始关联 ID,按策略决定是否重试 |
流式响应开始返回内容后,不能简单切换到另一个模型并把两边输出拼起来。那会产生重复、语义断裂和计费不一致。流式降级通常只允许发生在首个内容片段产生之前。
5. 模型能力矩阵
路由服务可以维护能力矩阵:
| Profile | Streaming | Tool Calling | JSON Schema | Vision | 数据级别 |
|---|---|---|---|---|---|
openai-main |
是 | 是 | 是 | 依模型而定 | 内部及公开 |
qwen-prod |
是 | 依模型和接口 | 依模型和接口 | 依模型 | 按租户策略 |
ollama-local |
是 | 依模型而定 | 依模型而定 | 依模型 | 本地受控 |
能力不能只从 Provider 名称推断。上线前应对"实际模型 + 实际端点 + 实际参数"做集成测试。
四、实战示例
下面实现一个简化的模型网关:业务侧传入逻辑模型 ID 和 Prompt,服务层路由到配置好的 Spring AI ChatClient。
1. 依赖配置
使用 Spring AI BOM 管理版本,添加 OpenAI 和 Ollama 模型适配器:
xml
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-bom</artifactId>
<version>2.0.1</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-openai</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-ollama</artifactId>
</dependency>
</dependencies>
OpenAI Starter 用于连接官方 OpenAI,也可以配置 OpenAI-compatible 服务;Ollama Starter 用于连接 Ollama 原生接口。仅添加依赖不代表两个 Provider 都已启用,具体自动配置和多模型 Bean 需要在项目中显式管理。
2. 配置端点与凭据
yaml
app:
ai:
models:
knowledge-answer:
provider: qwen
model: ${QWEN_CHAT_MODEL}
base-url: ${QWEN_BASE_URL}
api-key: ${QWEN_API_KEY}
supports-streaming: true
supports-tool-calling: true
data-levels: [PUBLIC, INTERNAL]
general-fast:
provider: openai
model: ${OPENAI_CHAT_MODEL}
base-url: ${OPENAI_BASE_URL:https://api.openai.com}
api-key: ${OPENAI_API_KEY}
supports-streaming: true
supports-tool-calling: true
data-levels: [PUBLIC, INTERNAL]
private-local:
provider: ollama
model: ${OLLAMA_CHAT_MODEL}
base-url: ${OLLAMA_BASE_URL:http://localhost:11434}
supports-streaming: true
supports-tool-calling: false
data-levels: [PUBLIC, INTERNAL, CONFIDENTIAL]
通义千问 OpenAI 兼容模式的 Base URL 会因地域和业务空间而不同。公共云常见地址形态为 https://dashscope.aliyuncs.com/compatible-mode/v1;子业务空间和其他地域可能使用包含 Workspace ID 的地址。不要把某个示例端点硬编码为所有账号通用值。
生产环境应将 API Key 存入 Secret Manager 或环境变量,不要提交到 YAML 文件或镜像。
3. 定义逻辑模型 Profile
java
public enum ProviderType {
OPENAI_COMPATIBLE,
OLLAMA
}
public enum DataLevel {
PUBLIC,
INTERNAL,
CONFIDENTIAL,
RESTRICTED
}
public record ModelProfile(
String id,
ProviderType provider,
String modelName,
boolean streaming,
boolean toolCalling,
boolean jsonSchema,
int maxContextTokens,
Set<DataLevel> allowedDataLevels) {
}
这里的 id 是业务使用的稳定逻辑名称;真实模型名和 Base URL 放在受控配置中,不由请求客户端任意传入。
4. 定义统一调用契约
java
public record ModelCommand(
String logicalModelId,
String systemPrompt,
String userPrompt,
DataLevel dataLevel,
boolean requireStreaming,
boolean requireToolCalling,
String requestId) {
}
public record ModelResult(
String text,
String provider,
String model,
Integer inputTokens,
Integer outputTokens,
long durationMillis,
String requestId) {
}
public interface ModelService {
ModelResult generate(ModelCommand command);
Flux<String> stream(ModelCommand command);
}
统一接口只暴露业务真正需要的能力,不要为了"看起来通用"而把所有厂商特有字段塞进一个巨大 DTO。特殊能力可以通过明确的能力扩展接口处理。
5. 创建 OpenAI-compatible ChatModel
Spring AI OpenAiChatModel 可以通过 OpenAiChatOptions 指定端点、密钥和模型。以下示例分别创建 OpenAI 和 Qwen 客户端:
java
@Configuration
public class OpenAiCompatibleModelConfig {
@Bean("openAiChatModel")
ChatModel openAiChatModel(OpenAiProperties properties) {
var options = OpenAiChatOptions.builder()
.baseUrl(properties.openAiBaseUrl())
.apiKey(properties.openAiApiKey())
.model(properties.openAiModel())
.build();
return OpenAiChatModel.builder()
.options(options)
.build();
}
@Bean("qwenChatModel")
ChatModel qwenChatModel(QwenProperties properties) {
var options = OpenAiChatOptions.builder()
.baseUrl(properties.baseUrl())
.apiKey(properties.apiKey())
.model(properties.model())
.build();
return OpenAiChatModel.builder()
.options(options)
.build();
}
}
不同兼容端点对路径、请求参数和模型名的要求可能不同。配置 Base URL 时应核对 Spring AI 期望的端点根路径,以及供应商是否要求 /v1 或 compatible-mode/v1。
6. 创建 Ollama 本地模型
java
@Configuration
public class OllamaModelConfig {
@Bean("ollamaChatModel")
ChatModel ollamaChatModel(OllamaProperties properties) {
var ollamaApi = OllamaApi.builder()
.baseUrl(properties.baseUrl())
.build();
return OllamaChatModel.builder()
.ollamaApi(ollamaApi)
.options(OllamaChatOptions.builder()
.model(properties.model())
.build())
.build();
}
}
先在运行 Ollama 的主机拉取模型:
bash
ollama pull qwen2.5
ollama list
如果 Spring Boot 应用运行在 Docker 容器中,localhost:11434 指向应用容器自身,不一定能访问宿主机的 Ollama。应使用正确的容器网络服务名或宿主机地址,并限制 Ollama 服务的网络暴露范围。
7. 为模型创建 ChatClient
java
@Configuration
public class ModelClientConfig {
@Bean("openAiChatClient")
ChatClient openAiChatClient(
@Qualifier("openAiChatModel") ChatModel model) {
return ChatClient.builder(model).build();
}
@Bean("qwenChatClient")
ChatClient qwenChatClient(
@Qualifier("qwenChatModel") ChatModel model) {
return ChatClient.builder(model).build();
}
@Bean("ollamaChatClient")
ChatClient ollamaChatClient(
@Qualifier("ollamaChatModel") ChatModel model) {
return ChatClient.builder(model).build();
}
}
正式项目应将 Spring AI 的观测注册表、ChatClientBuilderConfigurer 和自定义构建器配置纳入这些客户端,避免手动创建客户端后丢失统一观测或自定义器。
8. 实现模型路由
java
@Component
public class ChatClientRegistry {
private final Map<String, ChatClient> clients;
public ChatClientRegistry(
@Qualifier("openAiChatClient") ChatClient openAi,
@Qualifier("qwenChatClient") ChatClient qwen,
@Qualifier("ollamaChatClient") ChatClient ollama) {
this.clients = Map.of(
"general-fast", openAi,
"knowledge-answer", qwen,
"private-local", ollama
);
}
public ChatClient getRequired(String logicalModelId) {
ChatClient client = clients.get(logicalModelId);
if (client == null) {
throw new IllegalArgumentException(
"Unknown logical model: " + logicalModelId
);
}
return client;
}
}
实际路由器还要从配置中心加载 Profile,并校验任务所需能力、数据级别、租户允许模型和模型健康状态。
9. 实现统一同步与流式调用
java
@Service
public class DefaultModelService implements ModelService {
private final ChatClientRegistry registry;
public DefaultModelService(ChatClientRegistry registry) {
this.registry = registry;
}
@Override
public ModelResult generate(ModelCommand command) {
long started = System.nanoTime();
ChatClient client = registry.getRequired(command.logicalModelId());
ChatResponse response = client.prompt()
.system(command.systemPrompt())
.user(command.userPrompt())
.call()
.chatResponse();
String text = response.getResult()
.getOutput()
.getText();
long durationMillis =
TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - started);
return new ModelResult(
text,
command.logicalModelId(),
null,
null,
null,
durationMillis,
command.requestId()
);
}
@Override
public Flux<String> stream(ModelCommand command) {
if (!command.requireStreaming()) {
throw new IllegalArgumentException(
"This command did not request streaming"
);
}
return registry.getRequired(command.logicalModelId())
.prompt()
.system(command.systemPrompt())
.user(command.userPrompt())
.stream()
.content();
}
}
示例将模型名称和 Token 用量留空,生产实现应从实际 ChatResponse 元数据中读取可用字段,并将供应商返回值统一映射到内部统计对象。若供应商不返回 usage,应记录为未知,不要伪造精确用量。
10. 能力校验
java
public void validate(ModelProfile profile, ModelCommand command) {
if (command.requireStreaming() && !profile.streaming()) {
throw new UnsupportedOperationException(
"Selected model does not support streaming"
);
}
if (command.requireToolCalling() && !profile.toolCalling()) {
throw new UnsupportedOperationException(
"Selected model does not support tool calling"
);
}
if (!profile.allowedDataLevels().contains(command.dataLevel())) {
throw new AccessDeniedException(
"Model is not allowed for this data level"
);
}
}
能力校验应发生在请求发出之前,这样错误能转化为明确的应用层反馈,而不是等待供应商返回难以理解的 400 错误。
11. Qwen OpenAI-compatible 配置示意
阿里云百炼提供 OpenAI 兼容调用方式。公共云北京地域常见 Base URL 形态如下:
yaml
QWEN_BASE_URL: https://dashscope.aliyuncs.com/compatible-mode/v1
QWEN_CHAT_MODEL: qwen-plus
QWEN_API_KEY: ${DASHSCOPE_API_KEY}
子业务空间或其他地域可能需要使用包含 Workspace ID 的地域端点,并使用对应空间的 API Key。应以账户所在地域和 Model Studio 控制台提供的信息为准。
12. 本地模型准备
安装并启动 Ollama 后:
bash
ollama pull qwen2.5
ollama run qwen2.5
Ollama 默认服务地址通常为:
text
http://localhost:11434
本地模型是否能胜任复杂中文问答、长上下文和工具调用,需要用自己的任务集评测,不能仅凭模型能够启动就认为能力满足业务要求。
五、常见问题与实践建议
1. 同一个请求参数在不同模型上报错
部分模型不支持 temperature、top_p、response_format 或特定工具参数。建议将参数分成两层:
- 通用参数:应用层可安全依赖的子集;
- Provider 扩展参数:仅在特定模型能力声明通过后传入。
不要为了兼容所有端点而把最大公约数无限缩小,也不要把某家模型的私有参数假装成通用参数。
2. OpenAI-compatible 端点返回 404
检查:
- Base URL 是否包含正确路径前缀;
- Spring AI 是否会自行追加 API 路径;
- 地域、Workspace ID 是否正确;
- 模型名称是否属于该区域;
- 是否错误地重复拼接
/v1; - 请求使用的接口是否受兼容服务支持。
不要通过不断尝试任意 URL 来排错。使用服务商官方文档给出的完整端点。
3. 本地模型响应慢或启动失败
本地模型受硬件和模型大小影响明显。排查:
- 内存和显存是否足够;
- 模型是否已经下载;
- 上下文长度是否过大;
- Ollama 服务是否可从应用容器访问;
- 是否有其他进程占用 GPU;
- 模型启动时间是否超过应用超时。
本地部署减少了外部数据传输,但并不自动代表成本更低或性能更好。
4. 降级模型的输出质量变化
不同模型之间不仅措辞不同,结构化输出、工具调用和拒答策略也可能不同。降级前应:
- 确认目标模型支持当前能力;
- 使用同一评估集测试输出;
- 记录最终命中的模型 Profile;
- 将低置信或高风险结果转人工;
- 对关键任务避免静默降级。
5. 流式输出中途失败
如果已经向客户端发出部分文本,再切换到另一模型可能导致输出重复或前后矛盾。建议:
- 在首个 Token 前执行连接健康检查;
- 首 Token 前失败才允许切换;
- 首 Token 后以错误事件结束流;
- 保存已经发出的部分内容和最终状态;
- 允许用户明确选择"重新生成"。
6. Embedding 模型更换导致召回异常
Embedding 模型切换后,需要检查:
- 向量维度;
- Tokenization 和输入限制;
- 归一化行为;
- 模型版本;
- 现有向量索引是否需要重算。
建议建立 embedding_profile 和 index_generation,让每个向量集合与一个确定的 Embedding 配置绑定。
7. 多个 ChatModel Bean 注入冲突
当 Spring Context 中有多个 ChatModel 时,自动配置的 Builder 可能无法判断应使用哪个模型。可采用:
@Qualifier明确指定;- 标记一个
@Primary作为默认模型; - 为每个 Provider 显式创建 ChatClient;
- 使用 Spring AI 提供的
ChatClientBuilderConfigurer保留观测和定制。
不要让业务服务通过 ApplicationContext.getBean(String) 随意查找供应商 Bean,这会让依赖关系难以测试。
8. API Key 泄露和越权调用
生产环境中:
- Key 存在 Secret Manager;
- 配置日志对密钥脱敏;
- 不把 Key 返回给前端;
- 按服务和租户划分凭据;
- 设置供应商侧预算和限流;
- 定期轮换并审计;
- 限制模型可访问的数据级别。
六、进阶思考
1. Router 应按策略选择,不应由模型自由挑供应商
可以把路由策略写成明确规则:
text
if dataLevel >= CONFIDENTIAL:
use private model
else if task == TOOL_AGENT:
choose profile with toolCalling = true
else if budget is low:
choose low-cost profile
else:
choose default profile
路由决策需要可解释、可审计。不要让 Agent 自己传入任意 Provider URL,也不要将生产凭据暴露给模型。
2. Retry、Timeout、Circuit Breaker 分层
模型调用要设置:
- 连接超时;
- 响应超时;
- 最大重试次数;
- 指数退避和抖动;
- 并发限制;
- 熔断器;
- 全局 Token 和费用预算。
重试次数不宜过高。多个层级同时重试时会放大流量,例如 HTTP 客户端重试 3 次、服务层重试 3 次、队列又重试 5 次,最终可能产生数十次模型请求。
3. Embedding 索引版本化
建议将索引配置作为不可变版本:
text
knowledge-base-42
├─ generation-001: embedding-model-A, 1024 dims
└─ generation-002: embedding-model-B, 1536 dims
迁移流程:
text
创建新 generation
↓
离线重算向量
↓
跑召回评估
↓
影子流量对比
↓
切换 active generation
↓
观察后清理旧索引
不要在同一向量列中混存不同模型生成的向量。
4. 统一错误模型
不同 Provider 的错误码要映射成内部错误类型:
text
AUTHENTICATION_FAILED
RATE_LIMITED
MODEL_UNAVAILABLE
REQUEST_INVALID
CONTEXT_TOO_LARGE
CONTENT_REJECTED
UPSTREAM_TIMEOUT
UNKNOWN_PROVIDER_ERROR
错误对象还可以附带:
- 是否可重试;
- 建议等待时间;
- Provider Request ID;
- 对用户可展示的消息;
- 内部诊断信息。
不要把供应商原始错误正文原样返回给客户端,因为其中可能包含内部端点、账户信息或请求细节。
5. 模型健康检查和灰度
模型健康状态可通过以下方式维护:
- 定时轻量探测;
- 实际请求成功率;
- 首 Token 延迟;
- 429 和 5xx 比例;
- 输出解析成功率;
- 工具调用成功率。
灰度时可以按租户、用户或请求比例逐步切换,并保留快速回滚开关。
6. 调用可观测性
每次模型调用建议记录:
text
request_id
tenant_id
logical_model_id
provider
model_name
task_type
input_tokens
output_tokens
first_token_latency
total_latency
retry_count
fallback_from
status
provider_request_id
日志默认不记录完整敏感 Prompt。需要调试时,通过受控开关、脱敏和短期留存机制采集样本。
7. 为什么还要保留供应商特定适配
统一接口有助于迁移和路由,但过度抽象会抹去 Provider 的独特能力,例如:
- 特定 JSON Schema 模式;
- 推理等级;
- 多模态输入;
- 批量推理;
- 缓存控制;
- Provider 特有的工具调用参数。
推荐采用"统一主干 + 能力扩展":
text
统一 Chat Command
├─ 通用消息和超时
├─ 通用流式接口
└─ Provider Capability Extension
├─ Qwen 特定参数
├─ OpenAI 特定参数
└─ Ollama 本地选项
这样既能让大部分业务代码保持稳定,也能在确有需要时使用特定模型能力。
结论
兼容 OpenAI、通义千问和本地模型的关键,不是把所有模型伪装成完全一样,而是建立清晰的适配边界和可解释的路由策略。
本文的核心做法是:
- 使用 Spring AI
ChatModel/ChatClient作为调用适配基础; - 在业务层增加逻辑模型 ID 和 ModelProfile;
- 将供应商、模型、任务和能力分开管理;
- 使用能力校验避免不支持的请求发往模型;
- OpenAI-compatible 接口只复用协议,不假设功能完全一致;
- 用独立策略控制超时、重试、降级和成本;
- 将 Chat Model 与 Embedding Model 分开治理;
- 通过审计和指标记录每次真实命中的模型。
后续开发知识库问答时,应为生成模型和向量化模型分别确定 Profile、版本和数据策略;实现模型容灾时,再用真实评估结果决定哪些任务允许切换供应商。