Spring Boot整合Ollama:本地大模型开发实战指南
1. 为什么选择Spring Boot + Ollama?
如果你和我一样,是个Java开发者,最近肯定被各种AI大模型的消息刷屏了。看着别人用Python、Node.js轻松调用GPT、Claude,心里是不是有点痒?但一想到要把公司的业务数据送到别人的云端服务器,心里又直打鼓:数据安全怎么办?调用成本怎么控制?网络延迟会不会影响用户体验?
别急,我踩过这些坑,也找到了一个特别适合咱们Java开发者的解决方案:用Spring Boot整合Ollama,在本地跑大模型。
我简单说说这么干的好处。第一,数据完全本地化,你的对话记录、业务数据不出内网,安全可控,特别适合金融、医疗这些对隐私要求高的场景。第二,零API调用费用,模型下载下来,电费就是主要成本,想怎么测试就怎么测试,再也不用盯着账单心惊肉跳了。第三,网络零延迟,模型就在你本机或者内网服务器上,响应速度飞快,用户体验直接拉满。
Ollama是什么?你可以把它理解成大模型领域的Docker。它把各种开源大模型(比如Llama、Qwen、DeepSeek)打包成一个个“镜像”,你只需要一条命令就能拉取、运行。它自带一个标准的HTTP API(默认端口11434),这样我们的Spring Boot应用就能像调用任何其他REST服务一样去调用大模型了。
而Spring AI,是Spring官方推出的AI工程框架。它的目标很明确:把Spring生态那套熟悉的开发体验带到AI领域。你不用关心底层是调用的OpenAI还是Ollama,它提供了一套统一的API。今天我们用Ollama,明天想换别的供应商,业务代码几乎不用动。这对于追求稳定和可移植性的企业应用来说,简直是福音。
所以,这个组合可以理解为:Ollama负责提供稳定、易用的本地模型服务,Spring Boot负责构建健壮的业务应用,Spring AI则在中间充当“翻译官”和“粘合剂”。接下来,我就带你一步步把这个组合搭建起来。
2. 环境准备:给大模型一个家
工欲善其事,必先利其器。在写代码之前,我们得先把Ollama和模型准备好。这块我会讲得细一点,因为很多坑都埋在这里。
2.1 硬件与软件基础
首先看硬件。跑大模型,内存是关键。我以最常用的7B参数模型为例:
- 8GB内存:这是底线,能跑起来,但可能会比较卡顿,尤其是同时运行IDE和其他服务时。
- 16GB内存:推荐配置,运行流畅,还能留出余量给开发环境。
- 32GB+内存:体验最佳,可以尝试运行13B甚至更大模型,或者同时运行多个服务。
如果有NVIDIA显卡(GPU),那将是质的飞跃。Ollama能自动利用GPU进行推理加速,速度能提升几倍到几十倍。用nvidia-smi命令可以检查你的显卡是否被识别。CPU的话,现代的多核处理器(比如Intel i5/R5以上)就够用。
软件方面,你需要:
- 操作系统:Windows 10/11, macOS 或者 Linux 都可以。Ollama对主流系统支持都很好。
- Java开发环境:JDK 17或以上。Spring Boot 3.x 必须依赖JDK 17+,这是硬性要求。建议直接安装JDK 21 LTS版本。
- Maven 3.6+ 或 Gradle:用于项目管理。
- IDE:IntelliJ IDEA(首选)或 VS Code 等。
2.2 安装与配置Ollama
Ollama的安装简单到不可思议。
对于Windows/macOS用户:直接访问 Ollama官网,下载安装包,像安装普通软件一样完成安装。安装后,它通常会作为后台服务自动运行。
对于Linux用户,一条命令搞定:
curl -fsSL https://ollama.com/install.sh | sh
安装完成后,打开你的终端(Windows用PowerShell或CMD),输入 ollama --version 验证是否安装成功。
接下来是下载模型。Ollama的模型库很丰富,我们选一个对中文友好且性能不错的模型作为起步。我推荐 Qwen2.5:7B 或者 DeepSeek-R1:7B。
在终端中执行:
# 拉取Qwen2.5 7B模型
ollama pull qwen2.5:7b
# 或者拉取DeepSeek-R1 7B模型
ollama pull deepseek-r1:7b
这个过程需要下载几个GB的模型文件,时间取决于你的网速。喝杯咖啡等待一下。
下载完成后,运行模型进行测试:
ollama run qwen2.5:7b
你会进入一个交互式命令行,直接输入“你好”,看看模型是否能正常用中文回复你。按 Ctrl+D 可以退出。
关键一步:确保Ollama服务在运行。上面ollama run是前台运行测试。实际开发中,我们需要Ollama作为后台服务。通常安装程序已经帮你设置好了。你可以通过访问 http://localhost:11434 来验证。如果看到Ollama的API响应(可能是个404页面,但服务是活的),或者用命令 ollama serve 启动服务并保持终端打开。
3. 创建Spring Boot项目并集成Spring AI
好了,基础环境打牢,现在进入我们熟悉的Java领域。
3.1 使用Spring Initializr快速初始化
最方便的方法是使用 start.spring.io。我帮你把关键选项列出来:
- Project: Maven
- Language: Java
- Spring Boot: 选择最新的稳定版,比如
3.3.3 - Group/Artifact: 按你的项目命名,例如
com.example/ai-demo - Packaging: Jar
- Java: 17 或 21
在 Dependencies 一栏,点击“Add Dependencies”,添加:
- Spring Web:构建Web API必备。
- Lombok:简化POJO代码,选它。
- Spring AI:注意,在Initializr的列表里,你可能需要手动输入。更常见的做法是生成项目后,手动修改
pom.xml。
点击“Generate”下载项目压缩包,然后用IDE打开。
3.2 配置Maven依赖与仓库
打开项目中的 pom.xml 文件,这是关键。因为Spring AI的依赖还没有发布到Maven中央仓库,所以我们需要添加Spring的里程碑仓库。
在 <project> 标签下,添加或修改 <properties> 和 <repositories>:
<properties>
<java.version>21</java.version>
<!-- 锁定Spring AI版本,推荐使用1.0.0-M6或更高里程碑版本 -->
<spring-ai.version>1.0.0-M6</spring-ai.version>
</properties>
<!-- 添加Spring仓库 -->
<repositories>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
<!-- 如果需要快照版,可以添加snapshots仓库 -->
<repository>
<id>spring-snapshots</id>
<name>Spring Snapshots</name>
<url>https://repo.spring.io/snapshot</url>
<releases>
<enabled>false</enabled>
</releases>
</repository>
</repositories>
然后在 <dependencies> 部分,确保有以下依赖:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- Spring AI Ollama 集成 Starter -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-ollama-spring-boot-starter</artifactId>
<version>${spring-ai.version}</version>
</dependency>
<!-- 测试依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
保存文件,IDE会自动下载依赖。如果网络遇到问题,耐心等待或检查网络连接。
3.3 配置应用连接Ollama
接下来配置Spring Boot如何找到我们本地的Ollama服务。打开 src/main/resources/application.yml(如果没有就创建,或者用application.properties)。
我更喜欢YAML的清晰结构:
server:
port: 8080 # 我们的应用端口
spring:
application:
name: spring-ai-ollama-demo
ai:
ollama:
# Ollama服务的地址,默认就在本机11434端口
base-url: http://localhost:11434
chat:
# 指定我们要使用的模型,必须和ollama pull下来的名字一致
options:
model: qwen2.5:7b # 或者 deepseek-r1:7b
temperature: 0.7 # 创造性,0-1,越高回答越随机
top-p: 0.9 # 核采样参数,影响多样性
这里最重要的就是 spring.ai.ollama.base-url 和 spring.ai.ollama.chat.options.model,确保它们和你的Ollama环境匹配。temperature 参数你可以先按0.7来,这个值在0到1之间,值越高,模型的回答越天马行空;值越低,回答就越保守和确定。
4. 开发第一个AI对话接口
配置搞定,现在可以写代码了。我们会创建两个最常用的接口:同步响应和流式响应。
4.1 注入ChatClient并实现同步调用
Spring AI提供了不同层次的抽象。对于刚入门,我推荐使用 ChatClient,它比直接使用 ChatModel 更友好,功能也更丰富。
首先,创建一个Controller:
package com.example.aimodemo.controller;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor;
import org.springframework.ai.chat.memory.InMemoryChatMemory;
import org.springframework.ai.ollama.api.OllamaOptions;
import org.springframework.web.bind.annotation.*;
import reactor.core.publisher.Flux;
@RestController
@RequestMapping("/api/chat")
public class ChatController {
private final ChatClient chatClient;
// 通过构造器注入ChatClient
public ChatController(ChatClient.Builder chatClientBuilder) {
this.chatClient = chatClientBuilder
.defaultAdvisors(new MessageChatMemoryAdvisor(new InMemoryChatMemory())) // 添加对话记忆
.defaultOptions(OllamaOptions.builder()
.temperature(0.7)
.topP(0.9)
.build()) // 设置默认模型参数
.build();
}
/**
* 同步调用接口
* 提问后,等待模型完全生成答案,一次性返回
*/
@GetMapping("/ask")
public String askQuestion(@RequestParam String message) {
return chatClient.prompt()
.user(message) // 用户输入
.call() // 同步调用
.content(); // 获取文本内容
}
}
我来解释一下这段代码:
ChatClient.Builder是Spring AI自动配置好的,我们直接注入使用。.defaultAdvisors(...)添加了一个“对话记忆顾问”。InMemoryChatMemory会在内存中记住当前会话的上下文,这样你问“你好”,再问“我叫小明”,然后问“我叫什么?”,模型有可能知道你在说“小明”。(注意:这是一个简单的内存实现,重启应用会丢失,生产环境需要更持久化的方案)。.defaultOptions(...)设置了每次请求的默认参数,这里覆盖了application.yml中的部分设置。chatClient.prompt().user(message).call().content()这是一个流畅的API调用链,非常直观:创建提示 -> 设置用户消息 -> 调用模型 -> 获取内容。
启动你的Spring Boot应用(运行 Application 类的main方法),然后用浏览器或Postman测试:
GET http://localhost:8080/api/chat/ask?message=用Java写一个Hello World程序
你应该能收到一段Java代码作为回复。
4.2 实现流式响应(SSE)
同步调用对于短回答没问题,但如果模型需要生成一篇长文,用户会等很久才看到结果。流式响应(Server-Sent Events, SSE)可以让答案像打字一样一个字一个字地“流”回来,体验好很多。
在刚才的 ChatController 里增加一个方法:
/**
* 流式调用接口
* 答案会以数据流的形式逐步返回,适合前端实时显示
*/
@GetMapping(value = "/ask-stream", produces = "text/event-stream")
public Flux<String> askQuestionStream(@RequestParam String message) {
return chatClient.prompt()
.user(message)
.stream() // 关键:使用stream()而不是call()
.content();
}
注意这里的区别:
- 返回类型是
Flux<String>,这是Project Reactor的响应式流类型。 produces = "text/event-stream"声明这个接口返回的是SSE流。- 调用
.stream()方法,它会返回一个流。
测试这个接口,你需要使用支持SSE的客户端。用浏览器打开开发者工具,在Console里可以测试:
const eventSource = new EventSource('http://localhost:8080/api/chat/ask-stream?message=讲一个笑话');
eventSource.onmessage = (event) => {
console.log(event.data); // 你会看到笑话逐字出现
};
或者在Postman中,选择GET请求,在Tests标签页里写脚本监听。流式响应对于构建类似ChatGPT的聊天界面至关重要。
5. 进阶功能与实战技巧
基本的对话跑通了,我们来看看如何让它变得更实用、更强大。
5.1 提示词工程:让AI更懂你
直接扔问题给模型,得到的回答可能很泛。通过“提示词工程”,我们可以引导模型扮演特定角色,给出更符合预期的答案。
我们可以创建一个提示词模板服务:
package com.example.aimodemo.service;
import org.springframework.stereotype.Service;
@Service
public class PromptTemplateService {
private static final String CODE_ASSISTANT_TEMPLATE = """
你是一个资深的Java开发专家。请严格按照以下要求回答:
1. 代码示例必须准确、可运行。
2. 优先使用Java 17+的语法特性。
3. 解释要简洁,一针见血。
用户问题:{question}
""";
private static final String TRANSLATION_TEMPLATE = """
你是一个专业的翻译官。请将以下中文翻译成英文,要求:
1. 翻译准确,符合技术文档风格。
2. 保持专业术语的一致性。
待翻译文本:{text}
""";
public String buildCodePrompt(String question) {
return CODE_ASSISTANT_TEMPLATE.replace("{question}", question);
}
public String buildTranslationPrompt(String text) {
return TRANSLATION_TEMPLATE.replace("{text}", text);
}
}
然后在Controller里注入这个Service,在提问前先构建提示词:
String formattedPrompt = promptTemplateService.buildCodePrompt(userMessage);
String answer = chatClient.prompt().user(formattedPrompt).call().content();
这样一来,当你问“怎么遍历列表?”,模型会以Java专家的身份,给出带有代码示例的专业回答,而不是泛泛而谈。
5.2 处理多轮对话(上下文管理)
前面的 InMemoryChatMemory 提供了基础的记忆功能,但它默认可能只记住最近几条消息。我们可以更精细地控制。
Spring AI的 MessageChatMemoryAdvisor 需要你提供一个 ConversationId 来区分不同的对话会话。我们来改造一下Controller,支持基于会话的连续对话:
@RestController
@RequestMapping("/api/chat")
public class AdvancedChatController {
private final ChatClient chatClient;
public AdvancedChatController(ChatClient.Builder chatClientBuilder) {
this.chatClient = chatClientBuilder
// 注意:这里不再设置默认的Advisor,我们在每次请求时动态指定
.defaultOptions(OllamaOptions.builder().temperature(0.7).build())
.build();
}
@PostMapping("/conversation")
public String chatWithMemory(@RequestParam String message,
@RequestParam(defaultValue = "default-conversation") String conversationId) {
// 为每次请求创建带有特定conversationId的记忆顾问
ChatClient tailoredClient = this.chatClient.mutate()
.defaultAdvisors(new MessageChatMemoryAdvisor(
new InMemoryChatMemory(), // 可以替换为Redis等持久化实现
conversationId))
.build();
return tailoredClient.prompt()
.user(message)
.call()
.content();
}
}
这样,前端在发起一个对话序列时,可以传一个固定的 conversationId(比如用户ID+时间戳),那么模型就会记住这个会话窗口内的所有历史。当你问“我上面提到的那个方法,参数是什么?”,模型就有可能从上下文中找到答案。
5.3 文件内容读取与问答(RAG雏形)
一个更酷的功能是:让AI回答你上传文档里的问题。这需要“检索增强生成”(RAG)的思路。这里我给出一个极度简化的版本,演示如何读取文本文件并让AI基于此回答。
首先,添加一个处理文档的Service:
package com.example.aimodemo.service;
import org.springframework.ai.reader.tika.TikaDocumentReader;
import org.springframework.ai.document.Document;
import org.springframework.core.io.Resource;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.util.List;
import java.util.stream.Collectors;
@Service
public class DocumentQAService {
public String extractTextFromFile(MultipartFile file) throws IOException {
// 使用Spring AI内置的Tika解析器,支持PDF、Word、PPT、TXT等
// 这里简化处理,假设是文本文件
return new String(file.getBytes());
}
public String askQuestionAboutDocument(String documentText, String question) {
// 将文档内容和问题组合成一个详细的提示词
String prompt = String.format("""
请根据以下文档内容回答问题。如果文档中没有明确答案,请说“根据文档无法找到确切答案”。
文档内容:
%s
问题:%s
答案:
""", documentText, question);
// 在实际RAG中,这里应该:
// 1. 将文档切分成片段(Chunk)
// 2. 将片段向量化并存入向量数据库
// 3. 根据问题检索最相关的几个片段
// 4. 将相关片段和问题一起发给模型
// 此处为演示,直接将全部文档内容发送(仅适合小文档)
return prompt;
}
}
然后创建一个新的Controller端点:
@PostMapping(value = "/ask-with-doc", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public String askWithDocument(@RequestParam String question,
@RequestParam MultipartFile document) throws IOException {
String docText = documentQAService.extractTextFromFile(document);
String fullPrompt = documentQAService.askQuestionAboutDocument(docText, question);
return chatClient.prompt()
.user(fullPrompt)
.call()
.content();
}
这个例子虽然简单,但展示了核心流程。在生产环境中,你需要集成像Chroma、Milvus这样的向量数据库,以及Spring AI的 VectorStore 和 EmbeddingClient 来完成真正的语义检索,避免每次都将巨大文档全文发送给模型。
6. 调优、监控与问题排查
项目跑起来了,但怎么让它跑得更好、更稳呢?这部分分享一些我踩坑得来的经验。
6.1 关键模型参数调优
在 application.yml 或代码的 OllamaOptions 里,有几个参数直接影响输出质量和速度:
spring:
ai:
ollama:
chat:
options:
model: qwen2.5:7b
temperature: 0.7 # (0-1) 核心参数。写代码、事实问答建议0.1-0.3;创意写作、聊天建议0.7-0.9。
top-p: 0.9 # (0-1) 核采样。与temperature配合,通常0.8-0.95。
top-k: 40 # 只从概率最高的k个词中选。值越小越确定,越大越多样。
num-predict: 512 # 模型生成的最大token数。设太小会截断,设太大会生成无关内容。
repeat-penalty: 1.1 # 对重复内容的惩罚因子。>1.0可减少重复,设太高可能导致语法错误。
我的经验是:先保持默认,如果发现回答太啰嗦就降低temperature和top-p;如果发现总是重复,就提高repeat-penalty。
6.2 配置连接与超时
Ollama推理可能需要时间,尤其是长文本或复杂问题。我们需要配置合理的超时,避免HTTP请求断开。
在 application.yml 中添加:
spring:
ai:
ollama:
client:
connect-timeout: 30s # 连接Ollama服务的超时
read-timeout: 300s # 读取响应的超时,生成长文本时需要设长一点
如果遇到超时错误,先检查是不是 num-predict 设得太大,导致生成时间过长。对于流式响应,读超时通常不适用,因为连接是持久的。
6.3 日志记录与监控
调试AI应用,看清模型“吃进去什么,吐出来什么”很重要。我们可以配置一个拦截器来记录所有请求和响应。
创建一个配置类:
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.ollama.api.OllamaApi;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
@Slf4j
@Configuration
public class OllamaLoggingConfig {
@Bean
@Primary
public OllamaApi loggingOllamaApi(OllamaApi delegate) {
return new OllamaApi() {
@Override
public GenerateResponse generate(GenerateRequest request) {
log.info(">>> 发送给Ollama的请求: 模型={}, 提示词长度={}", request.model(), request.prompt().length());
// 谨慎记录完整提示词,可能包含敏感信息
// log.debug("完整提示词: {}", request.prompt());
GenerateResponse response = delegate.generate(request);
log.info("<<< 收到Ollama响应: token使用量={}", response.evalCount());
log.debug("响应内容: {}", response.response());
return response;
}
// 需要实现OllamaApi接口的其他方法...
};
}
}
这样,在 application.yml 中设置 logging.level.com.example.aimodemo.config.OllamaLoggingConfig=DEBUG,就能在控制台看到详细的交互日志,对于排查“为什么模型不回答”这类问题非常有用。
6.4 常见问题与解决
-
错误:
Connection refused连接到 localhost:11434- 检查:Ollama服务是否真的在运行?在终端执行
ollama list看看。 - 解决:运行
ollama serve并保持终端打开,或者将其配置为系统服务。
- 检查:Ollama服务是否真的在运行?在终端执行
-
错误:
Model 'qwen2.5:7b' not found- 检查:模型名是否拼写错误?用
ollama list查看本地已下载的模型列表。 - 解决:执行
ollama pull qwen2.5:7b下载正确模型。
- 检查:模型名是否拼写错误?用
-
响应速度极慢
- 检查:CPU占用是否100%?任务管理器中查看。
- 解决:确认是否使用了GPU。Ollama默认会尝试使用GPU。在终端运行Ollama时,观察启动日志是否有“Using GPU”字样。确保已安装正确的显卡驱动(如NVIDIA CUDA)。
-
流式响应不“流”
- 检查:前端是否正确处理了
text/event-stream?浏览器网络标签查看响应类型。 - 解决:确保Controller方法使用了
produces = "text/event-stream"且返回Flux<String>。用简单的SSE客户端脚本测试。
- 检查:前端是否正确处理了
-
内存不足(OOM)
- 现象:应用或Ollama进程崩溃。
- 解决:尝试更小的模型(如3B参数模型),或者为JVM和系统预留更多内存。关闭不必要的应用程序。
把这些步骤走完,你应该已经拥有了一个完全在本地运行、功能可扩展的Spring Boot AI应用。从简单的问答到带上下文的对话,再到基于文档的查询,这套基础框架都能支撑。最重要的是,你完全掌控了数据和流程,这对于很多实际项目来说,是选择技术方案的首要前提。剩下的,就是根据你的具体业务需求,在这些骨架上添加血肉了。
更多推荐



所有评论(0)