从零搭建 Spring AI 项目,实现 Java 调用本地 DeepSeek 大模型,并支持流式输出与角色设定。


前言

随着 AI 技术的快速发展,大模型已经逐渐成为企业开发的重要组成部分。

对于 Java 开发者来说,Spring 官方推出的 Spring AI 极大降低了接入大模型的门槛。

在本项目中,你将学习并完成:

✅ Spring AI 环境搭建

✅ Ollama 本地大模型部署

✅ DeepSeek-R1 模型接入

✅ Spring Boot 集成 AI 服务

✅ ChatClient 对话调用

✅ AI 流式输出(Streaming)

✅ 自定义 AI 人设(System Prompt)

✅ Lombok 简化开发

✅ AI 调用日志记录

✅ Vue3 前端项目部署

✅ AI 聊天页面实现

✅ 前后端接口联调

✅ 跨域问题解决

✅ AI 聊天会话管理

✅ 多轮对话功能实现

✅ 文件上传入口集成

✅ Spring AI + Ollama + DeepSeek 完整对话系统搭建

最终实现:

http://localhost:8080/ai/chat?prompt=你好

直接与本地大模型对话。

首先我们需要有jdk17、ollama、并且安装好deepseek-r1模型

通过ollama安装好模型(时间较长)

ollama run deepseek-r1:7b

安装好后在cmd中使用命令运行deepseek

(注意:这个界面不要关闭)

ollama run deepseek-r1:7b


一、创建 Spring Boot 项目

打开 IDEA,选择:

New Project → Spring Initializr

注意jdk最好一定要是17

创建项目


二、添加项目依赖

在依赖选择界面中添加:

1. Spring Web

用于提供 REST 接口能力。


2. MySQL Driver

后续存储聊天记录时使用。


3. Ollama

Spring AI 官方提供的 Ollama Starter。

最终依赖:

Spring Web
MySQL Driver
Ollama

点击:

Create

完成项目创建。


四、配置 Spring AI

打开:

src/main/resources/application.yaml

添加配置:

spring:
  application:
    name: heima-springai

  ai:
    ollama:
      base-url: http://127.0.0.1:11434

      chat:
        model: deepseek-r1:7b

  • 为什么用 application.yaml 后缀?

  • YAML 格式比传统 .properties结构化、易读,天然支持层级嵌套(如 spring.ai.ollama),配置 Spring AI 这类多层级配置时更简洁清晰,也更符合现代 Spring Boot 项目的主流规范。

  • 为什么要把 localhost 改成 127.0.0.1

  • 有些电脑默认把 localhost 解析为 IPv6 地址 ::1,但 Ollama 默认只监听 IPv4 的 127.0.0.1:11434,导致 Spring 连接超时。直接写死 127.0.0.1,可强制使用 IPv4 回环地址,绕开系统 IPv6 解析异常,保证能稳定连接到本地 Ollama 服务。


五、创建 ChatClient 配置

Spring AI 推荐通过 ChatClient 调用大模型。

创建:

config/CommonConfiguration.java

代码如下:

package com.itheima.heimaspringai.config;

import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.ollama.OllamaChatModel;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class CommonConfiguration {

    @Bean
    public ChatClient chatClient(OllamaChatModel model) {

        return ChatClient
                .builder(model)
                .build();
    }
}

配置完成效果

CommonConfiguration 配置类

该配置类用于向 Spring 容器中注册 ChatClient 对象,方便后续统一调用 AI 大模型服务。

代码说明:

  • @Bean:将 ChatClient 注册为 Spring Bean。
  • OllamaChatModel model:自动注入 Spring AI 提供的 Ollama 模型实例。
  • ChatClient.builder(model):基于指定的大模型创建聊天客户端。
  • build():完成构建并交由 Spring 容器管

六、创建聊天接口

创建:

controller/ChatController.java

代码:

package com.itheima.heimaspringai.controller;

import lombok.RequiredArgsConstructor;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/ai")
@RequiredArgsConstructor
public class ChatController {

    private final ChatClient chatClient;

    @RequestMapping("/chat")
    public String chat(String prompt){

        return chatClient
                .prompt()
                .user(prompt)
                .call()
                .content();
    }
}

创建 Controller


ChatController 控制器说明

该控制器用于对外提供 AI 对话接口,通过注入 ChatClient 与大模型进行交互,并将模型返回结果直接响应给前端。

