SpringAI快速上手教程

235 阅读9分钟

01-SpringAI的介绍

Spring AI 是由 VMware / Spring 团队推出的一个 面向 AI 的 Spring 官方项目,目标是让 Java / Spring 开发者 像用 Spring Data、Spring Security 一样,用统一、规范的方式接入大模型(LLM)和 AI 能力

一句话理解: Spring AI = 给大模型用的 Spring Framework

image.png

02-创建项目远程调用deepseek大模型

1 创建项目 image.png

Releases - Use Maven Central

<!-- Maven Central is included by default in Maven builds.
     You usually don’t need to configure it explicitly,
     but it's shown here for clarity. -->
<repositories>
    <repository>
        <id>central</id>
        <url>https://repo.maven.apache.org/maven2</url>
    </repository>
</repositories>

Snapshots - Add Snapshot Repositories

<repositories>
  <repository>
    <id>spring-snapshots</id>
    <name>Spring Snapshots</name>
    <url>https://repo.spring.io/snapshot</url>
    <releases>
      <enabled>false</enabled>
    </releases>
  </repository>
  <repository>
    <name>Central Portal Snapshots</name>
    <id>central-portal-snapshots</id>
    <url>https://central.sonatype.com/repository/maven-snapshots/</url>
    <releases>
      <enabled>false</enabled>
    </releases>
    <snapshots>
      <enabled>true</enabled>
    </snapshots>
  </repository>
</repositories>

deepseek dependency

<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-starter-model-deepseek</artifactId>
</dependency>

ChatController.java

package org.example.springaidemo.controller;

import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.deepseek.DeepSeekChatModel;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Flux;
import java.util.Map;

@RestController
public class ChatController {

    private final DeepSeekChatModel chatModel;

    @Autowired
    public ChatController(DeepSeekChatModel chatModel) {
        this.chatModel = chatModel;
    }

    @GetMapping("/ai/generate")
    public Map generate(@RequestParam(value = "message", defaultValue = "Tell me a joke") String message) {
        return Map.of("generation", chatModel.call(message));
    }

    @GetMapping(value = "/ai/generateStream", produces = "text/html; charset=UTF-8")
    public Flux<String> generateStream(@RequestParam(value = "message", defaultValue = "Tell me a joke") String message) {
        var prompt = new Prompt(new UserMessage(message));
        return chatModel.stream(prompt).map(ChatResponse -> ChatResponse.getResult().getOutput().getText());
    }
}

测试接口

http://localhost:8080/ai/generateStream?message=讲个笑话

image.png

03-SpringAI调用本地大模型ollama

1 引入依赖

<dependency>
   <groupId>org.springframework.ai</groupId>
   <artifactId>spring-ai-starter-model-ollama</artifactId>
</dependency>

2 配置yml

# In application.yml
server:
  port: 8080
  error:
    include-message: always
    include-binding-errors: always
    include-stacktrace: on_param
    include-exception: false

spring:
  ai:
    deepseek:
      api-key: sk-7fee8ebd8c0744ac9c1d91d9f0d81372
    ollama:
      chat:
        model: deepseek-r1-1.5b

3 chatController.java 调用本地ollama的大模型

package org.example.springaidemo.controller;

import jakarta.annotation.Resource;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.ollama.OllamaChatModel;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Flux;
import java.util.Map;

@RestController
public class ChatController {

    @Resource
    private OllamaChatModel chatModel;


    @GetMapping("/ai/generate")
    public Map generate(@RequestParam(value = "message", defaultValue = "Tell me a joke") String message) {
        return Map.of("generation", chatModel.call(message));
    }

    @GetMapping(value = "/ai/generateStream", produces = "text/html; charset=UTF-8")
    public Flux<String> generateStream(@RequestParam(value = "message", defaultValue = "Tell me a joke") String message) {
        var prompt = new Prompt(new UserMessage(message));
        return chatModel.stream(prompt).map(ChatResponse -> ChatResponse.getResult().getOutput().getText());
    }
}

04-SpringAI创建ChatClient示例实现AI人设

package org.example.springaidemo.controller;


