Spring Boot 接入大模型:从配置到实战的完整指南

Spring Boot 接入大模型:从配置到实战的完整指南

我们的电商 SaaS 平台已经配置了 DeepSeek、智谱、豆包、通义千问等 10+ AI 平台的 API Key,但 AI 模块尚未接入。本文从零开始,记录如何在 Spring Boot 项目中搭建 AI 能力------从依赖引入、多模型路由、对话功能到知识库 RAG 的完整流程。


一、现状与选型

1.1 我们已有的配置

项目 application.yaml 中已经配好了各平台的密钥:

平台 模型 用途
DeepSeek deepseek-chat 通用对话、代码生成
字节豆包 doubao-1-5-lite-32k 长文本处理
腾讯混元 hunyuan-turbo 中文理解
硅基流动 DeepSeek-R1-Distill-Qwen-7B 推理任务
讯飞星火 generalv3.5 教育场景
通义千问 dashscope 多模态
Midjourney mj-relax 图片生成

1.2 技术选型

选型理由

  • Spring AI:Spring 官方出品,统一了 ChatCompletion、ImageGeneration、VectorStore 的 API,后续换模型不需要改业务代码
  • 多模型路由:不同场景用不同模型,比如对话用 DeepSeek,图片用 Midjourney,长文本用豆包

二、第一步:引入依赖

2.1 BOM 版本管理

ymh-dependencies/pom.xml 中添加:

xml 复制代码
<properties>
    <spring-ai.version>1.0.0-M6</spring-ai.version>
</properties>

<dependencyManagement>
    <dependencies>
        <!-- Spring AI BOM -->
        <dependency>
            <groupId>org.springframework.ai</groupId>
            <artifactId>spring-ai-bom</artifactId>
            <version>${spring-ai.version}</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

2.2 Server 模块引入

ymh-server/pom.xml 中添加 AI 相关依赖:

xml 复制代码
<!-- Spring AI 核心 -->
<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-openai-spring-boot-starter</artifactId>
</dependency>

<!-- 通义千问适配 -->
<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-dashscope-spring-boot-starter</artifactId>
</dependency>

<!-- 智谱适配 -->
<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-zhipuai-spring-boot-starter</artifactId>
</dependency>

<!-- Redis 向量存储(知识库用) -->
<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-vector-store-redis-spring-boot-starter</artifactId>
</dependency>

<!-- Spring AI 测试 -->
<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-spring-boot-testcontainers</artifactId>
    <scope>test</scope>
</dependency>

三、第二步:多模型配置

3.1 application.yaml 配置

yaml 复制代码
spring:
  ai:
    # ========== 通用配置 ==========
    openai:
      api-key: ${AI_OPENAI_API_KEY:sk-aN6nWn3fILjrgLFT0fC4Aa60B72e4253826c77B29dC94f17}
      base-url: https://api.gptsapi.net
      chat:
        options:
          model: gpt-4o-mini
          temperature: 0.7
          max-tokens: 2048

    # 通义千问
    dashscope:
      api-key: ${AI_DASHSCOPE_API_KEY:sk-71800982914041848008480000000000}
      chat:
        options:
          model: qwen-turbo
          temperature: 0.7

    # 智谱 AI
    zhipuai:
      api-key: ${AI_ZHIPUAI_API_KEY:32f84543e54eee31f8d56b2bd6020573.3vh9idLJZ2ZhxDEs}
      chat:
        options:
          model: glm-4-flash
          temperature: 0.7

    # 向量存储(Redis)
    vectorstore:
      redis:
        initialize-schema: true
        index: knowledge_index
        prefix: "knowledge_segment:"

3.2 自定义模型配置(DeepSeek/豆包等非标准 OpenAI 兼容接口)

DeepSeek、豆包等平台有自己的 SDK,不走 Spring AI 的标准适配。我们在项目原有的 yudao.ai 命名空间下封装:

yaml 复制代码
yudao:
  ai:
    deep-seek:
      enable: true
      api-key: ${AI_DEEPSEEK_API_KEY}
      model: deepseek-chat
      base-url: https://api.deepseek.com/v1
    doubao:
      enable: true
      api-key: ${AI_DOUBAO_API_KEY}
      model: doubao-1-5-lite-32k-250115
      base-url: https://ark.cn-beijing.volcesapis.com

四、第三步:实现模型路由

这是最关键的一环------不同场景自动选择最合适的模型

4.1 模型路由接口定义

less 复制代码
/**
 * AI 模型路由策略枚举
 */
@Getter
@AllArgsConstructor
public enum AiModelRoute {

    /** 通用对话 - 速度快、成本低 */
    CHAT_FAST("deepseek", "deepseek-chat"),

    /** 复杂推理 - R1 推理能力强 */
    CHAT_REASONING("siliconflow", "deepseek-ai/DeepSeek-R1-Distill-Qwen-7B"),

    /** 长文本 - 支持 32K token */
    LONG_TEXT("doubao", "doubao-1-5-lite-32k-250115"),

    /** 代码生成 - DeepSeek Coder 擅长 */
    CODE_GENERATION("deepseek", "deepseek-coder"),

    /** 中文理解 - 混元对中文优化好 */
    CHINESE_UNDERSTANDING("hunyuan", "hunyuan-turbo"),

    /** 图片生成 - Midjourney */
    IMAGE_GENERATION("midjourney", "mj-relax"),

    /** 默认 - 通用对话 */
    DEFAULT("openai", "gpt-4o-mini");

    private final String provider;
    private final String modelName;
}

4.2 智能路由服务

scss 复制代码
@Service
@Slf4j
public class AiModelRouter {

    @Autowired
    private ChatClient chatClientRegistry; // Spring AI 提供的 ChatClient 注册表

    /**
     * 根据任务类型自动选择模型并执行对话
     */
    public String chat(AiModelRoute route, String userMessage) {
        ChatModel chatModel = resolveChatModel(route);
        if (chatModel == null) {
            log.warn("未找到匹配的模型路由: {}", route);
            return "暂时无法处理,请稍后重试";
        }

        ChatResponse response = chatModel.call(userMessage);
        String result = response.getResult().getOutput().getContent();
        log.info("路由 {} 完成,token 消耗: {}", route, response.getMetadata().getUsage());
        return result;
    }

    /**
     * 批量对话(用于知识库问答)
     */
    public List<String> batchChat(AiModelRoute route, List<String> messages) {
        ChatModel chatModel = resolveChatModel(route);
        return messages.stream()
            .map(chatModel::call)
            .map(r -> r.getResult().getOutput().getContent())
            .collect(Collectors.toList());
    }

    private ChatModel resolveChatModel(AiModelRoute route) {
        // 从 Spring AI 的 ChatModel Bean 中按 provider 查找
        Map<String, ChatModel> chatModels = chatClientRegistry.getChatModels();
        return chatModels.getOrDefault(route.getProvider(), chatModels.get("default"));
    }
}

4.3 对话 Controller

less 复制代码
@RestController
@RequestMapping("/admin-api/ai/chat")
@Tag(name = "AI 对话管理")
public class AiChatController {

    @Autowired
    private AiModelRouter aiModelRouter;

    @PostMapping("/completion")
    public CommonResult<AiChatRespDTO> completion(@RequestBody @Valid AiChatReqDTO req) {
        // 1. 校验用户是否有权限调用 AI
        Long userId = SecurityFrameworkUtils.getLoginUserId();
        checkAiQuota(userId);

        // 2. 根据场景选择模型
        AiModelRoute route = resolveRoute(req.getScene());

        // 3. 构建 Prompt(带系统提示词)
        String prompt = buildPrompt(req);

        // 4. 调用 AI
        String reply = aiModelRouter.chat(route, prompt);

        // 5. 记录对话日志
        saveChatLog(userId, req, reply);

        // 6. 扣减配额
        deductAiQuota(userId);

        return CommonResult.success(new AiChatRespDTO(reply));
    }