代码解析:

  • @RestController:标识为 Spring Boot REST 控制器,返回 JSON 或字符串数据。
  • @RequestMapping("/ai"):统一设置接口访问前缀为 /ai
  • @RequiredArgsConstructor:Lombok 自动生成构造方法,实现 ChatClient 的依赖注入。
  • private final ChatClient chatClient:注入 Spring AI 提供的聊天客户端对象。
  • chat() 方法:接收用户输入的 prompt,调用大模型生成回复并返回结果。
    • prompt():创建对话请求。
    • user(prompt):设置用户问题。
    • call():发送请求到大模型。
    • content():获取模型返回的文本内容。

七、解决 Lombok 报错

如果:

@RequiredArgsConstructor

出现红色报错。

需要在 pom.xml 添加:

<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.18.42</version>
</dependency>

刷新 Maven。


Lombok配置截图

@RequiredArgsConstructor@Data@Builder 等 Lombok 注解用于自动生成构造方法、getter/setter 和其他样板代码。引入 Lombok 依赖后,Spring Boot 能识别这些注解,消除 IDE 报错,提高开发效率,同时保持代码简洁。


八、启动项目测试

启动项目:

游览器访问:

http://localhost:8080/ai/chat?prompt=你是谁?

返回:

您好!我是由中国的深度求索(DeepSeek)公司开发的智能助手 DeepSeek-R1。

测试结果


九、实现流式输出

普通模式需要等待模型全部生成完成。

Spring AI 支持流式返回。

导入:

import reactor.core.publisher.Flux;

修改 Controller:

@RequestMapping(
        value = "/chat",
        produces = "text/html;charset=UTF-8"
)
public Flux<String> chat(String prompt){

    return chatClient
            .prompt(prompt)
            .stream()
            .content();
}

流式输出代码

流式输出解释(ChatController.java)

ChatController 中,使用了 Spring WebFluxFlux<String> 来实现流式响应:

核心点说明

  1. 流式输出 (.stream())
    • 调用 chatClient.prompt(prompt).stream() 会以流的方式返回 AI 生成的内容。
    • 优点:前端可以实时接收到生成结果,而不是等待整个回答完成。
    • 适合长文本或对话机器人场景。
  2. 返回类型 Flux<String>
    • Flux 是 Spring WebFlux 提供的响应式类型,可以支持多次异步数据推送。
    • 每生成一段文本,就通过 Flux 发送给前端,实现“边生成边显示”。
  3. 为什么要设置 produces

    produces = "text/html;charset=UTF-8"
    • 明确告诉浏览器返回内容的类型和编码,防止中文乱码。
    • 对于流式输出,Spring 会以 Server-Sent Events (SSE) 或分块响应的方式发送文本流,前端可以逐段解析和渲染。

流式输出效果

模型边生成边返回


十、自定义 AI 人设

很多场景下需要让 AI 扮演固定角色。

例如:

  • 客服

  • 面试官

  • 英语老师

  • 编程导师

修改CommonConfiguration:

@Bean
public ChatClient chatClient(OllamaChatModel model){

    return ChatClient
            .builder(model)

            .defaultSystem("""
                你是一个热血、可爱的智能助手,
                你的名字叫小团团,
                请以小团团身份回答问题。
            """)

            .build();
}

配置系统提示词

自定义 AI 人设

为了让 AI 具备固定的身份、语气和行为风格,可以在创建 ChatClient 时通过 defaultSystem() 设置系统提示词(System Prompt)。

defaultSystem() 本质上是为大模型设置全局系统提示词(System Prompt),能够统一控制 AI 的身份、语气和行为规则,是实现个性化 AI 助手的重要方式。


测试效果

访问:

http://localhost:8080/ai/chat?prompt=你是谁?

返回:

AI 已按照指定身份回答问题


十一、项目结构

最终项目结构如下:

src
 └─main
     ├─java
     │  └─com.itheima.heimaspringai
     │      ├─config
     │      │   └─CommonConfiguration.java
     │      ├─controller
     │      │   └─ChatController.java
     │      └─HeimaSpringAiApplication.java
     │
     └─resources
         └─application.yaml

核心流程解析

整个调用链如下:

浏览器
    ↓
ChatController
    ↓
ChatClient
    ↓
Spring AI
    ↓
Ollama
    ↓
DeepSeek-R1

关键代码:

chatClient
    .prompt()
    .user(prompt)
    .call()
    .content();

流式版本:

chatClient
    .prompt(prompt)
    .stream()
    .content();

十三、 开启会话日志,观察 Spring AI 请求过程

在完成 AI 人设配置后,为了方便调试和排查问题,我们可以开启 Spring AI 的会话日志功能。这样能够清晰地看到用户输入、提示词构建以及模型返回结果。

配置日志级别

application.yaml 中添加如下配置:

logging:
  level:
    org.springframework.ai.chat.client.advisor: debug
    com.itheima.heimaspringai: debug

配置说明

  • org.springframework.ai.chat.client.advisor

    • 开启 Spring AI Advisor 组件日志。

    • 可以查看 Prompt 构建、请求发送、响应返回等详细过程。

  • com.itheima.heimaspringai

    • 开启当前项目包下的日志输出。

    • 方便观察业务代码执行情况。

开启后,每次调用大模型时,控制台都会打印完整的会话信息。


十四、添加日志拦截器

为了记录 AI 对话内容,在 ChatClient 配置时增加日志 Advisor:

@Bean
public ChatClient chatClient(OllamaChatModel model){
    return ChatClient
            .builder(model)
            .defaultSystem("你是一个热血、可爱的智能助手,你的名字叫小团团,请以小团团的身份和语气回答问题")
            .defaultAdvisors(new SimpleLoggerAdvisor())
            .build();
}

代码说明

.defaultAdvisors(new SimpleLoggerAdvisor())

用于给 ChatClient 添加日志拦截器。

其作用包括:

  • 记录用户输入内容

  • 记录系统提示词(System Prompt)

  • 记录模型返回结果

  • 方便调试 Prompt 效果

  • 便于排查 AI 回答异常问题


十五、查看会话日志输出

项目启动后访问接口:

http://localhost:8080/ai/chat?prompt=你好

控制台会输出类似信息:

{
  "messageType":"ASSISTANT",
  "text":"你好呀!我是小团团,一个可爱又活泼的智能助手。有什么我可以为你做的吗?😊"
}


十六、 会话日志的作用

在实际开发中,会话日志非常重要,主要用于:

调试 Prompt

查看最终发送给大模型的提示词内容。

验证 AI 人设

确认系统角色是否成功生效。

排查回答异常

当模型输出不符合预期时,可以通过日志快速定位问题。

优化提示词工程

观察不同 Prompt 对回答质量的影响,从而持续优化 AI 效果。

十七. 对接前端项目,实现 AI 应用中心

前面已经完成了 Spring AI + Ollama + DeepSeek 的后端对话功能,接下来开始接入前端项目,实现完整的 AI 应用页面。

项目效果

首页提供多个 AI 应用入口:

  • AI 聊天

  • 哄哄模拟器

  • 智能客服

  • ChatPDF

用户点击对应功能即可进入具体业务页面。


十八. 部署并启动前端项目

百度网盘 请输入提取码

前端项目采用 Vue3 + Vite 开发。

项目结构如下:

spring-ai-nginx
├── conf
├── contrib
├── docs
├── html
├── logs
├── temp
└── nginx.exe

启动前端项目(直接点击nginx.exe就可以)

启动成功后访问:

http://localhost:5173

即可看到 AI 应用中心首页。


十九. 进入 AI 聊天模块

点击首页中的:

AI聊天

进入聊天页面。


二十. 解决CORS问题

跨域配置(CORS)

作用:

  • 解决前后端分离开发时的跨域访问问题。
  • 允许前端(如 localhost:5173)访问后端接口(如 localhost:8080)。
  • 开放指定请求方式(GET、POST、DELETE、OPTIONS)和请求头。
  • 避免浏览器出现 CORS 跨域拦截Failed to fetch 错误。

简而言之:

该配置用于允许前端项目正常调用 Spring Boot 后端接口,实现前后端联调。

由于前端运行在:

http://localhost:5173

后端运行在:

http://localhost:8080

属于不同端口,因此需要解决跨域问题。

示例配置:

@Configuration
public class CorsConfig {

    @Bean
    public WebMvcConfigurer webMvcConfigurer() {
        return new WebMvcConfigurer() {
            @Override
            public void addCorsMappings(CorsRegistry registry) {
                registry.addMapping("/**")
                        .allowedOrigins("http://localhost:5173")
                        .allowedMethods("*")
                        .allowedHeaders("*");
            }
        };
    }
}