import jakarta.annotation.Resource;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.deepseek.DeepSeekChatModel;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Flux;

@RestController
public class ChatController {

    @Resource
    private final DeepSeekChatModel chatModel;
    @Resource
    private ChatClient chatClient;

    @Autowired
    public ChatController(DeepSeekChatModel chatModel) {
        this.chatModel = chatModel;
    }


    @GetMapping("/ai/generate")
    public String generate(@RequestParam(value = "message", defaultValue = "Tell me a joke") String message) {
        return  chatClient.prompt("你是ai助手小黑").user( message).call().content();

    }

    @GetMapping(value = "/ai/generateStream", produces = "text/html; charset=UTF-8")
    public Flux<String> generateStream(@RequestParam(value = "message", defaultValue = "Tell me a joke") String message) {
        return  chatClient.prompt("你是ai助手小黑").user( message).stream().content();
    }
}

测试 image.png

05-SpringAI实现记录上下文

1 创建数据库

打开 navicat 进行创建

数据库的名称就叫

20260110-ai

字符集编码选择为 utf8mb4

创建成功

点击 查询的选项

然后把聊天记录表的 sql 语句给复制粘贴进来

CREATE TABLE `sys_history` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '序列号',
  `datetime` datetime DEFAULT NULL COMMENT '聊天时间',
  `content` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci COMMENT '聊天内容',
  `role` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT 'user' COMMENT '角色',
  `sessionId` bigint DEFAULT NULL COMMENT '会话Id',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=227 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC COMMENT='聊天记录';

2 导入依赖坐标

<!--Mysql驱动        -->
<dependency>
    <groupId>com.mysql</groupId>
    <artifactId>mysql-connector-j</artifactId>
    <version>8.3.0</version>
</dependency>
<!--  Mybatis——Plus 插件    -->
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-jsqlparser</artifactId>
</dependency>
<!--  Mybatis——Plus     -->
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-spring-boot3-starter</artifactId>
</dependency>

3 配置数据源

datasource:
  driver-class-name: com.mysql.cj.jdbc.Driver
  url: jdbc:mysql://localhost:3306/20260110-ai?allowPublicKeyRetrieval=true&useUnicode=true&useSSL=false&characterEncoding=utf8&serverTimezone=Asia/Shanghai
  username: root
  password: root

4 配置其他文件夹

config/AiConfig.java

package org.example.springaidemo.entity.impl;

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;

import org.example.springaidemo.entity.History;
import org.example.springaidemo.mapper.HistoryMapper;
import org.example.springaidemo.service.HistoryService;
import org.springframework.stereotype.Service;



@Service
public class HistoryServiceImpl extends ServiceImpl<HistoryMapper, History> implements HistoryService {

}

config/MybatisPlusConfig.java

package org.example.springaidemo.config;


import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;

import lombok.extern.slf4j.Slf4j;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration

@MapperScan("org.example.springaidemo.mapper")
@Slf4j
public class MybatisPlusConfig {
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        return interceptor;
    }
}

entity/History.java

package org.example.springaidemo.entity;


import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.io.Serializable;
import java.time.LocalDateTime;


/**
 * 聊天记录实体类
 */

@Data
@Builder(toBuilder = true)
@AllArgsConstructor
@NoArgsConstructor
@TableName("sys_history")

public class History implements Serializable {

    private static final long serialVersionUID = 1L;

    @TableId(value = "id", type = IdType.AUTO)
    private Long id;

    @Schema(title = "聊天时间")
    @TableField("datetime")

