This commit is contained in:
jack ning
2025-05-31 19:20:51 +08:00
parent 660cf072cb
commit 38b8d36bd5
17 changed files with 595 additions and 110 deletions

View File

@@ -0,0 +1,47 @@
# SpringAI RAG Controller 重构说明
## 问题描述
原有的 `SpringAIRagController` 使用了特定的 `OllamaChatModel`,当 Ollama 没有启用时会导致以下错误:
```
Parameter 2 of constructor in com.bytedesk.ai.springai.rag.SpringAIRagController required a bean of type 'org.springframework.ai.ollama.OllamaChatModel' that could not be found.
```
## 解决方案
将特定的 `OllamaChatModel` 替换为通用的 `ChatModel` 接口,使控制器能够适配任何 ChatModel 实现。
## 主要更改
### 1. SpringAIRagController.java
- **字段更改**: 将 `private final OllamaChatModel bytedeskOllamaChatModel` 改为 `private final ChatModel chatModel`
- **导入更改**: 移除 `OllamaChatModel` 导入,添加 `ChatModel` 导入
- **注解更改**: 添加 `@ConditionalOnBean(ChatModel.class)` 确保只有在有 ChatModel 可用时才创建控制器
- **方法更新**: 所有使用 `bytedeskOllamaChatModel` 的地方都替换为 `chatModel`
### 2. 新增配置类
#### ChatModelConfig.java
- 提供备用的 ChatModel 配置
- 只有在没有其他 ChatModel 实现时才创建 FallbackChatModel
-`bytedesk.features.java-ai=true` 配置控制
#### FallbackChatModel.java
- 简单的 ChatModel 实现作为备用
- 当没有其他 ChatModel 可用时返回友好的错误消息
- 提醒用户配置正确的 ChatModel 实现
## 兼容性
此更改向后兼容:
- 当 Ollama 启用时,会优先使用 OllamaChatModel
- 当其他 ChatModel 实现(如 OpenAI可用时会使用相应的实现
- 当没有任何 ChatModel 可用时,会使用 FallbackChatModel 避免启动错误
## 配置要求
确保在 `application.properties` 中设置:
```properties
bytedesk.features.java-ai=true
```
## 测试建议
1. 测试 Ollama 启用的情况
2. 测试 Ollama 禁用但有其他 ChatModel 的情况
3. 测试完全没有 ChatModel 的情况(应该使用 FallbackChatModel

View File

@@ -0,0 +1,47 @@
/*
* @Author: jackning 270580156@qq.com
* @Date: 2025-05-31 10:00:00
* @LastEditors: jackning 270580156@qq.com
* @LastEditTime: 2025-05-31 13:49:47
* @Description: bytedesk.com https://github.com/Bytedesk/bytedesk
* Please be aware of the BSL license restrictions before installing Bytedesk IM
* selling, reselling, or hosting Bytedesk IM as a service is a breach of the terms and automatically terminates your rights under the license.
* Business Source License 1.1: https://github.com/Bytedesk/bytedesk/blob/main/LICENSE
* contact: 270580156@qq.com
*
* Copyright (c) 2025 by bytedesk.com, All Rights Reserved.
*/
package com.bytedesk.ai.springai.config;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import lombok.extern.slf4j.Slf4j;
/**
* ChatModel配置类
* 提供ChatModel的默认实现当没有其他ChatModel可用时作为备用
*/
@Slf4j
@Configuration
public class ChatModelConfig {
/**
* 当没有其他ChatModel bean时提供一个默认的ChatModel实现
* 这个bean只有在启用Java AI功能时才会创建
*/
@Bean
@Primary
@ConditionalOnMissingBean(ChatModel.class)
@ConditionalOnProperty(name = "bytedesk.features.java-ai", havingValue = "true", matchIfMissing = false)
public ChatModel defaultChatModel() {
log.warn("No ChatModel implementation found, using fallback implementation");
// 返回一个简单的ChatModel实现作为备用
return new FallbackChatModel();
}
}

View File

@@ -0,0 +1,50 @@
/*
* @Author: jackning 270580156@qq.com
* @Date: 2025-05-31 10:00:00
* @LastEditors: jackning 270580156@qq.com
* @LastEditTime: 2025-05-31 10:00:00
* @Description: bytedesk.com https://github.com/Bytedesk/bytedesk
* Please be aware of the BSL license restrictions before installing Bytedesk IM
* selling, reselling, or hosting Bytedesk IM as a service is a breach of the terms and automatically terminates your rights under the license.
* Business Source License 1.1: https://github.com/Bytedesk/bytedesk/blob/main/LICENSE
* contact: 270580156@qq.com
*
* Copyright (c) 2025 by bytedesk.com, All Rights Reserved.
*/
package com.bytedesk.ai.springai.config;
import org.springframework.ai.chat.messages.AssistantMessage;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.chat.model.Generation;
import lombok.extern.slf4j.Slf4j;
import java.util.List;
/**
* 备用ChatModel实现
* 当没有其他ChatModel可用时使用此实现
*/
@Slf4j
public class FallbackChatModel implements ChatModel {
@Override
public ChatResponse call(Prompt prompt) {
log.warn("FallbackChatModel is being used - please configure a proper ChatModel implementation");
// 创建一个简单的响应
String responseText = "AI服务暂时不可用请稍后再试。请确保已正确配置ChatModel实现如Ollama、OpenAI等";
AssistantMessage assistantMessage = new AssistantMessage(responseText);
Generation generation = new Generation(assistantMessage);
return new ChatResponse(List.of(generation));
}
@Override
public String call(String message) {
return call(new Prompt(new UserMessage(message))).getResult().getOutput().getText();
}
}

View File

@@ -14,6 +14,8 @@
package com.bytedesk.ai.springai.rag;
import java.util.List;
import java.util.Map;
import java.util.HashMap;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.client.advisor.api.Advisor;
@@ -22,7 +24,7 @@ import org.springframework.ai.chat.messages.AssistantMessage;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.document.Document;
import org.springframework.ai.ollama.OllamaChatModel;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.rag.Query;
import org.springframework.ai.rag.advisor.RetrievalAugmentationAdvisor;
import org.springframework.ai.rag.generation.augmentation.ContextualQueryAugmenter;
@@ -36,8 +38,10 @@ import org.springframework.ai.rag.retrieval.search.VectorStoreDocumentRetriever;
import org.springframework.ai.vectorstore.SearchRequest;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.ai.vectorstore.filter.FilterExpressionBuilder;
import org.springframework.ai.embedding.EmbeddingModel;
import org.springframework.ai.vectorstore.elasticsearch.ElasticsearchVectorStore;
import io.micrometer.observation.ObservationRegistry;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
@@ -59,14 +63,14 @@ import lombok.extern.slf4j.Slf4j;
@RestController
@RequestMapping("/spring/ai/rag")
@RequiredArgsConstructor
@ConditionalOnProperty(name = "spring.ai.ollama.chat.enabled", havingValue = "true", matchIfMissing = false)
@ConditionalOnBean(ChatModel.class)
public class SpringAIRagController {
private final VectorStore vectorStore;
private final OllamaChatModel bytedeskOllamaChatModel;
private final EmbeddingModel embeddingModel;
// private final SpringAIVectorStoreService springAIVectorService;
private final ChatModel chatModel;
private final ObservationRegistry observationRegistry;
@@ -87,7 +91,7 @@ public class SpringAIRagController {
.build())
.build();
// 使用chatClient添加ObservationRegistry
ChatResponse response = ChatClient.builder(bytedeskOllamaChatModel, observationRegistry, null)
ChatResponse response = ChatClient.builder(chatModel, observationRegistry, null)
.build()
.prompt()
.advisors(qaAdvisor)
@@ -107,7 +111,7 @@ public class SpringAIRagController {
@RequestParam(value = "message", defaultValue = "什么时间考试?") String message,
@RequestParam(value = "kbUid", defaultValue = "") String kbUid) {
ChatClient chatClient = ChatClient.builder(bytedeskOllamaChatModel)
ChatClient chatClient = ChatClient.builder(chatModel)
.defaultAdvisors(QuestionAnswerAdvisor.builder(vectorStore)
.searchRequest(SearchRequest.builder().build())
.build())
@@ -136,13 +140,13 @@ public class SpringAIRagController {
.similarityThreshold(0.50)
.vectorStore(vectorStore)
.build())
// 允许为空
// .queryAugmenter(ContextualQueryAugmenter.builder()
// .allowEmptyContext(true)
// .build())
// 允许为空
// .queryAugmenter(ContextualQueryAugmenter.builder()
// .allowEmptyContext(true)
// .build())
.build();
String answer = ChatClient.builder(bytedeskOllamaChatModel)
String answer = ChatClient.builder(chatModel)
.defaultAdvisors(retrievalAugmentationAdvisor)
.build()
.prompt()
@@ -163,7 +167,7 @@ public class SpringAIRagController {
Advisor retrievalAugmentationAdvisor = RetrievalAugmentationAdvisor.builder()
.queryTransformers(RewriteQueryTransformer.builder()
.chatClientBuilder(ChatClient.builder(bytedeskOllamaChatModel).build().mutate())
.chatClientBuilder(ChatClient.builder(chatModel).build().mutate())
.build())
.documentRetriever(VectorStoreDocumentRetriever.builder()
.similarityThreshold(0.50)
@@ -171,7 +175,7 @@ public class SpringAIRagController {
.build())
.build();
String answer = ChatClient.builder(bytedeskOllamaChatModel)
String answer = ChatClient.builder(chatModel)
.defaultAdvisors(retrievalAugmentationAdvisor)
.build()
.prompt()
@@ -206,13 +210,13 @@ public class SpringAIRagController {
// conversation history and a follow-up query into a standalone query that
// captures the essence of the conversation.
CompressionQueryTransformer queryTransformer = CompressionQueryTransformer.builder()
.chatClientBuilder(ChatClient.builder(bytedeskOllamaChatModel).build().mutate())
.chatClientBuilder(ChatClient.builder(chatModel).build().mutate())
.build();
Query transformedQuery = queryTransformer.transform(query);
// 使用chatClient
String answer = ChatClient.builder(bytedeskOllamaChatModel)
String answer = ChatClient.builder(chatModel)
// .defaultAdvisors(retrievalAugmentationAdvisor)
.build()
.prompt()
@@ -241,13 +245,13 @@ public class SpringAIRagController {
// to provide better results when querying a target system, such as a vector
// store or a web search engine.
QueryTransformer queryTransformer = RewriteQueryTransformer.builder()
.chatClientBuilder(ChatClient.builder(bytedeskOllamaChatModel).build().mutate())
.chatClientBuilder(ChatClient.builder(chatModel).build().mutate())
.build();
Query transformedQuery = queryTransformer.transform(query);
// 使用chatClient
String answer = ChatClient.builder(bytedeskOllamaChatModel)
String answer = ChatClient.builder(chatModel)
// .defaultAdvisors(retrievalAugmentationAdvisor)
.build()
.prompt()
@@ -272,14 +276,14 @@ public class SpringAIRagController {
Query query = new Query("Hvad er Danmarks hovedstad?");
QueryTransformer queryTransformer = TranslationQueryTransformer.builder()
.chatClientBuilder(ChatClient.builder(bytedeskOllamaChatModel).build().mutate())
.chatClientBuilder(ChatClient.builder(chatModel).build().mutate())
.targetLanguage("english")
.build();
Query transformedQuery = queryTransformer.transform(query);
// 使用chatClient
String answer = ChatClient.builder(bytedeskOllamaChatModel)
String answer = ChatClient.builder(chatModel)
// .defaultAdvisors(retrievalAugmentationAdvisor)
.build()
.prompt()
@@ -302,14 +306,14 @@ public class SpringAIRagController {
@RequestParam(value = "kbUid", defaultValue = "") String kbUid) {
MultiQueryExpander queryExpander = MultiQueryExpander.builder()
.chatClientBuilder(ChatClient.builder(bytedeskOllamaChatModel).build().mutate())
.chatClientBuilder(ChatClient.builder(chatModel).build().mutate())
.numberOfQueries(3)
// .includeOriginal(false)
.build();
List<Query> queries = queryExpander.expand(new Query("How to run a Spring Boot app?"));
// 使用chatClient
String answer = ChatClient.builder(bytedeskOllamaChatModel)
String answer = ChatClient.builder(chatModel)
// .defaultAdvisors(retrievalAugmentationAdvisor)
.build()
.prompt()
@@ -393,7 +397,7 @@ public class SpringAIRagController {
ResponseEntity<JsonResult<?>> observedChat(
@RequestParam(value = "message", defaultValue = "什么时间考试?") String message) {
ChatClient chatClient = ChatClient.builder(bytedeskOllamaChatModel, observationRegistry, null)
ChatClient chatClient = ChatClient.builder(chatModel, observationRegistry, null)
.build();
ChatResponse response = chatClient.prompt()
@@ -406,4 +410,86 @@ public class SpringAIRagController {
return ResponseEntity.ok(JsonResult.success(response));
}
// 查看当前ElasticsearchVectorStore详情
// http://127.0.0.1:9003/spring/ai/rag/vector-store-info
@GetMapping("/vector-store-info")
ResponseEntity<JsonResult<?>> getVectorStoreInfo() {
log.info("Getting VectorStore info");
Map<String, Object> vectorStoreInfo = new HashMap<>();
try {
vectorStoreInfo.put("type", vectorStore.getClass().getSimpleName());
vectorStoreInfo.put("className", vectorStore.getClass().getName());
// 如果是ElasticsearchVectorStore获取更详细的信息
if (vectorStore instanceof ElasticsearchVectorStore) {
vectorStoreInfo.put("vectorStoreType", "ElasticsearchVectorStore");
vectorStoreInfo.put("status", "Active");
// 注意由于ElasticsearchVectorStore的内部属性可能是私有的
// 这里只能获取到公开的信息
vectorStoreInfo.put("description", "Elasticsearch-based vector store for semantic search");
} else {
vectorStoreInfo.put("vectorStoreType", "Unknown");
}
vectorStoreInfo.put("timestamp", System.currentTimeMillis());
} catch (Exception e) {
log.error("Error getting vector store info: {}", e.getMessage());
vectorStoreInfo.put("error", e.getMessage());
vectorStoreInfo.put("status", "Error");
}
return ResponseEntity.ok(JsonResult.success(vectorStoreInfo));
}
// 查看当前EmbeddingModel详情
// http://127.0.0.1:9003/spring/ai/rag/embedding-model-info
@GetMapping("/embedding-model-info")
ResponseEntity<JsonResult<?>> getEmbeddingModelInfo() {
log.info("Getting EmbeddingModel info");
Map<String, Object> embeddingModelInfo = new HashMap<>();
try {
embeddingModelInfo.put("type", embeddingModel.getClass().getSimpleName());
embeddingModelInfo.put("className", embeddingModel.getClass().getName());
// 获取embedding维度信息
try {
// 使用一个简单的测试文本来获取embedding维度
var testEmbedding = embeddingModel.embed("test");
embeddingModelInfo.put("dimensions", testEmbedding.length);
embeddingModelInfo.put("status", "Active");
embeddingModelInfo.put("testEmbeddingSize", testEmbedding.length);
} catch (Exception e) {
log.warn("Could not get embedding dimensions: {}", e.getMessage());
embeddingModelInfo.put("dimensionsError", e.getMessage());
embeddingModelInfo.put("status", "Error");
}
// 根据类名判断embedding模型类型
String className = embeddingModel.getClass().getSimpleName();
if (className.contains("Ollama")) {
embeddingModelInfo.put("modelType", "Ollama Embedding Model");
embeddingModelInfo.put("description", "Local Ollama embedding model (e.g., bgm-m3)");
} else if (className.contains("Zhipuai") || className.contains("ZhiPu")) {
embeddingModelInfo.put("modelType", "Zhipuai Embedding Model");
embeddingModelInfo.put("description", "Zhipuai cloud embedding model");
} else {
embeddingModelInfo.put("modelType", "Unknown Embedding Model");
}
embeddingModelInfo.put("timestamp", System.currentTimeMillis());
} catch (Exception e) {
log.error("Error getting embedding model info: {}", e.getMessage());
embeddingModelInfo.put("error", e.getMessage());
embeddingModelInfo.put("status", "Error");
}
return ResponseEntity.ok(JsonResult.success(embeddingModelInfo));
}
}

View File

@@ -1,4 +0,0 @@
# RAG
- [spring ai alibaba](https://java2ai.com/blog/spring-ai-alibaba-module-rag)
- [spring ai rag](https://docs.spring.io/spring-ai/reference/api/retrieval-augmented-generation.html?spm=0.29160081.0.0.75c73b5blqQmqQ)