这样前端即可正常调用后端接口。

  • @Configuration
    表示这是一个 Spring 配置类,会被 Spring 容器识别并加载。
  • implements WebMvcConfigurer
    实现 WebMvcConfigurer 接口,可以自定义 Spring MVC 的行为,比如跨域、拦截器、格式化器等。
  • addCorsMappings(CorsRegistry registry)
    这是配置跨域访问(CORS)的核心方法:
    • addMapping("/**"):表示对所有接口路径都允许跨域。
    • allowedOrigins("*"):允许任意域名访问(开发阶段常用,生产可指定域名)。
    • allowedMethods("GET","POST","DELETE","OPTIONS"):允许的 HTTP 方法。
    • allowedHeaders("*"):允许所有请求头。
  • 整体作用
    • 解决前端跨域问题,例如前端 localhost:5173 请求后端 localhost:8080 接口时,浏览器不会再报 CORS 错误。
    • 保证前端可以正常发送 AJAX 或 fetch 请求调用后端接口。

二十一. 前后端聊天接口打通

当前端发送:

你好,你是谁?

请求流程如下:

Vue页面
    ↓
Axios请求
    ↓
Spring Boot
    ↓
Spring AI
    ↓
Ollama
    ↓
DeepSeek-R1
    ↓
返回结果

后端收到消息后:

chatClient
        .prompt(prompt)
        .call()
        .content();

调用 DeepSeek 模型生成回复。

整体架构:

┌──────────────┐
│   Vue3前端    │
└──────┬───────┘
       │ HTTP
       ▼
┌──────────────┐
│ Spring Boot │
└──────┬───────┘
       │
       ▼
┌──────────────┐
│  Spring AI  │
└──────┬───────┘
       │
       ▼
┌──────────────┐
│   Ollama     │
└──────┬───────┘
       │
       ▼
┌──────────────┐
│ DeepSeek-R1 │
└──────────────┘

二十二、会话记忆功能

在 Spring AI 中实现对话上下文记忆,核心是通过 ChatMemory 存储对话历史,并配合 MessageChatMemoryAdvisor 实现多轮对话的上下文关联,以下是关键实现步骤:

1. 注册 ChatMemory 对象

ChatMemory 是 Spring AI 提供的对话历史存储接口,我们使用 MessageWindowChatMemory 实现,它默认保留最近 10 条对话消息,防止上下文过长;通过 MessageChatMemoryAdvisor 将会话记忆功能注入 ChatClient,让每次对话自动带上上下文历史:

2. 配置带会话记忆的 ChatClient

通过 MessageChatMemoryAdvisor 将会话记忆功能注入 ChatClient,让每次对话自动带上上下文历史:

3. 接口层绑定会话 ID

在 Controller 中,通过前端传递的 chatId 区分不同用户的对话会话,确保每个会话的上下文独立存储:

4、验证是否实现会话记忆

这部分代码其实是在实现 “会话历史管理”,核心目标是让 AI 支持多会话、多轮对话以及历史记录查询。

博客中建议按照下面的结构来写,逻辑会非常清晰。


二十三、会话历史功能实现

为了实现类似 ChatGPT 的多会话能力,需要解决三个问题:

  1. 管理会话ID

  2. 保存会话ID

  3. 查询历史会话

整体流程如下:

用户发起聊天
      │
      ▼
携带 chatId
      │
      ▼
保存 chatId
      │
      ▼
调用 AI
      │
      ▼
绑定 chatId 到 Memory
      │
      ▼
保存上下文消息
      │
      ▼
查询历史会话

一、管理会话ID

功能说明

会话ID(chatId)用于区分不同聊天窗口。

例如:

chat-001
 ├─ 你好
 ├─ 你是谁
 └─ 今天天气怎么样

chat-002
 ├─ Java是什么
 ├─ SpringBoot是什么
 └─ 如何学习Java

AI会根据不同的 chatId 维护不同的上下文。


ChatHistoryRepository接口

public interface ChatHistoryRepository {

    void save(String type, String chatId);

    List<String> getChatIds(String type);
}

作用

定义会话管理规范:

save()

保存会话ID

getChatIds()

获取某业务下的所有会话ID

例如:

chat
service
pdf

不同业务可以拥有自己的会话列表。


二、保存会话ID

功能说明

当用户第一次进入聊天页面时,需要记录当前会话。

