Skip to content

Commit 63374f6

Browse files
authored
merge: 프롬포트 엔지니어링
[feat] AI 챗봇 프롬포트 엔지니어링 #83
2 parents 2cbcc82 + 41f12b8 commit 63374f6

File tree

6 files changed

+430
-19
lines changed

6 files changed

+430
-19
lines changed

src/main/java/com/back/domain/chatbot/dto/GeminiRequestDto.java

Lines changed: 152 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package com.back.domain.chatbot.dto;
22

3-
import lombok.Getter;
4-
import lombok.Setter;
3+
import lombok.*;
54

65
import java.util.List;
76

@@ -10,6 +9,8 @@
109
public class GeminiRequestDto {
1110

1211
private List<Content> contents;
12+
private GenerationConfig generationConfig;
13+
private List<SafetySetting> safetySettings;
1314

1415
@Getter
1516
@Setter
@@ -31,7 +32,156 @@ public Part(String text) {
3132
}
3233
}
3334

35+
@Getter
36+
@Setter
37+
@Builder
38+
@NoArgsConstructor
39+
@AllArgsConstructor
40+
public static class GenerationConfig { // 생성 설정
41+
/**
42+
* Temperature (0.0 ~ 2.0)
43+
* - 낮을수록 (0.0): 일관되고 예측 가능한 응답
44+
* - 높을수록 (2.0): 창의적이고 다양한 응답
45+
* - 권장값: 0.7 ~ 1.0
46+
*/
47+
private Double temperature;
48+
49+
/**
50+
* Top-P (0.0 ~ 1.0)
51+
* - 누적 확률 임계값
52+
* - 0.95 = 상위 95% 확률의 토큰만 고려
53+
* - 낮을수록 더 집중된 응답
54+
*/
55+
private Double topP;
56+
57+
/**
58+
* Top-K (1 ~ 40)
59+
* - 고려할 토큰의 최대 개수
60+
* - 40 = 상위 40개 토큰만 고려
61+
* - 낮을수록 더 결정적인 응답
62+
*/
63+
private Integer topK;
64+
65+
/**
66+
* Max Output Tokens
67+
* - 응답의 최대 토큰 수 (출력 길이 제한)
68+
* - Gemini 1.5 Flash: 최대 8192 토큰
69+
* - Gemini 1.5 Pro: 최대 8192 토큰
70+
* - 한글 1글자 ≈ 1-2 토큰, 영어 3-4글자 ≈ 1 토큰
71+
*/
72+
private Integer maxOutputTokens;
73+
74+
/**
75+
* Stop Sequences
76+
* - 이 문자열을 만나면 생성 중단
77+
* - 예: ["끝", "END", "\n\n"]
78+
*/
79+
private List<String> stopSequences;
80+
81+
/**
82+
* Candidate Count (1 ~ 8)
83+
* - 생성할 응답 후보의 개수
84+
* - 여러 개 생성 후 최적 선택 가능
85+
*/
86+
private Integer candidateCount;
87+
}
88+
89+
@Getter
90+
@Setter
91+
@Builder
92+
@NoArgsConstructor
93+
@AllArgsConstructor
94+
public static class SafetySetting {
95+
private String category;
96+
private String threshold;
97+
98+
// 카테고리 상수
99+
public static final String HARM_CATEGORY_HARASSMENT = "HARM_CATEGORY_HARASSMENT";
100+
public static final String HARM_CATEGORY_HATE_SPEECH = "HARM_CATEGORY_HATE_SPEECH";
101+
public static final String HARM_CATEGORY_SEXUALLY_EXPLICIT = "HARM_CATEGORY_SEXUALLY_EXPLICIT";
102+
public static final String HARM_CATEGORY_DANGEROUS_CONTENT = "HARM_CATEGORY_DANGEROUS_CONTENT";
103+
104+
// 임계값 상수
105+
public static final String BLOCK_NONE = "BLOCK_NONE"; // 차단 안함
106+
public static final String BLOCK_LOW_AND_ABOVE = "BLOCK_LOW_AND_ABOVE"; // 낮음 이상 차단
107+
public static final String BLOCK_MEDIUM_AND_ABOVE = "BLOCK_MEDIUM_AND_ABOVE"; // 중간 이상 차단
108+
public static final String BLOCK_HIGH = "BLOCK_ONLY_HIGH"; // 높음만 차단
109+
}
110+
111+
// 기본 생성자 - 간단한 텍스트만
34112
public GeminiRequestDto(String message) {
35113
this.contents = List.of(new Content(message));
36114
}
115+
116+
// 파라미터 설정 포함 생성자
117+
public GeminiRequestDto(String message, GenerationConfig config) {
118+
this.contents = List.of(new Content(message));
119+
this.generationConfig = config;
120+
}
121+
122+
// 전체 설정 포함 생성자
123+
public GeminiRequestDto(String message, GenerationConfig config, List<SafetySetting> safetySettings) {
124+
this.contents = List.of(new Content(message));
125+
this.generationConfig = config;
126+
this.safetySettings = safetySettings;
127+
}
128+
129+
public static GeminiRequestDto createForCocktailChat(String message, boolean isDetailedResponse) {
130+
GenerationConfig config = GenerationConfig.builder()
131+
.temperature(0.8) // 적당한 창의성
132+
.topP(0.95) // 상위 95% 토큰 고려
133+
.topK(40) // 상위 40개 토큰
134+
.maxOutputTokens(isDetailedResponse ? 300 : 150) // 상세 답변 vs 간단 답변
135+
.candidateCount(1) // 하나의 응답만
136+
.stopSequences(List.of("끝.", "이상입니다.")) // 종료 구문
137+
.build();
138+
139+
// 안전 설정 (칵테일 관련이므로 비교적 관대하게)
140+
List<SafetySetting> safetySettings = List.of(
141+
SafetySetting.builder()
142+
.category(SafetySetting.HARM_CATEGORY_HARASSMENT)
143+
.threshold(SafetySetting.BLOCK_MEDIUM_AND_ABOVE)
144+
.build(),
145+
SafetySetting.builder()
146+
.category(SafetySetting.HARM_CATEGORY_HATE_SPEECH)
147+
.threshold(SafetySetting.BLOCK_MEDIUM_AND_ABOVE)
148+
.build(),
149+
SafetySetting.builder()
150+
.category(SafetySetting.HARM_CATEGORY_SEXUALLY_EXPLICIT)
151+
.threshold(SafetySetting.BLOCK_MEDIUM_AND_ABOVE)
152+
.build(),
153+
SafetySetting.builder()
154+
.category(SafetySetting.HARM_CATEGORY_DANGEROUS_CONTENT)
155+
.threshold(SafetySetting.BLOCK_LOW_AND_ABOVE) // 음주 관련이므로 더 엄격
156+
.build()
157+
);
158+
159+
return new GeminiRequestDto(message, config, safetySettings);
160+
}
161+
162+
// 간결한 답변용 프리셋
163+
public static GeminiRequestDto createBriefResponse(String message) {
164+
GenerationConfig config = GenerationConfig.builder()
165+
.temperature(0.5) // 더 일관된 답변
166+
.topP(0.8) // 더 집중된 선택
167+
.topK(20) // 적은 선택지
168+
.maxOutputTokens(100) // 짧은 답변
169+
.candidateCount(1)
170+
.build();
171+
172+
return new GeminiRequestDto(message, config);
173+
}
174+
175+
// 창의적 답변용 프리셋
176+
public static GeminiRequestDto createCreativeResponse(String message) {
177+
GenerationConfig config = GenerationConfig.builder()
178+
.temperature(1.2) // 높은 창의성
179+
.topP(0.98) // 더 다양한 선택
180+
.topK(40) // 많은 선택지
181+
.maxOutputTokens(500) // 긴 답변 허용
182+
.candidateCount(1)
183+
.build();
184+
185+
return new GeminiRequestDto(message, config);
186+
}
37187
}

