Dongjin1203 commited on
Commit
564c5be
·
1 Parent(s): e78ff28

쿼리 라우터 out_of_scope 키워드 추가, 숫자 사업 관련 키워드 추가, 짧은 질문 범위 수정

Browse files
src/generator/generator_gguf.py CHANGED
@@ -124,10 +124,18 @@ class GGUFGenerator:
124
  n_gpu_layers=self.n_gpu_layers,
125
  n_ctx=self.n_ctx,
126
  n_threads=self.n_threads,
127
- verbose=False,
128
  )
129
 
 
 
130
  logger.info("✅ GGUF 모델 로드 완료!")
 
 
 
 
 
 
131
 
132
  except FileNotFoundError as e:
133
  logger.error(f"❌ 모델 파일을 찾을 수 없습니다: {e}")
@@ -217,11 +225,17 @@ class GGUFGenerator:
217
  top_p=top_p,
218
  echo=False, # 프롬프트 반복 안 함
219
  stop=[
 
220
  "###", "\n\n###",
221
  "### 사용자", "\n사용자:",
222
  "</s>",
223
- "한국어 답변", "한국어로 답변", "지침:", # 메타 텍스트 차단
224
- "문장", "(문장" # 메타 번호 차단
 
 
 
 
 
225
  ],
226
  )
227
 
 
124
  n_gpu_layers=self.n_gpu_layers,
125
  n_ctx=self.n_ctx,
126
  n_threads=self.n_threads,
127
+ verbose=True, # ✅ 디버그 로그 활성화
128
  )
129
 
130
+ # ✅ 실제 적용된 n_ctx 확인
131
+ actual_n_ctx = self.model.n_ctx()
132
  logger.info("✅ GGUF 모델 로드 완료!")
133
+ logger.info(f" - 설정한 n_ctx: {self.n_ctx}")
134
+ logger.info(f" - 실제 n_ctx: {actual_n_ctx}")
135
+
136
+ if actual_n_ctx < self.n_ctx:
137
+ logger.warning(f"⚠️ n_ctx가 예상보다 작습니다: {actual_n_ctx} < {self.n_ctx}")
138
+ logger.warning(f" 메모리 부족일 수 있습니다. n_gpu_layers를 줄여보세요.")
139
 
140
  except FileNotFoundError as e:
141
  logger.error(f"❌ 모델 파일을 찾을 수 없습니다: {e}")
 
225
  top_p=top_p,
226
  echo=False, # 프롬프트 반복 안 함
227
  stop=[
228
+ # 구분자
229
  "###", "\n\n###",
230
  "### 사용자", "\n사용자:",
231
  "</s>",
232
+ # 메타 텍스트 차단
233
+ "한국어 답변", "한국어로 답변", "지침:",
234
+ "문장", "(문장",
235
+ # ✅ 질문 패턴 차단 (답변 후 질문 생성 방지)
236
+ "\n\n", # 단락 구분
237
+ "?", # 질문 기호
238
+ "요?", "까?", "나요?", "습니까?" # 질문 어미
239
  ],
240
  )
241
 
src/router/query_router.py CHANGED
@@ -5,61 +5,133 @@ import logging
5
  logger = logging.getLogger(__name__)
6
 
7
  class QueryRouter:
8
- """Query를 RAG vs Direct로 라우팅"""
 
9
 
 
 
 
 
 
10
  def __init__(self):
11
- # 키워드 정의
12
  self.greeting_keywords = [
13
- "안녕", "hi", "hello", "반가워", "처음"
14
  ]
15
-
 
16
  self.thanks_keywords = [
17
- "고마워", "감사", "thanks", "고맙"
18
  ]
19
-
 
20
  self.document_keywords = [
21
  # 돈 관련
22
- "예산", "비용", "금액", "원", "만원", "억",
23
  # 일정 관련
24
- "기한", "마감", "언제", "기간", "납기",
25
  # 문서 관련
26
- "요구사항", "제출", "서류", "양식", "평가",
27
  # 조직 관련
28
- "발주", "기관", "담당자", "연락처",
29
- # 사업 관련
30
- "사업명", "과업", "범위", "목적"
 
 
 
31
  ]
