지난 4편에서는 문서 자체를 FAQ 기반으로 재설계하는 것만으로도 RAG 챗봇의 답변 품질을 극적으로 향상시킬 수 있었습니다. 벡터 검색만으로도 꽤 만족스러운 결과를 얻을 수 있었죠.
하지만 여기서 만족하지 않고, 3편에서 계획했던 개선안 중 하나인 '메타데이터(Metadata) 필터링'을 적용해 챗봇을 한 단계 더 고도화해 보았습니다.
1. 왜 '메타데이터 필터링'이 필요한가?
FAQ 기반으로 문서를 잘 설계했지만, 검색 범위를 더 확실하게 좁히고 싶다는 욕심이 생겼습니다.
예를 들어, 제 업무 지식에는 'A-Pay 연동', 'B-배송 설정', 'C-인증 정책' 등 여러 카테고리가 섞여있습니다. 만약 사용자가 "설정 방법 알려줘"라고 모호하게 질문하면, AI가 A-Pay와 B-배송의 설정 방법을 섞어서 답변할 위험이 있습니다.
하지만 만약 UI에서 'A-Pay 연동' 카테고리를 먼저 선택하게 한 뒤 질문을 받는다면 어떨까요?
챗봇은 오직 'A-Pay 연동' 카테고리에 속한 문서들 안에서만 답변을 탐색하게 됩니다. 이렇게 하면:
- 정확도 향상: 관련 없는 문서(B-배송, C-인증)는 아예 검색 대상에서 제외되므로, 잘못된 정보를 참조할 가능성이 원천 차단됩니다.
- 속도 향상 (이론상): 전체 벡터를 검색하는 것보다 더 작은 부분집합(subset)에서 검색하므로 더 빠릅니다.
2. 1단계: 마크다운 문서에 메타데이터 정의하기
이 기능을 구현하기 위해, 4편에서 만들었던 FAQ 마크다운 문서 상단에 YAML 헤더(Frontmatter)를 이용해 메타데이터를 명시적으로 심어주었습니다.
---
doc_id: faq_apay_setup
title: A-Pay 연동 설정 방법
type: faq
category: 연동설정 # <-- 필터링에 사용할 핵심 메타데이터
priority: high
keywords: [APay, 설정, API키]
---
# Q: A-Pay 연동 설정은 어떻게 하나요?
## 즉시 답변
...
3. 2단계: 메타데이터를 추출하여 벡터 DB에 저장하기
이제 문서를 로드할 때, 이 YAML 헤더를 파싱하여 metadata 객체로, 그 외의 내용을 page_content로 분리해야 합니다.
A. 메타데이터 파싱 함수
다음은 마크다운 파일에서 YAML 헤더(메타데이터)와 실제 내용(마크다운 본문)을 분리해 주는 Python 함수입니다.
import yaml
from pathlib import Path
from langchain_core.documents import Document
def load_markdown_with_metadata(file_path: Path) -> tuple:
"""
마크다운 파일에서 YAML 메타데이터와 내용을 로드합니다.
Args:
file_path (Path): 마크다운 파일 경로
Returns:
tuple: (메타데이터 dict, 마크다운 내용 str)
"""
with open(file_path, "r", encoding="utf-8") as f:
content = f.read()
# '---'로 시작하는 YAML 헤더 파싱
if content.startswith("---"):
try:
end_of_yaml = content.find("\n---", 3)
if end_of_yaml != -1:
yaml_content = content[3:end_of_yaml]
metadata = yaml.safe_load(yaml_content) or {}
markdown_content = content[end_of_yaml + 4:].strip()
else:
# '---'가 닫히지 않은 경우
metadata = {}
markdown_content = content.strip()
except yaml.YAMLError as e:
print(f"YAML 파싱 오류: {e}")
metadata = {}
markdown_content = content.strip()
else:
# YAML 헤더가 없는 경우
metadata = {}
markdown_content = content.strip()
return metadata, markdown_content
B. Document 객체 생성 및 임베딩
이렇게 파싱한 metadata와 markdown_content를 Document 객체에 담아 벡터 DB에 저장합니다.
from langchain_community.embeddings import HuggingFaceEmbeddings
from langchain_community.vectorstores import Chroma
# (앞에서 파싱한 데이터로 documents 리스트를 만들었다고 가정)
# 예:
# metadata, markdown_content = load_markdown_with_metadata(file_path)
# metadata['source'] = str(file_path) # 소스 파일 경로도 메타데이터에 추가
# document = Document(
# page_content=markdown_content,
# metadata=metadata
# )
# documents.append(document)
# 임베딩 모델 로드
embeddings = HuggingFaceEmbeddings(
model_name="jhgan/ko-sroberta-multitask", # 한국어 특화 모델
model_kwargs={'device': 'cpu'},
encode_kwargs={'normalize_embeddings': True}
)
# Chroma 벡터스토어 생성
# from_documents를 호출할 때 Document의 metadata가 함께 저장됩니다.
vectorstore = Chroma.from_documents(
documents=documents,
embedding=embeddings,
persist_directory="./chroma_db",
collection_name="faq_with_metadata"
)
이제 chroma_db에는 벡터와 함께 각 문서의 category 정보가 저장되었습니다.
4. 3단계: 필터를 적용하여 검색 결과 좁히기
이제 similarity_search를 호출할 때 filter 인자를 넘겨주기만 하면 됩니다.
# 사용자가 '연동설정' 카테고리를 선택했다고 가정
category_filter = {"category": "연동설정"}
# Chroma DB에서 필터링 검색 수행
retrieved_docs = vectorstore.similarity_search(
"결제 설정 어떻게 해?",
k=3,
filter=category_filter # <-- 핵심!
)
# retrieved_docs 에는 'category'가 '연동설정'인 문서들 중에서만
# 유사도가 높은 Top 3 결과가 담기게 됩니다.
5. 테스트 결과: 의도대로 동작하는 필터
실제로 필터가 잘 동작하는지 테스트해 보았습니다.
시나리오:
- 사용자가 챗봇 UI에서 'A-Pay 연동' 카테고리를 선택 (filter 적용: {"category": "APay"})
- 사용자가 'A-Pay' 문서에는 없는 내용인, 다른 결제사 **'KCP'**에 대해 질문
Q: KCP의 취소 정책에 대해서 알려줘
R (AI 답변):
"제공된 'A-Pay 연동' 관련 문서에는 KCP의 취소 정책에 대한 정보가 포함되어 있지 않습니다.
문서에는 A-Pay가 신용카드, 가상계좌, 계좌이체를 지원한다는 내용만 명시되어 있습니다.
KCP의 취소 정책에 대한 상세 내용은 제공된 컨텍스트에서 확인할 수 없습니다."
결과는 성공적이었습니다.
만약 필터가 없었다면, 챗봇은 KCP 관련 문서를 찾아내려 하거나(만약 DB에 있다면), A-Pay 정보를 기반으로 잘못된 답변을 생성(환각)했을 수 있습니다. 하지만 필터를 통해 검색 범위를 'APay'로 명확히 제한했기 때문에, AI는 "내가 지금 보고 있는 문서에는 그 내용이 없다"고 정확하게 답변할 수 있었습니다.
6. 마치며
4편의 'FAQ 문서화'가 답변의 '품질(Quality)'을 높이는 작업이었다면, 5편의 '메타데이터 필터링'은 답변의 '정확도(Precision)'와 '신뢰도(Reliability)'를 높이는 작업이었습니다.
이제 챗봇은 사용자가 선택한 특정 카테고리 내에서만 답변을 생성하므로, 훨씬 더 안정적으로 동작하게 되었습니다.
다음 글에서는 3편에서 계획했던 마지막 단계, 즉 벡터 검색(의미 기반)과 키워드 검색(정확한 용어 기반)을 결합하는 '하이브리드 검색(Hybrid Search)'을 구현하여 검색 정확도를 극한까지 끌어올리는 과정을 공유하겠습니다.
'사이드프로젝트' 카테고리의 다른 글
| [사이드 프로젝트] #2. RAG 챗봇 개발기 (6) - 하이브리드 검색과 '의외의' 결과 (0) | 2025.11.21 |
|---|---|
| [사이드 프로젝트] #2 RAG 챗봇 개발기 (4) - 문서를 FAQ로 바꿨더니 답변 품질이 10배 향상됐다 (1) | 2025.11.08 |
| [사이드 프로젝트] #2. RAG 기반 '업무 지식' AI 챗봇 개발기 (3) - RAG 구축 삽질기 (0) | 2025.10.29 |
| [사이드 프로젝트] #2. RAG 기반 '업무 지식' AI 챗봇 개발기 (2) - 챗봇 프로토타입 및 RAG 기본개념정리 (0) | 2025.10.24 |
| [사이드 프로젝트] #2. RAG 기반 '업무 지식' AI 챗봇 개발기 (1) (0) | 2025.10.22 |