    private AiModelRoute resolveRoute(String scene) {
        return switch (scene) {
            case "code" -> AiModelRoute.CODE_GENERATION;
            case "reasoning" -> AiModelRoute.CHAT_REASONING;
            case "long-text" -> AiModelRoute.LONG_TEXT;
            case "chinese" -> AiModelRoute.CHINESE_UNDERSTANDING;
            default -> AiModelRoute.CHAT_FAST;
        };
    }
}

五、第四步:图片生成功能

5.1 ImageGeneration 配置

typescript 复制代码
@Configuration
public class AiImageConfig {

    @Bean
    public ImageModel midjourneyImageModel() {
        // Midjourney 通过自定义 HTTP 客户端实现
        return new MidjourneyImageModel(
            "https://api.holdai.top/mj",
            System.getProperty("ai.midjourney.api-key")
        );
    }

    @Bean
    public ImageModel stableDiffusionImageModel() {
        // Spring AI 原生支持 Stable Diffusion
        return new OpenAiImageModel(openAiApi());
    }
}

5.2 图片生成 Controller

less 复制代码
@RestController
@RequestMapping("/admin-api/ai/image")
@Tag(name = "AI 图片生成")
public class AiImageController {

    @Autowired
    private ImageModel imageModel;

    @PostMapping("/generate")
    public CommonResult<AiImageRespDTO> generate(@RequestBody @Valid AiImageReqDTO req) {
        // 构建生成请求
        GenerationRequest request = GenerationRequest.builder()
            .prompt(buildPrompt(req))       // 提示词
            .model(req.getModel() ?? "dall-e-3")
            .numberOfResults(req.getCount() ?? 1)
            .quality(req.getQuality() ?? "standard")
            .build();

        // 执行生成
        GenerationResult result = imageModel.call(request);

        // 返回图片 URL 列表
        List<String> urls = result.getResults().stream()
            .map(GenerationResponseItem::getUrl)
            .toList();

        return CommonResult.success(new AiImageRespDTO(urls));
    }
}

六、第五步:知识库 RAG(检索增强生成)

这是 AI 模块最有价值的场景------让大模型基于你的业务数据回答问题

6.1 RAG 架构

markdown 复制代码
用户提问
    ↓
┌──────────────────────────┐
│  1. 问题向量化             │ ← 用 Embedding 模型把问题转成向量
│     ↓                     │
│  2. 向量相似度搜索          │ ← 在 Redis 向量库中找最相似的文档片段
│     ↓                     │
│  3. 拼接上下文 + 原问题     │
│     ↓                     │
│  4. 发送给 LLM 生成回答     │
└──────────────────────────┘
    ↓
返回答案

6.2 文档分片与向量化

scss 复制代码
@Service
@Slf4j
public class AiKnowledgeService {

    @Autowired
    private VectorStore vectorStore;  // Spring AI 的 VectorStore 接口
    @Autowired
    private EmbeddingModel embeddingModel;

    /**
     * 上传文档到知识库
     */
    public void uploadDocument(Long documentId, MultipartFile file, String category) {
        try {
            // 1. 读取文档内容(支持 PDF/Word/TXT)
            String content = readDocumentContent(file);

            // 2. 文档分片(每片 500 字,重叠 50 字)
            List<TextSegment> segments = TextSplitter.builder()
                .chunkSize(500)
                .chunkOverlap(50)
                .build()
                .split(content);

            // 3. 为每个片段添加元数据
            List<Document> documents = segments.stream()
                .map(segment -> new Document(
                    segment.getText(),
                    new HashMap<String, Object>() {{
                        put("documentId", documentId);
                        put("category", category);
                        put("segmentIndex", segment.getIndex());
                        put("createdAt", Instant.now());
                    }}
                ))
                .toList();

            // 4. 存入向量库(自动调用 Embedding 模型生成向量)
            vectorStore.add(documents);

            log.info("文档上传成功: id={}, 分片数={}", documentId, documents.size());

        } catch (Exception e) {
            log.error("文档上传失败", e);
            throw new BusinessException("文档上传失败: " + e.getMessage());
        }
    }

    /**
     * 知识库问答(RAG)
     */
    public String qa(String question, String category) {
        // 1. 将问题转为向量
        float[] questionVector = embeddingModel.embed(question);

        // 2. 在向量库中搜索最相似的 5 个片段
        List<VectorStore.QueryResponse> responses = vectorStore.similaritySearch(
            SimilarityQuery.builder()
                .query(new Document(question))
                .topK(5)
                .similarityThreshold(0.7)
                .filterExpression("category == '" + category + "'")
                .build()
        );

        // 3. 拼接上下文
        String context = responses.stream()
            .map(r -> r.getDocument().getText())
            .collect(Collectors.joining("\n\n"));

        if (context.isEmpty()) {
            return "知识库中没有相关内容,请尝试其他问题";
        }

        // 4. 构建 Prompt(带上下文)
        String prompt = """
            你是一个智能客服助手。请根据以下参考资料回答问题。
            如果参考资料中没有相关内容,请如实告知"抱歉,知识库中没有相关信息"。

            === 参考资料 ===
            %s
            === 问题 ===
            %s
            === 回答 ===
            """.formatted(context, question);

        // 5. 调用 LLM 生成回答
        return aiModelRouter.chat(AiModelRoute.CHINESE_UNDERSTANDING, prompt);
    }
}

6.3 知识管理 API

less 复制代码
@RestController
@RequestMapping("/admin-api/ai/knowledge")
@Tag(name = "AI 知识库管理")
public class AiKnowledgeController {

    @Autowired
    private AiKnowledgeService knowledgeService;

    /** 上传文档 */
    @PostMapping("/document/upload")
    public CommonResult<Long> uploadDocument(
            @RequestParam("file") MultipartFile file,
            @RequestParam("categoryId") Long categoryId) {
        Long docId = knowledgeService.uploadDocument(file, categoryId);
        return CommonResult.success(docId);
    }

    /** 知识库问答 */
    @PostMapping("/qa")
    public CommonResult<AiQaRespDTO> qa(@RequestBody @Valid AiQaReqDTO req) {
        String answer = knowledgeService.qa(req.getQuestion(), req.getCategory());
        return CommonResult.success(new AiQaRespDTO(answer));
    }

    /** 删除文档 */
    @DeleteMapping("/document/{id}")
    public CommonResult<Boolean> deleteDocument(@PathVariable Long id) {
        knowledgeService.deleteDocument(id);
        return CommonResult.success(true);
    }
}

七、踩过的坑

坑 1:流式响应乱码

Spring AI 的流式输出默认使用 UTF-16,在 WebFlux 环境下会乱码。

解决方案 :在 application.yaml 中强制设置字符集:

yaml 复制代码
spring:
  mvc:
    charset: UTF-8  # 必须设置,否则 WebFlux 流式返回会乱码

坑 2:并发控制

多个用户同时调用 AI,API Key 的 QPS 限制很容易触达。

解决方案:用 Redisson 分布式锁做限流:

typescript 复制代码
@RateLimiter(key = "ai:quota:{userId}", permitsPerSecond = 5)
public String chat(String message) {
    // 每个用户每秒最多 5 次调用
}

坑 3:Token 计数不准

不同模型的 Token 计算方式不同,GPT-4 和 DeepSeek 的 tokenizer 不一样。

解决方案:统一使用 Spring AI 内置的 Token 计数器:

ini 复制代码
ChatResponse response = chatModel.call(prompt);
TokenUsage usage = response.getMetadata().getTokenUsage();
log.info("输入 token: {}, 输出 token: {}, 总计: {}",
    usage.getInputTokens(), usage.getOutputTokens(), usage.getTotalTokens());

