大模型还在硬编码?Spring AI 实现“动态热切换”全攻略(上)

摘要:在企业级 AI 开发中,单一模型往往难以适应多变的业务需求。本文将介绍如何基于 Spring AI 框架,构建一个支持动态切换底层模型(如 OpenAI、DeepSeek)、可配置参数(如温度、TopK)以及自定义角色人设的通用 AI 服务架构。我们将从数据库设计出发,深入讲解策略模式与工厂模式在 AI 中台建设中的实际应用。

0. 技术栈说明

本文基于以下核心技术栈:

  • JDK: 17+
  • Spring Boot: 3.3.x
  • Spring AI: 1.1.x

0.1 引入 Maven 依赖

在使用 Spring AI 之前,我们需要在 pom.xml 中引入 spring-ai-bom 来管理版本,并添加相应的 Starter 依赖。

xml 复制代码
<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.ai</groupId>
            <artifactId>spring-ai-bom</artifactId>
            <version>1.1.2</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>
​
<dependencies>
    <!-- WebFlux 支持流式响应 (SSE) -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-webflux</artifactId>
    </dependency>
​
    <!-- OpenAI 模型支持 (Spring AI 核心) -->
    <dependency>
        <groupId>org.springframework.ai</groupId>
        <artifactId>spring-ai-starter-model-openai</artifactId>
    </dependency>
​
    <!-- DeepSeek 模型支持 -->
    <dependency>
        <groupId>org.springframework.ai</groupId>
        <artifactId>spring-ai-deepseek</artifactId>
    </dependency>
​
    <!-- 智谱 AI 模型支持 -->
    <dependency>
        <groupId>org.springframework.ai</groupId>
        <artifactId>spring-ai-zhipuai</artifactId>
    </dependency>
​
    <!-- RAG 向量检索支持 -->
    <dependency>
         <groupId>org.springframework.ai</groupId>
         <artifactId>spring-ai-advisors-vector-store</artifactId>
    </dependency>
</dependencies>

1. 核心痛点与设计目标

在 Spring AI 的默认配置中,ChatModel 通常是单例的。但在实际业务场景中,我们经常面临以下挑战:

  1. 动态切换:管理员在后台配置不同的 API Key 和 Endpoint,系统需即时生效,无需重启。
  2. 个性化人设:不同的业务场景需要不同的"角色(System Prompt)",例如"客服助手"、"代码专家"等。
  3. 参数热配Temperature (温度)Top-P 等推理参数需支持数据库化配置,以便根据效果灵活调整。

为此,我们采用了 "数据库配置表 + 动态工厂模式" 的架构方案。

2. 数据库设计

为了实现上述目标,我们需要两张核心表:一张存储模型连接信息 ,另一张存储角色与 Prompt 配置

2.1 模型配置表 (ai_model_config)

这张表用于存储 LLM 的连接参数和推理参数。

关键字段解析:

  • api_endpoint : 标准的 OpenAI 接口地址是 api.openai.com,但国内许多大模型(如 DeepSeek, Moonshot)都兼容 OpenAI 协议。我们只需要将 Endpoint 修改为厂商提供的地址,即可复用同一套代码。
  • temperature : 值通常在 0 到 1(或 2)之间。数值越低 ,模型越保守、严谨,适合客服问答;数值越高,模型越发散、有创造力,适合写诗或头脑风暴。
  • top_p: 核采样概率。它与 Temperature 类似,也是控制生成多样性的参数。通常建议这两个参数只调整其中一个。
