관련 레포: https://github.com/AiResearch2025/FigVarietyRAGChat/tree/pure-RAG
GitHub - AiResearch2025/FigVarietyRAGChat: RAG 기반 희귀품종 무화과 챗봇 프로젝트
RAG 기반 희귀품종 무화과 챗봇 프로젝트. Contribute to AiResearch2025/FigVarietyRAGChat development by creating an account on GitHub.
github.com
🌏 AI Agent
✅ AI Agent가 해야 할 일
AI Agent는 사용자의 질문이 들어오면 곧바로 그 질문을 LLM에 돌리기 전에 우선 질문을 아래와 같이 분류하고, 각 분류에 따라서 우리가 정해놓은 로직에 따라서 처리한다.
- 사용자가 특정 품종 이름을 알고있는가?
- 그 품종이 실제 존재하는(또는 RAG 데이터에 존재하는) 품종인가?
- 현재 질문이 RAG 검색으로 해결 가능한가, 아니면 추가 질의가 필요한가?
🌏 AI Agent의 질문 분류 및 처리

✅ CHITCHAT
- 설명: "안녕", "고마워" 등과 같이 간단한 인사나 감사 표현이 포함된 일상적인 대화입니다.
- 코드 로직: chitchat_keywords = ["안녕", "고마워", "감사", "땡큐"] 키워드가 있는지 확인합니다.
- 예시
- "안녕?"
- "고마워"
- "답변 감사합니다"
- 처리
- 만약 질문에 "고마워", "감사", "땡큐"가 포함되어 있다면, "천만에요! 더 궁금한 점이 있으신가요?"라는
정해진 답변을 반환합니다. - 그 외의 경우(예: "안녕")는 "안녕하세요! 무화과에 대해 무엇이든 물어보세요."라는 인사말을 반환합니다.
- 만약 질문에 "고마워", "감사", "땡큐"가 포함되어 있다면, "천만에요! 더 궁금한 점이 있으신가요?"라는
✅ UNKNOWN_VARIETY_IMAGE_QUERY
- 설명: "사진", "이미지" 등의 키워드를 포함하여 이미지 속 무화과 품종을 물어보는 질문입니다.
- 코드 로직: image_keywords = ["사진", "이미지"] 키워드가 있는지 확인합니다.
- 예시
- "이 사진 속 무화과는 무슨 품종이야?"
- "이미지를 보여줄테니 품종을 알려줘"
- 처리: "현재는 이미지를 분석하여 품종을 식별하는 기능이 지원되지
않습니다."라는 고정된 메시지를 반환합니다.- 추후에 LLM을 통해 이미지의 내용을 질문자의 쿼리에 포함하는 방식으로 개선 예정
✅ KNOWN_VARIETY_TYPO
- 설명: 시스템에 미리 정의된, 사용자가 자주 틀리는 품종명 오타가 포함된 질문입니다.
- 코드 로직: known_typos = ["ciccio vero"] 리스트에 포함된 오타가 있는지 확인합니다. (현재는 "Ciccio Vero"만 해당)
- 예시
- "Ciccio Vero는 키우기 쉬운가요?" (→ 'Ciccio Nero'의 오타로 인식)
- 처리: 사용자에게 올바른 품종명을 되묻는 질문을 생성하여 반환합니다.
✅ KNOWN_VARIETY_NEEDS_CLARIFICATION
- 설명: 특정 품종 이름이 언급되었지만, "어때?", "좋아?" 와 같이 질문이 너무 광범위하여 구체적인 답변을 주기 어려운 경우입니다. 에이전트는 더 구체적인 질문을 하도록 유도합니다.
- 코드 로직: clarification_keywords = ["어때", "좋아", "키울만", "괜찮"] 키워드가 있는지 확인합니다.
- 예시
- "브런즈윅 품종 어때?"
- "하디 시카고 키울만 한가요?"
- 처리: data 딕셔너리에서 품종명(variety)을 가져온 뒤, "어떤 점이
궁금하신가요? 맛, 생산성, 나무 크기 등 구체적인 기준을 알려주시면
더 자세히 답변해 드릴 수 있습니다."라며 더 구체적인 정보를
요구하는 응답을 반환합니다.
✅ KNOWN_VARIETY_EXISTS
- 설명: 특정 품종 이름이 명확하게 포함된 구체적인 질문입니다. 에이전트는 이 품종에 대한 정보를 찾아 답변합니다.
- 코드 로직: 위의 TYPO나 NEEDS_CLARIFICATION에 해당하지 않으면서, 알려진 품종명(또는 별명)이 질문에 포함된 경우입니다.
- 예시
- "브런즈윅의 내한성은 어떤가요?"
- "스트로베리 베르테(SV) 품종의 특징 알려줘"
- 처리
- data 딕셔너리에서 전체 질문(query)과 품종명(variety)을
가져옵니다. - 벡터 데이터베이스에서 사용자의 전체
질문과 가장 유사한 정보 3개를 검색합니다. (self.vector_db.similarity_search(data['query'], k=3)를 호출) - 검색 결과가 없으면 "관련 정보를 찾지 못했습니다."라는
메시지를 반환합니다. - 결과가 있으면, 각 결과의 내용을 합쳐 가장 관련성 높은 정보를 모아 컨텍스트를 구성하고, RAG를 통해 좋은 품질의 답변을 제공합니다.
- data 딕셔너리에서 전체 질문(query)과 품종명(variety)을
✅ UNKNOWN_VARIETY_GENERAL_QUERY
- 설명: 특정 품종명이 언급되지 않은, 일반적인 무화과 관련 질문입니다. 에이전트는 질문의 키워드를 바탕으로 가장 관련성 높은 품종 정보를 추천해 줍니다.
- 코드 로직: 위 5가지 분류에 모두 해당하지 않는 모든 질문입니다.
- 예시
- "달콤한 품종 추천해줘"
- "추위에 강한 무화과는 뭐가 있어?"
- 처리
- 특징(features) 벡터 데이터베이스에서 질문 내용과
가장 유사한 정보 2개를 검색합니다. - 검색 결과가 없으면 "관련 품종을 찾지 못했습니다."라는
메시지를 반환합니다. - 결과가 있으면, 이를 바탕으로 RAG를 통해 높은 품질의 답변을 제공합니다.
- 특징(features) 벡터 데이터베이스에서 질문 내용과
Fig Agent는 사용자의 질문을 6가지 카테고리로 분류하여 각 상황에 맞는 최적의 답변을 제공하도록 설계했습니다.
🌏 질문 처리 결과

