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

文章来源声明: 原文作者:弈栈录; 来源站点:掘金; 原文链接:https://juejin.cn/post/7697221489348182067; 本文基于上述来源整理/加工,觅优补充点评,仅供技术学习交流。版权归原作者所有。
觅优短评

价值在于把“换模型”从代码改造降为配置变更;能力矩阵与流式降级边界尤其务实。适合多供应商、多任务并存的 Spring AI 团队落地前对照。

摘要 --

生产级 AI 后端通常不会永远只使用一个模型。不同任务对质量、延迟、价格、数据驻留和可用性的要求不同:复杂推理可能调用云端模型,普通分类可以使用低成本模型,敏感数据可以路由到本地模型;当供应商故障时,还需要有受控的降级方案。

如果业务代码直接依赖某一家模型的 SDK、模型名称和响应结构,切换供应商就会扩散到 Controller、业务服务和测试代码中。本文以 Spring AI 为基础,设计一层模型服务抽象,实现对 OpenAI、通义千问 OpenAI 兼容接口和 Ollama 本地模型的统一调用,并讨论:

  • Spring AI 的 ChatModel 抽象能解决什么、不能解决什么;
  • 如何将“供应商、模型、任务、能力”分开建模;
  • 如何配置多个云端和本地模型;
  • 如何实现模型路由、超时、重试和降级;
  • 为什么 Embedding 模型必须独立治理;
  • 如何处理流式调用、工具调用、模型能力差异和可观测性。

本文示例以 Spring AI 2.0.1 API 为参考,通义千问兼容地址、模型名称和可用能力会随地域、业务空间和产品版本变化,接入前应以当前账号的官方控制台与文档为准。

一、背景与问题

1. 为什么不能在业务代码里直接写供应商调用

早期实现可能直接在 Service 中调用某个 SDK:

var response = openAiClient.chat().completions().create(request);

这会让业务代码逐渐依赖:

  • 某个供应商的请求对象;
  • 特定模型名称;
  • 特殊参数和错误码;
  • 特定的流式事件结构;
  • 供应商专属的工具调用格式;
  • 特定的认证和重试策略。

一旦切换模型,业务层也要跟着改。更重要的是,不同任务可能适合不同模型,但代码里没有统一的路由入口。

2. “兼容 OpenAI”不代表能力完全相同

通义千问、Ollama、vLLM 和许多模型网关都提供 OpenAI 兼容接口。兼容通常意味着部分 URL、请求和响应字段相似,并不意味着所有能力完全一致。

差异可能包括:

  • 支持的聊天参数不同;
  • 工具调用能力和 JSON Schema 支持不同;
  • 流式结束事件格式不同;
  • Token 用量字段不完整;
  • 多模态输入限制不同;
  • 错误码、限流和重试语义不同;
  • Embedding API 与 Chat API 的兼容程度不同。

因此,服务抽象应隐藏供应商协议,但不能假装所有模型能力完全相同。

3. 需要区分的几个维度

模型选择不应只是一个字符串:

Provider:谁提供服务
Model:调用哪个模型
Task:当前在做什么
Capability:模型支持什么
Policy:当前请求允许用什么

例如:

任务路由策略
简单标题生成低成本云模型
企业知识库回答支持长上下文且稳定的模型
高敏感内部数据通过内部网关或本地模型
工具调用 Agent工具协议验证通过的模型
离线批处理价格优先、吞吐优先

4. 本文的设计目标

模型服务层需要做到:

  • 业务服务依赖稳定接口,不依赖供应商 SDK;
  • 管理员可配置多个模型端点;
  • 每次调用都能记录模型、任务、延迟和用量;
  • 可按任务选择模型;
  • 仅在安全条件满足时执行重试和降级;
  • 能力差异显式声明,不支持的请求提前拒绝;
  • Embedding 模型和 Chat 模型分别配置、分别版本化。

二、核心概念

1. Spring AI 的 ChatModel