    @JsonFormat( pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime datetime;
    @Schema(title = "聊天内容")
    @TableField("content")

    private String content;
    @Schema(title = "角色")
    @TableField("role")

    private String role;
    @Schema(title = "会话Id")
    @TableField("sessionId")

    private Long sessionId;
}

service/HistoryService.java

package org.example.springaidemo.service;

import com.baomidou.mybatisplus.extension.service.IService;
import org.example.springaidemo.entity.History;


public interface HistoryService extends IService<History> {

}

service/impl/HistoryServiceImpl.java

package org.example.springaidemo.service.impl;

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;

import org.example.springaidemo.entity.History;
import org.example.springaidemo.mapper.HistoryMapper;
import org.example.springaidemo.service.HistoryService;
import org.springframework.stereotype.Service;



@Service
public class HistoryServiceImpl extends ServiceImpl<HistoryMapper, History> implements HistoryService {

}

mapper/HistoryMapper.java

package org.example.springaidemo.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
import org.example.springaidemo.entity.History;


@Mapper
public interface HistoryMapper extends BaseMapper<History> {

}

更新chatController.java

使用MyBatis-Plus进行数据库操作

Spring AI框架的各种组件(ChatClient、消息类型等)

DeepSeek和Ollama两个AI模型(虽然导入了Ollama但未使用)

响应式编程库Reactor的Flux

package org.example.springaidemo.controller;


import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import jakarta.annotation.Resource;
import org.example.springaidemo.entity.History;
import org.example.springaidemo.service.HistoryService;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.messages.AssistantMessage;
import org.springframework.ai.chat.messages.Message;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.deepseek.DeepSeekChatModel;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Flux;

import java.time.LocalDateTime;
import java.util.List;
import java.util.stream.Collectors;

@RestController
public class ChatController {
    //依赖注入
    @Resource
    private final DeepSeekChatModel chatModel;
    @Resource
    private ChatClient chatClient;
    @Resource
    private HistoryService historyService;

    //构造函数注入
    @Autowired
    public ChatController(DeepSeekChatModel chatModel) {
        this.chatModel = chatModel;
    }

    /*
    * HTTP GET端点:/ai/generate
    接收用户消息参数,默认值为"Tell me a joke"
    返回AI助手"小黑"对用户消息的同步响应内容
    * */
    @GetMapping("/ai/generate")
    public String generate(@RequestParam(value = "message", defaultValue = "Tell me a joke") String message) {
        return  chatClient.prompt("你是ai助手小黑").user( message).call().content();

    }

    @GetMapping(value = "/ai/generateStream", produces = "text/html; charset=UTF-8")
    public Flux<String> generateStream(@RequestParam(value = "message", defaultValue = "Tell me a joke") String message,
                                       @RequestParam(value = "sessionId", defaultValue = " 1 ") long sessionId
                                       ) {
        //用户消息保存
        History userhistory = new History();
        userhistory.setDatetime(LocalDateTime.now());
        userhistory.setRole("user");
        userhistory.setContent(message);
        userhistory.setSessionId(sessionId);
        historyService.save(userhistory); //创建并保存用户发送的消息到数据库

        //获取历史对话
        //查询指定会话ID的所有历史记录
        //排除刚刚保存的当前用户消息(避免重复)
        List<History> histories = historyService.list(
                new LambdaQueryWrapper<History>().eq(History::getSessionId, sessionId).ne(History::getId , userhistory.getId())
        ) ;

        //转换历史消息格式: 将历史记录转换为AI模型能理解的消息格式
        List<Message> messages = histories.stream().map(history ->
                "user".equals(history.getRole())?new UserMessage(history.getContent()):new AssistantMessage(history.getContent())).collect(Collectors.toList());

        //数组 用来累积AI流式回复内容
        //使用数组是为了在lambda表达式中能够修改变量
        StringBuilder[] builder = {new StringBuilder()};

        //构建包含上下文的AI请求并返回流式响应
        Flux<String> stream = chatClient.prompt("你是ai助手小黑").user( message).messages( messages).stream().content();
        return  stream.doOnNext(s -> builder[0].append(s)) //当AI返回每个片段时,将其追加到builder中进行累积
                .doOnComplete(() -> {
                    History aihistory = new History();
                    aihistory.setId(null);
                    aihistory.setDatetime(LocalDateTime.now());
                    aihistory.setRole("assistant");
                    aihistory.setContent(builder[0].toString());
                    aihistory.setSessionId(sessionId);
                    historyService.save(aihistory);
                });
        /*
        当AI流式回复完成后,执行以下操作:
        创建新的历史记录对象
        设置时间戳
        设置角色为"assistant"
        将累积的完整回复内容保存
        设置会话ID
        保存到数据库
        这种方式确保了只有在AI完全生成回复后,才会将其保存到历史记录中,同时保持了流式响应的实时性*/
    }
}

06-语义向量化、向量数据库、RAG的概念

image.png

1 RAG 检索增强生成 Retrieval Augmented Generated

文本读取 - 文本分割 - 转向量化 - 存向量数据库 用户问题 - 向量化 - 检索相关内容 - 构造prompt - 大模型生成答案

07-SpringAI实现语义向量化

docs.spring.io/spring-ai/r…

导入的依赖

