Skip to content

Commit 62eae73

Browse files
authored
Refactor: QnA 에이전트 경로 변경 및 hallucination 방지 로직 추가
QnA 에이전트 내부 로직을 수정하여 tool을 거치지 않고 RAGSystem을 직접 호출하도록 경로를 변경했습니다. 또한, RAG로 답변할 수 없는 경우를 처리하여 환각(hallucination) 응답을 방지하는 로직을 추가했습니다.
2 parents 0d244bb + c709592 commit 62eae73

11 files changed

Lines changed: 250 additions & 130 deletions

File tree

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,5 @@ models/*.ckpt
1313

1414
# IDE 파일
1515
.idea/
16-
*.iml
16+
*.iml
17+

README.md

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,18 @@ docker compose up -d
2121

2222
```bash
2323
docker compose down
24-
```
24+
```
25+
26+
## 📊 데이터베이스 설정
27+
28+
### VectorDB 사용
29+
이 프로젝트는 기존 VectorDB의 pickle 파일들을 직접 사용합니다:
30+
- `core/qna/VectorDB/q_data.pkl`: 질문 임베딩 데이터
31+
- `core/qna/VectorDB/s_data.pkl`: 답변 스니펫 임베딩 데이터
32+
- `core/qna/VectorDB/k_data.pkl`: 키워드 임베딩 데이터
33+
34+
**장점**:
35+
- ChromaDB 없이도 동작
36+
- 빠른 로딩 속도
37+
- Git 저장소 크기 최적화
38+
- 간단한 구조

core/qna/agent/qna_agent.py

Lines changed: 146 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,160 @@
11
from core.shared.states.states import CustomsAgentState
22
from core.qna.main import RAGSystem
3+
from core.shared.utils.llm import get_llm
4+
from langchain_core.messages import HumanMessage
5+
import re
6+
7+
def compare_responses(llm_response: str, rag_response: str, comparison_llm) -> bool:
8+
"""
9+
LLM을 사용하여 두 응답의 유사성을 판단합니다.
10+
11+
Args:
12+
llm_response: LLM 기반 응답
13+
rag_response: RAG 기반 응답
14+
comparison_llm: 비교용 LLM 인스턴스
15+
16+
Returns:
17+
bool: True if responses are similar, False if they differ significantly
18+
"""
19+
comparison_prompt = f"""다음은 동일한 질문에 대한 두 개의 답변입니다.
20+
이 두 답변이 내용적으로 유사한지 판단해주세요.
21+
22+
답변 1: {llm_response}
23+
24+
답변 2: {rag_response}
25+
26+
두 답변의 핵심 내용이 유사하거나 동일하다면 "유사함"이라고 답하고,
27+
내용이 다르거나 모순되거나 추가 정보가 포함되어 있다면 "다름"이라고 답해주세요.
28+
29+
답변:"""
30+
31+
try:
32+
comparison_result = comparison_llm.invoke([HumanMessage(content=comparison_prompt)])
33+
comparison_text = str(comparison_result.content) if hasattr(comparison_result, 'content') else str(comparison_result)
34+
35+
# "유사함"이 포함되어 있으면 True 반환
36+
return "유사함" in comparison_text or "similar" in comparison_text.lower()
37+
except Exception as e:
38+
# LLM 비교 실패 시 기본 로직 사용
39+
print(f"LLM 비교 실패, 기본 로직 사용: {e}")
40+
41+
# 간단한 키워드 기반 비교
42+
def extract_keywords(text: str) -> set:
43+
customs_keywords = [
44+
'관세', '세금', '수입', '수출', '통관', '신고', '세율', '과세', '면세',
45+
'반입', '반출', '검사', '검역', '위험물', '금지', '제한', '허가',
46+
'서류', '증명', '신청', '처리', '기간', '비용', '요금', '부과'
47+
]
48+
49+
keywords = set()
50+
for keyword in customs_keywords:
51+
if keyword in text:
52+
keywords.add(keyword)
53+
return keywords
54+
55+
llm_keywords = extract_keywords(llm_response.lower())
56+
rag_keywords = extract_keywords(rag_response.lower())
57+
58+
if llm_keywords and rag_keywords:
59+
common_keywords = llm_keywords.intersection(rag_keywords)
60+
total_keywords = llm_keywords.union(rag_keywords)
61+
similarity = len(common_keywords) / len(total_keywords) if total_keywords else 0
62+
return similarity >= 0.7
63+
64+
return False
365

466
def qna_agent(state: CustomsAgentState) -> CustomsAgentState:
5-
"""QNA RAG 에이전트 - 실제 RAG 시스템 사용"""
67+
"""QNA 에이전트 - RAG 우선, 부족시 LLM 활용"""
668

7-
# RAG 시스템 초기화 및 데이터베이스 설정
8-
rag_system = RAGSystem()
9-
rag_system.setup_database()
69+
query = state["query"]
1070

11-
# RAG 시스템을 사용하여 답변 생성
12-
answer = rag_system.search_and_generate(
13-
query=state["query"],
71+
# 1. RAG 시스템을 사용한 응답 생성 (1차 우선)
72+
rag_system = RAGSystem()
73+
rag_response = rag_system.search_and_generate(
74+
query=query,
1475
top_k=5,
1576
show_details=False
1677
)
1778

18-
state["final_response"] = answer
79+
# 2. RAG 응답의 품질 평가
80+
def evaluate_rag_quality(rag_response: str, query: str) -> bool:
81+
"""LLM을 사용하여 RAG 응답이 충분한 정보를 제공하는지 평가"""
82+
llm = get_llm()
83+
84+
evaluation_prompt = f"""다음은 사용자의 질문과 RAG 시스템이 제공한 답변입니다.
85+
이 답변이 사용자의 질문에 대해 충분하고 정확한 정보를 제공하는지 판단해주세요.
86+
87+
사용자 질문: {query}
88+
89+
RAG 답변: {rag_response}
90+
91+
다음 중 하나에 해당하면 "부족함"이라고 답하고, 그렇지 않으면 "충분함"이라고 답해주세요:
92+
93+
1. 답변이 너무 짧거나 구체적이지 않은 경우
94+
2. "정확한 정보를 제공할 수 없다", "참고할 수 있는 문서가 없다" 등의 문구가 포함된 경우
95+
3. 질문에 대한 구체적인 답변이 아닌 일반적인 설명만 있는 경우
96+
4. 문서에 없는 내용을 임의로 생성했다고 명시된 경우
97+
98+
판단 결과:"""
99+
100+
try:
101+
evaluation_result = llm.invoke([HumanMessage(content=evaluation_prompt)])
102+
evaluation_text = str(evaluation_result.content) if hasattr(evaluation_result, 'content') else str(evaluation_result)
103+
104+
# "부족함"이 포함되어 있으면 False 반환
105+
return "부족함" not in evaluation_text and "insufficient" not in evaluation_text.lower()
106+
107+
except Exception as e:
108+
# 기본 로직: 간단한 키워드 체크
109+
insufficient_indicators = [
110+
"정확한 정보를 제공할 수 없다",
111+
"참고할 수 있는 문서가 없다",
112+
"문서에 없는 내용",
113+
"정보가 부족하다",
114+
"확실하지 않다"
115+
]
116+
117+
for indicator in insufficient_indicators:
118+
if indicator in rag_response:
119+
return False
120+
121+
# 길이 체크
122+
if len(rag_response.strip()) < 50:
123+
return False
124+
125+
return True
126+
127+
rag_quality_good = evaluate_rag_quality(rag_response, query)
128+
129+
# 3. 최종 응답 선택
130+
if not rag_quality_good:
131+
# RAG 응답이 부족한 경우 LLM 사용 + 불확실성 표시
132+
llm = get_llm()
133+
llm_prompt = f"""다음은 관세 관련 질문입니다. 사전 학습된 지식만을 사용하여 답변해주세요.
134+
135+
질문: {query}
136+
137+
답변:"""
138+
139+
llm_result = llm.invoke([HumanMessage(content=llm_prompt)])
140+
llm_response = str(llm_result.content) if hasattr(llm_result, 'content') else str(llm_result)
141+
142+
# 불확실성 표시 추가
143+
final_response = f"{llm_response}\n\n※ 이 답변은 불확실할 수 있습니다."
144+
response_source = "LLM (RAG 응답 부족)"
145+
146+
else:
147+
# RAG로 충분히 답변할 수 있는 경우 RAG만 사용
148+
final_response = rag_response
149+
response_source = "RAG (외부 지식 기반)"
150+
151+
state["final_response"] = final_response
19152
state["intermediate_results"]["qna"] = {
20-
"response": answer,
21-
"query": state["query"]
153+
"rag_response": rag_response,
154+
"rag_quality_good": rag_quality_good,
155+
"selected_response": final_response,
156+
"response_source": response_source,
157+
"query": query
22158
}
23159

24-
25160
return state

core/qna/agent/test_qna_agent.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# -*- coding: utf-8 -*-
2+
import sys
3+
import os
4+
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '../../..')))
5+
6+
from core.qna.agent.qna_agent import qna_agent
7+
from core.shared.states.states import CustomsAgentState
8+
9+
if __name__ == "__main__":
10+
# Test query
11+
test_query = "How to declare customs?"
12+
state = CustomsAgentState(query=test_query)
13+
result = qna_agent(state)
14+
15+
print("=== QNA AGENT TEST RESULT ===")
16+
print("Query:", test_query)
17+
print("\nFinal Response:", result["final_response"])
18+
print("\nResponse Source:", result["intermediate_results"]["qna"]["response_source"])
19+
print("Responses Similar:", result["intermediate_results"]["qna"]["responses_similar"])
20+
print("\nLLM Response:", result["intermediate_results"]["qna"]["llm_response"])
21+
print("\nRAG Response:", result["intermediate_results"]["qna"]["rag_response"])

core/qna/config.py

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,22 @@
11
import torch
2+
import os
3+
import warnings
4+
5+
warnings.filterwarnings("ignore", category=FutureWarning)
6+
warnings.filterwarnings("ignore", message=".*resume_download.*")
7+
8+
CURRENT_DIR = os.path.dirname(os.path.abspath(__file__))
9+
PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.dirname(CURRENT_DIR)))
210

311
# Model configurations
412
MODEL_NAME = 'BM-K/KoSimCSE-roberta'
513
DEVICE = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
614

7-
# ChromaDB configurations
8-
CHROMA_DB_PATH = "./chroma_db"
9-
COLLECTION_NAMES = {
10-
"question": "q_embeddings",
11-
"snippet": "s_embeddings",
12-
"keyword": "k_embeddings"
13-
}
14-
15-
# Data file paths
15+
# VectorDB data file paths
1616
DATA_FILES = {
17-
"question": "VectorDB/q_data.pkl",
18-
"snippet": "VectorDB/s_data.pkl",
19-
"keyword": "VectorDB/k_data.pkl"
17+
"question": os.path.join(CURRENT_DIR, "VectorDB", "q_data.pkl"),
18+
"snippet": os.path.join(CURRENT_DIR, "VectorDB", "s_data.pkl"),
19+
"keyword": os.path.join(CURRENT_DIR, "VectorDB", "k_data.pkl")
2020
}
2121

2222
# Search configurations

core/qna/database.py

Lines changed: 0 additions & 51 deletions
This file was deleted.

core/qna/encoder.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import torch
55
import numpy as np
66
from transformers import AutoTokenizer, AutoModel
7-
from config import MODEL_NAME, DEVICE
7+
from core.qna.config import MODEL_NAME, DEVICE
88

99

1010
class TextEncoder:

core/qna/generator.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import os
55
from openai import OpenAI
66
from dotenv import load_dotenv
7-
from config import OPENAI_MODEL, GENERATION_TEMPERATURE, MAX_REFERENCE_DOCS
7+
from core.qna.config import OPENAI_MODEL, GENERATION_TEMPERATURE, MAX_REFERENCE_DOCS
88

99
load_dotenv()
1010

0 commit comments

Comments
 (0)