Spring AI 提供统一的 ChatModel 接口,主要用于发送 Prompt 并返回 ChatResponse;它也提供流式调用能力。不同供应商的模型适配器实现这一接口,使上层可以用相近的方式调用不同模型。

这层抽象可以屏蔽:

  • HTTP 请求细节;
  • 常见消息角色;
  • 基础同步和流式调用;
  • 通用响应结构;
  • 一部分通用模型参数。

但它不会自动解决:

  • 哪个任务应该调用哪个模型;
  • 不同模型能力是否一致;
  • 凭据和租户权限;
  • 费用预算与限流;
  • 跨供应商的错误映射;
  • 安全降级和审计。

这些属于应用自己的模型服务层。

2. Provider、ModelProfile 与 ModelClient

建议将三者分开:

对象示例职责
ProviderOpenAI、DashScope、Ollama端点、鉴权和协议适配
ModelProfile`qwen-plus-prod`某个端点下的具体模型和默认参数
ModelClientSpring AI `ChatModel` / `ChatClient`执行调用

业务调用只引用逻辑模型 ID,例如 knowledge-answer,由服务端配置映射到真实 Profile。

3. 能力描述

每个 ModelProfile 应描述能力:

supportsStreaming
supportsToolCalling
supportsJsonSchema
supportsVision
maxContextTokens
maxOutputTokens
inputModalities
outputModalities

路由前先检查任务需求与模型能力。不能把一个不支持工具调用的本地模型路由到依赖 Function Calling 的 Agent 流程。

4. Chat 模型与 Embedding 模型要分开

Chat 模型生成文本,Embedding 模型把文本映射成向量。它们的接口、维度、模型版本和变更成本都不同。

项目Chat ModelEmbedding Model
输入对话消息一段或多段文本
输出文本、工具调用、结构化结果浮点向量
常见评估质量、延迟、成本召回效果、维度和成本
更换影响通常可切换请求路由通常需要重建向量索引
版本策略按任务路由与索引版本绑定

不能只因为两个 Embedding API 都“兼容 OpenAI”就混用向量。

5. 多供应商模型组合

一个应用可以同时使用:

OpenAIChatModel
  └─ 官方 OpenAI 或兼容端点

QwenChatModel
  └─ OpenAI 兼容模式连接阿里云百炼

OllamaChatModel
  └─ 本机或内网模型服务

Spring AI 允许应用中存在多个模型实现,但要显式解决 Spring Bean 注入歧义,并为每个客户端配置正确的观测和定制逻辑。

三、工作原理

1. 模型调用的分层结构

Controller
    ↓
业务服务:知道任务目标,不知道供应商
    ↓
ModelService:鉴权、路由、策略和观测
    ↓
ModelRouter:选择逻辑模型对应的 Profile
    ↓
Provider Adapter:Spring AI ChatClient / ChatModel
    ↓
OpenAI / DashScope / Ollama

2. 一次调用流程

1. 根据当前用户和任务构造 ModelRequest
2. 进行租户、配额和数据分级检查
3. 解析逻辑模型 ID
4. 检查模型能力是否满足任务要求
5. 选择 Provider Adapter
6. 执行调用并记录 request_id
7. 标准化响应和错误
8. 记录 Token、延迟、模型版本和成本
9. 根据业务契约返回结果

3. 模型路由

路由可以从简单映射开始:

knowledge-answer -> qwen-plus-prod
title-generation -> openai-mini-prod
private-summary -> ollama-private

之后再根据数据级别、模型健康度、成本和能力增加策略:

数据级别
    ↓
任务能力要求
    ↓
租户允许的模型
    ↓
模型健康度和配额
    ↓
成本与延迟策略
    ↓
最终模型 Profile

不要允许客户端直接传入任意 Base URL 或任意模型名,否则可能造成 SSRF、越权调用和费用失控。客户端最多选择管理员预先开放的逻辑模型 ID。

4. 重试与降级

重试和降级不能对所有错误都执行:

错误建议
连接超时、服务不可用可对幂等请求有限重试
429 限流按 `Retry-After` 或退避策略
401 / 403不重试,检查凭据和权限
400 参数不兼容不重试,修正请求
内容安全拒绝不应绕过拒绝转发到其他模型
响应解析失败记录原始关联 ID,按策略决定是否重试

流式响应开始返回内容后,不能简单切换到另一个模型并把两边输出拼起来。那会产生重复、语义断裂和计费不一致。流式降级通常只允许发生在首个内容片段产生之前。

5. 模型能力矩阵

路由服务可以维护能力矩阵:

ProfileStreamingTool CallingJSON SchemaVision数据级别
`openai-main`是是是依模型而定内部及公开
`qwen-prod`是依模型和接口依模型和接口依模型按租户策略
`ollama-local`是依模型而定依模型而定依模型本地受控

能力不能只从 Provider 名称推断。上线前应对“实际模型 + 实际端点 + 实际参数”做集成测试。

四、实战示例

下面实现一个简化的模型网关:业务侧传入逻辑模型 ID 和 Prompt,服务层路由到配置好的 Spring AI ChatClient。

1. 依赖配置

使用 Spring AI BOM 管理版本,添加 OpenAI 和 Ollama 模型适配器:

<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. 配置端点与凭据

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

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. 定义统一调用契约

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 客户端:

@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 本地模型

@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 的主机拉取模型:

ollama pull qwen2.5
ollama list

如果 Spring Boot 应用运行在 Docker 容器中,localhost:11434 指向应用容器自身,不一定能访问宿主机的 Ollama。应使用正确的容器网络服务名或宿主机地址,并限制 Ollama 服务的网络暴露范围。

7. 为模型创建 ChatClient

@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. 实现模型路由

@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. 实现统一同步与流式调用

@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. 能力校验

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 形态如下:

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 后:

ollama pull qwen2.5
ollama run qwen2.5

Ollama 默认服务地址通常为:

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 应按策略选择,不应由模型自由挑供应商

可以把路由策略写成明确规则:

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 索引版本化

建议将索引配置作为不可变版本:

knowledge-base-42
  ├─ generation-001: embedding-model-A, 1024 dims
  └─ generation-002: embedding-model-B, 1536 dims

迁移流程:

创建新 generation
    ↓
离线重算向量
    ↓
跑召回评估
    ↓
影子流量对比
    ↓
切换 active generation
    ↓
观察后清理旧索引

不要在同一向量列中混存不同模型生成的向量。

4. 统一错误模型

不同 Provider 的错误码要映射成内部错误类型:

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. 调用可观测性

每次模型调用建议记录:

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 特有的工具调用参数。

推荐采用“统一主干 + 能力扩展”:

统一 Chat Command
  ├─ 通用消息和超时
  ├─ 通用流式接口
  └─ Provider Capability Extension
       ├─ Qwen 特定参数
       ├─ OpenAI 特定参数
       └─ Ollama 本地选项

这样既能让大部分业务代码保持稳定,也能在确有需要时使用特定模型能力。

结论

兼容 OpenAI、通义千问和本地模型的关键,不是把所有模型伪装成完全一样,而是建立清晰的适配边界和可解释的路由策略。

本文的核心做法是:

  • 使用 Spring AI ChatModel / ChatClient 作为调用适配基础;
  • 在业务层增加逻辑模型 ID 和 ModelProfile;
  • 将供应商、模型、任务和能力分开管理;
  • 使用能力校验避免不支持的请求发往模型;
  • OpenAI-compatible 接口只复用协议,不假设功能完全一致;
  • 用独立策略控制超时、重试、降级和成本;
  • 将 Chat Model 与 Embedding Model 分开治理;
  • 通过审计和指标记录每次真实命中的模型。

后续开发知识库问答时,应为生成模型和向量化模型分别确定 Profile、版本和数据策略;实现模型容灾时,再用真实评估结果决定哪些任务允许切换供应商。

参考资料