sql 复制代码
CREATE TABLE `ai_model_config` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
  `config_name` varchar(100) NOT NULL COMMENT '配置名称',
  `api_provider` varchar(50) NOT NULL COMMENT 'API 提供商 (核心字段,决定调用哪个策略,如 openai, deepseek)',
  `model_id` varchar(100) NOT NULL COMMENT '模型标识 (如 gpt-4o, deepseek-chat)',
  `is_enabled` tinyint(1) DEFAULT '1' COMMENT '是否启用(1-启用 0-禁止)',
  `is_default` tinyint(1) DEFAULT '0' COMMENT '是否默认(1-是 0-否)',
  `role_id` bigint DEFAULT NULL COMMENT '绑定角色与提示词ID(可为空,用于设置 System Prompt)',
  `api_key` varchar(255) DEFAULT NULL COMMENT 'API Key (敏感信息)',
  `api_endpoint` varchar(255) DEFAULT NULL COMMENT 'API 端点 (用于适配兼容 OpenAI 协议的国内模型)',
  `temperature` double DEFAULT NULL COMMENT '温度 (控制回答的随机性)',
  `top_p` double DEFAULT NULL COMMENT 'Top P',
  `max_tokens` int DEFAULT NULL COMMENT '最大 Token 数',
  PRIMARY KEY (`id`)
) COMMENT='AI 模型配置表';

2.2 角色与 RAG 配置表 (ai_role_prompt)

这张表定义了 AI 的"人设"以及 RAG(检索增强生成)的相关规则。

关键字段解析:

  • system_prompt: 系统提示词。这是发给 AI 的第一句话,规定了 AI 的身份和行为准则(例如:"你是一个资深的 Java 架构师,只回答技术问题")。
  • similarity_threshold: 相似度阈值(0.0 - 1.0)。在进行向量检索时,如果文档与问题的相似度低于这个值,就会被过滤掉。这能有效防止 AI 被不相关的文档误导。
sql 复制代码
CREATE TABLE `ai_role_prompt` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
  `role_name` varchar(100) NOT NULL COMMENT '角色名称',
  `description` varchar(255) DEFAULT NULL COMMENT '角色描述',
  `is_enabled` tinyint(1) DEFAULT '1' COMMENT '是否启用',
  `system_prompt` text COMMENT '系统提示词(System Prompt)',\
  `is_rag_enabled` tinyint(1) DEFAULT '0' COMMENT '是否启用 RAG',
  `rag_template` text COMMENT 'RAG 提示词模板',
  `top_k` int DEFAULT '5' COMMENT '检索文档数量 topK',
  `similarity_threshold` double DEFAULT '0.7' COMMENT '相似度阈值(0-1)',
  `custom_filter` varchar(255) DEFAULT NULL COMMENT '自定义过滤表达式',
  PRIMARY KEY (`id`)
) COMMENT='AI 角色与提示词配置表';

3. 动态工厂核心实现拆解

DynamicChatClientFactory 是本系统的核心。我们通过分步解析,来看看它是如何工作的。

3.1 步骤一:加载默认配置

首先,我们需要从数据库中查询当前生效的配置。这里使用了 MyBatis-Plus 的 LambdaQuery,只查询 is_default=1is_enabled=1 的记录。

arduino 复制代码
// Service 层方法调用
AiModelConfig config = aiModelConfigService.getDefaultConfig();
if (config == null) {
    throw new BizException(ResultCode.NOT_FOUND, "未找到默认启用的模型配置,请先在模型配置中设置默认模型");
}

3.2 步骤二:策略路由与模型构建

不同的厂商(Provider)初始化方式不同。我们使用策略模式 ,根据 api_provider 字段(如 "openai", "deepseek")找到对应的策略实现类,构建出底层的 ChatModel

ChatModel vs ChatClient:

  • ChatModel: 是 Spring AI 的底层接口,负责与具体的大模型 API 进行 HTTP 通信(类似 JDBC Driver)。
  • ChatClient: 是 Spring AI 的高层接口,提供了 fluent API(链式调用),负责处理 Prompt 封装、Advisor 拦截等逻辑(类似 JdbcTemplate)。
ini 复制代码
// 获取策略并构建底层的 ChatModel
ModelChatStrategy strategy = modelChatStrategyFactory.getStrategy(config.getApiProvider());
ChatModel chatModel = strategy.buildChatModel(config);

3.3 步骤三:System Prompt 组装

这里处理 AI 的"人设"。我们在代码中内置了一个默认的通用人设(植入品牌信息),同时支持从 ai_role_prompt 表中加载动态配置来覆盖默认值。

ini 复制代码
// 设置默认的 System Prompt(内置兜底)
String systemPrompt = """
        你是lanjii(岚迹)的客服助手,在用户第一次问你的时候,你要表明自己的身份,并且要说明 lanjii 是什么。
        针对用户的问题总是能以友好愉悦的方式去进行交流,希望带给客户很好的体验。
        你总是会用中文去回答问题。
        """;
​
// 如果配置了角色,覆盖默认 System Prompt
if (config.getRoleId() != null) {
    AiRolePrompt rolePrompt = aiRolePromptService.getById(config.getRoleId());
    if (rolePrompt != null && rolePrompt.getSystemPrompt() != null && !rolePrompt.getSystemPrompt().isEmpty()) {
        systemPrompt = rolePrompt.getSystemPrompt();
    }
}

3.4 步骤四:注入增强器 (Advisors)

Advisor 是 Spring AI 1.x 引入的重要概念,类似于 Spring AOP 的切面。它允许我们在不修改核心业务逻辑的情况下,动态地为 ChatClient 增加能力。

  • MessageChatMemoryAdvisor: 负责自动管理对话历史。它会在发请求前把历史记录拼接到 Prompt 中,在收到响应后把新对话存入 Memory。
  • VectorStoreChatMemoryAdvisor: 负责 RAG 检索。
scss 复制代码
// 组装 ChatClient,注入记忆和向量检索能力
return ChatClient.builder(chatModel)
        .defaultSystem(systemPrompt)
        .defaultAdvisors(
                MessageChatMemoryAdvisor.builder(chatMemory).build(),
                VectorStoreChatMemoryAdvisor.builder(vectorStore).build()
        )
        .build();

4. 工厂类完整代码清单

将上述步骤整合,我们得到了完整的工厂类代码:

ini 复制代码
package com.lanjii.biz.admin.ai.service;
​
import com.lanjii.biz.admin.ai.model.entity.AiModelConfig;
import com.lanjii.biz.admin.ai.model.entity.AiRolePrompt;
import com.lanjii.biz.admin.ai.strategy.ModelChatStrategy;
import com.lanjii.biz.admin.ai.strategy.ModelChatStrategyFactory;
import com.lanjii.common.exception.BizException;
import com.lanjii.core.resp.ResultCode;
import lombok.RequiredArgsConstructor;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor;
import org.springframework.ai.chat.client.advisor.vectorstore.VectorStoreChatMemoryAdvisor;
import org.springframework.ai.chat.memory.ChatMemory;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.stereotype.Component;
​
/**
 * 根据数据库中的模型配置动态构建 {@link ChatClient}
 *
 * @author lanjii
 */
@Component
@RequiredArgsConstructor
public class DynamicChatClientFactory {
​
    private final AiModelConfigService aiModelConfigService;
    private final AiRolePromptService aiRolePromptService;
    private final ModelChatStrategyFactory modelChatStrategyFactory;
    private final ChatMemory chatMemory;
    private final VectorStore vectorStore;
​
    /**
     * 构建默认模型
     */
    public ChatClient buildDefaultClient() {
        // 加载默认配置
        AiModelConfig config = aiModelConfigService.getDefaultConfig();
        if (config == null) {
            throw new BizException(ResultCode.NOT_FOUND, "未找到默认启用的模型配置,请先在模型配置中设置默认模型");
        }
        
        // 策略路由构建模型
        ModelChatStrategy strategy = modelChatStrategyFactory.getStrategy(config.getApiProvider());
        ChatModel chatModel = strategy.buildChatModel(config);
​
        // 初始化默认提示词
        String systemPrompt = """
                你是lanjii(岚迹)的客服助手,在用户第一次问你的时候,你要表明自己的身份,并且要说明 lanjii 是什么。
                针对用户的问题总是能以友好愉悦的方式去进行交流,希望带给客户很好的体验。
                你总是会用中文去回答问题。
                """;
​
        // 动态覆盖提示词
        if (config.getRoleId() != null) {
            AiRolePrompt rolePrompt = aiRolePromptService.getById(config.getRoleId());
            if (rolePrompt != null && rolePrompt.getSystemPrompt() != null && !rolePrompt.getSystemPrompt().isEmpty()) {
                systemPrompt = rolePrompt.getSystemPrompt();
            }
        }
​
        // 构建增强版客户端
        return ChatClient.builder(chatModel)
                .defaultSystem(systemPrompt)
                .defaultAdvisors(
                        MessageChatMemoryAdvisor.builder(chatMemory).build(),
                        VectorStoreChatMemoryAdvisor.builder(vectorStore).build()
                )
                .build();
    }
}