src/main/java/com/back/domain/chatbot/service/ChatbotService.java

Lines changed: 57 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,15 @@
66
import com.back.domain.chatbot.repository.ChatConversationRepository;
77
import lombok.RequiredArgsConstructor;
88
import lombok.extern.slf4j.Slf4j;
9+
import org.springframework.beans.factory.annotation.Value;
10+
import org.springframework.core.io.Resource;
911
import org.springframework.stereotype.Service;
1012
import org.springframework.transaction.annotation.Transactional;
13+
import org.springframework.util.StreamUtils;
1114

15+
import jakarta.annotation.PostConstruct;
16+
import java.io.IOException;
17+
import java.nio.charset.StandardCharsets;
1218
import java.util.List;
1319
import java.util.UUID;
1420

@@ -20,6 +26,25 @@ public class ChatbotService {
2026
private final GeminiApiService geminiApiService;
2127
private final ChatConversationRepository chatConversationRepository;
2228

29+
@Value("classpath:prompts/chatbot-system-prompt.txt")
30+
private Resource systemPromptResource;
31+
32+
@Value("classpath:prompts/chatbot-response-rules.txt")
33+
private Resource responseRulesResource;
34+
35+
@Value("${chatbot.history.max-conversation-count:5}")
36+
private int maxConversationCount;
37+
38+
private String systemPrompt;
39+
private String responseRules;
40+
41+
@PostConstruct
42+
public void init() throws IOException {
43+
this.systemPrompt = StreamUtils.copyToString(systemPromptResource.getInputStream(), StandardCharsets.UTF_8);
44+
this.responseRules = StreamUtils.copyToString(responseRulesResource.getInputStream(), StandardCharsets.UTF_8);
45+
log.info("챗봇 시스템 프롬프트가 로드되었습니다. (길이: {} 문자)", systemPrompt.length());
46+
}
47+
2348
@Transactional
2449
public ChatResponseDto sendMessage(ChatRequestDto requestDto) {
2550
String sessionId = requestDto.getSessionId();
@@ -50,27 +75,44 @@ public ChatResponseDto sendMessage(ChatRequestDto requestDto) {
5075
}
5176

5277
private String buildContextualMessage(String userMessage, String sessionId) {
53-
List<ChatConversation> recentConversations = chatConversationRepository
54-
.findBySessionIdOrderByCreatedAtAsc(sessionId);
55-
56-
if (recentConversations.isEmpty()) {
57-
return "당신은 칵테일 전문 챗봇입니다. 칵테일에 관련된 질문에 친근하고 도움이 되는 답변을 해주세요. 질문: " + userMessage;
58-
}
78+
List<ChatConversation> recentConversations = getRecentConversations(sessionId);
5979

6080
StringBuilder contextBuilder = new StringBuilder();
61-
contextBuilder.append("당신은 칵테일 전문 챗봇입니다. 다음은 이전 대화 내용입니다:\n\n");
81+
contextBuilder.append(systemPrompt).append("\n\n");
82+
83+
appendConversationHistory(contextBuilder, recentConversations);
84+
appendCurrentQuestion(contextBuilder, userMessage);
85+
appendResponseInstructions(contextBuilder);
86+
87+
return contextBuilder.toString();
88+
}
89+
90+
private List<ChatConversation> getRecentConversations(String sessionId) {
91+
return chatConversationRepository.findBySessionIdOrderByCreatedAtAsc(sessionId);
92+
}
6293

63-
int maxHistory = Math.min(recentConversations.size(), 5);
64-
for (int i = Math.max(0, recentConversations.size() - maxHistory); i < recentConversations.size(); i++) {
65-
ChatConversation conv = recentConversations.get(i);
66-
contextBuilder.append("사용자: ").append(conv.getUserMessage()).append("\n");
67-
contextBuilder.append("챗봇: ").append(conv.getBotResponse()).append("\n\n");
94+
private void appendConversationHistory(StringBuilder contextBuilder, List<ChatConversation> conversations) {
95+
if (!conversations.isEmpty()) {
96+
contextBuilder.append("=== 이전 대화 기록 ===\n");
97+
98+
int maxHistory = Math.min(conversations.size(), maxConversationCount);
99+
int startIdx = Math.max(0, conversations.size() - maxHistory);
100+
101+
for (int i = startIdx; i < conversations.size(); i++) {
102+
ChatConversation conv = conversations.get(i);
103+
contextBuilder.append("사용자: ").append(conv.getUserMessage()).append("\n");
104+
contextBuilder.append("AI 바텐더: ").append(conv.getBotResponse()).append("\n\n");
105+
}
106+
contextBuilder.append("=================\n\n");
68107
}
108+
}
69109

70-
contextBuilder.append("새로운 질문: ").append(userMessage);
71-
contextBuilder.append("\n\n이전 대화 맥락을 고려하여 친근하고 도움이 되는 답변을 해주세요.");
110+
private void appendCurrentQuestion(StringBuilder contextBuilder, String userMessage) {
111+
contextBuilder.append("현재 사용자 질문: ").append(userMessage).append("\n\n");
112+
}
72113

73-
return contextBuilder.toString();
114+
private void appendResponseInstructions(StringBuilder contextBuilder) {
115+
contextBuilder.append(responseRules);
74116
}
75117

76118
@Transactional(readOnly = true)

0 commit comments

Comments
 (0)