基于SpringBoot+SpringAI+Ollama開發(fā)智能問(wèn)答系統(tǒng)
引言
在人工智能技術(shù)飛速發(fā)展的今天,大語(yǔ)言模型(LLM)已成為開發(fā)者工具箱中不可或缺的一部分。然而,依賴云端API服務(wù)不僅存在數(shù)據(jù)隱私問(wèn)題,還可能產(chǎn)生高昂成本。本文將介紹如何利用SpringBoot、SpringAI框架結(jié)合Ollama本地大模型服務(wù),搭建一個(gè)完全運(yùn)行在本地Windows環(huán)境下的智能問(wèn)答系統(tǒng)。
技術(shù)棧概述
SpringBoot與SpringAI
SpringBoot作為Java生態(tài)中最流行的應(yīng)用框架,提供了快速構(gòu)建生產(chǎn)級(jí)應(yīng)用的能力。SpringAI是Spring生態(tài)系統(tǒng)中的新興成員,專門為AI集成設(shè)計(jì),它簡(jiǎn)化了與各種大語(yǔ)言模型的交互過(guò)程,提供了統(tǒng)一的API接口。
Ollama本地模型服務(wù)
Ollama是一個(gè)開源項(xiàng)目,允許開發(fā)者在本地運(yùn)行和管理大型語(yǔ)言模型。它支持多種開源模型,包括Llama、Mistral等,并提供了簡(jiǎn)單的API接口。通過(guò)Ollama,我們可以在不依賴互聯(lián)網(wǎng)連接的情況下使用強(qiáng)大的語(yǔ)言模型能力。
環(huán)境準(zhǔn)備
硬件要求
Windows 10/11操作系統(tǒng)
至少16GB RAM(推薦32GB或以上)
NVIDIA顯卡(可選,可加速推理)
軟件安裝
1.安裝Ollama:
訪問(wèn)Ollama官網(wǎng)(https://ollama.ai
)下載Windows版本并安裝
2.驗(yàn)證Ollama安裝:
ollama list
項(xiàng)目搭建
創(chuàng)建SpringBoot項(xiàng)目
使用Spring Initializr(https://start.spring.io)創(chuàng)建項(xiàng)目,選擇以下依賴:
- Spring Web
- Lombok
- Spring AI (如未列出可手動(dòng)添加)
配置pom.xml
確保包含SpringAI Ollama依賴:
<dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-ollama-spring-boot-starter</artifactId> <version>0.8.1</version> </dependency>
應(yīng)用配置
application.yml配置:
spring: ai: ollama: base-url: http://localhost:11434 chat: model: deepseek options: temperature: 0.7 top-p: 0.9
核心功能實(shí)現(xiàn)
問(wèn)答服務(wù)層
創(chuàng)建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); } }
控制器實(shí)現(xiàn)
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); } }
提示工程優(yōu)化
為提高回答質(zhì)量,我們可以實(shí)現(xiàn)提示模板:
PromptTemplateService.java:
@Service public class PromptTemplateService { private static final String QA_TEMPLATE = """ 你是一個(gè)專業(yè)的AI助手,請(qǐng)根據(jù)以下要求回答問(wèn)題: 1. 回答要專業(yè)、準(zhǔn)確 2. 如果問(wèn)題涉及不確定信息,請(qǐng)明確說(shuō)明 3. 保持回答簡(jiǎn)潔明了 問(wèn)題:{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); }
高級(jí)功能實(shí)現(xiàn)
對(duì)話歷史管理
實(shí)現(xiàn)簡(jiǎn)單的對(duì)話記憶功能:
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); }
文件內(nèi)容問(wèn)答
實(shí)現(xiàn)基于上傳文檔的問(wèn)答功能:
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) { // 簡(jiǎn)化的相關(guān)性匹配 - 實(shí)際項(xiàng)目應(yīng)使用嵌入向量 return chunks.stream() .filter(chunk -> chunk.toLowerCase().contains(question.toLowerCase())) .findFirst() .orElse(""); } }
添加文檔問(wèn)答端點(diǎn):
@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 = """ 基于以下文檔內(nèi)容回答問(wèn)題: 文檔相關(guān)部分: {context} 問(wèn)題:{question} """.replace("{context}", context) .replace("{question}", question); String answer = qaService.generateAnswer(prompt); return ResponseEntity.ok(answer); }
前端交互實(shí)現(xiàn)
簡(jiǎn)單HTML界面
resources/static/index.html:
<!DOCTYPE html> <html> <head> <title>本地AI問(wèn)答系統(tǒng)</title> <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script> </head> <body> <h1>本地問(wèn)答系統(tǒng)</h1> <div> <textarea id="question" rows="4" cols="50"></textarea> </div> <button onclick="askQuestion()">提問(wèn)</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 = "出錯(cuò): " + error.message; }); } </script> </body> </html>
流式響應(yīng)界面
添加流式問(wèn)答HTML:
<div style="margin-top: 30px;"> <h2>流式問(wèn)答</h2> <textarea id="streamQuestion" rows="4" cols="50"></textarea> <button onclick="askStreamQuestion()">流式提問(wèn)</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>
性能優(yōu)化與調(diào)試
模型參數(shù)調(diào)優(yōu)
在application.yml中調(diào)整模型參數(shù):
spring: ai: ollama: chat: options: temperature: 0.5 # 控制創(chuàng)造性(0-1) top-p: 0.9 # 核采樣閾值 num-predict: 512 # 最大token數(shù)
日志記錄
配置日志以監(jiān)控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請(qǐng)求: {}", request); GenerateResponse response = delegate.generate(request); log.debug("Ollama響應(yīng): {}", response); return response; } }
超時(shí)設(shè)置
配置連接超時(shí):
spring: ai: ollama: client: connect-timeout: 30s read-timeout: 5m
安全加固
API認(rèn)證
添加簡(jiǎn)單的API密鑰認(rèn)證:
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, "無(wú)效的API密鑰"); return; } filterChain.doFilter(request, response); } }
部署與運(yùn)行
啟動(dòng)Ollama服務(wù)
在Windows命令行中:
ollama serve
運(yùn)行SpringBoot應(yīng)用
在IDE中直接運(yùn)行主類,或使用Maven命令:
mvn spring-boot:run
系統(tǒng)測(cè)試
訪問(wèn) http://localhost:8080 測(cè)試問(wèn)答功能,或使用Postman測(cè)試API端點(diǎn)。
擴(kuò)展思路
向量數(shù)據(jù)庫(kù)集成
考慮集成Chroma或Milvus等向量數(shù)據(jù)庫(kù)實(shí)現(xiàn)更精準(zhǔn)的文檔檢索:
@Configuration public class VectorStoreConfig { @Bean public VectorStore vectorStore(EmbeddingClient embeddingClient) { return new SimpleVectorStore(embeddingClient); } @Bean public EmbeddingClient embeddingClient(OllamaApi ollamaApi) { return new OllamaEmbeddingClient(ollamaApi); } }
多模型切換
實(shí)現(xiàn)動(dòng)態(tài)模型選擇:
@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")); } }
總結(jié)
本文詳細(xì)介紹了如何使用SpringBoot、SpringAI和Ollama在本地Windows環(huán)境搭建一個(gè)功能完整的大模型問(wèn)答系統(tǒng)。通過(guò)這個(gè)方案,開發(fā)者可以:
- 完全在本地運(yùn)行AI服務(wù),保障數(shù)據(jù)隱私
- 利用Spring生態(tài)快速構(gòu)建生產(chǎn)級(jí)應(yīng)用
- 靈活選擇不同的開源模型
- 實(shí)現(xiàn)基礎(chǔ)的問(wèn)答到復(fù)雜的文檔分析功能
隨著本地AI技術(shù)的不斷進(jìn)步,這種架構(gòu)將為更多企業(yè)應(yīng)用提供安全、可控的AI解決方案。讀者可以根據(jù)實(shí)際需求擴(kuò)展本文示例,如增加更多模型支持、優(yōu)化提示工程或集成更復(fù)雜的業(yè)務(wù)邏輯。
到此這篇關(guān)于基于SpringBoot+SpringAI+Ollama開發(fā)智能問(wèn)答系統(tǒng)的文章就介紹到這了,更多相關(guān)SpringBoot SpringAI Ollama實(shí)現(xiàn)智能問(wèn)答內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Spring?Boot?使用觀察者模式實(shí)現(xiàn)實(shí)時(shí)庫(kù)存管理的步驟
在現(xiàn)代軟件開發(fā)中,實(shí)時(shí)數(shù)據(jù)處理非常關(guān)鍵,本文提供了一個(gè)使用SpringBoot和觀察者模式開發(fā)實(shí)時(shí)庫(kù)存管理系統(tǒng)的詳細(xì)教程,步驟包括創(chuàng)建項(xiàng)目、定義實(shí)體類、實(shí)現(xiàn)觀察者模式、集成Spring框架、創(chuàng)建RESTful?API端點(diǎn)和測(cè)試應(yīng)用等,這將有助于開發(fā)者構(gòu)建能夠即時(shí)響應(yīng)庫(kù)存變化的系統(tǒng)2024-09-09Spring使用注解更簡(jiǎn)單的讀取和存儲(chǔ)對(duì)象的方法
這篇文章主要介紹了Spring使用注解更簡(jiǎn)單的讀取和存儲(chǔ)對(duì)象的方法,本文通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友參考下吧2023-07-07解決使用RestTemplate時(shí)報(bào)錯(cuò)RestClientException的問(wèn)題
這篇文章主要介紹了解決使用RestTemplate時(shí)報(bào)錯(cuò)RestClientException的問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-08-08mybatis多數(shù)據(jù)源動(dòng)態(tài)切換的完整步驟
這篇文章主要給大家介紹了關(guān)于mybatis多數(shù)據(jù)源動(dòng)態(tài)切換的完整步驟,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-11-11spring?Cloud微服務(wù)阿里開源TTL身份信息的線程間復(fù)用
這篇文章主要為大家介紹了spring?Cloud微服務(wù)中使用阿里開源TTL身份信息的線程間復(fù)用,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-01-01Java高級(jí)特性基礎(chǔ)之反射五連問(wèn)
反射賦予了我們?cè)谶\(yùn)行時(shí)分析類以及執(zhí)行類中方法的能力。通過(guò)反射你可以獲取任意一個(gè)類的所有屬性和方法,你還可以調(diào)用這些方法和屬性。本文就來(lái)和大家詳細(xì)聊聊Java中的反射,感興趣的可以了解一下2023-01-01SpringBoot超詳細(xì)講解集成Flink的部署與打包方法
昨天折騰了下SpringBoot與Flink集成,實(shí)際上集成特簡(jiǎn)單,主要是部署打包的問(wèn)題折騰了不少時(shí)間。想打出的包直接可以java -jar運(yùn)行,同時(shí)也可以flink run運(yùn)行,或者在flink的dashboard上上傳點(diǎn)擊啟動(dòng)。結(jié)果是不行,但是使用不同的插件打包還是可以的2022-05-05如何實(shí)現(xiàn)springboot中controller之間的相互調(diào)用
這篇文章主要介紹了實(shí)現(xiàn)springboot中controller之間的相互調(diào)用方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-06-06