5. 架构思考:性能与灵活性的权衡

在上述实现中,我们每次调用 buildDefaultClient() 时,都会执行一次"查库 + 策略构建 + Client 组装"的全流程。

  • 优势 :实现了 100% 的热更新。管理员在后台修改了 API Key 或 Temperature,下一个请求立即生效,没有任何延迟。
  • 劣势 :在高并发场景下,频繁创建 ChatModel 对象可能会带来一定的 GC 压力。

进阶优化方向 :如果您的系统 QPS 很高,可以引入 本地缓存 (如 Caffeine)

  1. config.getId() 作为缓存 Key。
  2. 将构建好的 ChatClient 作为缓存 Value。
  3. 设置较短的过期时间(如 1 分钟),或者利用 Spring Cache 的 @CacheEvict 在配置更新时主动清除缓存。

这样既保留了动态性,又兼顾了性能。

6. 项目资源

  • Gitee 源码仓库gitee.com/leven2018/l...这里提供了项目的完整源代码、SQL 脚本以及后续的功能更新。
  • 在线体验地址http://106.54.167.194/admin/login无需本地部署,您可以直接访问此系统体验文中的动态模型配置与流式对话功能。

7. 小结

在本文中,我们完成了最底层的"基础设施"建设。通过 ai_model_configai_role_prompt 两张表,配合 DynamicChatClientFactory,我们实现了一个灵活的 AI 核心。

在下篇中,我们将进入业务层,深入拆解如何在 Service 中利用这个工厂,实现手动控制的 RAG 检索与 SSE 流式响应。

相关推荐
CC羊39127 分钟前
生图绘图旗舰模型评测:Nano banana Pro、GPT Image 1.5与Seedream 4.5在架构、画质与一致性上的核心差异与选型建议
aigc·openai
耀耀_很无聊9 分钟前
16_大文件上传方案:分片上传、断点续传与秒传
java·spring boot·后端
只想提前退休10 分钟前
个人心得-搭建GitLab社区版服务器
后端
+VX:Fegn089514 分钟前
计算机毕业设计|基于springboot + vue旅游网系统(源码+数据库+文档)
数据库·vue.js·spring boot·后端·课程设计
洛卡卡了16 分钟前
2025:从用 AI 到学 AI,我最轻松也最忙碌的一年
人工智能·后端·ai编程
VX:Fegn089517 分钟前
计算机毕业设计|基于springboot + vue小区居民物业管理系统(源码+数据库+文档)
数据库·vue.js·spring boot·后端·课程设计
xuejianxinokok17 分钟前
rust trait 相比于传统的 oop 有哪些优点?
后端·rust
superman超哥22 分钟前
Rust Rc与Arc的引用计数机制:共享所有权的两种实现
开发语言·后端·rust·编程语言·rust rc与arc·引用计数机制·共享所有权
ghostwritten25 分钟前
go.mod 与go.sum有什么区别?
开发语言·后端·golang
hhzz27 分钟前
Springboot项目中使用POI操作Excel(详细教程系列1/3)
spring boot·后端·excel·poi·easypoi