坑 4:Embedding 模型选型

Spring AI 默认的 Embedding 模型是 OpenAI 的 text-embedding-ada-002,中文效果一般。

推荐替换为

  • 通义千问 embedding:text-embedding-v3,中文效果好,免费额度大

  • 智谱 embedding:embedding-2,免费

    spring: ai: zhipuai: embedding: options: model: embedding-2

坑 5:超时时间太短

默认 60 秒超时,遇到复杂推理或图片生成直接超时。

解决方案

yaml 复制代码
spring:
  ai:
    openai:
      chat:
        options:
          timeout: PT120S  # 120 秒
    dashscope:
      chat:
        options:
          timeout: PT180S  # 图片生成可能需要更久

八、AI 配额管理

多租户 SaaS 场景下,每个租户的 AI 调用次数需要管控。

8.1 配额表设计

sql 复制代码
CREATE TABLE `ai_quota` (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户 ID',
  `user_id` bigint NOT NULL COMMENT '用户 ID',
  `month` varchar(7) NOT NULL COMMENT '月份,如 2026-07',
  `total_quota` int NOT NULL DEFAULT 1000 COMMENT '总配额(次)',
  `used_quota` int NOT NULL DEFAULT 0 COMMENT '已用配额',
  `primary_key` (`tenant_id`, `month`)
);

8.2 配额拦截器

java 复制代码
@Component
public class AiQuotaInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
                              Object handler) throws Exception {
        // 只拦截 AI 相关的接口
        if (!request.getRequestURI().startsWith("/admin-api/ai/")) {
            return true;
        }

        Long tenantId = TenantContextHolder.getTenantId();
        String month = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy-MM"));

        // 查询配额
        AiQuotaDO quota = quotaMapper.selectByTenantAndMonth(tenantId, month);
        if (quota.getUsedQuota() >= quota.getTotalQuota()) {
            response.setStatus(429);
            response.getWriter().write("本月 AI 调用次数已达上限");
            return false;
        }

        return true;
    }
}

九、总结

  1. Spring AI 是最佳选择:统一了 ChatCompletion、ImageGeneration、VectorStore 的 API,换模型只需改配置
  2. 多模型路由是关键:不同场景用不同模型,成本和效果兼顾
  3. RAG 是最有价值的场景:知识库问答能让 AI 真正服务于业务,而不是泛泛而谈
  4. 配额管理不能少:多租户 SaaS 必须管控每个租户的用量,防止被滥用
  5. 流式响应要处理编码:WebFlux 环境下 UTF-8 是必须的

相关推荐
青石路1 小时前
如果写Redis序列化与读Redis序列化不一致,你觉得会发生什么
java·redis
煎饼学大模型1 小时前
架构决定上限:Skill 知识架构的三次重构实践
java·重构·架构·skill
llwszx1 小时前
【Java/Go后端手撸原生Agent(第五篇):多工具并行调用 + BashTool执行引擎 + Judge证据链升级】
java·后端·golang·状态机·pydantic·agnet·llm-as-judge
奈何不吃鱼1 小时前
【SpringBoot】业务多线程四大实战场景
java·spring boot·多线程
问商十三载2 小时前
2026法律行业AI引擎生成式优化怎么优化?三层专业加固提排名,零成本提33%引用率附适配表
java·前端·人工智能·算法
野生风长2 小时前
c++类和对象(this指针,重载operator,习题总结)
java·开发语言·c++
霸道流氓气质2 小时前
基于 Spring 事务同步机制的事务后置动作收集器 Starter 实践
java·后端·spring
过期动态2 小时前
【LeetCode 热题 100】找到字符串中所有字母异位词
java·数据结构·算法·leetcode·职场和发展·rabbitmq
刘小八2 小时前
Spring AI Tool Calling 生产化:参数校验、权限控制与超时隔离
java·人工智能·spring