내한성과 같이 feature_db에 포함되지 않았던 내용은 솔직히 정보가 없다고 응답했습니다.
또한 정보를 충분히 구할 수 있는 당도 기반 품종 추천 역시 정확하게 응답했습니다.
🌏 AI Agent 전체 코드
# === Fig Variety AI Agent (v4.0 - Direct Gemini SDK) ===
import os
from enum import Enum, auto
from langchain_community.vectorstores import FAISS
from langchain_huggingface import HuggingFaceEmbeddings
import google.generativeai as genai
from dotenv import load_dotenv
# .env 파일에서 환경 변수를 로드합니다.
load_dotenv()
class QueryCategory(Enum):
"""사용자 질문의 의도를 나타내는 분류 Enum"""
KNOWN_VARIETY_EXISTS = auto()
KNOWN_VARIETY_TYPO = auto()
KNOWN_VARIETY_NEEDS_CLARIFICATION = auto()
UNKNOWN_VARIETY_GENERAL_QUERY = auto()
UNKNOWN_VARIETY_IMAGE_QUERY = auto()
CHITCHAT = auto()
class FigAgent:
"""
사용자 질문을 분류하고 RAG 파이프라인을 관리하는 AI 에이전트 (Gemini SDK 직접 호출 버전)
"""
def __init__(self, varieties_db_path="./vector_db", features_db_path="./features_db", model_name="intfloat/multilingual-e5-base"):
print("🤖 Fig Agent (Direct Gemini SDK)를 초기화하는 중입니다...")
self.embedding = HuggingFaceEmbeddings(model_name=model_name)
try:
self.varieties_db = FAISS.load_local(varieties_db_path, self.embedding, allow_dangerous_deserialization=True)
self.features_db = FAISS.load_local(features_db_path, self.embedding, allow_dangerous_deserialization=True)
except Exception as e:
print(f"DB 로드 실패: {e}")
raise
# --- LLM 설정 (google-generativeai SDK 직접 사용) ---
google_api_key = os.getenv("GEMINI_API_KEY")
if not google_api_key:
raise ValueError("GEMINI_API_KEY 환경 변수를 찾을 수 없습니다. .env 파일을 확인해주세요.")
genai.configure(api_key=google_api_key)
self.llm = genai.GenerativeModel('gemini-2.5-flash')
# --- Retriever 설정 (LangChain 사용) ---
self.retriever = self.features_db.as_retriever(search_kwargs={'k': 3})
# --- 별명 및 오타 처리 시스템 ---
self.variety_aliases = {
"Brunswick": ["brunswick", "브런즈윅"],
"Ciccio_Nero": ["ciccio nero", "씨씨오 네로", "ciccio vero"],
"Coll_De_Dama_Rimada": ["coll de dama rimada", "콜 드 다마 리마다"],
"Hardy_Chicago": ["hardy chicago", "하디 시카고", "시카고"],
"Horaishi": ["horaishi", "호래시", "봉래시"],
"Strawberry_Verte": ["strawberry verte", "스트로베리 베르테", "sv"]
}
self.known_typos = ["ciccio vero"]
self.sorted_aliases = []
for canonical, alias_list in self.variety_aliases.items():
for alias in alias_list:
self.sorted_aliases.append((alias, canonical))
self.sorted_aliases.sort(key=lambda x: len(x[0]), reverse=True)
print("✅ Agent 초기화 완료.")
def _generate_rag_response(self, query: str) -> str:
"""RAG 파이프라인을 수동으로 실행하여 답변을 생성합니다."""
print(f" 🔍 '{query}'에 대한 관련 문서 검색 중...")
docs = self.retriever.get_relevant_documents(query)
if not docs:
return "관련 정보를 찾지 못했습니다. 질문을 조금 더 구체적으로 해주시겠어요?"
# 검색된 문서 내용을 하나의 컨텍스트 문자열로 합칩니다.
context = "\n".join([doc.page_content for doc in docs])
prompt_template = f"""
당신은 무화과 품종 전문가입니다. 사용자의 질문에 대해 아래의 '검색된 정보'를 바탕으로 친절하고 명확하게 답변해주세요.
답변은 반드시 한국어로 작성해야 합니다. 정보가 부족하여 답변할 수 없는 경우, "정보가 부족하여 답변하기 어렵습니다."라고 솔직하게 말해주세요.
[검색된 정보]
{context}
[사용자 질문]
{query}
[전문가 답변]
"""
print(" 🧠 LLM이 답변 생성 중...")
try:
response = self.llm.generate_content(prompt_template)
return response.text
except Exception as e:
print(f" ❗️ LLM 호출 중 오류 발생: {e}")
return "답변을 생성하는 중에 오류가 발생했습니다."
def _extract_variety_from_query(self, query: str) -> tuple[str, str] | None:
"""질문에서 (매칭된 별명, 표준 품종명) 튜플을 추출합니다."""
query_lower = query.lower()
for alias, canonical in self.sorted_aliases:
if alias in query_lower:
return (alias, canonical)
return None
def _classify_query(self, query: str) -> tuple[QueryCategory, dict]:
"""질문을 분석하여 카테고리와 관련 데이터를 반환합니다."""
chitchat_keywords = ["안녕", "고마워", "감사", "땡큐"]
if any(keyword in query for keyword in chitchat_keywords):
return QueryCategory.CHITCHAT, {}
image_keywords = ["사진", "이미지", "이 무화과"]
if any(keyword in query for keyword in image_keywords):
return QueryCategory.UNKNOWN_VARIETY_IMAGE_QUERY, {}
variety_info = self._extract_variety_from_query(query)
if variety_info:
matched_alias, canonical_name = variety_info
canonical_name_display = canonical_name.replace('_', ' ')
if matched_alias in self.known_typos:
return QueryCategory.KNOWN_VARIETY_TYPO, {"suggestion": canonical_name_display}
clarification_keywords = ["어때", "좋아", "키울만", "괜찮"]
if any(keyword in query for keyword in clarification_keywords):
return QueryCategory.KNOWN_VARIETY_NEEDS_CLARIFICATION, {"variety": canonical_name_display}
return QueryCategory.KNOWN_VARIETY_EXISTS, {"query": query, "variety": canonical_name_display}
return QueryCategory.UNKNOWN_VARIETY_GENERAL_QUERY, {"query": query}
def handle_query(self, query: str) -> str:
"""질문을 받아 분류하고, 각 케이스에 맞는 응답을 생성합니다."""
category, data = self._classify_query(query)
print(f"🧠 Agent 분류 결과: {category.name}")
if category == QueryCategory.CHITCHAT:
return "천만에요! 더 궁금한 점이 있으신가요?" if any(k in query for k in ["고마워", "감사", "땡큐"]) else "안녕하세요! 무화과에 대해 무엇이든 물어보세요."
elif category == QueryCategory.UNKNOWN_VARIETY_IMAGE_QUERY:
return "현재는 이미지를 분석하여 품종을 식별하는 기능이 지원되지 않습니다."
elif category == QueryCategory.KNOWN_VARIETY_TYPO:
return f"혹시 '{data['suggestion']}' 품종을 말씀하신 건가요?"
elif category == QueryCategory.KNOWN_VARIETY_NEEDS_CLARIFICATION:
return f"'{data['variety']}' 품종에 대해 어떤 점이 궁금하신가요? 맛, 생산성, 나무 크기 등 구체적인 기준을 알려주시면 더 자세히 답변해 드릴 수 있습니다."
elif category == QueryCategory.KNOWN_VARIETY_EXISTS:
print(f" ▶️ '{data['variety']}'에 대한 정보 검색 및 답변 생성 시작...")
return self._generate_rag_response(data['query'])
elif category == QueryCategory.UNKNOWN_VARIETY_GENERAL_QUERY:
print(" ▶️ 특징 DB에서 추천 품종 검색 및 답변 생성 시작...")
return self._generate_rag_response(data['query'])
else:
return "죄송합니다. 질문을 이해하지 못했습니다."
# === Agent 실행 예시 ===
if __name__ == '__main__':
try:
agent = FigAgent()
queries_to_test = [
"안녕?",
"브런즈윅의 내한성은 어떤가요?",
"Ciccio Vero는 키우기 쉬운가요?",
"브런즈윅은 키울만 한가요?",
"달콤한 품종 추천해줘",
"이 사진 속 무화과는 무슨 품종이야?",
"고마워"
]
for q in queries_to_test:
print(f"\n👤 사용자 질문: {q}")
response = agent.handle_query(q)
print(f"🤖 Agent 응답: {response}")
except Exception as e:
print(f"Agent 실행 중 오류 발생: {e}")
🌏 Client로 대화형 즐겨보기(?)
(.venv) (base) ➜ FigVarietyRAGChat git:(fig_agent) ✗ python fig_client.py
🤖 Fig Agent (Direct Gemini SDK)를 초기화하는 중입니다...
✅ Agent 초기화 완료.
--- 무화과 품종 챗봇 ---
안녕하세요! 무화과에 대해 궁금한 점을 물어보세요.
(종료하시려면 'exit' 또는 'quit'을 입력하세요)
👤 나: 안녕?? 난 깃짱이야 ㅎㅎ
🧠 Agent 분류 결과: CHITCHAT
🤖 Agent: 안녕하세요! 무화과에 대해 무엇이든 물어보세요.
👤 나: 내가 키우는 무화과가 있는데 진짜 열매가 겁나 크거든?? 이게 뭔지 알겠어?
🧠 Agent 분류 결과: UNKNOWN_VARIETY_GENERAL_QUERY
▶️ 특징 DB에서 추천 품종 검색 및 답변 생성 시작...
🔍 '내가 키우는 무화과가 있는데 진짜 열매가 겁나 크거든?? 이게 뭔지 알겠어?'에 대한 관련 문서 검색 중...
/Users/gitchan/Projects/FigVarietyRAGChat/fig_agent.py:71: LangChainDeprecationWarning: The method `BaseRetriever.get_relevant_documents` was deprecated in langchain-core 0.1.46 and will be removed in 1.0. Use :meth:`~invoke` instead.
docs = self.retriever.get_relevant_documents(query)
🧠 LLM이 답변 생성 중...
WARNING: All log messages before absl::InitializeLog() is called are written to STDERR
E0000 00:00:1761035138.036196 11995233 alts_credentials.cc:93] ALTS creds ignored. Not running on GCP and untrusted ALTS is not enabled.
🤖 Agent: 안녕하세요! 무화과 품종 전문가입니다. 열매가 '겁나게 크다'는 말씀에 어떤 품종인지 궁금하시군요.
제가 가진 정보에 따르면, **브런즈윅(Brunswick)** 품종이 **대형 ~ 초대형**의 열매 크기를 자랑합니다. 따라서 고객님의 무화과가 아주 큰 열매를 맺는다면 브런즈윅 품종일 가능성이 높습니다.
👤 나: 브레바 먹을 수 있는 품종??
🧠 Agent 분류 결과: UNKNOWN_VARIETY_GENERAL_QUERY
▶️ 특징 DB에서 추천 품종 검색 및 답변 생성 시작...
🔍 '브레바 먹을 수 있는 품종??'에 대한 관련 문서 검색 중...
🧠 LLM이 답변 생성 중...
🤖 Agent: 안녕하세요! 무화과 품종 전문가입니다. 고객님의 질문에 대해 검색된 정보를 바탕으로 답변해 드리겠습니다.
브레바(Breba)를 생산하여 드실 수 있는 품종으로는 **브런즈윅(Brunswick)**이 있습니다. 브런즈윅은 풍부한 단맛(브라운 슈가 타입)을 자랑하는 품종이기도 합니다.
콜 드 다마 리마다(Col de Dame Rimada)의 경우, 브레바 생산 여부가 "X 또는 불명확"으로 표시되어 있어, 현재 정보만으로는 브레바 생산 품종으로 추천해 드리기 어렵습니다.
👤 나: 한국이랑 가까운 나라에서 유래한 무화과는?
🧠 Agent 분류 결과: UNKNOWN_VARIETY_GENERAL_QUERY
▶️ 특징 DB에서 추천 품종 검색 및 답변 생성 시작...
🔍 '한국이랑 가까운 나라에서 유래한 무화과는?'에 대한 관련 문서 검색 중...
🧠 LLM이 답변 생성 중...
🤖 Agent: 제공된 정보에 따르면, 한국과 가까운 나라에서 유래한 무화과 품종은 없습니다. 검색된 정보에는 이탈리아, 미국, 포르투갈에서 유래한 품종들만 명시되어 있어, 사용자님의 질문에 직접적으로 답변하기는 어렵습니다.
👤 나: ㅋㅋ 그러면 미국에서 유래한거는?
🧠 Agent 분류 결과: UNKNOWN_VARIETY_GENERAL_QUERY
▶️ 특징 DB에서 추천 품종 검색 및 답변 생성 시작...
🔍 'ㅋㅋ 그러면 미국에서 유래한거는?'에 대한 관련 문서 검색 중...
🧠 LLM이 답변 생성 중...
🤖 Agent: 미국에서 유래한 무화과 품종은 "하디 시카고"입니다.
👤 나: exit
🤖 Agent: 이용해주셔서 감사합니다.
겁나,, 기특하자나,,?
RAG을 활용하니 LLM만 있었다면 모른다고 절대 안할텐데 모르는건 모른다고 딱 강제할 수도 있었다.
도감을 다 먹이면 정말 쓸만 하겠어요 ㅎㅎ