例如:

chat
 ├─ 1001
 ├─ 1002
 └─ 1003

后续用户可以看到自己的历史会话。


InMemoryChatHistoryRepository实现


数据结构

private final Map<String, List<String>> chatHistory
        = new HashMap<>();

结构如下:

{
    "chat":[
        "1001",
        "1002",
        "1003"
    ],

    "pdf":[
        "2001",
        "2002"
    ]
}

save方法

@Override
public void save(String type, String chatId) {

    List<String> charIds =
            chatHistory.computeIfAbsent(
                    type,
                    k -> new ArrayList<>());

    if(charIds.contains(chatId)){
        return;
    }

    charIds.add(chatId);
}

代码解析

1. 获取会话列表
chatHistory.computeIfAbsent(
    type,
    k -> new ArrayList<>()
);

等价于:

if(!chatHistory.containsKey(type)){
    chatHistory.put(type,new ArrayList<>());
}

List<String> chatIds =
        chatHistory.get(type);

作用:

如果业务类型不存在,则自动创建集合。


2. 去重
if(charIds.contains(chatId)){
    return;
}

避免重复保存。

例如:

1001
1001
1001

最终只保留一条记录。


3. 保存会话
charIds.add(chatId);

保存当前会话ID。


ChatController中调用

chatHistoryRepository.save("chat",chatId);

执行流程

用户发送消息
      │
      ▼
chatController
      │
      ▼
save(chatId)
      │
      ▼
保存到Map
      │
      ▼
调用AI

绑定会话上下文

.advisors(
    a->a.param(
        ChatMemory.CONVERSATION_ID,
        chatId
    )
)

作用

告诉 Spring AI:

当前聊天属于哪个会话

例如:

chatId = 1001

则后续所有消息:

用户:你好

AI:你好

用户:你是谁

AI:我是AI助手

都会保存到:

1001

对应的上下文中。


三、查询历史会话

会话保存后,需要提供查询接口。


查询会话ID列表

@GetMapping("/{type}")
public List<String> getChatIds(
        @PathVariable("type")
        String type){

    return chatHistoryRepository
            .getChatIds(type);
}

接口

GET /ai/history/chat

返回:

[
  "1001",
  "1002",
  "1003"
]

getChatIds实现

建议放图:

@Override
public List<String> getChatIds(String type) {

    return chatHistory.getOrDefault(
            type,
            List.of()
    );
}

作用

如果不存在该业务类型:

chat
pdf
service

直接返回空集合:

[]

避免空指针异常。


查询具体会话内容

@GetMapping("/{type}/{chatId}")
public List<MessageVO> getChatHistory(
        @PathVariable("type") String type,
        @PathVariable("chatId") String chatId) {

    List<Message> messages =
            chatMemory.get(chatId);

    if(messages == null){
        return List.of();
    }

    return messages.stream()
            .map(MessageVO::new)
            .toList();
}

查询流程

chatId
   │
   ▼
ChatMemory
   │
   ▼
获取Message集合
   │
   ▼
转换为MessageVO
   │
   ▼
返回前端

MessageVO作用

@Data
public class MessageVO {

    private String role;

    private String content;
}

转换角色

switch(message.getMessageType())

Spring AI返回:

USER
ASSISTANT

转换成:

{
  "role":"user",
  "content":"你好"
}
{
  "role":"assistant",
  "content":"你好,请问有什么可以帮助您?"
}

方便前端渲染聊天记录。

┌────────────────────┐
│                  用户发送消息            │
│                 prompt + chatId         │
└─────────┬──────────┘
                          │
                         ▼
┌────────────────────┐
│              ChatController.chat       │
│                 (聊天入口)                  │
└─────────┬──────────┘
                          │
          ├─────────────────────────────┐
          │                                                                           │
          ▼                                                                         ▼

【1. 保存会话ID】                               【2. AI对话并保存聊天记录】

chatHistoryRepository.save()                          chatClient.prompt()
          │                                                                           │
          ▼                                                                         ▼
InMemoryChatHistoryRepository                              advisors()
          │                                                                      设置会话ID
          ▼                                                                         │
Map<String,List<String>>                                             ▼
          │                                                              ChatMemory
          │                                                                         │
         ▼                                                                       ▼
保存 chatId                                                  保存用户消息和AI回复

(type=chat)
chat
 ├── chat001
 ├── chat002
 └── chat003

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