大模型服务抽象:兼容 OpenAI、通义千问与本地模型

摘要

生产级 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、版本和数据策略;实现模型容灾时,再用真实评估结果决定哪些任务允许切换供应商。

参考资料

相关推荐
鶴哥只手遮天1 小时前
Spring Framework 7.0.9 深度剖析-06-Bean 生命周期与依赖注入原理
java·后端·开源
唐钰小宝B1 小时前
深度解读 JDK 8、JDK 11、JDK 17 和 JDK 21 的区别
java·开发语言·windows
golang学习记1 小时前
最新版VS Code开发Spring Boot项目保姆级教程
java·spring boot·vscode·后端
高频因子挖掘机2 小时前
别急着搭数据平台:量化策略开发如何用最小闭环验证想法
后端·github·api
叶总没有会2 小时前
RabbitMQ基础--java客户端
java·rabbitmq·java-rabbitmq
CCblandy2 小时前
一致性哈希到底省了多少搬迁?本机实测 3 轮:取模 6→7 搬 85.8%,V=128 只搬 14.3%,虚拟节点太少会被倾斜骗
后端·python·云原生·性能优化·架构
code_slave(码畜)2 小时前
微服务架构落地:OpenFeign 服务远程调用 + 线程池隔离架构实战 —— 超时控制、重试机制、线程池隔离,防雪崩
spring boot·spring cloud·微服务·云原生·架构·openfeign
小天源2 小时前
智叟云集定时采集与消息推送架构——实现可恢复的订阅通知流程
架构·sass·权限管理·多租户·服务商 api·ai采集
小陈不好吃2 小时前
Java 泛型与类型擦除:从 PECS 原则到 TypeToken 绕过
java·开发语言·windows·pecs 原则·typetoken绕过