        <dependency>
            <groupId>com.alibaba.cloud.ai</groupId>
            <artifactId>spring-ai-alibaba-starter-dashscope</artifactId>
        </dependency>
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>com.alibaba.cloud.ai</groupId>
                <artifactId>spring-ai-alibaba-bom</artifactId>
                <version>1.0.0.2</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
    </dependencyManagement>

配置示例

    ollama:
      chat:
        model: deepseek-r1:8b
      embedding:
        enabled: false
    dashscope:
      api-key: 
      embedding:
        enabled: true
        options:
          model: text-embedding-v4

示例方法

   @GetMapping("/ai/embedding")
    public Map embed(@RequestParam(value = "message", defaultValue = "Tell me a joke") String message) {
        EmbeddingResponse embeddingResponse = this.embeddingModel.embedForResponse(List.of(message));
        return Map.of("embedding", embeddingResponse);
    }

08-SpringAI实现文本分割

docs.spring.io/spring-ai/r…

因为在日常工作的过程中, 文本都是以文档的形式进行存储的, 常见的文档格式有 txt 文件、markdown 文件、pdf 文件, 需要把这个文档给读出来, 读出来文档以后才能进行分割, SpringAI 给我们定义了文本读取的规范规范接口, 同时给我们提供了很多类型的文本阅读工具 , 例如 JSON 类型的、txt 类型的、MarkDown 类型的、PDF 类型等

PDF 的有两种读取方式

一种是按页读取

另外一种是按章节读取

他们都是按照 SpringAI 的接口进行实现,特别的规范

1 添加依赖

<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-pdf-document-reader</artifactId>
</dependency>

2 测试方法

@Test
void testPdf(){
    PagePdfDocumentReader pdfReader = new PagePdfDocumentReader("classpath:/阿里巴巴Java开发手册(终极版).pdf",
            PdfDocumentReaderConfig.builder()
                    .withPageTopMargin(0)
                    .withPageExtractedTextFormatter(ExtractedTextFormatter.builder()
                            .withNumberOfTopTextLinesToDelete(0)
                            .build())
                    .withPagesPerDocument(1)
                    .build());
    System.out.println( pdfReader.read().size());
}

3 按Token进行分割


@Test
void testPdf(){
    PagePdfDocumentReader pdfReader = new PagePdfDocumentReader("classpath:/阿里巴巴Java开发手册(终极版).pdf",
            PdfDocumentReaderConfig.builder()
                    .withPageTopMargin(0)
                    .withPageExtractedTextFormatter(ExtractedTextFormatter.builder()
                            .withNumberOfTopTextLinesToDelete(0)
                            .build())
                    .withPagesPerDocument(1)
                    .build());


    TokenTextSplitter splitter = new TokenTextSplitter(1000, 400, 10, 100000, true);
    splitter.apply(pdfReader.read()).forEach(document -> {
    System.out.printf(Arrays.toString(embeddingModel.embed(document)));
    });
}

image.png

09-SpringAI实现向量数据库的存储

1 docker 配置

维度选择 1024 保持一致 image.png

2 依赖

<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-starter-vector-store-qdrant</artifactId>
</dependency>
spring:
  ai:
    vectorstore:
      qdrant:
        host: localhost
        port: 6334
        collection-name: vector_store

10-SpringAI实现检索增强

package org.example.springaidemo.controller;


import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import jakarta.annotation.Resource;
import org.example.springaidemo.entity.History;
import org.example.springaidemo.service.HistoryService;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.messages.AssistantMessage;
import org.springframework.ai.chat.messages.Message;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.deepseek.DeepSeekChatModel;
import org.springframework.ai.document.Document;
import org.springframework.ai.embedding.EmbeddingModel;
import org.springframework.ai.embedding.EmbeddingResponse;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Flux;

import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

@RestController
public class ChatController {
    //依赖注入
    @Resource
    private final DeepSeekChatModel chatModel;
    @Resource
    private ChatClient chatClient;
    @Resource
    private HistoryService historyService;
    @Resource
    private EmbeddingModel embeddingModel;
    @Resource
    private VectorStore vectorStore;


