diff --git a/src/main/java/com/going/server/domain/chatbot/service/ChatbotServiceImpl.java b/src/main/java/com/going/server/domain/chatbot/service/ChatbotServiceImpl.java index 434c8a1..5646ee8 100644 --- a/src/main/java/com/going/server/domain/chatbot/service/ChatbotServiceImpl.java +++ b/src/main/java/com/going/server/domain/chatbot/service/ChatbotServiceImpl.java @@ -83,8 +83,15 @@ public CreateChatbotResponseDto createAnswerWithRAG(String graphStrId, CreateCha // RAG: 사용자 질문 String userQuestion = createChatbotRequestDto.getChatContent(); + // RAG : 키워드 추출 + List keywords = extractKeywords(userQuestion); + System.out.println("[RAG] 추출된 키워드: " + keywords); + // RAG: 유사 노드 검색 및 문장 추출 - List matchedNodes = graphNodeRepository.findByKeyword(userQuestion); + List matchedNodes = graphNodeRepository.findByGraphIdAndKeywords(graphId, keywords); +// List matchedNodes = graphNodeRepository.findByGraphIdAndKeywordsWithEdges(graphId, keywords); + System.out.println("[RAG] matchedNodes: " + matchedNodes); + List candidateSentences = matchedNodes.stream() .map(GraphNode::getIncludeSentence) .filter(Objects::nonNull) @@ -96,6 +103,7 @@ public CreateChatbotResponseDto createAnswerWithRAG(String graphStrId, CreateCha // RAG: 최종 프롬프트 구성 String finalPrompt = promptBuilder.buildPrompt(filteredChunks, userQuestion); + System.out.println("finalPrompt: " + finalPrompt); // RAG: 메타정보 수집 List retrievedChunks = new ArrayList<>(filteredChunks); @@ -284,4 +292,18 @@ public CreateChatbotResponseDto recommendVideo(String graphId, CreateChatbotRequ private void deletePreviousChat(Long graphId) { chattingRepository.deleteByGraphId(graphId); } + + + // RAG : 키워드 추출 + private List extractKeywords(String text) { + List stopwords = List.of("은", "는", "이", "가", "을", "를", "에", "의", "와", "과", "에서", "하다"); + + return Arrays.stream(text.split("[\\s,.!?]+")) + .map(word -> word.replaceAll("(은|는|이|가|을|를|에|의|와|과|에서)$", "")) // ✅ 조사 제거 + .map(String::toLowerCase) + .filter(word -> word.length() > 1 && !stopwords.contains(word)) + .distinct() + .limit(5) + .collect(Collectors.toList()); + } } diff --git a/src/main/java/com/going/server/domain/graph/repository/GraphNodeRepository.java b/src/main/java/com/going/server/domain/graph/repository/GraphNodeRepository.java index d93ad69..4df135b 100644 --- a/src/main/java/com/going/server/domain/graph/repository/GraphNodeRepository.java +++ b/src/main/java/com/going/server/domain/graph/repository/GraphNodeRepository.java @@ -17,12 +17,26 @@ default GraphNode getByNode(Long nodeId) { @Query("MATCH (n:GraphNode) RETURN COALESCE(MAX(n.node_id), 0)") Long findMaxNodeId(); - // RAG: 키워드 기반 노드 검색 + // RAG: 키워드 기반 노드 검색 (엣지 포함 x) @Query(""" - MATCH (n:GraphNode) - WHERE toLower(n.includeSentence) CONTAINS toLower($keyword) - OR toLower(n.label) CONTAINS toLower($keyword) + MATCH (g:Graph)-[:HAS_NODE]->(n:GraphNode) + WHERE id(g) = $graphId AND + ANY(kw IN $keywords WHERE + toLower(n.label) CONTAINS toLower(kw) OR + toLower(n.includeSentence) CONTAINS toLower(kw)) RETURN n """) - List findByKeyword(String keyword); + List findByGraphIdAndKeywords(Long graphId, List keywords); + + // RAG: 키워드 기반 노드 검색 (엣지 포함) + @Query(""" + MATCH (g:Graph)-[:HAS_NODE]->(n:GraphNode) + OPTIONAL MATCH (n)-[r:RELATED]->(m:GraphNode) + WHERE id(g) = $graphId AND + ANY(kw IN $keywords WHERE + toLower(n.label) CONTAINS toLower(kw) OR + toLower(n.includeSentence) CONTAINS toLower(kw)) + RETURN DISTINCT n, collect(r) AS edges + """) + List findByGraphIdAndKeywordsWithEdges(Long graphId, List keywords); } diff --git a/src/main/java/com/going/server/domain/openai/service/RAGAnswerCreateService.java b/src/main/java/com/going/server/domain/openai/service/RAGAnswerCreateService.java index da9ff96..8afd16d 100644 --- a/src/main/java/com/going/server/domain/openai/service/RAGAnswerCreateService.java +++ b/src/main/java/com/going/server/domain/openai/service/RAGAnswerCreateService.java @@ -3,15 +3,14 @@ import com.going.server.domain.chatbot.entity.Chatting; import com.going.server.domain.chatbot.entity.Sender; import com.going.server.domain.openai.dto.ChatCompletionRequestDto; -import com.theokanning.openai.completion.chat.ChatMessage; import com.theokanning.openai.OpenAiService; +import com.theokanning.openai.completion.chat.ChatMessage; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import java.util.ArrayList; import java.util.List; -// RAG 대화 @Service @RequiredArgsConstructor public class RAGAnswerCreateService { @@ -19,11 +18,18 @@ public class RAGAnswerCreateService { // 시스템 역할 설정 private static final String SYSTEM_PROMPT = """ - 당신은 초등학생의 이해를 돕는 친절하고 정확한 지식 튜터입니다. - - 아래 제공된 데이터를 기반으로 질문에 대해 매우 길고 정확하게 설명해주세요. - - 만약 참고 데이터가 없다면, 교육 도메인의 일반적인 지식을 바탕으로 충실하게 답변해주세요. - - 반드시 한글로만 응답하고, 인사말이나 불필요한 문장은 생략한 대답만 반환하세요. - """; + 당신은 초등학생의 이해를 돕는 친절하고 정확한 지식 튜터입니다. + - 아래 제공된 데이터를 기반으로 질문에 대해 매우 길고 정확하게 설명해주세요. + - 만약 참고 데이터가 없다면, 교육 도메인의 일반적인 지식을 바탕으로 충실하게 답변해주세요. + - 반드시 한글로만 응답하고, 인사말이나 불필요한 문장은 생략한 대답만 반환하세요. + """; + + // 모델 스펙 정의 + private static final String MODEL_NAME = "gpt-4-0125-preview"; +// private static final String MODEL_NAME = "gpt-4o"; + + private static final double TEMPERATURE = 0.3 ; + private static final int MAX_TOKENS = 3000; // 기존 채팅 이력을 기반으로 GPT 응답 생성 public String chat(List chatHistory, String question) { @@ -35,33 +41,10 @@ public String chat(List chatHistory, String question) { messages.add(new ChatMessage("user", question)); // 새로운 질문 // DTO 기반 요청 생성 - ChatCompletionRequestDto request = ChatCompletionRequestDto.builder() - .model("gpt-4-0125-preview") - .temperature(0.3) - .maxTokens(3500) - .messages(messages) - .build(); + ChatCompletionRequestDto request = buildRequest(messages); // OpenAI 모델에게 질문 및 응답 생성 - return openAiService.createChatCompletion(request.toRequest()) - .getChoices() - .get(0) - .getMessage() - .getContent(); - } - - private List convertHistoryToMessages(List chatHistory) { - return chatHistory.stream() - .map(chat -> new ChatMessage( - convertSenderToRole(chat.getSender()), - chat.getContent() - )) - .toList(); - } - - // Chatting 엔티티의 Sender(Enum type) -> OpenAI 역할 문자열 변환 - private String convertSenderToRole(Sender sender) { - return sender == Sender.USER ? "user" : "assistant"; + return getResponseText(request); } // RAG 컨텍스트 기반 + 기존 채팅 이력을 함께 사용하는 GPT 응답 생성 @@ -76,17 +59,41 @@ public String chatWithContext(List chatHistory, String finalPrompt) { // 마지막 질문을 RAG 컨텍스트 기반으로 전달 messages.add(new ChatMessage("user", finalPrompt)); - ChatCompletionRequestDto request = ChatCompletionRequestDto.builder() - .model("gpt-4-0125-preview") - .temperature(0.3) - .maxTokens(3500) + ChatCompletionRequestDto request = buildRequest(messages); + return getResponseText(request); + } + + // 요청 생성 메서드 + private ChatCompletionRequestDto buildRequest(List messages) { + return ChatCompletionRequestDto.builder() + .model(MODEL_NAME) + .temperature(TEMPERATURE) + .maxTokens(MAX_TOKENS) .messages(messages) .build(); + } + // 응답 추출 메서드 + private String getResponseText(ChatCompletionRequestDto request) { return openAiService.createChatCompletion(request.toRequest()) .getChoices() .get(0) .getMessage() .getContent(); } -} \ No newline at end of file + + // Chatting 엔티티를 OpenAI ChatMessage로 변환 + private List convertHistoryToMessages(List chatHistory) { + return chatHistory.stream() + .map(chat -> new ChatMessage( + convertSenderToRole(chat.getSender()), + chat.getContent() + )) + .toList(); + } + + // Chatting 엔티티의 Sender(Enum type) -> OpenAI 역할 문자열 변환 + private String convertSenderToRole(Sender sender) { + return sender == Sender.USER ? "user" : "assistant"; + } +}