32
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
33
  def classify(self, query: str) -> dict:
 
 
 
 
 
 
 
 
 
 
34
  query_lower = query.lower()
 
35
 
36
- # 짧은 질문일 때만 인사/감사 체크
37
- if len(query) < 20: # ← is_short 대신 직접 체크
 
 
 
 
 
 
 
 
 
 
 
38
  if any(kw in query_lower for kw in self.thanks_keywords):
 
39
  return {
40
  'type': 'thanks',
41
- 'confidence': 0.9,
42
  'reason': '감사 인사 감지'
43
  }
44
-
45
- elif any(kw in query_lower for kw in self.greeting_keywords):
 
 
46
  return {
47
  'type': 'greeting',
48
- 'confidence': 0.9,
49
  'reason': '인사 감지'
50
  }
51
 
52
- # 문서 관련 판별
53
- if any(kw in query_lower for kw in self.document_keywords):
 
 
 
 
 
54
  return {
55
  'type': 'document',
56
- 'confidence': 0.85,
57
- 'reason': '문서 키워드 감지'
58
  }
 
 
 
 
 
 
59
 
60
- # 3. 기본값
 
 
 
 
 
 
 
 
 
61
  return {
62
  'type': 'out_of_scope',
63
- 'confidence': 0.5,
64
- 'reason': 'RFP 키워드 없음'
65
  }
 
5
  logger = logging.getLogger(__name__)
6
 
7
  class QueryRouter:
8
+ """
9
+ Query를 RAG vs Direct로 라우팅 (하이브리드 버전)
10
 
11
+ improved + lee 버전의 장점 결합:
12
+ - improved: out_of_scope 키워드로 명확한 비RFP 질문 감지
13
+ - lee: 숫자 + 사업 키워드 조합으로 맥락 파악
14
+ """
15
+
16
  def __init__(self):
17
+ # 인사 키워드
18
  self.greeting_keywords = [
19
+ "안녕", "hi", "hello", "반가워", "처음", "인사"
20
  ]
21
+
22
+ # 감사 키워드
23
  self.thanks_keywords = [
24
+ "고마워", "감사", "thanks", "고맙", "땡큐"
25
  ]
26
+
27
+ # RFP/입찰 관련 키워드
28
  self.document_keywords = [
29
  # 돈 관련
30
+ "예산", "비용", "금액", "원", "만원", "억", "억원",
31
  # 일정 관련
32
+ "기한", "마감", "언제", "기간", "납기", "일정",
33
  # 문서 관련
34
+ "요구사항", "제출", "서류", "양식", "평가", "rfp", "제안서",
35
  # 조직 관련
36
+ "발주", "기관", "담당자", "연락처", "부처", "지자체",
37
+ # 사업/계약 관련
38
+ "사업", "사업명", "과업", "범위", "목적", "계약", "입찰",
39
+ "공고", "프로젝트", "위탁", "용역", "협상", "제안",
40
+ # 제도/규정 관련
41
+ "법", "규정", "기준", "조건", "중소기업", "대기업"
42
  ]
43
+
44
+ # ✅ out_of_scope 키워드 (improved 버전에서 가져옴)
45
+ self.out_of_scope_keywords = [
46
+ # 음식
47
+ "점심", "저녁", "아침", "식사", "밥", "메뉴", "맛집", "음식", "요리",
48
+ # 날씨/일상
49
+ "날씨", "기온", "비", "눈", "추워", "더워",
50
+ # 엔터테인먼트
51
+ "영화", "드라마", "게임", "노래", "음악", "유튜브",
52
+ # 여행/취미
53
+ "여행", "관광", "휴가", "취미", "운동", "등산",
54
+ # 금융/투자 (RFP와 무관)
55
+ "주식", "코인", "비트코인", "투자", "펀드", "부동산",
56
+ # 기타
57
+ "사랑", "연애", "데이트", "친구", "가족"
58
+ ]
59
+
60
  def classify(self, query: str) -> dict:
61
+ """
62
+ 쿼리 분류
63
+
64
+ Returns:
65
+ dict: {
66
+ 'type': 'greeting' | 'thanks' | 'document' | 'out_of_scope',
67
+ 'confidence': 0.0~1.0,
68
+ 'reason': str
69
+ }
70
+ """
71
  query_lower = query.lower()
72
+ query_length = len(query)
73
 
74
+ # 1. 명확한 out_of_scope 먼저 체크 (improved 로직)
75
+ for keyword in self.out_of_scope_keywords:
76
+ if keyword in query_lower:
77
+ logger.info(f"🚫 out_of_scope 감지: '{keyword}' 키워드")
78
+ return {
79
+ 'type': 'out_of_scope',
80
+ 'confidence': 0.95,
81
+ 'reason': f'비RFP 키워드 감지: {keyword}'
82
+ }
83
+
84
+ # 2. 짧은 질문일 때만 인사/감사 체크 (lee의 25자 기준 사용)
85
+ if query_length < 25:
86
+ # 감사
87
  if any(kw in query_lower for kw in self.thanks_keywords):
88
+ logger.info(f"🙏 thanks 감지")
89
  return {
90
  'type': 'thanks',
91
+ 'confidence': 0.90,
92
  'reason': '감사 인사 감지'
93
  }
94
+
95
+ # 인사
96
+ if any(kw in query_lower for kw in self.greeting_keywords):
97
+ logger.info(f"👋 greeting 감지")
98
  return {
99
  'type': 'greeting',
100
+ 'confidence': 0.90,
101
  'reason': '인사 감지'
102
  }
103
 
104
+ # 3. RFP/문서 관련 키워드 체크 (동적 신뢰도)
105
+ document_matches = sum(1 for kw in self.document_keywords if kw in query_lower)
106
+
107
+ if document_matches > 0:
108
+ # 매칭된 키워드 수에 따라 신뢰도 조���
109
+ confidence = min(0.70 + (document_matches * 0.05), 0.95)
110
+ logger.info(f"📄 document 감지: {document_matches}개 키워드 매칭")
111
  return {
112
  'type': 'document',
113
+ 'confidence': confidence,
114
+ 'reason': f'RFP 키워드 {document_matches}개 감지'
115
  }
116
+
117
+ # ✅ 4. 숫자 + 사업 키워드 조합 체크 (lee 로직)
118
+ # "12개월 사업", "5억원 프로젝트" 같은 맥락 파악
119
+ has_number = any(ch.isdigit() for ch in query)
120
+ business_terms = ["사업", "과업", "계획", "프로젝트", "용역"]
121
+ has_business = any(term in query_lower for term in business_terms)
122
 
123
+ if has_number and has_business:
124
+ logger.info(f"🔢 document 감지: 숫자 + 사업 키워드 조합")
125
+ return {
126
+ 'type': 'document',
127
+ 'confidence': 0.65,
128
+ 'reason': '숫자와 사업 키워드 동시 감지'
129
+ }
130
+
131
+ # 5. 기본값: out_of_scope (improved의 0.6 사용)
132
+ logger.info(f"🚫 out_of_scope (기본값): RFP 키워드 없음")
133
  return {
134
  'type': 'out_of_scope',
135
+ 'confidence': 0.60,
136
+ 'reason': 'RFP 관련 키워드 미감지'
137
  }
src/visualization/chatbot_app.py CHANGED
@@ -166,7 +166,7 @@ def initialize_rag(model_type):
166
  rag = GGUFRAGPipeline(
167
  config=config,
168
  n_gpu_layers=35, # T4에서 전체 레이어 GPU 사용
169
- n_ctx=2048, # 컨텍스트 길이
170
  n_threads=4, # CPU 스레드 (GPU 사용 시 낮게)
171
  max_new_tokens=512, # 최대 생성 토큰
172
  temperature=0.7,
 
166
  rag = GGUFRAGPipeline(
167
  config=config,
168
  n_gpu_layers=35, # T4에서 전체 레이어 GPU 사용
169
+ n_ctx=4096, # 컨텍스트 길이
170
  n_threads=4, # CPU 스레드 (GPU 사용 시 낮게)
171
  max_new_tokens=512, # 최대 생성 토큰
172
  temperature=0.7,