    //构造函数注入
    @Autowired
    public ChatController(DeepSeekChatModel chatModel) {
        this.chatModel = chatModel;
    }

    /*
    * HTTP GET端点:/ai/generate
    接收用户消息参数,默认值为"Tell me a joke"
    返回AI助手"小黑"对用户消息的同步响应内容
    * */
    @GetMapping("/ai/generate")
    public String generate(@RequestParam(value = "message", defaultValue = "Tell me a joke") String message) {
        return  chatClient.prompt("你是ai助手小黑").user( message).call().content();

    }

    @GetMapping(value = "/ai/generateStream", produces = "text/html; charset=UTF-8")
    public Flux<String> generateStream(@RequestParam(value = "message", defaultValue = "Tell me a joke") String message,
                                       @RequestParam(value = "sessionId", defaultValue = " 1 ") long sessionId
                                       ) {

        List<Document> documentList = vectorStore.similaritySearch(message);
        StringBuilder contextBuilder = new StringBuilder();
        documentList.forEach(document -> contextBuilder.append(document).append("\n"));
        String prompt  = "你是编程助手小红 , 以下是本次会话传递的信息"+contextBuilder.toString()+
                "/n 请根据以上信息,回答用户的问题:"+message;
        //用户消息保存
        History userhistory = new History();
        userhistory.setDatetime(LocalDateTime.now());
        userhistory.setRole("user");
        userhistory.setContent(message);
        userhistory.setSessionId(sessionId);
        historyService.save(userhistory); //创建并保存用户发送的消息到数据库

        //获取历史对话
        //查询指定会话ID的所有历史记录
        //排除刚刚保存的当前用户消息(避免重复)
        List<History> histories = historyService.list(
                new LambdaQueryWrapper<History>().eq(History::getSessionId, sessionId).ne(History::getId , userhistory.getId())
        ) ;

        //转换历史消息格式: 将历史记录转换为AI模型能理解的消息格式
        List<Message> messages = histories.stream().map(history ->
                "user".equals(history.getRole())?new UserMessage(history.getContent()):new AssistantMessage(history.getContent())).collect(Collectors.toList());

        //数组 用来累积AI流式回复内容
        //使用数组是为了在lambda表达式中能够修改变量
        StringBuilder[] builder = {new StringBuilder()};

        //构建包含上下文的AI请求并返回流式响应
        Flux<String> stream = chatClient.prompt(prompt).user( message).messages( messages).stream().content();
        return  stream.doOnNext(s -> builder[0].append(s)) //当AI返回每个片段时,将其追加到builder中进行累积
                .doOnComplete(() -> {
                    History aihistory = new History();
                    aihistory.setId(null);
                    aihistory.setDatetime(LocalDateTime.now());
                    aihistory.setRole("assistant");
                    aihistory.setContent(builder[0].toString());
                    aihistory.setSessionId(sessionId);
                    historyService.save(aihistory);
                });
        /*
        当AI流式回复完成后,执行以下操作:
        创建新的历史记录对象
        设置时间戳
        设置角色为"assistant"
        将累积的完整回复内容保存
        设置会话ID
        保存到数据库
        这种方式确保了只有在AI完全生成回复后,才会将其保存到历史记录中,同时保持了流式响应的实时性*/
    }

    @GetMapping("/ai/embedding")
    public Map embed(@RequestParam(value = "message", defaultValue = "Tell me a joke") String message) {
        EmbeddingResponse embeddingResponse = this.embeddingModel.embedForResponse(List.of(message));
        return Map.of("embedding", embeddingResponse);
    }
}