🧠 LLM 엔지니어링

LangChain과 Chroma를 활용한 문서 임베딩 시각화 실습 01

개발자 린다씨 2025. 4. 26. 14:43
반응형

🧠 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)}")

💡 이 단계는 나중에 "어떤 유형의 문서만 검색에 포함시킬지", "정책 문서만 답변에 활용할지" 등을 결정할 때 매우 유용합니다.

반응형