🌏 앞으로는?
✅ LLM Configuration
현재 Fig Agent는 Gemini SDK의 기본 설정을 사용하고 있어, 응답이 다소 장황한 경우가 있다. 물론 RAG를 통해서 데이터를 제공할 수 없는 경우에는 백엔드에서 자동으로 모른다는 답을 내놓기에 할루시네이션을 억제하고는 있지만, LLM 수준에서 이를 조절하기 위해서는 Config 기반 제어를 추가할 수 있다.
- Temperature(창의성) 제어: 단답형 정보 중심 질문에서는 temperature=0로 낮추어 일관된 사실 위주로, 품종 추천이나 조언형 질문에서는 temperature=0.8 정도로 높여 다양성을 확보한다.
- Top-K / Top-P 설정: top_p나 top_k를 조정해, LLM이 불필요한 맥락을 덜 생성하고 더 압축된 답변을 하도록 유도할 수 있다.
- max_output_tokens 제한: 응답 길이를 300자 내외로 제한해, 실제 사용자 경험에서 피로감을 줄일 수 있다.
✅ 최신 데이터 우선 검색 구조
현재 RAG 검색은 FAISS 기반의 벡터 유사도만 사용하고 있어, 최신 데이터 반영이 어렵다는 한계가 있다. 동일한 내용이 있을 때 최신 데이터를 더 우선하기 위해서는 아래와 같은 방법을 사용해볼 수 있다!
- 타임스탬프 기반 가중치 추가
search_kwargs={'k':3, 'filter': lambda doc: doc.metadata['timestamp'] > threshold} - 문서 저장 시 timestamp 메타데이터를 함께 저장하고, 검색 시 최신 데이터에 더 높은 가중치를 준다.
- 주기적 재임베딩 파이프라인
- 자동 크롤링 또는 사용자가 추가 입력한 품종 데이터를 주기적으로 재임베딩하여 최신 벡터 DB로 갱신한다.
✅ 멀티 에이전트(Multi-Agent) 구조로 확장
현재 Fig Agent는 단일 파이프라인에서 모든 질문을 분류하고 처리하고 있다. 하지만 RAG가 확장되면 좀더 역할에 특화한 LLM 구조를 사용할 수 있다.
[Agent 역할 예시]
| Classifier Agent | 질문을 6가지 카테고리로 분류 | 현재 FigAgent의 _classify_query 역할 |
| Retriever Agent | variety_db, feature_db 중 어디서 검색할지 결정 | 품종명 중심인지, 특징 중심인지 판단 |
| Answering Agent | 검색된 결과 기반으로 LLM에게 프롬프트 구성 및 응답 생성 | temperature 조절 포함 |
| Feedback Agent | 사용자의 후속 질문(clarification)을 학습해 RAG 개선 | “좀 더 구체적으로 알려줘” → context 강화 |
이 구조를 LangGraph 등의 스타일로 구성하면, 각 에이전트가 병렬/직렬로 협력하며 질문 분류, 문서 검색, 답변 생성, 피드백 반영까지 완전히 자동화된 챗봇 시스템으로 발전할 수 있을 것이다.
🌏 마무리
RAG를 직접 구현해 보면서 느낀건 복잡하다,,?
랭체인/랭그래프 바탕으로 프로젝트를 개선시켜보겠다 향후에

도움이 되었다면, 공감/댓글을 달아주면 깃짱에게 큰 힘이 됩니다!🌟
비밀댓글과 메일을 통해 오는 개인적인 질문은 받지 않고 있습니다. 꼭 공개댓글로 남겨주세요!
'PROJECT > RAG 기반 희귀품종 무화과 챗봇' 카테고리의 다른 글
| [AI/RAG] RAG 기반 희귀품종 무화과 LLM 챗봇 프로젝트(2): 벡터DB 구축 (ft. 무화과 도감) (1) | 2025.10.21 |
|---|---|
| [AI/RAG] RAG 기반 희귀품종 무화과 LLM 챗봇 프로젝트(1): 프로젝트의 시작과 구상 (AI Agent, RAG 적용) (2) | 2025.10.20 |