🧠 LangChain + Chroma + t-SNE로 문서 임베딩 시각화하기
문서를 벡터로 변환하고 시각화하여 RAG 검색의 기반을 시각적으로 이해해 보는 실습입니다.
📦 1. 필수 라이브러리 임포트
import os # 🧭 파일 경로 탐색 및 환경 변수 관리용 표준 라이브러리
import glob # 📂 폴더 내 특정 패턴의 파일을 한 번에 가져올 때 사용 (예: *.txt)
from dotenv import load_dotenv # 🔐 .env 파일에 저장된 환경변수(예: API 키) 불러오는 함수
import gradio as gr # 🧑💻 Gradio 라이브러리 - 웹 UI를 손쉽게 만들 수 있음 (LLM 챗봇 인터페이스 구축 시 사용)
📌 설명:
os
,glob
: 파일 경로와 폴더 내 파일 탐색load\_dotenv
:.env
환경 변수 불러오기gradio
: 추후 인터페이스(UI) 구축용
📚 2. LangChain 및 시각화 관련 모듈 임포트
from langchain.document_loaders import DirectoryLoader, TextLoader
# 📥 문서 로딩 도구
# - DirectoryLoader: 폴더에 있는 여러 문서들을 한 번에 불러올 때 사용
# - TextLoader: 개별 텍스트 파일(.txt 등)을 불러올 때 사용
from langchain.text_splitter import CharacterTextSplitter
# ✂️ 긴 문서를 청크(작은 단위)로 나누는 도구
# - LLM 입력 한도를 넘지 않도록 문서를 적절히 분할하는 데 사용됨
from langchain.schema import Document
# 📄 LangChain에서 사용하는 문서 객체 형식
# - 텍스트 내용 + 메타데이터를 함께 다루기 위한 구조
from langchain_openai import OpenAIEmbeddings, ChatOpenAI
# 🧠 OpenAI 기반 기능
# - OpenAIEmbeddings: 텍스트를 벡터로 변환 (검색, 유사도 비교 등에 사용)
# - ChatOpenAI: GPT 모델과의 대화를 위한 인터페이스
from langchain_chroma import Chroma
# 🗃️ Chroma 벡터 저장소 연결
# - 벡터화된 문서를 저장하고 검색할 수 있는 벡터 데이터베이스
import numpy as np
# ➗ 수치 계산을 위한 대표적인 파이썬 수학 라이브러리
from sklearn.manifold import TSNE
# 📉 고차원 벡터를 시각화를 위해 2차원 또는 3차원으로 줄여주는 차원 축소 알고리즘
import plotly.graph_objects as go
# 📊 인터랙티브한 그래프(3D, 산점도 등)를 그릴 수 있는 시각화 도구
✅ 해결 방법: 필요한 모듈 설치
import Chroma ModuleNotFoundError: No module named 'langchain_openai'
이 에러는 langchain\_openai
모듈이 설치되어 있지 않아서 생긴 겁니다.
LangChain v0.1 이후로 OpenAI 관련 모듈은 langchain-openai
라는 별도 패키지로 분리되었고, 이를 따로 설치해야 합니다.
아래 명령어로 해결할 수 있어요:
!pip install -U langchain-openai
또한, Chroma
관련 에러가 뜰 수도 있으니 같이 설치해 두면 좋아요:
!pip install -U langchain-chroma
💰 3. 비용 고려: 저비용 LLM 및 벡터 저장소 이름 설정
# 💰 비용을 고려해 저렴한 모델을 사용함
MODEL = "gpt-4o-mini"
# 🤖 사용할 LLM 모델 이름 설정
# - gpt-4o-mini: OpenAI의 최신 저비용 모델 중 하나
db_name = "vector_db"
# 🗃️ 벡터 데이터베이스 이름 설정
# - 문서 임베딩 정보를 저장할 데이터베이스 이름
# - 검색 기반 생성(RAG)에서 임베딩된 청크들을 저장하는 데 사용됨
📌 설명
gpt-4o-mini
는 OpenAI의 최신 경량 모델로 비용은 낮추면서도 성능은 유지할 수 있는 옵션입니다.
RAG 파이프라인에서 문서를 벡터화한 결과는 벡터 DB(vector\_db)
에 저장됩니다.
이 벡터 저장소는 나중에 검색과 응답 생성을 위한 핵심 자원이 됩니다.
🔐 4. 환경 변수 불러오기 및 OpenAI 키 설정
# 🔐 .env 파일에 저장된 환경변수 불러오기
# - .env 파일은 API 키 같은 민감한 정보를 코드에 직접 쓰지 않고 따로 관리할 수 있게 해줌
# - override=True: 이미 존재하는 환경변수도 .env 값을 덮어씀
load_dotenv(override=True)
# 🧠 OpenAI API 키 설정
# - os.getenv()로 환경변수 OPENAI_API_KEY 값을 불러와서
# - os.environ에 다시 명시적으로 설정 (혹시라도 누락되었을 경우를 대비함)
# - 기본값('your-key-if-not-using-env')은 .env가 없을 때 임시 키로 사용
os.environ['OPENAI_API_KEY'] = os.getenv('OPENAI_API_KEY', 'your-key-if-not-using-env')
💡 .env
파일 예시는 이렇게 구성되어 있어야 해요:
OPENAI_API_KEY=sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxx
.env
파일은 .gitignore
에 꼭 추가해서 깃허브에 올라가지 않게 해야 합니다!
📂 5. LangChain의 로더를 사용해 문서 읽어오기
# - knowledge-base/ 폴더 내 모든 하위 폴더(.md 파일 포함)를 순회하며 문서를 불러옴
folders = glob.glob("knowledge-base/*")
# 📁 knowledge-base 하위의 모든 폴더 경로를 리스트로 가져옴
text_loader_kwargs = {'encoding': 'utf-8'}
# 🔄 위 설정이 안 먹히는 경우 (일부 Windows 사용자) 아래 줄을 사용하면 자동 인코딩 감지 가능
# text_loader_kwargs = {'autodetect_encoding': True}
documents = [] # 🗃️ 최종적으로 로딩된 모든 문서를 저장할 리스트
for folder in folders:
doc_type = os.path.basename(folder)
# 🏷️ 현재 폴더 이름을 문서 유형으로 설정 (예: "faq", "policies" 등)
# 📥 현재 폴더 내의 모든 .md 파일을 불러오는 DirectoryLoader 생성
loader = DirectoryLoader(
folder, # 탐색할 폴더 경로
glob="**/*.md", # 하위 폴더까지 포함한 모든 .md 파일을 대상
loader_cls=TextLoader, # 파일을 읽을 때 사용할 로더 클래스
loader_kwargs=text_loader_kwargs # 인코딩 설정 전달
)
folder_docs = loader.load() # 📄 해당 폴더 내 모든 문서를 로드
for doc in folder_docs:
doc.metadata["doc_type"] = doc_type
# 🏷️ 문서 메타데이터에 'doc_type' 필드를 추가해 문서의 출처 폴더명 저장
documents.append(doc) # 📌 전체 문서 리스트에 추가
이 구조 덕분에 나중에 문서 검색할 때
"어떤 폴더에서 왔는지"를 기준으로 필터링하거나 분류할 수 있어요.
📄 1. 실제 .md
파일 예시 (knowledge-base/policies/privacy.md)
# Privacy Policy
We are committed to protecting your personal data.
This policy outlines how we collect, use, and store your information.
## Data Collection
We collect your name, email, and usage data.
## Data Usage
We use your data to improve our services and personalize your experience.
## Contact
If you have questions, contact privacy@cozy-linda.com
🧩 2. documents
구조 예시 (DirectoryLoader
로 불러온 결과)
LangChain의 TextLoader
는 각 .md
파일을 Document
객체로 변환해 줍니다.
예를 들면:
from langchain.schema import Document
Document(
page_content="We are committed to protecting your personal data.\nThis policy outlines how...",
metadata={
'source': 'knowledge-base/policies/privacy.md',
'doc_type': 'policies' # 위에서 추가한 값
}
)
즉, documents
리스트는 이렇게 생긴 Document
객체들의 리스트입니다:
documents = [
Document(page_content="...", metadata={'source': 'knowledge-base/policies/privacy.md', 'doc_type': 'policies'}),
Document(page_content="...", metadata={'source': 'knowledge-base/faq/claims.md', 'doc_type': 'faq'}),
...
]
이 구조는 RAG 검색에 최적화돼 있어서 나중에:
- 특정 문서 유형(
doc\_type == 'faq'
)만 검색하거나, - 원본 경로를 통해 문서를 링크하거나,
- 문서 필터링을 적용하는 데 유용해요.
✂️ 6. 문서를 LLM 입력용 청크로 분할
이 코드는 문서를 LLM 입력에 적합하도록 적절한 크기로 나누는 핵심 단계입니다.
# ✂️ 문서를 LLM에 넣기 좋은 크기의 청크(chunk)로 분할하는 단계
# - 긴 문서는 LLM 입력 한도를 넘기 때문에, 적당한 크기로 나눠야 함
# - CharacterTextSplitter는 '문자 수 기준'으로 자르는 LangChain 도구
text_splitter = CharacterTextSplitter(
chunk_size=1000, # 📏 각 청크당 최대 1,000자까지 포함
chunk_overlap=200 # 🔁 청크 간에 200자씩 겹치게 설정 (문맥 단절을 줄이기 위해)
)
# 🧩 위에서 불러온 documents 리스트를 설정된 기준으로 나누기
# - 결과는 chunk 단위의 Document 객체 리스트가 됨
# - 이 청크들은 나중에 벡터화하여 검색에 활용될 예정
chunks = text_splitter.split_documents(documents)
💡 chunk_overlap이 중요한 이유는, 문단이 중간에 끊기면 의미가 손상될 수 있어서 앞뒤 문맥이 일부 겹치게 해서 자연스럽게 이어지도록 하기 위함이에요.
📊 7. 전체 청크 개수 확인
len(chunks)
는 지금까지 분할한 문서 청크의 총개수를 확인하는 코드입니다.
# 📦 전체 청크(문서 조각)의 개수 확인
# - 위에서 split_documents()로 나눈 결과물이 chunks 리스트에 저장됨
# - 이 개수는 곧 처리하게 될 벡터(임베딩)의 수와 같음
# - 나중에 벡터 데이터베이스에 들어갈 문서 수를 가늠할 수 있음
len(chunks)
# 예: 132라면, 총 132개의 청크(문서 조각)를 생성했다는 의미
필요하면 아래 코드처럼 가장 짧거나 긴 청크의 길이도 확인해 볼 수 있어요:
min(len(c.page_content) for c in chunks) # 가장 짧은 청크의 길이
max(len(c.page_content) for c in chunks) # 가장 긴 청크의 길이
이렇게 하면 문서 청크 분할이 적절히 잘 되었는지도 점검할 수 있어요!
🏷️ 8. 문서 유형 확인
이 코드는 불러온 문서들이 어떤 "종류(doc_type)"로 구성되어 있는지를 중복 없이 확인하는 용도입니다.
아주 중요한 메타데이터 점검 단계이기도 해요.
# 🏷️ 문서 유형(doc_type) 종류 확인
# - 각 청크(chunk)의 metadata에는 'doc_type'이라는 키가 있음 (ex: 'faq', 'policies', 'manual' 등)
# - 우리가 문서 로드할 때 폴더 이름을 기준으로 이 값을 설정해뒀음
# - set()을 사용해 중복 없이 어떤 종류의 문서들이 있는지 확인함
doc_types = set(chunk.metadata['doc_type'] for chunk in chunks)
# 🖨️ 결과 출력
# - 예: Document types found: faq, policies, tutorials
print(f"Document types found: {', '.join(doc_types)}")
💡 이 단계는 나중에 "어떤 유형의 문서만 검색에 포함시킬지", "정책 문서만 답변에 활용할지" 등을 결정할 때 매우 유용합니다.
'🧠 LLM 엔지니어링' 카테고리의 다른 글
LLM 문서에서 특정 키워드 추출하는 방법 02 (Shopwise 지식기반 예제) (1) | 2025.04.25 |
---|---|
LLM 문서에서 특정 키워드 추출하는 방법 01 (Shopwise 지식기반 예제) (1) | 2025.04.24 |
LangChain으로 시작하는 RAG 파이프라인 구축 여정 (1) | 2025.04.23 |
RAG와 벡터 임베딩 이해하기 (1) | 2025.04.22 |
LLM 챗봇 만들기: 전자상거래 회사 지식 기반 질문응답 시스템 (RAG 구현 예제) (1) | 2025.04.21 |