从零搭建 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. OllamaSpring 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(); } }创建 ControllerChatController 控制器说明该控制器用于对外提供 AI 对话接口通过注入ChatClient与大模型进行交互并将模型返回结果直接响应给前端。代码解析RestController标识为 Spring Boot REST 控制器返回 JSON 或字符串数据。RequestMapping(/ai)统一设置接口访问前缀为/ai。RequiredArgsConstructorLombok 自动生成构造方法实现ChatClient的依赖注入。private final ChatClient chatClient注入 Spring AI 提供的聊天客户端对象。chat()方法接收用户输入的prompt调用大模型生成回复并返回结果。prompt()创建对话请求。user(prompt)设置用户问题。call()发送请求到大模型。content()获取模型返回的文本内容。七、解决 Lombok 报错如果RequiredArgsConstructor出现红色报错。需要在 pom.xml 添加dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId version1.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;修改 ControllerRequestMapping( value /chat, produces text/html;charsetUTF-8 ) public FluxString chat(String prompt){ return chatClient .prompt(prompt) .stream() .content(); }流式输出代码流式输出解释ChatController.java在ChatController中使用了Spring WebFlux的FluxString来实现流式响应核心点说明流式输出 (.stream())调用chatClient.prompt(prompt).stream()会以流的方式返回 AI 生成的内容。优点前端可以实时接收到生成结果而不是等待整个回答完成。适合长文本或对话机器人场景。返回类型FluxStringFlux是 Spring WebFlux 提供的响应式类型可以支持多次异步数据推送。每生成一段文本就通过Flux发送给前端实现“边生成边显示”。为什么要设置producesproduces text/html;charsetUTF-8明确告诉浏览器返回内容的类型和编码防止中文乱码。对于流式输出Spring 会以Server-Sent Events (SSE)或分块响应的方式发送文本流前端可以逐段解析和渲染。流式输出效果模型边生成边返回十、自定义 AI 人设很多场景下需要让 AI 扮演固定角色。例如客服面试官英语老师编程导师修改CommonConfigurationBean 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配置时增加日志 AdvisorBean 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 的多会话能力需要解决三个问题管理会话ID保存会话ID查询历史会话整体流程如下用户发起聊天 │ ▼ 携带 chatId │ ▼ 保存 chatId │ ▼ 调用 AI │ ▼ 绑定 chatId 到 Memory │ ▼ 保存上下文消息 │ ▼ 查询历史会话一、管理会话ID功能说明会话IDchatId用于区分不同聊天窗口。例如chat-001 ├─ 你好 ├─ 你是谁 └─ 今天天气怎么样 chat-002 ├─ Java是什么 ├─ SpringBoot是什么 └─ 如何学习JavaAI会根据不同的 chatId 维护不同的上下文。ChatHistoryRepository接口public interface ChatHistoryRepository { void save(String type, String chatId); ListString getChatIds(String type); }作用定义会话管理规范save()保存会话IDgetChatIds()获取某业务下的所有会话ID例如chat service pdf不同业务可以拥有自己的会话列表。二、保存会话ID功能说明当用户第一次进入聊天页面时需要记录当前会话。例如chat ├─ 1001 ├─ 1002 └─ 1003后续用户可以看到自己的历史会话。InMemoryChatHistoryRepository实现数据结构private final MapString, ListString chatHistory new HashMap();结构如下{ chat:[ 1001, 1002, 1003 ], pdf:[ 2001, 2002 ] }save方法Override public void save(String type, String chatId) { ListString 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()); } ListString 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 ListString getChatIds( PathVariable(type) String type){ return chatHistoryRepository .getChatIds(type); }接口GET /ai/history/chat返回[ 1001, 1002, 1003 ]getChatIds实现建议放图Override public ListString getChatIds(String type) { return chatHistory.getOrDefault( type, List.of() ); }作用如果不存在该业务类型chat pdf service直接返回空集合[]避免空指针异常。查询具体会话内容GetMapping(/{type}/{chatId}) public ListMessageVO getChatHistory( PathVariable(type) String type, PathVariable(chatId) String chatId) { ListMessage 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▼ │MapString,ListString ▼│ ChatMemory│ │▼ ▼保存 chatId 保存用户消息和AI回复(typechat)chat├── chat001├── chat002└── chat003