基于SpringBoot+SpringAI+Ollama開發(fā)智能問答系統(tǒng)
引言
在人工智能技術(shù)飛速發(fā)展的今天,大語言模型(LLM)已成為開發(fā)者工具箱中不可或缺的一部分。然而,依賴云端API服務不僅存在數(shù)據(jù)隱私問題,還可能產(chǎn)生高昂成本。本文將介紹如何利用SpringBoot、SpringAI框架結(jié)合Ollama本地大模型服務,搭建一個完全運行在本地Windows環(huán)境下的智能問答系統(tǒng)。
技術(shù)棧概述
SpringBoot與SpringAI
SpringBoot作為Java生態(tài)中最流行的應用框架,提供了快速構(gòu)建生產(chǎn)級應用的能力。SpringAI是Spring生態(tài)系統(tǒng)中的新興成員,專門為AI集成設計,它簡化了與各種大語言模型的交互過程,提供了統(tǒng)一的API接口。
Ollama本地模型服務
Ollama是一個開源項目,允許開發(fā)者在本地運行和管理大型語言模型。它支持多種開源模型,包括Llama、Mistral等,并提供了簡單的API接口。通過Ollama,我們可以在不依賴互聯(lián)網(wǎng)連接的情況下使用強大的語言模型能力。
環(huán)境準備
硬件要求
Windows 10/11操作系統(tǒng)
至少16GB RAM(推薦32GB或以上)
NVIDIA顯卡(可選,可加速推理)
軟件安裝
1.安裝Ollama:
訪問Ollama官網(wǎng)(https://ollama.ai)下載Windows版本并安裝
2.驗證Ollama安裝:
ollama list
項目搭建
創(chuàng)建SpringBoot項目
使用Spring Initializr(https://start.spring.io)創(chuàng)建項目,選擇以下依賴:
- 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
核心功能實現(xiàn)
問答服務層
創(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);
}
}控制器實現(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ì)量,我們可以實現(xiàn)提示模板:
PromptTemplateService.java:
@Service
public class PromptTemplateService {
private static final String QA_TEMPLATE = """
你是一個專業(yè)的AI助手,請根據(jù)以下要求回答問題:
1. 回答要專業(yè)、準確
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);
}
高級功能實現(xiàn)
對話歷史管理
實現(xiàn)簡單的對話記憶功能:
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)容問答
實現(xià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) {
// 簡化的相關(guān)性匹配 - 實際項目應使用嵌入向量
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 = """
基于以下文檔內(nèi)容回答問題:
文檔相關(guān)部分:
{context}
問題:{question}
""".replace("{context}", context)
.replace("{question}", question);
String answer = qaService.generateAnswer(prompt);
return ResponseEntity.ok(answer);
}前端交互實現(xiàn)
簡單HTML界面
resources/static/index.html:
<!DOCTYPE html>
<html>
<head>
<title>本地AI問答系統(tǒng)</title>
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
</head>
<body>
<h1>本地問答系統(tǒng)</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>性能優(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請求: {}", 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
系統(tǒng)測試
訪問 http://localhost:8080 測試問答功能,或使用Postman測試API端點。
擴展思路
向量數(shù)據(jù)庫集成
考慮集成Chroma或Milvus等向量數(shù)據(jù)庫實現(xià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);
}
}
多模型切換
實現(xiàn)動態(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é)
本文詳細介紹了如何使用SpringBoot、SpringAI和Ollama在本地Windows環(huán)境搭建一個功能完整的大模型問答系統(tǒng)。通過這個方案,開發(fā)者可以:
- 完全在本地運行AI服務,保障數(shù)據(jù)隱私
- 利用Spring生態(tài)快速構(gòu)建生產(chǎn)級應用
- 靈活選擇不同的開源模型
- 實現(xiàn)基礎的問答到復雜的文檔分析功能
隨著本地AI技術(shù)的不斷進步,這種架構(gòu)將為更多企業(yè)應用提供安全、可控的AI解決方案。讀者可以根據(jù)實際需求擴展本文示例,如增加更多模型支持、優(yōu)化提示工程或集成更復雜的業(yè)務邏輯。
到此這篇關(guān)于基于SpringBoot+SpringAI+Ollama開發(fā)智能問答系統(tǒng)的文章就介紹到這了,更多相關(guān)SpringBoot SpringAI Ollama實現(xiàn)智能問答內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

