1. 为什么你需要Spring AI + Ollama?
如果你是一名Java开发者,最近肯定被各种AI大模型的消息刷屏了。但你可能也遇到了几个头疼的问题:调用OpenAI、DeepSeek这些云端API,一来有网络限制,二来数据要出境,三来费用也不便宜。更关键的是,很多企业内部项目对数据安全有严格要求,根本不允许把数据传到外部服务。
我刚开始接触大模型时也踩过这些坑。后来发现,其实有个很香的方案:在本地电脑上跑大模型。听起来是不是很酷?这就是Ollama能干的事。而Spring AI,就是Spring官方推出的AI集成框架,它能让你像调用普通服务一样调用大模型,代码写起来特别“Spring范儿”。
简单来说,这个组合能给你带来:
- 数据完全本地化:所有对话、处理都在你电脑上完成,隐私安全有保障
- 零API费用:一次部署,无限次调用,再也不用担心账单超标
- 开发体验统一:用你熟悉的Spring Boot方式开发AI应用,学习成本极低
- 灵活选模型:Llama 3、Qwen、DeepSeek,想用哪个就拉哪个,随时切换
我实测下来,在一台16GB内存的MacBook Pro上跑7B参数的模型,响应速度完全能满足开发测试需求。下面我就带你从零开始,一步步搭建这个环境。
2. 环境准备:硬件、软件和模型选择
2.1 硬件要求:你的电脑够用吗?
很多人担心本地跑大模型需要顶级显卡,其实不然。我用自己的开发机(MacBook Pro 16GB)和一台老款Windows笔记本(8GB内存)都试过,完全可行。
最低配置建议:
- CPU:近5年的Intel i5或AMD Ryzen 5以上
- 内存:8GB(跑7B模型勉强够用),强烈推荐16GB以上
- 硬盘:至少20GB可用空间(模型文件都很大)
- 显卡:有N卡最好(能加速),没有也能跑CPU版本
我的实测数据:
- Qwen2.5:7B:8GB内存下占用约5GB,响应速度2-5秒
- Llama 3.2:3B:8GB内存下占用约3GB,响应速度1-3秒
- DeepSeek-R1:7B:需要16GB内存,响应速度3-8秒
如果你只有8GB内存,我建议从Qwen2.5:1.8B或Llama 3.2:1B这种小模型开始,体验会好很多。
2.2 软件安装:三件套搞定
第一步:安装Ollama这是最核心的一步。Ollama就像大模型领域的Docker,一条命令就能拉取和运行模型。
去官网下载对应系统的安装包(https://ollama.com),安装过程就是一路下一步。安装完成后,打开终端(Mac/Linux)或PowerShell(Windows),输入:
ollama --version看到版本号就说明安装成功了。
第二步:拉取你的第一个模型Ollama支持很多开源模型,我推荐新手从这几个开始:
# 中文理解不错的轻量模型 ollama pull qwen2.5:1.8b # 英文表现更好的模型 ollama pull llama3.2:1b # 如果想试试更大的(确保内存够) ollama pull deepseek-r1:7b拉取过程可能会比较久,取决于你的网速。我拉取7B模型大概用了半小时。拉取完成后,运行这个命令测试:
ollama run qwen2.5:1.8b你会进入一个交互式对话界面,输入“你好”看看效果。按Ctrl+D退出。
第三步:验证Ollama服务Ollama默认在11434端口启动HTTP服务。打开浏览器访问:
http://localhost:11434应该能看到Ollama的欢迎页面。更实用的验证方式是:
# 查看已安装的模型 ollama list # 查看模型详情 ollama show qwen2.5:1.8b # 调用API测试 curl http://localhost:11434/api/generate -d '{ "model": "qwen2.5:1.8b", "prompt": "你好,介绍一下你自己", "stream": false }'如果一切正常,恭喜你,本地大模型环境已经准备好了!
3. 创建Spring Boot项目并集成Spring AI
3.1 项目初始化:避开那些坑
我建议直接用Spring Initializr创建项目,省去手动配置的麻烦。访问 https://start.spring.io,按这个配置选择:
- Project:Maven
- Language:Java
- Spring Boot:3.2.5或更高(我用的3.2.6)
- Java:17或21(推荐21,性能更好)
- Dependencies:
- Spring Web
- Lombok
- Spring Boot DevTools(热部署,开发必备)
注意:Spring AI依赖需要特殊配置Spring AI的依赖不在默认的Maven中央仓库,需要手动添加仓库地址。这是第一个容易踩坑的地方。
下载生成的项目后,打开pom.xml,在<project>标签内添加:
<properties> <java.version>21</java.version> <!-- 使用最新的稳定版 --> <spring-ai.version>1.0.0</spring-ai.version> </properties> <repositories> <repository> <id>spring-snapshots</id> <name>Spring Snapshots</name> <url>https://repo.spring.io/snapshot</url> <releases> <enabled>false</enabled> </releases> </repository> <repository> <id>spring-milestones</id> <name>Spring Milestones</name> <url>https://repo.spring.io/milestone</url> <snapshots> <enabled>false</enabled> </snapshots> </repository> </repositories>然后在<dependencies>中添加Spring AI的Ollama starter:
<dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-ollama-spring-boot-starter</artifactId> </dependency>为什么这么配置?
- Spring AI 1.0.0是第一个稳定版,API相对成熟
- Spring Snapshots仓库包含最新的快照版本,Milestones包含里程碑版本
- 不配置这些仓库,Maven会找不到Spring AI依赖
3.2 配置文件:连接本地大模型
接下来配置application.yml(我习惯用yaml,更清晰):
server: port: 8080 # 默认端口,可以按需修改 spring: application: name: spring-ai-ollama-demo ai: ollama: base-url: http://localhost:11434 # Ollama服务地址 chat: options: model: qwen2.5:1.8b # 默认使用的模型 temperature: 0.7 # 创造性,0-1之间,越高回答越随机 top-p: 0.9 # 核采样,控制回答多样性 num-predict: 512 # 最大生成token数关键参数解释:
temperature:我一般设0.7,平衡创造性和准确性。写代码建议0.3-0.5,创意写作可以0.8-1.0top-p:和temperature配合使用,0.9是个不错的默认值num-predict:限制回答长度,防止生成过长的废话
如果你有多个模型,可以在代码中动态指定,这里只是默认配置。
4. 基础API开发:同步与流式调用
4.1 同步调用:最简单的聊天接口
先创建一个简单的Controller,实现最基本的问答功能:
import org.springframework.ai.chat.ChatResponse; import org.springframework.ai.chat.prompt.Prompt; import org.springframework.ai.ollama.OllamaChatClient; import org.springframework.ai.ollama.api.OllamaOptions; import org.springframework.web.bind.annotation.*; @RestController @RequestMapping("/api/chat") public class ChatController { private final OllamaChatClient chatClient; // 构造器注入,Spring推荐的方式 public ChatController(OllamaChatClient chatClient) { this.chatClient = chatClient; } @GetMapping("/simple") public String simpleChat(@RequestParam String message) { // 创建提示,可以指定模型参数 Prompt prompt = new Prompt( message, OllamaOptions.builder() .model("qwen2.5:1.8b") .temperature(0.7f) .build() ); // 同步调用,等待完整响应 ChatResponse response = chatClient.call(prompt); // 提取回答内容 return response.getResult().getOutput().getContent(); } }启动应用,用浏览器或Postman测试:
http://localhost:8080/api/chat/simple?message=用Java写一个Hello World程序你会看到模型返回的代码。但这里有个问题:如果回答很长,用户要等很久才能看到结果。这时候就需要流式响应。
4.2 流式调用:像ChatGPT一样实时显示
流式调用是现在AI应用的标配,用户能看到文字一个个蹦出来的效果。Spring AI对此支持得很好:
import reactor.core.publisher.Flux; import org.springframework.http.MediaType; @GetMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE) public Flux<String> streamChat(@RequestParam String message) { Prompt prompt = new Prompt(message); return chatClient.stream(prompt) .map(chatResponse -> { // 每次只返回新增的内容 String content = chatResponse.getResult().getOutput().getContent(); return content; }); }前端如何配合?如果你有前端页面,可以这样调用:
<script> async function streamChat() { const question = document.getElementById('question').value; const answerDiv = document.getElementById('answer'); answerDiv.innerHTML = ''; // 使用EventSource接收流式响应 const eventSource = new EventSource( `/api/chat/stream?message=${encodeURIComponent(question)}` ); eventSource.onmessage = (event) => { answerDiv.innerHTML += event.data; }; eventSource.onerror = () => { eventSource.close(); }; } </script>我踩过的坑:
- 响应头必须正确:
produces = MediaType.TEXT_EVENT_STREAM_VALUE不能少 - Flux要正确处理:Spring WebFlux和传统Servlet的流式处理方式不同
- 超时设置:长时间对话要配置超时,默认可能只有30秒
4.3 高级参数调优:让回答更符合预期
不同的场景需要不同的模型参数。我整理了几个常用配置:
public ChatResponse chatWithOptions(String message, String scenario) { OllamaOptions.Builder optionsBuilder = OllamaOptions.builder() .model("qwen2.5:1.8b"); // 根据场景调整参数 switch (scenario) { case "code": // 写代码:降低随机性,提高准确性 optionsBuilder .temperature(0.3f) .topP(0.8f) .numPredict(1024); // 代码可能较长 break; case "creative": // 创意写作:提高创造性 optionsBuilder .temperature(0.9f) .topP(0.95f) .repeatPenalty(1.1f); // 避免重复 break; case "analysis": // 分析任务:平衡型 optionsBuilder .temperature(0.5f) .topP(0.9f) .numPredict(256); // 分析回答不宜过长 break; default: optionsBuilder.temperature(0.7f); } Prompt prompt = new Prompt(message, optionsBuilder.build()); return chatClient.call(prompt); }参数经验值:
- 写代码:temperature 0.2-0.4,topP 0.8-0.9
- 翻译任务:temperature 0.1-0.3,追求准确
- 故事生成:temperature 0.8-1.0,topP 0.95
- 逻辑推理:temperature 0.4-0.6,保持一定创造性
5. 实战:构建一个完整的问答系统
5.1 服务层设计:业务与AI分离
在实际项目中,我建议把AI调用封装在Service层,这样业务逻辑更清晰:
import org.springframework.stereotype.Service; import org.springframework.ai.chat.client.ChatClient; import org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor; import org.springframework.ai.chat.memory.InMemoryChatMemory; @Service public class AIService { private final ChatClient chatClient; public AIService(ChatClient.Builder chatClientBuilder) { // 使用ChatClient.Builder,功能更强大 this.chatClient = chatClientBuilder .defaultAdvisors( new MessageChatMemoryAdvisor(new InMemoryChatMemory()) ) .defaultOptions( OllamaOptions.builder() .model("qwen2.5:1.8b") .temperature(0.7f) .build() ) .build(); } // 带上下文的对话 public String chatWithContext(String sessionId, String userMessage) { return chatClient.prompt() .user(userMessage) .advisors(a -> a.param("chatMemoryConversationId", sessionId)) .call() .content(); } // 带系统指令的对话 public String chatWithSystemPrompt(String userMessage) { String systemPrompt = """ 你是一个专业的Java开发助手,请遵循以下规则: 1. 代码要规范,有详细注释 2. 解释要通俗易懂 3. 如果不知道,直接说不知道,不要编造 """; return chatClient.prompt() .system(systemPrompt) .user(userMessage) .call() .content(); } }ChatClient vs OllamaChatClient:
OllamaChatClient:简单直接,适合快速上手ChatClient:功能更丰富,支持Advisor、Memory等高级特性- 我建议从
OllamaChatClient开始,熟悉后再用ChatClient
5.2 对话记忆:实现多轮对话
AI应用的核心价值之一就是能记住上下文。Spring AI提供了内置的Memory支持:
import org.springframework.ai.chat.memory.ChatMemory; import org.springframework.ai.chat.memory.InMemoryChatMemory; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class ChatConfig { @Bean public ChatMemory chatMemory() { // 使用内存存储,生产环境可以考虑Redis return new InMemoryChatMemory(); } @Bean public ChatClient chatClient(ChatModel chatModel, ChatMemory chatMemory) { return ChatClient.builder(chatModel) .defaultAdvisors( new MessageChatMemoryAdvisor(chatMemory) ) .defaultSystemText(""" 你是一个有帮助的AI助手,回答要简洁准确。 如果用户的问题涉及代码,请提供可运行的示例。 """) .build(); } } // 在Controller中使用 @PostMapping("/conversation") public String continueConversation( @RequestParam String sessionId, @RequestParam String message) { // 相同的sessionId会共享对话历史 return chatClient.prompt() .user(message) .advisors(a -> a.param("chatMemoryConversationId", sessionId)) .call() .content(); }内存管理技巧:
- 设置最大轮数:避免内存无限增长
- 定期清理:可以定时清理过期会话
- 持久化:重要对话可以存数据库
5.3 错误处理与重试机制
网络不稳定或模型加载慢时,需要有健壮的错误处理:
import org.springframework.retry.annotation.Retryable; import org.springframework.retry.annotation.Backoff; @Service public class RobustAIService { private final OllamaChatClient chatClient; // 重试机制:最多3次,每次间隔2秒 @Retryable( retryFor = {IOException.class, TimeoutException.class}, maxAttempts = 3, backoff = @Backoff(delay = 2000) ) public String reliableChat(String message) { try { Prompt prompt = new Prompt(message); ChatResponse response = chatClient.call(prompt); return response.getResult().getOutput().getContent(); } catch (Exception e) { // 记录日志 log.error("AI调用失败: {}", e.getMessage()); // 返回友好提示 return "抱歉,AI服务暂时不可用,请稍后重试。"; } } // 超时控制 public String chatWithTimeout(String message) { return Mono.fromCallable(() -> { Prompt prompt = new Prompt(message); return chatClient.call(prompt); }) .timeout(Duration.ofSeconds(30)) // 30秒超时 .onErrorReturn(ChatResponse.fallback("请求超时")) .block() .getResult().getOutput().getContent(); } }我遇到的典型问题:
- Ollama服务重启:模型更新或内存不足时可能重启
- 响应超时:复杂问题可能需要更长时间
- 内存不足:大模型回答长文本时可能OOM
6. 高级功能:多模型切换与文件处理
6.1 动态模型切换:一个应用用多个模型
实际项目中,我们可能需要根据场景切换不同模型:
import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.annotation.Bean; @Configuration public class MultiModelConfig { @Bean @Qualifier("codeModel") public OllamaChatClient codeChatClient() { return new OllamaChatClient( OllamaApi.builder().baseUrl("http://localhost:11434").build(), OllamaOptions.builder() .model("codellama:7b") // 专门写代码的模型 .temperature(0.3f) .build() ); } @Bean @Qualifier("creativeModel") public OllamaChatClient creativeChatClient() { return new OllamaChatClient( OllamaApi.builder().baseUrl("http://localhost:11434").build(), OllamaOptions.builder() .model("llama3.2:3b") // 创意写作模型 .temperature(0.9f) .build() ); } } @Service public class ModelRouterService { private final Map<String, OllamaChatClient> modelClients; public ModelRouterService( @Qualifier("codeModel") OllamaChatClient codeClient, @Qualifier("creativeModel") OllamaChatClient creativeClient) { modelClients = Map.of( "code", codeClient, "creative", creativeClient, "default", codeClient // 默认模型 ); } public String routeChat(String modelType, String message) { OllamaChatClient client = modelClients.getOrDefault( modelType, modelClients.get("default") ); return client.call(message).getResult().getOutput().getContent(); } }使用场景举例:
- 代码生成 → codellama
- 文案创作 → llama3.2
- 逻辑推理 → qwen2.5
- 快速响应 → 小参数模型
6.2 文件内容处理:让AI读文档
很多场景需要AI处理上传的文档。这里实现一个简单的文件问答:
import org.springframework.web.multipart.MultipartFile; import java.nio.charset.StandardCharsets; @PostMapping(value = "/ask-with-file", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) public String askWithFile( @RequestParam String question, @RequestParam MultipartFile file) throws IOException { // 读取文件内容 String fileContent = new String(file.getBytes(), StandardCharsets.UTF_8); // 构建提示,包含文件内容 String prompt = String.format(""" 请根据以下文档内容回答问题: 文档内容: %s 问题:%s 要求: 1. 答案必须基于文档内容 2. 如果文档中没有相关信息,请明确说明 3. 回答要简洁明了 """, fileContent, question); return chatClient.call(prompt).getResult().getOutput().getContent(); }处理大文件的技巧:
- 分块处理:大文件分成多个chunk
- 摘要提取:先让AI生成摘要,再基于摘要问答
- 向量搜索:结合向量数据库做语义搜索(高级功能)
6.3 图片理解:多模态能力
如果你的模型支持多模态(如llava),还可以处理图片:
import org.springframework.ai.chat.messages.Media; import org.springframework.ai.chat.messages.UserMessage; import org.springframework.util.MimeTypeUtils; @PostMapping("/analyze-image") public String analyzeImage(@RequestParam MultipartFile imageFile) throws IOException { byte[] imageData = imageFile.getBytes(); // 创建包含图片的消息 UserMessage userMessage = new UserMessage( "描述这张图片的内容", List.of(new Media(MimeTypeUtils.IMAGE_PNG, imageData)) ); Prompt prompt = new Prompt( List.of(userMessage), OllamaOptions.builder() .model("llava:7b") // 多模态模型 .build() ); ChatResponse response = chatClient.call(prompt); return response.getResult().getOutput().getContent(); }注意:
- 需要先拉取多模态模型:
ollama pull llava:7b - 图片处理比较耗资源,建议异步处理
- 图片大小有限制,需要在前端或后端做压缩
7. 性能优化与生产部署
7.1 连接池与超时配置
生产环境必须配置合理的超时和连接池:
spring: ai: ollama: client: connect-timeout: 10s # 连接超时 read-timeout: 120s # 读取超时,长文本需要更长时间 max-connections: 20 # 最大连接数 max-connections-per-route: 10 # 每路由最大连接数我的经验值:
- 简单问答:read-timeout 30s
- 代码生成:read-timeout 60s
- 长文档分析:read-timeout 120s+
- 连接数根据并发量调整,一般10-20足够
7.2 监控与日志
添加详细的日志,方便排查问题:
import org.slf4j.Logger; import org.slf4j.LoggerFactory; @Service public class LoggingAIService { private static final Logger log = LoggerFactory.getLogger(LoggingAIService.class); private final OllamaChatClient chatClient; public String chatWithLogging(String message) { long startTime = System.currentTimeMillis(); try { log.info("开始AI调用,消息长度: {}", message.length()); ChatResponse response = chatClient.call(new Prompt(message)); String result = response.getResult().getOutput().getContent(); long duration = System.currentTimeMillis() - startTime; log.info("AI调用完成,耗时: {}ms,结果长度: {}", duration, result.length()); // 记录token使用情况(如果模型支持) if (response.getMetadata().getUsage() != null) { log.debug("Token使用 - 输入: {}, 输出: {}, 总计: {}", response.getMetadata().getUsage().getPromptTokens(), response.getMetadata().getUsage().getGenerationTokens(), response.getMetadata().getUsage().getTotalTokens()); } return result; } catch (Exception e) { log.error("AI调用失败,消息: {}, 错误: {}", message, e.getMessage(), e); throw new RuntimeException("AI服务调用失败", e); } } }7.3 缓存策略
频繁的相同问题可以缓存结果:
import org.springframework.cache.annotation.Cacheable; @Service public class CachedAIService { @Cacheable(value = "aiResponses", key = "#message.hashCode()") public String getCachedResponse(String message) { // 只有缓存未命中时才真正调用AI return chatClient.call(message).getResult().getOutput().getContent(); } // 带参数的缓存 @Cacheable(value = "aiResponses", key = "T(org.springframework.util.DigestUtils).md5DigestAsHex(#message.getBytes())") public String getCachedResponseWithParams(String message, float temperature) { OllamaOptions options = OllamaOptions.builder() .temperature(temperature) .build(); return chatClient.call(new Prompt(message, options)) .getResult().getOutput().getContent(); } }缓存策略建议:
- 技术问题:缓存时间可以长一些(1小时)
- 实时信息:不缓存或很短时间(1分钟)
- 个性化回答:基于用户ID和问题组合缓存key
7.4 Docker部署
生产环境建议用Docker部署,方便管理:
# Dockerfile FROM openjdk:21-jdk-slim WORKDIR /app # 复制Maven构建结果 COPY target/spring-ai-ollama-demo.jar app.jar # 安装Ollama RUN apt-get update && apt-get install -y curl RUN curl -fsSL https://ollama.ai/install.sh | sh # 拉取模型(可以在构建时预拉取,减少启动时间) RUN ollama pull qwen2.5:1.8b # 启动脚本 COPY entrypoint.sh /entrypoint.sh RUN chmod +x /entrypoint.sh ENTRYPOINT ["/entrypoint.sh"]#!/bin/bash # entrypoint.sh # 启动Ollama服务 ollama serve & OLLAMA_PID=$! # 等待Ollama启动 sleep 10 # 启动Spring Boot应用 java -jar app.jar # 清理 kill $OLLAMA_PID部署注意事项:
- 资源限制:给容器分配足够内存
- 模型存储:模型文件很大,考虑持久化卷
- 健康检查:添加/actuator/health端点
- 滚动更新:避免服务中断
8. 常见问题与解决方案
8.1 模型加载失败
问题:启动时提示模型不存在或加载失败
解决:
# 1. 检查模型是否已下载 ollama list # 2. 如果不存在,重新拉取 ollama pull qwen2.5:1.8b # 3. 检查Ollama服务状态 curl http://localhost:11434/api/tags # 4. 查看日志 ollama logs8.2 内存不足
问题:运行大模型时OOM
解决:
- 使用更小模型:7B → 3B → 1.8B
- 调整JVM参数:
# application.yml spring: ai: ollama: chat: options: num-gpu: 0 # 强制使用CPU,减少显存占用 num-thread: 4 # 限制CPU线程数- 增加交换空间(Linux/Mac):
sudo fallocate -l 8G /swapfile sudo chmod 600 /swapfile sudo mkswap /swapfile sudo swapon /swapfile8.3 响应速度慢
问题:AI响应时间过长
优化:
- 使用量化模型:Q4_K_M比Q8_0快很多
- 调整参数:减少num-predict,降低temperature
- 硬件加速:如果有N卡,确保CUDA可用
- 批处理:多个问题一起问
8.4 中文支持不好
问题:中文回答质量差
解决:
- 选择中文优化模型:Qwen系列、ChatGLM3等
- 调整提示词:明确要求用中文回答
- 微调模型:用自己的数据微调(高级)
9. 实际项目经验分享
我在最近的一个内部知识库项目中用了这套方案,有些经验值得分享:
项目背景:公司内部技术文档问答系统,涉及敏感信息不能上云。
技术选型:
- 模型:Qwen2.5:7B(中文好,7B参数在16G内存跑得动)
- 向量数据库:Chroma(轻量,和Spring AI集成简单)
- 前端:Vue 3 + SSE流式显示
踩过的坑:
第一次启动超慢:因为Spring AI会在启动时检查模型,如果模型没加载就报错。解决方案是加个健康检查,等Ollama ready再启动业务。
长文本截断:默认的上下文长度可能不够。需要在Ollama启动时指定更大上下文:
ollama run qwen2.5:7b --num-ctx 4096- 并发问题:Ollama默认并发能力有限,需要调整:
# Ollama配置 OLLAMA_NUM_PARALLEL=4 OLLAMA_MAX_LOADED_MODELS=2- 内存泄漏:长时间运行后内存增长。定期重启服务,或者用
-XX:+UseG1GC优化GC。
性能数据:
- 平均响应时间:2.8秒(7B模型)
- 支持并发:10个用户同时使用
- 内存占用:应用500MB + 模型5GB
- 月度成本:0元(对比OpenAI API约$200/月)
给新手的建议:
- 从1.8B或3B模型开始,别一上来就搞70B
- 一定要加超时和重试,网络不稳定是常态
- 监控Token使用,避免意外长文本
- 准备fallback方案,AI不是100%可靠
这套方案跑了大半年,稳定性和成本都让人满意。最大的好处是数据完全可控,不用担心隐私问题。现在团队已经基于这个搭建了三个内部AI应用,开发效率提升很明显。
如果你也在考虑本地AI方案,我强烈推荐Spring AI + Ollama这个组合。它可能不是性能最强的,但绝对是Java开发者最友好、最快速的入门方案。