基于SpringBoot+SpringAI+Ollama开发智能问答系统

 更新时间:2025年06月08日 10:23:21   作者:码农阿豪@新空间  
在人工智能技术飞速发展的今天,大语言模型(LLM)已成为开发者工具箱中不可或缺的一部分,本文将介绍如何利用SpringBoot、SpringAI框架结合Ollama本地大模型服务,搭建一个完全运行在本地Windows环境下的智能问答系统,有需要的可以了解下

引言

在人工智能技术飞速发展的今天,大语言模型(LLM)已成为开发者工具箱中不可或缺的一部分。然而,依赖云端API服务不仅存在数据隐私问题,还可能产生高昂成本。本文将介绍如何利用SpringBoot、SpringAI框架结合Ollama本地大模型服务,搭建一个完全运行在本地Windows环境下的智能问答系统。

技术栈概述

SpringBoot与SpringAI

SpringBoot作为Java生态中最流行的应用框架,提供了快速构建生产级应用的能力。SpringAI是Spring生态系统中的新兴成员,专门为AI集成设计,它简化了与各种大语言模型的交互过程,提供了统一的API接口。

Ollama本地模型服务

Ollama是一个开源项目,允许开发者在本地运行和管理大型语言模型。它支持多种开源模型,包括Llama、Mistral等,并提供了简单的API接口。通过Ollama,我们可以在不依赖互联网连接的情况下使用强大的语言模型能力。

环境准备

硬件要求

Windows 10/11操作系统

至少16GB RAM(推荐32GB或以上)

NVIDIA显卡(可选,可加速推理)

软件安装

1.安装Ollama:

访问Ollama官网(https://ollama.ai)下载Windows版本并安装

2.验证Ollama安装:

ollama list

项目搭建

创建SpringBoot项目

使用Spring Initializr(https://start.spring.io)创建项目,选择以下依赖:

  • Spring Web
  • Lombok
  • Spring AI (如未列出可手动添加)

配置pom.xml

确保包含SpringAI Ollama依赖:

<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-ollama-spring-boot-starter</artifactId>
    <version>0.8.1</version>
</dependency>

应用配置

application.yml配置:

spring:
  ai:
    ollama:
      base-url: http://localhost:11434
      chat:
        model: deepseek
        options:
          temperature: 0.7
          top-p: 0.9

核心功能实现

问答服务层

创建QAService类:

@Service
public class QAService {
    
    private final OllamaChatClient chatClient;
    
    public QAService(OllamaChatClient chatClient) {
        this.chatClient = chatClient;
    }
    
    public String generateAnswer(String prompt) {
        return chatClient.call(prompt);
    }
    
    public Flux<String> generateStreamAnswer(String prompt) {
        return chatClient.stream(prompt);
    }
}

控制器实现

QAController.java:

@RestController
@RequestMapping("/api/qa")
public class QAController {
    
    private final QAService qaService;
    
    public QAController(QAService qaService) {
        this.qaService = qaService;
    }
    
    @PostMapping("/ask")
    public ResponseEntity<String> askQuestion(@RequestBody String question) {
        String answer = qaService.generateAnswer(question);
        return ResponseEntity.ok(answer);
    }
    
    @GetMapping(value = "/ask-stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    public Flux<String> askQuestionStream(@RequestParam String question) {
        return qaService.generateStreamAnswer(question);
    }
}

提示工程优化

为提高回答质量,我们可以实现提示模板:

PromptTemplateService.java:

@Service
public class PromptTemplateService {
    
    private static final String QA_TEMPLATE = """
            你是一个专业的AI助手,请根据以下要求回答问题:
            1. 回答要专业、准确
            2. 如果问题涉及不确定信息,请明确说明
            3. 保持回答简洁明了
            
            问题:{question}
            """;
    
    public String buildPrompt(String question) {
        return QA_TEMPLATE.replace("{question}", question);
    }
}

更新QAService使用提示模板:

public String generateAnswer(String prompt) {
    String formattedPrompt = promptTemplateService.buildPrompt(prompt);
    return chatClient.call(formattedPrompt);
}

高级功能实现

对话历史管理

实现简单的对话记忆功能:

ConversationManager.java:

@Service
@Scope(value = WebApplicationContext.SCOPE_SESSION, proxyMode = ScopedProxyMode.TARGET_CLASS)
public class ConversationManager {
    
    private final List<String> conversationHistory = new ArrayList<>();
    
    public void addExchange(String userInput, String aiResponse) {
        conversationHistory.add("用户: " + userInput);
        conversationHistory.add("AI: " + aiResponse);
    }
    
    public String getConversationContext() {
        return String.join("\n", conversationHistory);
    }
    
    public void clear() {
        conversationHistory.clear();
    }
}

更新提示模板以包含历史:

public String buildPrompt(String question, String history) {
    return QA_TEMPLATE.replace("{history}", history)
                     .replace("{question}", question);
}

文件内容问答

实现基于上传文档的问答功能:

DocumentService.java:

@Service
public class DocumentService {
    
    private final ResourceLoader resourceLoader;
    private final TextSplitter textSplitter;
    
    public DocumentService(ResourceLoader resourceLoader) {
        this.resourceLoader = resourceLoader;
        this.textSplitter = new TokenTextSplitter();
    }
    
    public List<String> processDocument(MultipartFile file) throws IOException {
        String content = new String(file.getBytes(), StandardCharsets.UTF_8);
        return textSplitter.split(content);
    }
    
    public String extractRelevantParts(List<String> chunks, String question) {
        // 简化的相关性匹配 - 实际项目应使用嵌入向量
        return chunks.stream()
                .filter(chunk -> chunk.toLowerCase().contains(question.toLowerCase()))
                .findFirst()
                .orElse("");
    }
}

添加文档问答端点:

@PostMapping(value = "/ask-with-doc", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public ResponseEntity<String> askWithDocument(
        @RequestParam String question,
        @RequestParam MultipartFile document) throws IOException {
    
    List<String> chunks = documentService.processDocument(document);
    String context = documentService.extractRelevantParts(chunks, question);
    
    String prompt = """
            基于以下文档内容回答问题:
            
            文档相关部分:
            {context}
            
            问题:{question}
            """.replace("{context}", context)
              .replace("{question}", question);
    
    String answer = qaService.generateAnswer(prompt);
    return ResponseEntity.ok(answer);
}

前端交互实现

简单HTML界面

resources/static/index.html:

<!DOCTYPE html>
<html>
<head>
    <title>本地AI问答系统</title>
    <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
</head>
<body>
    <h1>本地问答系统</h1>
    <div>
        <textarea id="question" rows="4" cols="50"></textarea>
    </div>
    <button onclick="askQuestion()">提问</button>
    <div id="answer" style="margin-top: 20px; border: 1px solid #ccc; padding: 10px;"></div>
    
    <script>
        function askQuestion() {
            const question = document.getElementById('question').value;
            document.getElementById('answer').innerText = "思考中...";
            
            axios.post('/api/qa/ask', question, {
                headers: { 'Content-Type': 'text/plain' }
            })
            .then(response => {
                document.getElementById('answer').innerText = response.data;
            })
            .catch(error => {
                document.getElementById('answer').innerText = "出错: " + error.message;
            });
        }
    </script>
</body>
</html>

流式响应界面

添加流式问答HTML:

<div style="margin-top: 30px;">
    <h2>流式问答</h2>
    <textarea id="streamQuestion" rows="4" cols="50"></textarea>
    <button onclick="askStreamQuestion()">流式提问</button>
    <div id="streamAnswer" style="margin-top: 20px; border: 1px solid #ccc; padding: 10px;"></div>
</div>

​​​​​​​<script>
    function askStreamQuestion() {
        const question = document.getElementById('streamQuestion').value;
        const answerDiv = document.getElementById('streamAnswer');
        answerDiv.innerText = "";
        
        const eventSource = new EventSource(`/api/qa/ask-stream?question=${encodeURIComponent(question)}`);
        
        eventSource.onmessage = function(event) {
            answerDiv.innerText += event.data;
        };
        
        eventSource.onerror = function() {
            eventSource.close();
        };
    }
</script>

性能优化与调试

模型参数调优

在application.yml中调整模型参数:

spring:
  ai:
    ollama:
      chat:
        options:
          temperature: 0.5  # 控制创造性(0-1)
          top-p: 0.9        # 核采样阈值
          num-predict: 512  # 最大token数

日志记录

配置日志以监控AI交互:

@Configuration
public class LoggingConfig {
    
    @Bean
    public Logger.Level feignLoggerLevel() {
        return Logger.Level.FULL;
    }
    
    @Bean
    public OllamaApi ollamaApi(Client client, ObjectProvider<HttpMessageConverterCustomizer> customizers) {
        return new OllamaApiInterceptor(new OllamaApi(client, customizers));
    }
}

​​​​​​​class OllamaApiInterceptor implements OllamaApi {
    
    private static final Logger log = LoggerFactory.getLogger(OllamaApiInterceptor.class);
    private final OllamaApi delegate;
    
    public OllamaApiInterceptor(OllamaApi delegate) {
        this.delegate = delegate;
    }
    
    @Override
    public GenerateResponse generate(GenerateRequest request) {
        log.info("Ollama请求: {}", request);
        GenerateResponse response = delegate.generate(request);
        log.debug("Ollama响应: {}", response);
        return response;
    }
}

超时设置

配置连接超时:

spring:
  ai:
    ollama:
      client:
        connect-timeout: 30s
        read-timeout: 5m

安全加固

API认证

添加简单的API密钥认证:

SecurityConfig.java:

@Configuration
@EnableWebSecurity
public class SecurityConfig {
    
    @Value("${app.api-key}")
    private String apiKey;
    
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/api/**").authenticated()
                .anyRequest().permitAll()
            )
            .addFilterBefore(new ApiKeyFilter(apiKey), UsernamePasswordAuthenticationFilter.class)
            .csrf().disable();
        return http.build();
    }
}
​​​​​​​class ApiKeyFilter extends OncePerRequestFilter {
    
    private final String expectedApiKey;
    
    public ApiKeyFilter(String expectedApiKey) {
        this.expectedApiKey = expectedApiKey;
    }
    
    @Override
    protected void doFilterInternal(HttpServletRequest request, 
                                   HttpServletResponse response, 
                                   FilterChain filterChain) throws ServletException, IOException {
        String apiKey = request.getHeader("X-API-KEY");
        
        if (!expectedApiKey.equals(apiKey)) {
            response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "无效的API密钥");
            return;
        }
        
        filterChain.doFilter(request, response);
    }
}

部署与运行

启动Ollama服务

在Windows命令行中:

ollama serve

运行SpringBoot应用

在IDE中直接运行主类,或使用Maven命令:

mvn spring-boot:run

系统测试

访问 http://localhost:8080 测试问答功能,或使用Postman测试API端点。

扩展思路

向量数据库集成

考虑集成Chroma或Milvus等向量数据库实现更精准的文档检索:

@Configuration
public class VectorStoreConfig {
    
    @Bean
    public VectorStore vectorStore(EmbeddingClient embeddingClient) {
        return new SimpleVectorStore(embeddingClient);
    }
    
    @Bean
    public EmbeddingClient embeddingClient(OllamaApi ollamaApi) {
        return new OllamaEmbeddingClient(ollamaApi);
    }
}

多模型切换

实现动态模型选择:

@Service
public class ModelSelectorService {
    
    private final Map<String, ChatClient> clients;
    
    public ModelSelectorService(
            OllamaChatClient deep seekClient,
            OllamaChatClient llamaClient) {
        this.clients = Map.of(
            "deep seek", deep seekClient,
            "llama", llamaClient
        );
    }
    
    public ChatClient getClient(String modelName) {
        return clients.getOrDefault(modelName, clients.get("deep seek"));
    }
}

总结

本文详细介绍了如何使用SpringBoot、SpringAI和Ollama在本地Windows环境搭建一个功能完整的大模型问答系统。通过这个方案,开发者可以:

  • 完全在本地运行AI服务,保障数据隐私
  • 利用Spring生态快速构建生产级应用
  • 灵活选择不同的开源模型
  • 实现基础的问答到复杂的文档分析功能

随着本地AI技术的不断进步,这种架构将为更多企业应用提供安全、可控的AI解决方案。读者可以根据实际需求扩展本文示例,如增加更多模型支持、优化提示工程或集成更复杂的业务逻辑。

到此这篇关于基于SpringBoot+SpringAI+Ollama开发智能问答系统的文章就介绍到这了,更多相关SpringBoot SpringAI Ollama实现智能问答内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • Spring Boot 使用观察者模式实现实时库存管理的步骤

    Spring Boot 使用观察者模式实现实时库存管理的步骤

    在现代软件开发中,实时数据处理非常关键,本文提供了一个使用SpringBoot和观察者模式开发实时库存管理系统的详细教程,步骤包括创建项目、定义实体类、实现观察者模式、集成Spring框架、创建RESTful API端点和测试应用等,这将有助于开发者构建能够即时响应库存变化的系统
    2024-09-09
  • Spring使用注解更简单的读取和存储对象的方法

    Spring使用注解更简单的读取和存储对象的方法

    这篇文章主要介绍了Spring使用注解更简单的读取和存储对象的方法,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友参考下吧
    2023-07-07
  • 解决使用RestTemplate时报错RestClientException的问题

    解决使用RestTemplate时报错RestClientException的问题

    这篇文章主要介绍了解决使用RestTemplate时报错RestClientException的问题,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2021-08-08
  • mybatis多数据源动态切换的完整步骤

    mybatis多数据源动态切换的完整步骤

    这篇文章主要给大家介绍了关于mybatis多数据源动态切换的完整步骤,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2020-11-11
  • spring Cloud微服务阿里开源TTL身份信息的线程间复用

    spring Cloud微服务阿里开源TTL身份信息的线程间复用

    这篇文章主要为大家介绍了spring Cloud微服务中使用阿里开源TTL身份信息的线程间复用,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2023-01-01
  • MyBatis持久层框架的用法知识小结

    MyBatis持久层框架的用法知识小结

    MyBatis 本是apache的一个开源项目iBatis,接下来通过本文给大家介绍MyBatis持久层框架的用法知识小结,非常不错,具有参考借鉴价值,感兴趣的朋友一起学习吧
    2016-07-07
  • Java高级特性基础之反射五连问

    Java高级特性基础之反射五连问

    反射赋予了我们在运行时分析类以及执行类中方法的能力。通过反射你可以获取任意一个类的所有属性和方法,你还可以调用这些方法和属性。本文就来和大家详细聊聊Java中的反射,感兴趣的可以了解一下
    2023-01-01
  • SpringBoot超详细讲解集成Flink的部署与打包方法

    SpringBoot超详细讲解集成Flink的部署与打包方法

    昨天折腾了下SpringBoot与Flink集成,实际上集成特简单,主要是部署打包的问题折腾了不少时间。想打出的包直接可以java -jar运行,同时也可以flink run运行,或者在flink的dashboard上上传点击启动。结果是不行,但是使用不同的插件打包还是可以的
    2022-05-05
  • 如何实现springboot中controller之间的相互调用

    如何实现springboot中controller之间的相互调用

    这篇文章主要介绍了实现springboot中controller之间的相互调用方式,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2021-06-06
  • Spring Boot2.X国际化文件编写配置

    Spring Boot2.X国际化文件编写配置

    这篇文章主要介绍了Spring Boot2.X国际化文件编写配置,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
    2020-02-02

最新评论