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以上)就够用。

软件方面,你需要:

  1. 操作系统:Windows 10/11, macOS 或者 Linux 都可以。Ollama对主流系统支持都很好。
  2. Java开发环境:JDK 17或以上。Spring Boot 3.x 必须依赖JDK 17+,这是硬性要求。建议直接安装JDK 21 LTS版本。
  3. Maven 3.6+Gradle:用于项目管理。
  4. 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”,添加:

  1. Spring Web:构建Web API必备。
  2. Lombok:简化POJO代码,选它。
  3. 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-urlspring.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(); // 获取文本内容
    }
}

我来解释一下这段代码:

  1. ChatClient.Builder 是Spring AI自动配置好的,我们直接注入使用。
  2. .defaultAdvisors(...) 添加了一个“对话记忆顾问”。InMemoryChatMemory 会在内存中记住当前会话的上下文,这样你问“你好”,再问“我叫小明”,然后问“我叫什么?”,模型有可能知道你在说“小明”。(注意:这是一个简单的内存实现,重启应用会丢失,生产环境需要更持久化的方案)。
  3. .defaultOptions(...) 设置了每次请求的默认参数,这里覆盖了application.yml中的部分设置。
  4. 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的 VectorStoreEmbeddingClient 来完成真正的语义检索,避免每次都将巨大文档全文发送给模型。

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可减少重复,设太高可能导致语法错误。

我的经验是:先保持默认,如果发现回答太啰嗦就降低temperaturetop-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 常见问题与解决

  1. 错误:Connection refused 连接到 localhost:11434

    • 检查:Ollama服务是否真的在运行?在终端执行 ollama list 看看。
    • 解决:运行 ollama serve 并保持终端打开,或者将其配置为系统服务。
  2. 错误:Model 'qwen2.5:7b' not found

    • 检查:模型名是否拼写错误?用 ollama list 查看本地已下载的模型列表。
    • 解决:执行 ollama pull qwen2.5:7b 下载正确模型。
  3. 响应速度极慢

    • 检查:CPU占用是否100%?任务管理器中查看。
    • 解决:确认是否使用了GPU。Ollama默认会尝试使用GPU。在终端运行Ollama时,观察启动日志是否有“Using GPU”字样。确保已安装正确的显卡驱动(如NVIDIA CUDA)。
  4. 流式响应不“流”

    • 检查:前端是否正确处理了 text/event-stream?浏览器网络标签查看响应类型。
    • 解决:确保Controller方法使用了 produces = "text/event-stream" 且返回 Flux<String>。用简单的SSE客户端脚本测试。
  5. 内存不足(OOM)

    • 现象:应用或Ollama进程崩溃。
    • 解决:尝试更小的模型(如3B参数模型),或者为JVM和系统预留更多内存。关闭不必要的应用程序。

把这些步骤走完,你应该已经拥有了一个完全在本地运行、功能可扩展的Spring Boot AI应用。从简单的问答到带上下文的对话,再到基于文档的查询,这套基础框架都能支撑。最重要的是,你完全掌控了数据和流程,这对于很多实际项目来说,是选择技术方案的首要前提。剩下的,就是根据你的具体业务需求,在这些骨架上添加血肉了。

Logo

欢迎加入DeepSeek 技术社区。在这里,你可以找到志同道合的朋友,共同探索AI技术的奥秘。

更多推荐