From ba6a3a8b8765cec60279741c090710fc75a4fd8d Mon Sep 17 00:00:00 2001 From: GerHerMo Date: Wed, 24 Sep 2025 16:10:41 +0900 Subject: [PATCH 1/7] feat: build dependencies add --- build.gradle.kts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/build.gradle.kts b/build.gradle.kts index 6381f721..b56dbeb6 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -50,6 +50,9 @@ dependencies { annotationProcessor("org.projectlombok:lombok") + //Spring AI + implementation(platform("org.springframework.ai:spring-ai-bom:1.0.0-M4")) + implementation("org.springframework.ai:spring-ai-vertex-ai-gemini-spring-boot-starter") //test testImplementation("org.springframework.boot:spring-boot-starter-test") From d30746613e9faf041a3f2137d4bc19f6a482b8de Mon Sep 17 00:00:00 2001 From: GerHerMo Date: Wed, 24 Sep 2025 16:47:53 +0900 Subject: [PATCH 2/7] feat: build add. --- build.gradle.kts | 7 ++++++- src/main/resources/application.yml | 16 +++++++++++++++- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index b56dbeb6..c54497c7 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -52,7 +52,12 @@ dependencies { //Spring AI implementation(platform("org.springframework.ai:spring-ai-bom:1.0.0-M4")) - implementation("org.springframework.ai:spring-ai-vertex-ai-gemini-spring-boot-starter") + + //Vertex + //implementation("org.springframework.ai:spring-ai-vertex-ai-gemini-spring-boot-starter") + + //OpenAI + implementation("org.springframework.ai:spring-ai-openai-spring-boot-starter") //test testImplementation("org.springframework.boot:spring-boot-starter-test") diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 9021f781..307fafa0 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -48,6 +48,16 @@ spring: user-info-uri: https://openapi.naver.com/v1/nid/me user-name-attribute: response + ai: + openai: + api-key: ${GEMINI_API_KEY} + chat: + base-url: "https://generativelanguage.googleapis.com/v1beta/openai/" + options: + model: "gemini-2.0-flash-lite" + temperature: 0.0 + completions-path: "/chat/completions" + springdoc: default-produces-media-type: application/json;charset=UTF-8 @@ -60,6 +70,7 @@ server: enabled: true force: true +# 추후 삭제 예정 gemini: api: key: ${GEMINI_API_KEY} @@ -104,4 +115,7 @@ management: chatbot: history: - max-conversation-count: 5 \ No newline at end of file + max-conversation-count: 5 + system: + prompt-file: classpath:prompts/chatbot-system-prompt.txt + rules-file: classpath:prompts/chatbot-response-rules.txt \ No newline at end of file From 8b14b1f03ab494c7d16dcb5c60436cc243573553 Mon Sep 17 00:00:00 2001 From: GerHerMo Date: Wed, 24 Sep 2025 17:31:25 +0900 Subject: [PATCH 3/7] fix: build error fix - add milestone repository --- build.gradle.kts | 5 +- .../chatbot/service/ChatbotService.java | 134 ++++++++++++------ src/main/resources/application.yml | 2 +- 3 files changed, 92 insertions(+), 49 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index c54497c7..f6a6aec9 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -22,6 +22,9 @@ configurations { repositories { mavenCentral() + // Spring AI 마일스톤 리포지토리 추가 + maven { url = uri("https://repo.spring.io/milestone") } + maven { url = uri("https://repo.spring.io/snapshot") } } dependencies { @@ -67,4 +70,4 @@ dependencies { tasks.withType { useJUnitPlatform() -} +} \ No newline at end of file diff --git a/src/main/java/com/back/domain/chatbot/service/ChatbotService.java b/src/main/java/com/back/domain/chatbot/service/ChatbotService.java index b0e4156e..09abccec 100644 --- a/src/main/java/com/back/domain/chatbot/service/ChatbotService.java +++ b/src/main/java/com/back/domain/chatbot/service/ChatbotService.java @@ -6,6 +6,18 @@ import com.back.domain.chatbot.repository.ChatConversationRepository; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +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.prompt.PromptTemplate; +import org.springframework.ai.chat.prompt.SystemPromptTemplate; +import org.springframework.ai.chat.messages.Message; +import org.springframework.ai.chat.messages.UserMessage; +import org.springframework.ai.chat.messages.AssistantMessage; +import org.springframework.ai.chat.messages.SystemMessage; +import org.springframework.ai.chat.client.ChatClient; +import org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor; +import org.springframework.ai.chat.memory.InMemoryChatMemory; import org.springframework.beans.factory.annotation.Value; import org.springframework.core.io.Resource; import org.springframework.stereotype.Service; @@ -15,17 +27,21 @@ import jakarta.annotation.PostConstruct; import java.io.IOException; import java.nio.charset.StandardCharsets; -import java.util.List; -import java.util.UUID; +import java.time.LocalDateTime; +import java.util.*; +import java.util.stream.Collectors; @Service @RequiredArgsConstructor @Slf4j public class ChatbotService { - private final GeminiApiService geminiApiService; + private final ChatModel chatModel; private final ChatConversationRepository chatConversationRepository; + // 세션별 메모리 관리를 위한 Map + private final Map sessionMemories = new HashMap<>(); + @Value("classpath:prompts/chatbot-system-prompt.txt") private Resource systemPromptResource; @@ -37,12 +53,25 @@ public class ChatbotService { private String systemPrompt; private String responseRules; + private ChatClient chatClient; @PostConstruct public void init() throws IOException { - this.systemPrompt = StreamUtils.copyToString(systemPromptResource.getInputStream(), StandardCharsets.UTF_8); - this.responseRules = StreamUtils.copyToString(responseRulesResource.getInputStream(), StandardCharsets.UTF_8); - log.info("챗봇 시스템 프롬프트가 로드되었습니다. (길이: {} 문자)", systemPrompt.length()); + this.systemPrompt = StreamUtils.copyToString( + systemPromptResource.getInputStream(), + StandardCharsets.UTF_8 + ); + this.responseRules = StreamUtils.copyToString( + responseRulesResource.getInputStream(), + StandardCharsets.UTF_8 + ); + + // ChatClient 초기화 + this.chatClient = ChatClient.builder(chatModel) + .defaultSystem(systemPrompt) + .build(); + + log.info("Spring AI 챗봇 초기화 완료. 시스템 프롬프트 길이: {} 문자", systemPrompt.length()); } @Transactional @@ -53,66 +82,68 @@ public ChatResponseDto sendMessage(ChatRequestDto requestDto) { } try { - String contextualMessage = buildContextualMessage(requestDto.getMessage(), sessionId); + // 세션별 메모리 가져오기 또는 생성 + InMemoryChatMemory chatMemory = getOrCreateSessionMemory(sessionId); - String botResponse = geminiApiService.generateResponse(contextualMessage).block(); + // 이전 대화 기록 로드 + loadConversationHistory(sessionId, chatMemory); - ChatConversation conversation = ChatConversation.builder() - .userId(requestDto.getUserId()) - .userMessage(requestDto.getMessage()) - .botResponse(botResponse) - .sessionId(sessionId) - .build(); + // ChatClient를 사용한 응답 생성 + String response = chatClient.prompt() + .system(systemPrompt) + .user(buildUserMessage(requestDto.getMessage())) + .advisors(new MessageChatMemoryAdvisor(chatMemory)) + .call() + .content(); - chatConversationRepository.save(conversation); + // 대화 저장 + saveConversation(requestDto, response, sessionId); - return new ChatResponseDto(botResponse, sessionId); + return new ChatResponseDto(response, sessionId); } catch (Exception e) { log.error("채팅 응답 생성 중 오류 발생: ", e); - return new ChatResponseDto("죄송합니다. 오류가 발생했습니다. 다시 시도해주세요.", sessionId); + return new ChatResponseDto( + "죄송합니다. 오류가 발생했습니다. 다시 시도해주세요.", + sessionId + ); } } - private String buildContextualMessage(String userMessage, String sessionId) { - List recentConversations = getRecentConversations(sessionId); - - StringBuilder contextBuilder = new StringBuilder(); - contextBuilder.append(systemPrompt).append("\n\n"); - - appendConversationHistory(contextBuilder, recentConversations); - appendCurrentQuestion(contextBuilder, userMessage); - appendResponseInstructions(contextBuilder); - - return contextBuilder.toString(); - } - - private List getRecentConversations(String sessionId) { - return chatConversationRepository.findBySessionIdOrderByCreatedAtAsc(sessionId); + private InMemoryChatMemory getOrCreateSessionMemory(String sessionId) { + return sessionMemories.computeIfAbsent( + sessionId, + k -> new InMemoryChatMemory() + ); } - private void appendConversationHistory(StringBuilder contextBuilder, List conversations) { - if (!conversations.isEmpty()) { - contextBuilder.append("=== 이전 대화 기록 ===\n"); + private void loadConversationHistory(String sessionId, InMemoryChatMemory chatMemory) { + List recentConversations = + chatConversationRepository.findBySessionIdOrderByCreatedAtAsc(sessionId); - int maxHistory = Math.min(conversations.size(), maxConversationCount); - int startIdx = Math.max(0, conversations.size() - maxHistory); + int maxHistory = Math.min(recentConversations.size(), maxConversationCount); + int startIdx = Math.max(0, recentConversations.size() - maxHistory); - for (int i = startIdx; i < conversations.size(); i++) { - ChatConversation conv = conversations.get(i); - contextBuilder.append("사용자: ").append(conv.getUserMessage()).append("\n"); - contextBuilder.append("AI 바텐더: ").append(conv.getBotResponse()).append("\n\n"); - } - contextBuilder.append("=================\n\n"); + for (int i = startIdx; i < recentConversations.size(); i++) { + ChatConversation conv = recentConversations.get(i); + chatMemory.add(new UserMessage(conv.getUserMessage())); + chatMemory.add(new AssistantMessage(conv.getBotResponse())); } } - private void appendCurrentQuestion(StringBuilder contextBuilder, String userMessage) { - contextBuilder.append("현재 사용자 질문: ").append(userMessage).append("\n\n"); + private String buildUserMessage(String userMessage) { + return userMessage + "\n\n" + responseRules; } - private void appendResponseInstructions(StringBuilder contextBuilder) { - contextBuilder.append(responseRules); + private void saveConversation(ChatRequestDto requestDto, String response, String sessionId) { + ChatConversation conversation = ChatConversation.builder() + .userId(requestDto.getUserId()) + .userMessage(requestDto.getMessage()) + .botResponse(response) + .sessionId(sessionId) + .build(); + + chatConversationRepository.save(conversation); } @Transactional(readOnly = true) @@ -124,4 +155,13 @@ public List getChatHistory(String sessionId) { public List getUserChatHistory(Long userId, String sessionId) { return chatConversationRepository.findByUserIdAndSessionIdOrderByCreatedAtAsc(userId, sessionId); } + + // 메모리 정리를 위한 메서드 (오래된 세션 제거) + public void cleanupInactiveSessions() { + // 필요시 구현: 일정 시간 이상 사용하지 않은 세션 메모리 제거 + sessionMemories.entrySet().removeIf(entry -> { + // 구현 예: 30분 이상 비활성 세션 제거 + return false; // 실제 로직 구현 필요 + }); + } } \ No newline at end of file diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 307fafa0..12c2152b 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -56,7 +56,7 @@ spring: options: model: "gemini-2.0-flash-lite" temperature: 0.0 - completions-path: "/chat/completions" + completions-path: "/chat/completions" # gemini 사용시 v1/chat/completions 로 들어가지 않음 springdoc: default-produces-media-type: application/json;charset=UTF-8 From 07064440cf5a895e0b04e38478c0038eb8082d42 Mon Sep 17 00:00:00 2001 From: GerHerMo Date: Thu, 25 Sep 2025 09:46:32 +0900 Subject: [PATCH 4/7] feat: cbs modified --- .../chatbot/service/ChatbotService.java | 245 +++++++++++++++--- 1 file changed, 204 insertions(+), 41 deletions(-) diff --git a/src/main/java/com/back/domain/chatbot/service/ChatbotService.java b/src/main/java/com/back/domain/chatbot/service/ChatbotService.java index 09abccec..0853eccc 100644 --- a/src/main/java/com/back/domain/chatbot/service/ChatbotService.java +++ b/src/main/java/com/back/domain/chatbot/service/ChatbotService.java @@ -6,18 +6,15 @@ import com.back.domain.chatbot.repository.ChatConversationRepository; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -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.prompt.PromptTemplate; -import org.springframework.ai.chat.prompt.SystemPromptTemplate; -import org.springframework.ai.chat.messages.Message; -import org.springframework.ai.chat.messages.UserMessage; -import org.springframework.ai.chat.messages.AssistantMessage; -import org.springframework.ai.chat.messages.SystemMessage; import org.springframework.ai.chat.client.ChatClient; import org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor; +import org.springframework.ai.chat.client.advisor.QuestionAnswerAdvisor; import org.springframework.ai.chat.memory.InMemoryChatMemory; +import org.springframework.ai.chat.model.ChatModel; +import org.springframework.ai.chat.messages.*; +import org.springframework.ai.document.Document; +import org.springframework.ai.vectorstore.VectorStore; +import org.springframework.ai.vectorstore.SearchRequest; import org.springframework.beans.factory.annotation.Value; import org.springframework.core.io.Resource; import org.springframework.stereotype.Service; @@ -29,7 +26,7 @@ import java.nio.charset.StandardCharsets; import java.time.LocalDateTime; import java.util.*; -import java.util.stream.Collectors; +import java.util.concurrent.ConcurrentHashMap; @Service @RequiredArgsConstructor @@ -39,8 +36,8 @@ public class ChatbotService { private final ChatModel chatModel; private final ChatConversationRepository chatConversationRepository; - // 세션별 메모리 관리를 위한 Map - private final Map sessionMemories = new HashMap<>(); + // 세션별 메모리 관리 (Thread-Safe) + private final ConcurrentHashMap sessionMemories = new ConcurrentHashMap<>(); @Value("classpath:prompts/chatbot-system-prompt.txt") private Resource systemPromptResource; @@ -51,6 +48,12 @@ public class ChatbotService { @Value("${chatbot.history.max-conversation-count:5}") private int maxConversationCount; + @Value("${spring.ai.vertex.ai.gemini.chat.options.temperature:0.8}") + private Double temperature; + + @Value("${spring.ai.vertex.ai.gemini.chat.options.max-output-tokens:300}") + private Integer maxTokens; + private String systemPrompt; private String responseRules; private ChatClient chatClient; @@ -66,36 +69,55 @@ public void init() throws IOException { StandardCharsets.UTF_8 ); - // ChatClient 초기화 + // ChatClient 고급 설정 this.chatClient = ChatClient.builder(chatModel) .defaultSystem(systemPrompt) + .defaultOptions(ChatOptionsBuilder.builder() + .withTemperature(temperature) + .withMaxTokens(maxTokens) + .build()) .build(); - log.info("Spring AI 챗봇 초기화 완료. 시스템 프롬프트 길이: {} 문자", systemPrompt.length()); + log.info("Spring AI 챗봇 초기화 완료. Temperature: {}, MaxTokens: {}", temperature, maxTokens); } @Transactional public ChatResponseDto sendMessage(ChatRequestDto requestDto) { - String sessionId = requestDto.getSessionId(); - if (sessionId == null || sessionId.isEmpty()) { - sessionId = UUID.randomUUID().toString(); - } + String sessionId = ensureSessionId(requestDto.getSessionId()); try { - // 세션별 메모리 가져오기 또는 생성 + // 메시지 타입 감지 + MessageType messageType = detectMessageType(requestDto.getMessage()); + + // 세션별 메모리 가져오기 InMemoryChatMemory chatMemory = getOrCreateSessionMemory(sessionId); // 이전 대화 기록 로드 loadConversationHistory(sessionId, chatMemory); - // ChatClient를 사용한 응답 생성 - String response = chatClient.prompt() - .system(systemPrompt) - .user(buildUserMessage(requestDto.getMessage())) - .advisors(new MessageChatMemoryAdvisor(chatMemory)) + // ChatClient 빌더 생성 + var promptBuilder = chatClient.prompt() + .system(buildSystemMessage(messageType)) + .user(buildUserMessage(requestDto.getMessage(), messageType)) + .advisors(new MessageChatMemoryAdvisor(chatMemory, maxConversationCount)); + + // RAG 기능 활성화 (칵테일 정보 검색) + if (vectorStore != null && shouldUseRAG(messageType)) { + promptBuilder.advisors(new QuestionAnswerAdvisor( + vectorStore, + SearchRequest.defaults().withTopK(3) + )); + } + + // 응답 생성 + String response = promptBuilder + .options(getOptionsForMessageType(messageType)) .call() .content(); + // 응답 후처리 + response = postProcessResponse(response, messageType); + // 대화 저장 saveConversation(requestDto, response, sessionId); @@ -103,13 +125,16 @@ public ChatResponseDto sendMessage(ChatRequestDto requestDto) { } catch (Exception e) { log.error("채팅 응답 생성 중 오류 발생: ", e); - return new ChatResponseDto( - "죄송합니다. 오류가 발생했습니다. 다시 시도해주세요.", - sessionId - ); + return handleError(sessionId, e); } } + private String ensureSessionId(String sessionId) { + return (sessionId == null || sessionId.isEmpty()) + ? UUID.randomUUID().toString() + : sessionId; + } + private InMemoryChatMemory getOrCreateSessionMemory(String sessionId) { return sessionMemories.computeIfAbsent( sessionId, @@ -118,34 +143,128 @@ private InMemoryChatMemory getOrCreateSessionMemory(String sessionId) { } private void loadConversationHistory(String sessionId, InMemoryChatMemory chatMemory) { - List recentConversations = + List conversations = chatConversationRepository.findBySessionIdOrderByCreatedAtAsc(sessionId); - int maxHistory = Math.min(recentConversations.size(), maxConversationCount); - int startIdx = Math.max(0, recentConversations.size() - maxHistory); + // 최근 N개의 대화만 메모리에 로드 + conversations.stream() + .skip(Math.max(0, conversations.size() - maxConversationCount)) + .forEach(conv -> { + chatMemory.add(new UserMessage(conv.getUserMessage())); + chatMemory.add(new AssistantMessage(conv.getBotResponse())); + }); + } + + private String buildSystemMessage(MessageType type) { + StringBuilder sb = new StringBuilder(systemPrompt); - for (int i = startIdx; i < recentConversations.size(); i++) { - ChatConversation conv = recentConversations.get(i); - chatMemory.add(new UserMessage(conv.getUserMessage())); - chatMemory.add(new AssistantMessage(conv.getBotResponse())); + // 메시지 타입별 추가 지시사항 + switch (type) { + case RECIPE: + sb.append("\n\n【레시피 답변 모드】정확한 재료 비율과 제조 순서를 강조하세요."); + break; + case RECOMMENDATION: + sb.append("\n\n【추천 모드】다양한 선택지와 각각의 특징을 설명하세요."); + break; + case QUESTION: + sb.append("\n\n【질문 답변 모드】정확하고 신뢰할 수 있는 정보를 제공하세요."); + break; + default: + break; } + + return sb.toString(); } - private String buildUserMessage(String userMessage) { + private String buildUserMessage(String userMessage, MessageType type) { return userMessage + "\n\n" + responseRules; } + private ChatOptions getOptionsForMessageType(MessageType type) { + return switch (type) { + case RECIPE -> ChatOptionsBuilder.builder() + .withTemperature(0.3) // 정확성 중시 + .withMaxTokens(400) // 레시피는 길게 + .build(); + case RECOMMENDATION -> ChatOptionsBuilder.builder() + .withTemperature(0.9) // 다양성 중시 + .withMaxTokens(250) + .build(); + case QUESTION -> ChatOptionsBuilder.builder() + .withTemperature(0.7) // 균형 + .withMaxTokens(200) + .build(); + default -> ChatOptionsBuilder.builder() + .withTemperature(temperature) + .withMaxTokens(maxTokens) + .build(); + }; + } + + private boolean shouldUseRAG(MessageType type) { + // 레시피나 추천 요청시 RAG 활성화 + return type == MessageType.RECIPE || type == MessageType.RECOMMENDATION; + } + + private String postProcessResponse(String response, MessageType type) { + // 응답 길이 제한 확인 + if (response.length() > 500) { + response = response.substring(0, 497) + "..."; + } + + // 이모지 추가 (타입별) + if (type == MessageType.RECIPE && !response.contains("🍹")) { + response = "🍹 " + response; + } + + return response; + } + private void saveConversation(ChatRequestDto requestDto, String response, String sessionId) { ChatConversation conversation = ChatConversation.builder() .userId(requestDto.getUserId()) .userMessage(requestDto.getMessage()) .botResponse(response) .sessionId(sessionId) + .createdAt(LocalDateTime.now()) .build(); chatConversationRepository.save(conversation); } + private ChatResponseDto handleError(String sessionId, Exception e) { + String errorMessage = "죄송합니다. 잠시 후 다시 시도해주세요."; + + if (e.getMessage().contains("rate limit")) { + errorMessage = "요청이 너무 많습니다. 잠시 후 다시 시도해주세요."; + } else if (e.getMessage().contains("timeout")) { + errorMessage = "응답 시간이 초과되었습니다. 다시 시도해주세요."; + } + + return new ChatResponseDto(errorMessage, sessionId); + } + + public enum MessageType { + RECIPE, RECOMMENDATION, QUESTION, CASUAL_CHAT + } + + private MessageType detectMessageType(String message) { + String lower = message.toLowerCase(); + + if (lower.contains("레시피") || lower.contains("만드는") || + lower.contains("제조") || lower.contains("recipe")) { + return MessageType.RECIPE; + } else if (lower.contains("추천") || lower.contains("어때") || + lower.contains("뭐가 좋") || lower.contains("recommend")) { + return MessageType.RECOMMENDATION; + } else if (lower.contains("?") || lower.contains("뭐") || + lower.contains("어떻") || lower.contains("왜")) { + return MessageType.QUESTION; + } + + return MessageType.CASUAL_CHAT; + } + @Transactional(readOnly = true) public List getChatHistory(String sessionId) { return chatConversationRepository.findBySessionIdOrderByCreatedAtAsc(sessionId); @@ -156,12 +275,56 @@ public List getUserChatHistory(Long userId, String sessionId) return chatConversationRepository.findByUserIdAndSessionIdOrderByCreatedAtAsc(userId, sessionId); } - // 메모리 정리를 위한 메서드 (오래된 세션 제거) + // 정기적인 메모리 정리 (스케줄러로 호출) public void cleanupInactiveSessions() { - // 필요시 구현: 일정 시간 이상 사용하지 않은 세션 메모리 제거 + long thirtyMinutesAgo = System.currentTimeMillis() - (30 * 60 * 1000); + sessionMemories.entrySet().removeIf(entry -> { - // 구현 예: 30분 이상 비활성 세션 제거 - return false; // 실제 로직 구현 필요 + // 실제로는 마지막 사용 시간을 추적해야 함 + return false; }); + + log.info("세션 메모리 정리 완료. 현재 활성 세션: {}", sessionMemories.size()); + } +} + +// ChatOptions 빌더 헬퍼 클래스 +class ChatOptionsBuilder { + private Double temperature; + private Integer maxTokens; + private Double topP; + private Integer topK; + + public static ChatOptionsBuilder builder() { + return new ChatOptionsBuilder(); + } + + public ChatOptionsBuilder withTemperature(Double temperature) { + this.temperature = temperature; + return this; } -} \ No newline at end of file + + public ChatOptionsBuilder withMaxTokens(Integer maxTokens) { + this.maxTokens = maxTokens; + return this; + } + + public ChatOptionsBuilder withTopP(Double topP) { + this.topP = topP; + return this; + } + + public ChatOptionsBuilder withTopK(Integer topK) { + this.topK = topK; + return this; + } + + public ChatOptions build() { + // 실제 ChatOptions 객체 생성 로직 + // Spring AI의 실제 API에 맞게 조정 필요 + return null; // placeholder + } +} + +// ChatOptions placeholder (실제 Spring AI API에 맞게 조정 필요) +interface ChatOptions {} \ No newline at end of file From d6ada47dabe54fd90afddd20d1ee684ba599bcc0 Mon Sep 17 00:00:00 2001 From: GerHerMo Date: Thu, 25 Sep 2025 10:08:16 +0900 Subject: [PATCH 5/7] refactor: del:DTO, GemService,Config & modi:ChatbotService, application.yml --- .../domain/chatbot/dto/GeminiRequestDto.java | 187 ------------------ .../domain/chatbot/dto/GeminiResponseDto.java | 41 ---- .../chatbot/service/ChatbotService.java | 81 ++------ .../chatbot/service/GeminiApiService.java | 180 ----------------- .../back/global/appConfig/GeminiConfig.java | 24 --- src/main/resources/application.yml | 7 +- 6 files changed, 18 insertions(+), 502 deletions(-) delete mode 100644 src/main/java/com/back/domain/chatbot/dto/GeminiRequestDto.java delete mode 100644 src/main/java/com/back/domain/chatbot/dto/GeminiResponseDto.java delete mode 100644 src/main/java/com/back/domain/chatbot/service/GeminiApiService.java delete mode 100644 src/main/java/com/back/global/appConfig/GeminiConfig.java diff --git a/src/main/java/com/back/domain/chatbot/dto/GeminiRequestDto.java b/src/main/java/com/back/domain/chatbot/dto/GeminiRequestDto.java deleted file mode 100644 index 59169729..00000000 --- a/src/main/java/com/back/domain/chatbot/dto/GeminiRequestDto.java +++ /dev/null @@ -1,187 +0,0 @@ -package com.back.domain.chatbot.dto; - -import lombok.*; - -import java.util.List; - -@Getter -@Setter -public class GeminiRequestDto { - - private List contents; - private GenerationConfig generationConfig; - private List safetySettings; - - @Getter - @Setter - public static class Content { - private List parts; - - public Content(String text) { - this.parts = List.of(new Part(text)); - } - } - - @Getter - @Setter - public static class Part { - private String text; - - public Part(String text) { - this.text = text; - } - } - - @Getter - @Setter - @Builder - @NoArgsConstructor - @AllArgsConstructor - public static class GenerationConfig { // 생성 설정 - /** - * Temperature (0.0 ~ 2.0) - * - 낮을수록 (0.0): 일관되고 예측 가능한 응답 - * - 높을수록 (2.0): 창의적이고 다양한 응답 - * - 권장값: 0.7 ~ 1.0 - */ - private Double temperature; - - /** - * Top-P (0.0 ~ 1.0) - * - 누적 확률 임계값 - * - 0.95 = 상위 95% 확률의 토큰만 고려 - * - 낮을수록 더 집중된 응답 - */ - private Double topP; - - /** - * Top-K (1 ~ 40) - * - 고려할 토큰의 최대 개수 - * - 40 = 상위 40개 토큰만 고려 - * - 낮을수록 더 결정적인 응답 - */ - private Integer topK; - - /** - * Max Output Tokens - * - 응답의 최대 토큰 수 (출력 길이 제한) - * - Gemini 1.5 Flash: 최대 8192 토큰 - * - Gemini 1.5 Pro: 최대 8192 토큰 - * - 한글 1글자 ≈ 1-2 토큰, 영어 3-4글자 ≈ 1 토큰 - */ - private Integer maxOutputTokens; - - /** - * Stop Sequences - * - 이 문자열을 만나면 생성 중단 - * - 예: ["끝", "END", "\n\n"] - */ - private List stopSequences; - - /** - * Candidate Count (1 ~ 8) - * - 생성할 응답 후보의 개수 - * - 여러 개 생성 후 최적 선택 가능 - */ - private Integer candidateCount; - } - - @Getter - @Setter - @Builder - @NoArgsConstructor - @AllArgsConstructor - public static class SafetySetting { - private String category; - private String threshold; - - // 카테고리 상수 - public static final String HARM_CATEGORY_HARASSMENT = "HARM_CATEGORY_HARASSMENT"; - public static final String HARM_CATEGORY_HATE_SPEECH = "HARM_CATEGORY_HATE_SPEECH"; - public static final String HARM_CATEGORY_SEXUALLY_EXPLICIT = "HARM_CATEGORY_SEXUALLY_EXPLICIT"; - public static final String HARM_CATEGORY_DANGEROUS_CONTENT = "HARM_CATEGORY_DANGEROUS_CONTENT"; - - // 임계값 상수 - public static final String BLOCK_NONE = "BLOCK_NONE"; // 차단 안함 - public static final String BLOCK_LOW_AND_ABOVE = "BLOCK_LOW_AND_ABOVE"; // 낮음 이상 차단 - public static final String BLOCK_MEDIUM_AND_ABOVE = "BLOCK_MEDIUM_AND_ABOVE"; // 중간 이상 차단 - public static final String BLOCK_HIGH = "BLOCK_ONLY_HIGH"; // 높음만 차단 - } - - // 기본 생성자 - 간단한 텍스트만 - public GeminiRequestDto(String message) { - this.contents = List.of(new Content(message)); - } - - // 파라미터 설정 포함 생성자 - public GeminiRequestDto(String message, GenerationConfig config) { - this.contents = List.of(new Content(message)); - this.generationConfig = config; - } - - // 전체 설정 포함 생성자 - public GeminiRequestDto(String message, GenerationConfig config, List safetySettings) { - this.contents = List.of(new Content(message)); - this.generationConfig = config; - this.safetySettings = safetySettings; - } - - public static GeminiRequestDto createForCocktailChat(String message, boolean isDetailedResponse) { - GenerationConfig config = GenerationConfig.builder() - .temperature(0.8) // 적당한 창의성 - .topP(0.95) // 상위 95% 토큰 고려 - .topK(40) // 상위 40개 토큰 - .maxOutputTokens(isDetailedResponse ? 300 : 150) // 상세 답변 vs 간단 답변 - .candidateCount(1) // 하나의 응답만 - .stopSequences(List.of("끝.", "이상입니다.")) // 종료 구문 - .build(); - - // 안전 설정 (칵테일 관련이므로 비교적 관대하게) - List safetySettings = List.of( - SafetySetting.builder() - .category(SafetySetting.HARM_CATEGORY_HARASSMENT) - .threshold(SafetySetting.BLOCK_MEDIUM_AND_ABOVE) - .build(), - SafetySetting.builder() - .category(SafetySetting.HARM_CATEGORY_HATE_SPEECH) - .threshold(SafetySetting.BLOCK_MEDIUM_AND_ABOVE) - .build(), - SafetySetting.builder() - .category(SafetySetting.HARM_CATEGORY_SEXUALLY_EXPLICIT) - .threshold(SafetySetting.BLOCK_MEDIUM_AND_ABOVE) - .build(), - SafetySetting.builder() - .category(SafetySetting.HARM_CATEGORY_DANGEROUS_CONTENT) - .threshold(SafetySetting.BLOCK_LOW_AND_ABOVE) // 음주 관련이므로 더 엄격 - .build() - ); - - return new GeminiRequestDto(message, config, safetySettings); - } - - // 간결한 답변용 프리셋 - public static GeminiRequestDto createBriefResponse(String message) { - GenerationConfig config = GenerationConfig.builder() - .temperature(0.5) // 더 일관된 답변 - .topP(0.8) // 더 집중된 선택 - .topK(20) // 적은 선택지 - .maxOutputTokens(100) // 짧은 답변 - .candidateCount(1) - .build(); - - return new GeminiRequestDto(message, config); - } - - // 창의적 답변용 프리셋 - public static GeminiRequestDto createCreativeResponse(String message) { - GenerationConfig config = GenerationConfig.builder() - .temperature(1.2) // 높은 창의성 - .topP(0.98) // 더 다양한 선택 - .topK(40) // 많은 선택지 - .maxOutputTokens(500) // 긴 답변 허용 - .candidateCount(1) - .build(); - - return new GeminiRequestDto(message, config); - } -} \ No newline at end of file diff --git a/src/main/java/com/back/domain/chatbot/dto/GeminiResponseDto.java b/src/main/java/com/back/domain/chatbot/dto/GeminiResponseDto.java deleted file mode 100644 index c239ccfd..00000000 --- a/src/main/java/com/back/domain/chatbot/dto/GeminiResponseDto.java +++ /dev/null @@ -1,41 +0,0 @@ -package com.back.domain.chatbot.dto; - -import lombok.Getter; -import lombok.Setter; - -import java.util.List; - -@Getter -@Setter -public class GeminiResponseDto { - - private List candidates; - - @Getter - @Setter - public static class Candidate { - private Content content; - } - - @Getter - @Setter - public static class Content { - private List parts; - } - - @Getter - @Setter - public static class Part { - private String text; - } - - public String getGeneratedText() { - if (candidates != null && !candidates.isEmpty() && - candidates.get(0).content != null && - candidates.get(0).content.parts != null && - !candidates.get(0).content.parts.isEmpty()) { - return candidates.get(0).content.parts.get(0).text; - } - return "응답을 생성할 수 없습니다."; - } -} \ No newline at end of file diff --git a/src/main/java/com/back/domain/chatbot/service/ChatbotService.java b/src/main/java/com/back/domain/chatbot/service/ChatbotService.java index 0853eccc..07e80378 100644 --- a/src/main/java/com/back/domain/chatbot/service/ChatbotService.java +++ b/src/main/java/com/back/domain/chatbot/service/ChatbotService.java @@ -8,13 +8,10 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.ai.chat.client.ChatClient; import org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor; -import org.springframework.ai.chat.client.advisor.QuestionAnswerAdvisor; import org.springframework.ai.chat.memory.InMemoryChatMemory; import org.springframework.ai.chat.model.ChatModel; import org.springframework.ai.chat.messages.*; -import org.springframework.ai.document.Document; -import org.springframework.ai.vectorstore.VectorStore; -import org.springframework.ai.vectorstore.SearchRequest; +import org.springframework.ai.openai.OpenAiChatOptions; import org.springframework.beans.factory.annotation.Value; import org.springframework.core.io.Resource; import org.springframework.stereotype.Service; @@ -48,10 +45,10 @@ public class ChatbotService { @Value("${chatbot.history.max-conversation-count:5}") private int maxConversationCount; - @Value("${spring.ai.vertex.ai.gemini.chat.options.temperature:0.8}") - private Double temperature; + @Value("${spring.ai.openai.chat.options.temperature:0.8}") + private Float temperature; - @Value("${spring.ai.vertex.ai.gemini.chat.options.max-output-tokens:300}") + @Value("${spring.ai.openai.chat.options.max-tokens:300}") private Integer maxTokens; private String systemPrompt; @@ -72,7 +69,7 @@ public void init() throws IOException { // ChatClient 고급 설정 this.chatClient = ChatClient.builder(chatModel) .defaultSystem(systemPrompt) - .defaultOptions(ChatOptionsBuilder.builder() + .defaultOptions(OpenAiChatOptions.builder() .withTemperature(temperature) .withMaxTokens(maxTokens) .build()) @@ -101,13 +98,7 @@ public ChatResponseDto sendMessage(ChatRequestDto requestDto) { .user(buildUserMessage(requestDto.getMessage(), messageType)) .advisors(new MessageChatMemoryAdvisor(chatMemory, maxConversationCount)); - // RAG 기능 활성화 (칵테일 정보 검색) - if (vectorStore != null && shouldUseRAG(messageType)) { - promptBuilder.advisors(new QuestionAnswerAdvisor( - vectorStore, - SearchRequest.defaults().withTopK(3) - )); - } + // RAG 기능은 향후 구현 예정 (Vector DB 설정 필요) // 응답 생성 String response = promptBuilder @@ -180,31 +171,27 @@ private String buildUserMessage(String userMessage, MessageType type) { return userMessage + "\n\n" + responseRules; } - private ChatOptions getOptionsForMessageType(MessageType type) { + private OpenAiChatOptions getOptionsForMessageType(MessageType type) { return switch (type) { - case RECIPE -> ChatOptionsBuilder.builder() - .withTemperature(0.3) // 정확성 중시 + case RECIPE -> OpenAiChatOptions.builder() + .withTemperature(0.3f) // 정확성 중시 .withMaxTokens(400) // 레시피는 길게 .build(); - case RECOMMENDATION -> ChatOptionsBuilder.builder() - .withTemperature(0.9) // 다양성 중시 + case RECOMMENDATION -> OpenAiChatOptions.builder() + .withTemperature(0.9f) // 다양성 중시 .withMaxTokens(250) .build(); - case QUESTION -> ChatOptionsBuilder.builder() - .withTemperature(0.7) // 균형 + case QUESTION -> OpenAiChatOptions.builder() + .withTemperature(0.7f) // 균형 .withMaxTokens(200) .build(); - default -> ChatOptionsBuilder.builder() + default -> OpenAiChatOptions.builder() .withTemperature(temperature) .withMaxTokens(maxTokens) .build(); }; } - private boolean shouldUseRAG(MessageType type) { - // 레시피나 추천 요청시 RAG 활성화 - return type == MessageType.RECIPE || type == MessageType.RECOMMENDATION; - } private String postProcessResponse(String response, MessageType type) { // 응답 길이 제한 확인 @@ -288,43 +275,3 @@ public void cleanupInactiveSessions() { } } -// ChatOptions 빌더 헬퍼 클래스 -class ChatOptionsBuilder { - private Double temperature; - private Integer maxTokens; - private Double topP; - private Integer topK; - - public static ChatOptionsBuilder builder() { - return new ChatOptionsBuilder(); - } - - public ChatOptionsBuilder withTemperature(Double temperature) { - this.temperature = temperature; - return this; - } - - public ChatOptionsBuilder withMaxTokens(Integer maxTokens) { - this.maxTokens = maxTokens; - return this; - } - - public ChatOptionsBuilder withTopP(Double topP) { - this.topP = topP; - return this; - } - - public ChatOptionsBuilder withTopK(Integer topK) { - this.topK = topK; - return this; - } - - public ChatOptions build() { - // 실제 ChatOptions 객체 생성 로직 - // Spring AI의 실제 API에 맞게 조정 필요 - return null; // placeholder - } -} - -// ChatOptions placeholder (실제 Spring AI API에 맞게 조정 필요) -interface ChatOptions {} \ No newline at end of file diff --git a/src/main/java/com/back/domain/chatbot/service/GeminiApiService.java b/src/main/java/com/back/domain/chatbot/service/GeminiApiService.java deleted file mode 100644 index e6870a4d..00000000 --- a/src/main/java/com/back/domain/chatbot/service/GeminiApiService.java +++ /dev/null @@ -1,180 +0,0 @@ -package com.back.domain.chatbot.service; - -import com.back.domain.chatbot.dto.GeminiRequestDto; -import com.back.domain.chatbot.dto.GeminiResponseDto; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Service; -import org.springframework.web.reactive.function.client.WebClient; -import reactor.core.publisher.Mono; - -@Service -@RequiredArgsConstructor -@Slf4j -public class GeminiApiService { - - private final WebClient geminiWebClient; - - @Value("${gemini.api.key}") - private String apiKey; - - // 기본 응답 - public Mono generateResponse(String userMessage) { - GeminiRequestDto requestDto = new GeminiRequestDto(userMessage); - - return callGeminiApi(requestDto); - } - - // 간단한 응답 생성 (100 토큰) - public Mono generateBriefResponse(String userMessage) { - GeminiRequestDto requestDto = GeminiRequestDto.createBriefResponse(userMessage); - - return callGeminiApi(requestDto); - } - - // 상세한 응답 생성 (200 토큰, 구조화된 답변) - public Mono generateDetailedResponse(String userMessage) { - GeminiRequestDto requestDto = GeminiRequestDto.createForCocktailChat(userMessage, true); - - return callGeminiApi(requestDto); - } - - // 창의적인 응답 생성 (500 토큰, 높은 창의성) - public Mono generateCreativeResponse(String userMessage) { - GeminiRequestDto requestDto = GeminiRequestDto.createCreativeResponse(userMessage); - - return callGeminiApi(requestDto); - } - - // 사용자 지정 파라미터로 응답 생성 ( 커스텀 ) - public Mono generateCustomResponse( - String userMessage, - Double temperature, - Integer maxTokens, - Double topP, - Integer topK - ) { - GeminiRequestDto.GenerationConfig config = GeminiRequestDto.GenerationConfig.builder() - .temperature(temperature != null ? temperature : 0.8) - .maxOutputTokens(maxTokens != null ? maxTokens : 200) - .topP(topP != null ? topP : 0.95) - .topK(topK != null ? topK : 40) - .candidateCount(1) - .build(); - - GeminiRequestDto requestDto = new GeminiRequestDto(userMessage, config); - - return callGeminiApi(requestDto); - } - - // 메시지 유형에 따른 최적화된 응답 생성 - public Mono generateOptimizedResponse(String userMessage, MessageType messageType) { - GeminiRequestDto requestDto = switch (messageType) { - case RECIPE -> createRecipeRequest(userMessage); - case RECOMMENDATION -> createRecommendationRequest(userMessage); - case QUESTION -> createQuestionRequest(userMessage); - case CASUAL_CHAT -> createCasualChatRequest(userMessage); - default -> new GeminiRequestDto(userMessage); - }; - - return callGeminiApi(requestDto); - } - - // 레시피 요청 (구조화된 답변 필요) - private GeminiRequestDto createRecipeRequest(String message) { - GeminiRequestDto.GenerationConfig config = GeminiRequestDto.GenerationConfig.builder() - .temperature(0.3) // 낮은 temperature로 정확성 향상 - .topP(0.8) - .topK(20) - .maxOutputTokens(400) // 레시피는 좀 더 길게 - .candidateCount(1) - .build(); - - return new GeminiRequestDto(message, config); - } - - // 추천 요청 (적당한 창의성) - private GeminiRequestDto createRecommendationRequest(String message) { - GeminiRequestDto.GenerationConfig config = GeminiRequestDto.GenerationConfig.builder() - .temperature(0.9) // 다양한 추천을 위해 높게 - .topP(0.95) - .topK(40) - .maxOutputTokens(250) - .candidateCount(1) - .build(); - - return new GeminiRequestDto(message, config); - } - - // 일반 질문 (균형잡힌 설정) - private GeminiRequestDto createQuestionRequest(String message) { - GeminiRequestDto.GenerationConfig config = GeminiRequestDto.GenerationConfig.builder() - .temperature(0.7) - .topP(0.9) - .topK(30) - .maxOutputTokens(200) - .candidateCount(1) - .build(); - - return new GeminiRequestDto(message, config); - } - - // 캐주얼한 대화 (자연스러움 중시) - private GeminiRequestDto createCasualChatRequest(String message) { - GeminiRequestDto.GenerationConfig config = GeminiRequestDto.GenerationConfig.builder() - .temperature(1.0) - .topP(0.95) - .topK(40) - .maxOutputTokens(150) - .candidateCount(1) - .build(); - - return new GeminiRequestDto(message, config); - } - - // Gemini API 호출 공통 메서드 - private Mono callGeminiApi(GeminiRequestDto requestDto) { - log.debug("Gemini API 호출 - Temperature: {}, MaxTokens: {}", - requestDto.getGenerationConfig() != null ? requestDto.getGenerationConfig().getTemperature() : "default", - requestDto.getGenerationConfig() != null ? requestDto.getGenerationConfig().getMaxOutputTokens() : "default" - ); - - return geminiWebClient.post() - .uri("?key=" + apiKey) - .bodyValue(requestDto) - .retrieve() - .bodyToMono(GeminiResponseDto.class) - .map(GeminiResponseDto::getGeneratedText) - .doOnSuccess(response -> log.debug("응답 길이: {} 글자", response != null ? response.length() : 0)) - .doOnError(error -> log.error("Gemini API 호출 실패: ", error)) - .onErrorReturn("죄송합니다. 현재 응답을 생성할 수 없습니다. 잠시 후 다시 시도해주세요."); - } - - // 유형 ENUM 정의 - public enum MessageType { - RECIPE, // 레시피 요청 - RECOMMENDATION, // 추천 요청 - QUESTION, // 일반 질문 - CASUAL_CHAT, // 캐주얼 대화 - UNKNOWN // 분류 불가 - } - - // 메시지 유형 감지 (간단한 키워드 기반) - public static MessageType detectMessageType(String message) { - String lowerMessage = message.toLowerCase(); - - if (lowerMessage.contains("레시피") || lowerMessage.contains("만드는") || - lowerMessage.contains("제조") || lowerMessage.contains("recipe")) { - return MessageType.RECIPE; - } else if (lowerMessage.contains("추천") || lowerMessage.contains("어때") || - lowerMessage.contains("뭐가 좋") || lowerMessage.contains("recommend")) { - return MessageType.RECOMMENDATION; - } else if (lowerMessage.contains("?") || lowerMessage.contains("뭐") || - lowerMessage.contains("어떻") || lowerMessage.contains("왜")) { - return MessageType.QUESTION; - } else { - return MessageType.CASUAL_CHAT; - } - } -} \ No newline at end of file diff --git a/src/main/java/com/back/global/appConfig/GeminiConfig.java b/src/main/java/com/back/global/appConfig/GeminiConfig.java deleted file mode 100644 index 9695ed7b..00000000 --- a/src/main/java/com/back/global/appConfig/GeminiConfig.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.back.global.appConfig; - -import org.springframework.beans.factory.annotation.Value; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.web.reactive.function.client.WebClient; - -@Configuration -public class GeminiConfig { - - @Value("${gemini.api.url}") - private String geminiApiUrl; - - @Value("${gemini.api.model-name}") - private String modelName; - - @Bean - public WebClient geminiWebClient() { - return WebClient.builder() - .baseUrl(geminiApiUrl + "/" + modelName + ":generateContent") - .defaultHeader("Content-Type", "application/json") - .build(); - } -} \ No newline at end of file diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 12c2152b..1424c3b3 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -54,9 +54,10 @@ spring: chat: base-url: "https://generativelanguage.googleapis.com/v1beta/openai/" options: - model: "gemini-2.0-flash-lite" - temperature: 0.0 - completions-path: "/chat/completions" # gemini 사용시 v1/chat/completions 로 들어가지 않음 + model: "gemini-2.0-flash-exp" + temperature: 0.8 + max-tokens: 300 + completions-path: "/chat/completions" springdoc: default-produces-media-type: application/json;charset=UTF-8 From 3eeb0a4a87b18d4eed42515bf9321b3c0eebbaaa Mon Sep 17 00:00:00 2001 From: GerHerMo Date: Thu, 25 Sep 2025 10:20:32 +0900 Subject: [PATCH 6/7] fix: compile error fix --- .../domain/chatbot/service/ChatbotService.java | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/main/java/com/back/domain/chatbot/service/ChatbotService.java b/src/main/java/com/back/domain/chatbot/service/ChatbotService.java index 07e80378..54f48eff 100644 --- a/src/main/java/com/back/domain/chatbot/service/ChatbotService.java +++ b/src/main/java/com/back/domain/chatbot/service/ChatbotService.java @@ -46,7 +46,7 @@ public class ChatbotService { private int maxConversationCount; @Value("${spring.ai.openai.chat.options.temperature:0.8}") - private Float temperature; + private Double temperature; @Value("${spring.ai.openai.chat.options.max-tokens:300}") private Integer maxTokens; @@ -96,7 +96,7 @@ public ChatResponseDto sendMessage(ChatRequestDto requestDto) { var promptBuilder = chatClient.prompt() .system(buildSystemMessage(messageType)) .user(buildUserMessage(requestDto.getMessage(), messageType)) - .advisors(new MessageChatMemoryAdvisor(chatMemory, maxConversationCount)); + .advisors(new MessageChatMemoryAdvisor(chatMemory)); // RAG 기능은 향후 구현 예정 (Vector DB 설정 필요) @@ -138,11 +138,12 @@ private void loadConversationHistory(String sessionId, InMemoryChatMemory chatMe chatConversationRepository.findBySessionIdOrderByCreatedAtAsc(sessionId); // 최근 N개의 대화만 메모리에 로드 + String sessionIdForMemory = sessionId; conversations.stream() .skip(Math.max(0, conversations.size() - maxConversationCount)) .forEach(conv -> { - chatMemory.add(new UserMessage(conv.getUserMessage())); - chatMemory.add(new AssistantMessage(conv.getBotResponse())); + chatMemory.add(sessionIdForMemory, new UserMessage(conv.getUserMessage())); + chatMemory.add(sessionIdForMemory, new AssistantMessage(conv.getBotResponse())); }); } @@ -174,15 +175,15 @@ private String buildUserMessage(String userMessage, MessageType type) { private OpenAiChatOptions getOptionsForMessageType(MessageType type) { return switch (type) { case RECIPE -> OpenAiChatOptions.builder() - .withTemperature(0.3f) // 정확성 중시 + .withTemperature(0.3) // 정확성 중시 .withMaxTokens(400) // 레시피는 길게 .build(); case RECOMMENDATION -> OpenAiChatOptions.builder() - .withTemperature(0.9f) // 다양성 중시 + .withTemperature(0.9) // 다양성 중시 .withMaxTokens(250) .build(); case QUESTION -> OpenAiChatOptions.builder() - .withTemperature(0.7f) // 균형 + .withTemperature(0.7) // 균형 .withMaxTokens(200) .build(); default -> OpenAiChatOptions.builder() From 19777f0f3413029f4a68ce28fca53b8b46ffc5b9 Mon Sep 17 00:00:00 2001 From: GerHerMo Date: Thu, 25 Sep 2025 10:46:55 +0900 Subject: [PATCH 7/7] refactor: maxtoken=800 --- src/main/resources/application.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 1424c3b3..18096670 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -56,7 +56,7 @@ spring: options: model: "gemini-2.0-flash-exp" temperature: 0.8 - max-tokens: 300 + max-tokens: 800 completions-path: "/chat/completions" springdoc: