no image
RAG project Insights
높은 품질이란 무엇인지 미리 정의하자모든 질문에 대한 답변을 할 수는 없음가능한 부분 / 가능하지 않은 부분을 나누고가능한 부분에서는 고도화하는 끈기가 필요하다
2025.09.14
no image
Naive RAG의 한계와 발전
Naive RAG의 문제점 Advanced RAGEvolution of RAG-based Systems: Naive RAG, Advanced RAG & Modular RAG | DrJulija's Notebook🔹 Pre-Retrieval (검색 전 최적화)목표: 좋은 인덱스와 데이터 구조 만들기데이터 품질 개선: 불필요한 정보 제거, 모호성 해결, 사실성 검증, 최신화인덱스 구조 최적화: 적절한 청크 크기, 그래프 구조 활용메타데이터 추가: 날짜, 챕터, 목적 등 메타데이터 삽입 → 검색 필터링 강화청크 최적화: 너무 크거나 작은 청크는 비효율적 → 문서 특성에 맞게 조정주요 기법슬라이딩 윈도우: 청크 간 겹침으로 맥락 유지오토 머징 검색(Auto-Merging Retrieval): 작은 블록 검색..
2025.09.14
no image
수요일_RAG에 대해서(Generation)
우선 오늘 배웠던 전체 내용을 반영한 코드입니다. 아래는 기본적인 RAG 구조 이해를 위한 뼈대코드(skeleton code) 입니다. 각 단계별 모듈의 내용을 앞으로 상황에 맞게 변경하면서 문서에 적합한 구조를 찾아갈 수 있습니다. (각 단계별로 다양한 옵션을 설정하거나 새로운 기법을 적용할 수 있습니다.) #전체 코드from langchain_text_splitters import RecursiveCharacterTextSplitterfrom langchain_community.document_loaders import PyMuPDFLoaderfrom langchain_community.vectorstores import FAISSfrom langchain_core.output_parsers impo..
2025.09.11
no image
수요일_RAG에 대해서(Retriever)
우리가 구글에서 검색어를 입력하고 0.2초 만에 수많은 결과를 받아볼 수 있는 건, 단순히 우연이 아닙니다. 그 뒤에는 수십억 개의 웹페이지를 미리 정리하고 색인화해둔 거대한 인덱싱 시스템이 있기 때문이죠.AI 시대에 들어서면서 우리도 비슷한 도전에 직면하게 되었습니다. 방대한 문서 더미에서 사용자의 질문과 관련된 정보를 순식간에 찾아내는 것. 이것이 바로 RAG(Retrieval-Augmented Generation) 시스템의 핵심 과제입니다. 인덱스에서 답까지, 리트라이버의 역할하지만 잘 정리된 인덱스만 있다고 끝이 아닙니다. 사용자의 질문이 들어왔을 때, 이 인덱스를 똑똑하게 활용해서 가장 적절한 문서들을 골라내는 역할이 필요한데, 이것이 바로 리트라이버(Retriever)가 하는 일입니다. 리트라..
2025.09.11
no image
수요일_RAG에 대해서(Indexing) - OpenAIEmbeddings, Chroma, FAISS
📌 들어가면서... 인덱싱을 왜할까?인덱싱 없으면 어떻게 될까?책에서 특정 내용 찾기로 비유해보면: 인덱스 없는 책 (선형 검색)찾고 싶은 단어가 있으면 1페이지부터 끝까지 하나씩 뒤져야 함500페이지 책이면 평균 250페이지 봐야 함시간: 매우 오래 걸림 😰 인덱스 있는 책 (인덱싱된 검색)뒤쪽 "찾아보기"에서 단어 찾기 → 바로 페이지 번호 확인해당 페이지로 바로 이동시간: 몇 초면 끝 ⚡벡터 데이터베이스에서도 동일! 임베딩의 역할: "의미를 숫자로 변환하는 번역기"임베딩의 마법: "의미적 유사성" 같은 의미, 다른 표현도 찾아줌:검색: "강아지"찾아지는 것: "개", "멍멍이", "반려견", "puppy"이유: 모두 비슷한 숫자 벡터로 변환되기 때문! 언어가 달라도 찾아줌:검색: "안녕하세요"찾..
2025.09.11
no image
수요일_RAG에 대해서(Text Splitter)
Split Text A) CharacterTextSplitter (가장 단순)기본 분할 기준: "\n\n" (단일 문자 기준)길이 측정: 문자 수핵심 파라미터:separator(기본 "\n\n"), chunk_size(예: 250자), chunk_overlap(예: 50자), length_function(예: len)용도: 규칙 단순/빠른 시도, 구조 약한 텍스트B) RecursiveCharacterTextSplitter (권장 기본)분할 순서(기본 목록): ["\n\n", "\n", " ", ""]→ 단락 → 문장 → 단어 순으로 최대한 의미 단위 유지길이 측정: 문자 수권장값 예시: chunk_size=250, chunk_overlap=50, length_function=len, is_separato..
2025.09.11

 

높은 품질이란 무엇인지 미리 정의하자

모든 질문에 대한 답변을 할 수는 없음

가능한 부분 / 가능하지 않은 부분을 나누고

가능한 부분에서는 고도화하는 끈기가 필요하다

Naive RAG의 문제점

 

 


Advanced RAG

Evolution of RAG-based Systems: Naive RAG, Advanced RAG & Modular RAG | DrJulija's Notebook

🔹 Pre-Retrieval (검색 전 최적화)

목표: 좋은 인덱스와 데이터 구조 만들기

  • 데이터 품질 개선: 불필요한 정보 제거, 모호성 해결, 사실성 검증, 최신화
  • 인덱스 구조 최적화: 적절한 청크 크기, 그래프 구조 활용
  • 메타데이터 추가: 날짜, 챕터, 목적 등 메타데이터 삽입 → 검색 필터링 강화
  • 청크 최적화: 너무 크거나 작은 청크는 비효율적 → 문서 특성에 맞게 조정

주요 기법

  • 슬라이딩 윈도우: 청크 간 겹침으로 맥락 유지
  • 오토 머징 검색(Auto-Merging Retrieval): 작은 블록 검색 후 관련 블록 합쳐 제공
  • 추상 임베딩(Abstract Embedding): 문서 요약(abstract) 기반 검색
  • 메타데이터 필터링: 메타데이터 활용해 검색 정밀도↑
  • 그래프 인덱싱: 엔티티와 관계를 그래프로 표현해 연관성 강화

🔹 Retrieval (검색 단계 최적화)

목표: 쿼리와 문서 청크의 의미 공간 정렬

  • 도메인 지식 파인튜닝: 도메인 특화 데이터셋으로 임베딩 모델 개선
  • 유사도 측정 최적화: 쿼리–청크 벡터 간 거리 계산 방식 선택

대표 유사도 지표

  • 코사인 유사도(Cosine Similarity)
  • 유클리드 거리(Euclidean Distance, L2)
  • 내적(Dot Product)
  • L2 제곱 거리(L2 Squared Distance)
  • 맨해튼 거리(Manhattan Distance)

🔹 Post-Retrieval (검색 후 최적화)

목표: 불필요한 문서 제거하고, LLM이 효율적으로 활용하게 만들기

  • 리랭킹(Reranking): 검색된 청크를 재정렬해 Top-K 관련성 높은 것만 사용
  • 프롬프트 압축(Prompt Compression): 불필요한 맥락 제거, 요약을 통해 LLM에 전달할 컨텍스트 길이 축소

Modular RAG

개념:
Modular RAG는 Advanced RAG의 다양한 기법과 모듈을 조합해 전체 RAG 시스템의 성능을 높이는 표준적 패러다임.
각 모듈이 특정 기능을 담당하여 유연하고 확장성 있는 RAG 파이프라인을 구성할 수 있음.

 

📊 Modular RAG 정리표

단계  핵심 모듈  주요 기능
Indexing Chunk Optimization 작은/큰 청크 최적화, 주변 문맥 포함
  Structural Organization 문서를 섹션·단락·표 단위로 계층적 구조화, 지식 그래프 활용
  Metadata 활용 카테고리, 날짜, 목적 등 메타데이터 기반 인덱싱
Pre-Retrieval Query Transformation 질문을 변형·재작성 (HyDE, 역-HyDE 등)
  Query Expansion 원 쿼리를 여러 서브쿼리로 확장
  Query Construction Text-to-SQL, Text-to-Cypher로 구조적 쿼리 생성
Retrieval Retriever Fine-Tuning (FT) LLM·도메인 데이터 기반 학습, Positive/Negative pair 훈련
  Retriever Source 다양화 문장, 청크, 문서, 엔티티, 서브그래프 단위 검색
  Retriever Selection 임베딩, 키워드, 하이브리드 검색 조합
Post-Retrieval Rerank 다양한 문서 유형(Code, Table 포함) 재정렬
  Compression 긴 프롬프트 요약, 불필요한 정보 제거
  Selection LLM이 중요한 청크만 선택
Generation Generator FT 대규모 LLM 파인튜닝, 외부 지식 결합
  Verification 사실 검증, 위키·지식 그래프 참조, 개인정보/헛소리 탐지
Extra Modules Routing 쿼리 성격 분석 후 적절한 파이프라인 분기 (요약 vs 검색 등)
  Orchestration 쿼리 → 판단(Judge) → 검색/생성 → 출력 흐름 제어
  Scheduling 필요 시 재검색을 반복하여 최적 결과 확보
  Knowledge Guide 지식 그래프 기반 추론 경로를 따라 검색 및 생성

한 줄 요약:
Modular RAG는 검색, 메모리, 퓨전, 라우팅 같은 모듈을 조합해 상황에 맞게 작동하는 유연한 RAG 시스템으로, 오늘날 RAG 애플리케이션의 표준 구조가 되었다.


Agentic RAG

 

1. 정의 🤖

  • 즉, LLM이 스스로 검색 전략을 결정하고, 필요한 경우 반복적으로 검색-추론-생성을 수행합니다. Agentic RAG는 LLM이 검색 전략을 스스로 설계하고 반복적으로 실행·검증하는 능동형 RAG로, 복잡한 질문과 불완전한 데이터 환경에서 특히 강력합니다.

2. 기존 RAG와의 차이 

구분 Naive/Advanced/Modular RAG Agentic RAG
역할 단순히 검색된 문서를 LLM 입력에 넣고 답변 생성 LLM이 검색과 reasoning을 주도적으로 제어
검색 방식 고정된 Retriever, 정해진 파이프라인 LLM이 "추가 검색 필요/필요 없음"을 판단
추론(Reasoning) 주어진 문서 범위 내에서만 답 생성 "계획 → 실행 → 검증" 루프를 통해 점진적 추론
강점 빠르고 단순 복잡한 질문에도 다단계 추론 가능, 더 정확한 답변

3. 핵심 아이디어

  • 계획(Planning): LLM이 쿼리를 분석해 "무엇을 검색해야 하는가?"를 계획
  • 행동(Action): 필요한 검색 요청을 수행 (예: 벡터DB, 웹 검색, SQL 쿼리 등)
  • 검증(Verification): 가져온 문서가 유효한지/답변에 적합한지 확인
  • 루프(Iteration): 필요하면 추가 검색을 반복

4. 주요 기술 요소

  • Tool Use (도구 사용): LLM이 검색 엔진, DB, API 등을 직접 호출
  • Self-Reflection (자기검증): "이 답변이 충분한가?"를 스스로 평가
  • Adaptive Retrieval (적응형 검색): 단순 검색 vs 다단계 검색 여부를 상황에 따라 결정
  • Multi-hop Reasoning (다단계 추론): 여러 검색을 연결해 복잡한 질문에 답변

Self RAG

1. 정의

  • Self-RAG는 LLM이 스스로 검색 필요성과 활용 방식을 판단하고, 자기 반성을 통해 답변 품질을 높이는 RAG입니다.

2. 기존 RAG와 차이

구분 전통적 RAG Self-RAG
검색 방식 항상 쿼리 → 검색 → 답변 LLM이 “검색 필요 여부”를 판단
흐름 고정된 파이프라인 유연한 파이프라인 (검색 생략 가능)
출력 제어 검색 결과 기반 단일 답변 답변 전에 “이 답변은 신뢰 가능한가?”를 반성(reflect)
장점 간단, 빠름 효율적(불필요한 검색 줄임), 정확성 ↑, 환각(hallucination) ↓

3. 핵심 아이디어

  • 검색 필요성 판단: 쿼리만으로 답변 가능하면 검색 생략
  • 자기 반성(Self-Reflection): LLM이 “내 답변이 근거에 맞는가?”를 검토
  • 검색 활용 방식 선택:
    • 검색 결과를 그대로 인용
    • 여러 문서 비교 후 통합
    • 혹은 무시하고 자체 지식 사용

4. Self-RAG 동작 과정 (간단 흐름도)

  1. 사용자 질문 입력
  2. LLM이 검색 필요 여부 판단
    • 필요 → 벡터DB/검색엔진 호출
    • 불필요 → 내부 지식 기반으로 답변
  3. 답변 초안 작성
  4. Self-Reflection: 답변이 충분히 근거 기반인지 점검
    • 부족하다면 재검색 & 보완
  5. 최종 답변 생성

5. 장점

  • 🔹 효율성: 불필요한 검색을 줄여 속도·비용 절약
  • 🔹 정확성: 자기 검증 단계를 통해 잘못된 답변 감소
  • 🔹 유연성: 질문의 난이도와 성격에 따라 검색 전략을 동적으로 조정

Corrective RAG

1. 정의

  • Corrective RAG는 기존 RAG의 약점인 잘못된 검색 결과나 불필요한 맥락(노이즈) 문제를 보완하기 위한 기법이에요.
  • 핵심은 LLM이 검색된 문서를 무조건 활용하는 것이 아니라, 오류를 감지하고 보정(correct)하는 과정을 포함한다는 점입니다.

2. 동작 원리

  1. 검색 단계
    • 벡터DB/검색엔진에서 관련 청크를 가져옴
  2. 검증 단계(Validation)
    • LLM이 검색된 문서의 정확성·관련성을 점검
    • 불필요한 문서나 잘못된 정보를 필터링
  3. 보정 단계(Correction)
    • 문서가 부족하거나 불완전하다면 추가 검색 수행
    • 또는 LLM 자체 지식으로 보완 답변 생성
  4. 최종 응답 생성
    • 신뢰할 수 있는 근거 위주로 답변

3. 주요 기법

  • Answer Validation: 검색된 정보와 LLM 답변을 비교해 일치 여부 확인
  • Hallucination Detection: 근거 없는 부분을 표시하거나 제거
  • Iterative Retrieval: 검색 결과가 부족할 경우 반복 검색으로 보완
  • Fallback Strategy: 검색이 실패했을 때 LLM 자체 지식이나 외부 소스를 활용

4. 기존 RAG와의 차이

구분 Naive/Advanced RAG Corrective RAG
검색 활용 검색된 문서를 그대로 활용 검색 결과의 정확성·관련성을 점검 후 보정
오류 처리 문서 오류 → 그대로 답변 반영 위험 오류 감지 → 추가 검색 or 답변 수정
강점 단순, 빠름 더 신뢰성 있는 답변, 환각(hallucination) 감소

Adaptive RAG

1. 정의

  • Adaptive RAG는 이름 그대로 질문과 상황에 맞게 검색 전략을 동적으로 조정하는 RAG 방식입니다.
  • 한 가지 고정된 검색 방법을 쓰는 게 아니라, 쿼리 성격·난이도·응답 품질 요구에 따라 “검색할지 / 몇 개 검색할지 / 어떤 검색 방식을 쓸지”를 바꿔줍니다.

2. 동작 원리

  1. 쿼리 분석
    • 질문이 단순 fact인지, 복합 reasoning인지 분류
    • 예: “한국 대통령은 누구?” vs “2010년 이후 한국 대통령의 외교정책 변화를 요약해줘”
  2. 검색 전략 선택
    • 간단한 질문 → 검색 안 하거나 Top-1 검색만
    • 복잡한 질문 → 다중 쿼리 생성, Multi-hop 검색, 다양한 데이터소스 활용
  3. 동적 조정(Adaptation)
    • 검색 결과가 불충분하면 추가 검색 반복
    • 결과가 너무 많으면 필터링·요약 후 사용

3. 핵심 특징

  • Adaptive Retrieval Depth
    • 검색 단계를 1회만 할지, 반복적으로 할지 자동 결정
  • Adaptive Chunk Selection
    • 답변에 맞게 더 작은 청크 or 큰 문서 단위로 검색
  • Adaptive Modality
    • 텍스트뿐 아니라 필요 시 표, 그래프, 웹 검색 등 다른 소스 활용

4. 기존 RAG와 차이

구분 전통적 RAG Adaptive RAG
검색 방식 항상 동일한 방식 (Top-K 고정) 질문·상황에 맞게 Top-K, multi-hop, 반복 검색 조정
효율성 불필요한 검색도 수행 최소 검색으로 효율 ↑
정확성 복잡한 질문엔 부족 질문 난이도에 맞는 전략 적용 → 정확성 ↑

📊 RAG 변형 기법 비교표

구분 Self-RAG Agentic RAG Corrective RAG   Adaptive RAG
핵심 개념 LLM이 검색 필요 여부를 스스로 판단하고, 자기 반성(Self-reflection)으로 답변 품질 개선 LLM이 에이전트처럼 능동적으로 검색 전략을 계획·실행·검증 검색 결과를 검증·보정하여 오류와 환각(Hallucination)을 줄임 질문 성격에 따라 검색 깊이·범위·방식을 동적으로 조정
검색 전략 검색 여부를 선택 (검색 vs 자체 지식) 다단계(Multi-hop) 검색, 반복 검색 가능 검색 결과 재검증, 필요 시 추가 검색 단순 질문→간단 검색 / 복잡 질문→다중·반복 검색
주요 특징 불필요한 검색을 줄여 효율↑ LLM이 검색·추론 전체를 제어 잘못된 검색 결과를 교정 상황 맞춤형 검색 전략으로 최적화
장점 비용·속도 절약, 정확성 ↑ 복잡한 질문 처리, 추론력 강화 신뢰도 ↑, 오류·헛소리 감소 효율성과 정확성 동시 확보
예시 “한국 대통령은 누구?” → 자체 지식으로 답, 검색 생략 “2015년 이후 노벨상 주제?” → 연도별 검색·통합 “아인슈타인 노벨상 연도?” → 잘못된 검색 교정 후 정확 답변 “노벨상 연구 주제 요약” → 질문 난이도에 맞춰 검색 횟수·범위 조정

한 줄 요약

  • Self-RAG → 검색 여부를 스스로 판단
  • Agentic RAG → 검색·추론을 능동적으로 제어
  • Corrective RAG → 잘못된 결과를 검증·보정
  • Adaptive RAG → 질문 성격에 따라 검색 전략을 유연하게 조정
유형 Self-RAG Agentic RAG Corrective RAG Adaptive RAG
검색 여부 필요할 때만 능동적 제어 오류 시 추가 검색 상황 따라 유연 조정
LLM 역할 필요성 판단 + 자기검증 계획·실행·검증 루프 오류 감지·보정 전략 선택·최적화
강점 효율성↑ 추론력↑ 신뢰도↑ 효율+정확성↑

 

우선 오늘 배웠던 전체 내용을 반영한 코드입니다.

 

아래는 기본적인 RAG 구조 이해를 위한 뼈대코드(skeleton code) 입니다.

각 단계별 모듈의 내용을 앞으로 상황에 맞게 변경하면서 문서에 적합한 구조를 찾아갈 수 있습니다.

(각 단계별로 다양한 옵션을 설정하거나 새로운 기법을 적용할 수 있습니다.)

#전체 코드
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_community.document_loaders import PyMuPDFLoader
from langchain_community.vectorstores import FAISS
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables import RunnablePassthrough
from langchain_core.prompts import PromptTemplate
from langchain_openai import ChatOpenAI, OpenAIEmbeddings

# 단계 1: 문서 로드(Load Documents)
loader = PyMuPDFLoader("./data/extract_text/AI_Paradigm_Shift_Driven_by_DeepSeek.pdf")
docs = loader.load()

# 단계 2: 문서 분할(Split Documents)
text_splitter = RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=50)
split_documents = text_splitter.split_documents(docs)

# 단계 3: 임베딩(Embedding) 생성
embeddings = AzureOpenAIEmbeddings(
    model=AOAI_DEPLOY_EMBED_3_LARGE,
    openai_api_version="2024-02-01",
    api_key= AOAI_API_KEY,
    azure_endpoint=AOAI_ENDPOINT
    )

# 단계 4: DB 생성(Create DB) 및 저장
# 벡터스토어를 생성합니다.
vectorstore = FAISS.from_documents(documents=split_documents, embedding=embeddings)

# 단계 5: 검색기(Retriever) 생성
# 문서에 포함되어 있는 정보를 검색하고 생성합니다.
retriever = vectorstore.as_retriever()

# 단계 6: 프롬프트 생성(Create Prompt)
# 프롬프트를 생성합니다.
prompt = PromptTemplate.from_template(
    """You are an assistant for question-answering tasks. 
Use the following pieces of retrieved context to answer the question. 
If you don't know the answer, just say that you don't know. 
Answer in Korean.

#Context: 
{context}

#Question:
{question}

#Answer:"""
)

# 단계 7: 언어모델(LLM) 생성
# 모델(LLM) 을 생성합니다.

# ChatOpenAI 언어 모델을 초기화합니다. temperature는 0으로 설정합니다.
# ChatOpenAI 언어 모델을 초기화합니다. temperature는 0으로 설정합니다.
llm = AzureChatOpenAI(
    openai_api_version="2024-02-01",
    azure_deployment=AOAI_DEPLOY_GPT4O_MINI,
    temperature=0.0,
    api_key= AOAI_API_KEY,  
    azure_endpoint=AOAI_ENDPOINT
)


# 단계 8: 체인(Chain) 생성
chain = (
    {"context": retriever, "question": RunnablePassthrough()}
    | prompt
    | llm
    | StrOutputParser()
)

코드 설명

이 구조에 대해 오늘 공부한 내용을 바탕으로 설명해보겠습니다.

8단계 구조 상세 설명

1단계: 문서 로드 (Load Documents)

loader = PyMuPDFLoader("./data/extract_text/AI_Paradigm_Shift_Driven_by_DeepSeek.pdf")
docs = loader.load()
  • 목적: PDF 파일을 읽어들임
  • 비유: 도서관에서 책을 가져오는 과정
  • PDF → 텍스트로 변환해서 컴퓨터가 처리할 수 있게 만듦

2단계: 문서 분할 (Split Documents)

text_splitter = RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=50)
split_documents = text_splitter.split_documents(docs)
  • 목적: 긴 문서를 작은 조각(chunk)으로 나눔
  • 비유: 큰 책을 페이지별로 나누는 것
  • chunk_size=1000: 각 조각을 1000자 정도로 제한
  • chunk_overlap=50: 조각들 사이에 50자씩 겹치게 해서 문맥 유지

3단계: 임베딩 생성 (Embedding)

embeddings = AzureOpenAIEmbeddings(...)
  • 목적: 텍스트를 숫자 벡터로 변환
  • 비유: 단어/문장을 좌표평면의 점으로 표현
  • 비슷한 의미의 텍스트는 비슷한 위치에 배치됨
  • 컴퓨터가 "의미적 유사성"을 계산할 수 있게 됨

4단계: 벡터 DB 생성 (Create Vector Database)

vectorstore = FAISS.from_documents(documents=split_documents, embedding=embeddings)
  • 목적: 임베딩된 문서 조각들을 저장하고 빠르게 검색할 수 있는 DB 구축
  • 비유: 도서관의 카드 목록 시스템
  • FAISS: 페이스북이 만든 고속 벡터 검색 라이브러리

5단계: 검색기 생성 (Create Retriever)

retriever = vectorstore.as_retriever()
  • 목적: 질문과 관련된 문서 조각들을 찾는 도구
  • 비유: 도서관 사서 역할
  • 질문을 임베딩으로 변환 → 가장 비슷한 문서 조각들 반환

6단계: 프롬프트 생성 (Create Prompt)

prompt = PromptTemplate.from_template("""
You are an assistant for question-answering tasks. 
Use the following pieces of retrieved context to answer the question...
""")
  • 목적: AI에게 역할과 답변 방식을 지시
  • 비유: 시험지의 "답안 작성 요령"
  • Context(검색된 문서) + Question을 조합해서 AI에게 전달

7단계: 언어모델 생성 (Create LLM)

llm = AzureChatOpenAI(...)
  • 목적: 실제로 답변을 생성하는 AI 모델 설정
  • temperature=0.0: 일관되고 정확한 답변을 위해 창의성 최소화

8단계: 체인 생성 (Create Chain)

chain = (
    {"context": retriever, "question": RunnablePassthrough()}
    | prompt
    | llm
    | StrOutputParser()
)
  • 목적: 모든 단계를 연결해서 하나의 파이프라인으로 만듦
  • 흐름: 질문 입력 → 관련 문서 검색 → 프롬프트 생성 → AI 답변 → 결과 출력

마무리하며

처음 RAG 구조를 보면서 가장 인상 깊었던 것은 "AI도 참고서가 필요하다"는 점이었습니다. 아무리 똑똑한 AI라도 모든 정보를 기억할 수 없고, 특히 최신 정보나 특정 문서의 세부 내용은 알 수 없다는 현실적 한계를 인정하고, 이를 검색과 생성을 결합하여 해결한 아이디어가 매우 실용적이라고 생각했습니다.

 

복잡해 보이는 AI 시스템이 실제로는 상당히 접근 가능한 도구들로 구성되어 있다는 점이 놀라웠습니다. LangChain 같은 프레임워크 덕분에 대학생 수준에서도 충분히 이해하고 구현할 수 있는 수준까지 기술의 진입장벽이 낮아졌다고 느꼈습니다.

 

이 RAG 파이프라인을 통해 AI 엔지니어링의 핵심은 알고리즘 개발보다는 시스템 설계와 데이터 처리에 있다는 것을 깨달았습니다. 앞으로는 다양한 임베딩 기법, 벡터 검색 최적화, 프롬프트 엔지니어링 등을 더 깊이 공부해보고 싶습니다.

 

무엇보다 이런 기술을 실제 문제 해결에 어떻게 적용할 수 있을지 고민하며 실습 프로젝트를 진행해보는 것이 중요하다고 생각합니다.

 

우리가 구글에서 검색어를 입력하고 0.2초 만에 수많은 결과를 받아볼 수 있는 건, 단순히 우연이 아닙니다. 그 뒤에는 수십억 개의 웹페이지를 미리 정리하고 색인화해둔 거대한 인덱싱 시스템이 있기 때문이죠.

AI 시대에 들어서면서 우리도 비슷한 도전에 직면하게 되었습니다. 방대한 문서 더미에서 사용자의 질문과 관련된 정보를 순식간에 찾아내는 것. 이것이 바로 RAG(Retrieval-Augmented Generation) 시스템의 핵심 과제입니다.

 

인덱스에서 답까지, 리트라이버의 역할

하지만 잘 정리된 인덱스만 있다고 끝이 아닙니다. 사용자의 질문이 들어왔을 때, 이 인덱스를 똑똑하게 활용해서 가장 적절한 문서들을 골라내는 역할이 필요한데, 이것이 바로 리트라이버(Retriever)가 하는 일입니다.

 

리트라이버는 단순히 비슷한 문서를 찾는 것을 넘어서, 다양한 전략을 통해 검색 품질을 높입니다. 때로는 유사도 점수로 관련성이 낮은 문서를 걸러내고, 때로는 MMR 알고리즘으로 중복을 피하면서 다양한 관점의 정보를 가져오기도 하죠. 심지어 여러 각도에서 질문을 재구성해서 놓칠 수 있는 정보까지 찾아내는 똑똑함을 보여줍니다.

 

우리가 풀어갈 이야기

이번 포스트에서는 이러한 인덱싱 시스템을 어떻게 구축하고, 효과적인 리트라이버를 어떻게 활용할 수 있는지 실제 코드와 함께 차근차근 알아보겠습니다.


1. VectorStoreRetriever 개요

VectorStoreRetriever는 "벡터 데이터베이스를 쉽게 검색할 수 있게 해주는 도구"입니다.

벡터 데이터베이스(FAISS, Chroma 등)는 강력하지만 사용법이 복잡해요. VectorStoreRetriever는 이것을 간단하게 만들어주는 "검색 전용 리모컨" 같은 존재라고 생각하시면 됩니다.

  • 벡터 저장소의 유사도 검색, MMR 등 다양한 검색 방법 지원
  • as_retriever() 메서드로 VectorStore를 검색기로 변환

2. 기본 사용법

Retriever 초기화

# VectorStore를 검색기로 변환
retriever = db.as_retriever()

# 검색 실행
docs = retriever.invoke("검색 쿼리")

invoke() 메서드 주요 매개변수

  • k: 반환할 문서 개수 (기본 4)
  • score_threshold: 최소 유사도 점수
  • fetch_k: 필터링 전 후보 문서 수 (기본 20)
  • lambda_mult: MMR에서 다양성(0에 가까울수록 다양성↑, 1에 가까울수록 유사성↑)
  • filter: 메타데이터 기반 필터링 가능

3. 검색 유형별 활용

3.1 기본 유사도 검색 + k 설정

retriever = db.as_retriever(
    search_kwargs={"k": 3}  # 상위 3개 문서만 반환
)

3.2 임계값 기반 검색 (similarity_score_threshold)

retriever = db.as_retriever(
    search_type="similarity_score_threshold",
    search_kwargs={"score_threshold": 0.8}  # 0.8 이상 유사도만
)
  • 장점: 관련성 낮은 문서 자동 필터링
  • 주의: 임계값이 너무 높으면 결과가 없을 수 있음

3.3 MMR (Maximal Marginal Relevance)

retriever = db.as_retriever(
    search_type="mmr",
    search_kwargs={
        "k": 5,              # 최종 반환 문서 수
        "fetch_k": 20,       # 1차 검색 문서 수
        "lambda_mult": 0.5   # 다양성 조절 (0~1)
    }
)

MMR 파라미터 설명:

  • fetch_k: 먼저 가져올 후보 문서 수
  • lambda_mult:
    • 0에 가까움 → 다양성 중시
    • 1에 가까움 → 유사성 중시

MMR DeepDive !! 🏄‍♀️

(Maximal Marginal Relevance) 

MMR(Maximal Marginal Relevance)은 검색 결과의 중복을 피하는 똑똑한 알고리즘입니다.

일반 검색은 비슷한 문서만 반복해서 가져오지만, MMR은 관련성 높으면서도 서로 다른 문서들을 선별합니다.

lambda_mult 파라미터로 유사성과 다양성의 균형을 조절할 수 있어요.

  • 1에 가까우면 → 유사성 중시
  • 0에 가까우면 → 다양성 중시

결과: 풍부하고 다양한 정보 제공! 🌟

 

수학적 공식으로 이해하기

MMR 점수 = λ×(쿼리와의 유사도) + (1-λ)×(기존 선택과의 차별성)

  • 유사도: 사용자 질문과 얼마나 관련있는가?
  • 차별성: 이미 선택된 문서들과 얼마나 다른가?

핵심 원리: 임베딩 벡터의 거리 측정

  • 비슷한 의미 = 벡터 거리가 가까움
  • 다른 의미 = 벡터 거리가 멀음

MMR은 이 "거리"를 이용해:

  1. 쿼리와 가까운 것 (관련성)
  2. 이미 선택된 것과 먼 것 (다양성)

두 조건을 수학적으로 균형잡아 선택!

결론: MMR은 벡터 공간에서의 거리 계산을 통해 "관련있으면서도 다양한" 결과를 수학적으로 보장할 수 있습니다! 📊✨

 

가장 비슷한 걸 찾아줘야 하는데 왜 다양성을 고려하는거야?

사용자 질문: "파이썬 배우고 싶어"

사용자의 실제 의도:

  • 파이썬 문법도 알고 싶고
  • 실제 프로젝트 예시도 보고 싶고
  • 어떤 분야에 활용되는지도 궁금하고
  • 학습 로드맵도 알고 싶음

가장 비슷한 것만 찾으면:

  1. "파이썬 기초 문법 - 변수와 자료형" (유사도: 0.95)
  2. "파이썬 기본 문법 - 조건문과 반복문" (유사도: 0.94)
  3. "파이썬 문법 완벽 가이드" (유사도: 0.93)
  4. "파이썬 기초 - 함수 사용법" (유사도: 0.92)

결과: 전부 "기초 문법"만! 사용자가 원하는 다른 정보는 전혀 없음 😰

MMR의 진짜 목적

🎯 사용자 만족도 관점

🎪 "완벽한 정보 세트" 만들기

사용자 한 번의 검색으로:

  • ✅ 가장 관련성 높은 정보 (1순위)
  • ✅ 보완적 관점의 정보 (2순위)
  • ✅ 실용적 활용법 (3순위)
  • ✅ 심화 학습 방향 (4순위)

그럼 언제 "유사도만" 사용해야 할까?

✅ 유사도만 필요한 경우:

  • FAQ 검색 (정확한 답변 하나만 필요)
  • 특정 오류 해결법 검색
  • 정확한 수치나 공식 찾기

✅ MMR 필요한 경우:

  • 학습 자료 검색 (다양한 관점 필요)
  • 아이디어 발굴
  • 종합적 이해가 필요한 주제

결론: "가장 비슷한 것"도 중요하지만, 실제 사용자는 도움이 되는 정보 세트를 원해요. MMR은 한 번의 검색으로 더 풍부한 답변을 제공하는 전략입니다! 🎯✨

 


4. 고급 검색 기법

4.1 MultiQueryRetriever

문제점: 단일 쿼리로는 다양한 관점의 정보 놓칠 수 있음

해결책: LLM이 자동으로 여러 관점의 쿼리 생성

from langchain.retrievers.multi_query import MultiQueryRetriever

# 하나의 질문 → 여러 관점의 질문들 자동 생성
multi_retriever = MultiQueryRetriever.from_llm(
    retriever=retriever,
    llm=llm
)

장점:

  • 검색 결과의 다양성 증가
  • 프롬프트 튜닝 자동화
  • 더 풍부한 문서 집합 확보

4.2 LongContextReorder (긴 문맥 재정렬)

문제점: 문서가 10개 이상일 때 모델 성능 저하 발생

해결책: 문서 순서 재배열로 성능 향상

from langchain.document_transformers import LongContextReorder

# 문서 재정렬기 생성
reordering = LongContextReorder()

# 검색된 문서들 재정렬
docs = retriever.invoke("쿼리")
reordered_docs = reordering.transform_documents(docs)
  • 문제: 문서를 10개 이상 모델에 넣으면,
    → 모델이 중간 부분을 대충 넘어가 버려서 중요한 내용을 무시하는 경우가 생김.
  • 예시:
    책을 읽을 때 맨 앞, 맨 끝은 잘 기억하는데,
    중간 챕터는 흐릿하게 기억나는 경험과 비슷해요.
  • 해결: 검색된 문서 순서를 다시 배열합니다.
    • 가장 관련된 문서 → 시작과 끝 위치
    • 덜 관련된 문서 → 중간 위치
    • 모델이 중요한 정보에 더 잘 접근 가능
  • 효과:
    • 모델이 중요한 정보를 놓치지 않고 잘 활용
    • 긴 컨텍스트에서도 성능 유지

 


5. LCEL Chain 활용

체인 구성 방식

LCEL 체인 흐름도 그림(입력 → 프롬프트 → 모델 → 파서 → 출력)

chain = prompt | model | output_parser
  • 여러 개의 단계를 “파이프(|)”로 연결하는 방식이에요.
  • 앞 단계의 결과가 → 다음 단계의 입력으로 자동 전달됩니다.
  • 마치 파이프라인처럼 순서대로 흐른다고 보면 돼요.

실제 적용 예시

  1. prompt : 질문을 정리하거나 가이드라인을 붙여주는 단계
  2. model : LLM이 실제로 답변을 생성하는 단계
  3. output_parser : 모델이 낸 답변을 사람이 쓰기 좋은 형태로 가공

6. 실무 활용 팁

검색 성능 최적화

  • k값 조절: 너무 많으면 노이즈, 너무 적으면 정보 부족
  • 임계값 설정: 데이터 특성에 맞게 0.7~0.9 사이에서 조정
  • MMR 활용: 중복 제거와 다양성이 중요한 경우
  • 문서 재정렬: 긴 컨텍스트 처리 시 필수

주의사항

  • search_type과 search_kwargs 조합 확인
  • 메타데이터 구조 정확히 파악 후 필터링
  • lambda_mult 값에 따른 결과 차이 테스트 필요

7. 🔎 Retriever 정리 & 느낀 점

RAG 파이프라인에서 Retriever는 질문(쿼리)을 받아서 벡터 저장소(예: FAISS, Chroma)에서 관련 문서들을 꺼내오는 역할을 합니다.
Retriever를 잘 설계해야 LLM이 불필요한 정보를 덜 보고, 중요한 맥락을 더 정확히 반영할 수 있습니다.

 

Retriever를 공부하면서 단순히 “검색만 하는 도구”가 아니라, 검색 품질이 전체 RAG 성능을 좌우한다는 걸 깨달았습니다.

  • 단순 유사도 검색만으로는 아쉬운 경우가 많고,
  • MultiQueryRetriever 같은 방법을 쓰면 더 다양한 각도의 정보를 얻을 수 있었어요.
  • 또한 LongContextReorder는 “모델이 긴 문서를 어떻게 소화하는지”라는 실질적인 문제를 해결해준다는 점에서 인상 깊었습니다.

결국, Retriever는 RAG의 심장부라는 생각이 들었어요. 앞으로 프로젝트를 할 때는 단순히 LLM을 붙이는 것보다, 어떤 검색 전략을 쓰고 어떻게 문서를 다룰지를 더 신경 써야겠다고 느꼈습니다.


📌 들어가면서... 인덱싱을 왜할까?

인덱싱 없으면 어떻게 될까?

책에서 특정 내용 찾기로 비유해보면:

 

인덱스 없는 책 (선형 검색)

찾고 싶은 단어가 있으면 1페이지부터 끝까지 하나씩 뒤져야 함

500페이지 책이면 평균 250페이지 봐야 함

시간: 매우 오래 걸림 😰

 

인덱스 있는 책 (인덱싱된 검색)

뒤쪽 "찾아보기"에서 단어 찾기 → 바로 페이지 번호 확인

해당 페이지로 바로 이동

시간: 몇 초면 끝 ⚡

벡터 데이터베이스에서도 동일!

임베딩의 역할: "의미를 숫자로 변환하는 번역기"

임베딩의 마법: "의미적 유사성"

 

같은 의미, 다른 표현도 찾아줌:

검색: "강아지"

찾아지는 것: "개", "멍멍이", "반려견", "puppy"

이유: 모두 비슷한 숫자 벡터로 변환되기 때문!

 

언어가 달라도 찾아줌:

검색: "안녕하세요"

찾아지는 것: "Hello", "Hi", "안녕", "Good morning"

이유: OpenAI 임베딩이 다국어 의미를 이해하기 때문!

DB 저장의 핵심 포인트

저장되는 것:

원본 텍스트: "파이썬 프로그래밍 기초"

임베딩 벡터: [0.2, -0.1, 0.8, ...] ← 검색의 핵심!

메타데이터: {제목, 날짜, 출처 등}

 

검색 과정:

1. 쿼리 → 임베딩 벡터로 변환

2. DB의 모든 임베딩 벡터와 수학적 거리 계산

3. 가장 가까운 벡터들 찾기

4. 해당하는 원본 텍스트 반환

 

결론: 임베딩은 "의미를 수학으로 표현"해서, 컴퓨터가 "비슷한 의미"를 계산할 수 있게 해주는 핵심 기술입니다!

DB에는 이 "의미 숫자"들이 저장되어 있어요.

 
 

OpenAIEmbeddings 

1. 문서 임베딩(Document Embedding)

  • 정의: 문서 내용을 수치 벡터로 변환하는 과정.
  • 효과: 문서의 의미를 벡터 공간에 매핑 → 검색, 분류, 유사도 계산에 활용.
  • 대표 모델: BERT, GPT (문맥 정보 포착)
  • 방법: 토큰화된 문서를 모델에 입력 → 토큰 임베딩 벡터 생성 → 평균 → 문서 임베딩
  • 활용: 문서 분류, 감성 분석, 문서 간 유사도 계산 등

 


2. OpenAIEmbeddings 함수

  • embeddings.embed_query(text) - 단일 쿼리 임베딩
    • 쿼리 문자열 → 임베딩 벡터 변환
    • 유사한 텍스트 검색, 유사도 계산 등에 활용
    • 활용 예시:
      • 검색 쿼리 벡터화
      • 실시간 유사도 계산
      • 사용자 입력 처리
  •  embeddings.embed_documents([text]) - 다중 문서 임베딩
    • 문서 리스트 → 임베딩 벡터 리스트 반환
    • 반환값을 변수(doc_result)에 저장해 활용
    • 활용 예시:
      • 문서 데이터베이스 구축
      • 배치 처리로 효율성 향상
      • 벡터 저장소 생성
  • 차원 지정
    • text-embedding-3-small → 기본 1536차원
    • dimensions=1024 옵션으로 차원 축소 가능
# 차원 축소로 저장 공간 및 처리 속도 개선
embeddings_reduced = OpenAIEmbeddings(
    model="text-embedding-3-small",
    dimensions=1024  # 1536 → 1024로 축소
)

query_vector = embeddings_reduced.embed_query("테스트 쿼리")
print(f"축소된 차원: {len(query_vector)}")  # 1024

 


3. 유사도 계산

  • scikit-learn 등으로 코사인 유사도 계산 가능
  • 예시 문장:
    • sentence1 = "안녕하세요? 반갑습니다."
    • sentence2 = "안녕하세요? 반갑습니다!"
    • sentence3 = "안녕하세요? 만나서 반가워요."
    • sentence4 = "Hi, nice to meet you."
    • sentence5 = "I like to eat apples."
import numpy as np
from sklearn.metrics.pairwise import cosine_similarity
from langchain.embeddings import OpenAIEmbeddings
import pandas as pd

# OpenAI Embeddings 초기화
embeddings = OpenAIEmbeddings(model="text-embedding-3-small")

# 문장들을 임베딩 벡터로 변환
sentence_vectors = embeddings.embed_documents(sentences)

# numpy 배열로 변환
vectors_array = np.array(sentence_vectors)
print(f"벡터 형태: {vectors_array.shape}")  # (5, 1536)

# 코사인 유사도 계산
similarity_matrix = cosine_similarity(vectors_array)

# 최대 유사도 찾기
max_idx = np.unravel_index(np.argmax(similarity_no_diag), similarity_no_diag.shape)
max_similarity = similarity_no_diag[max_idx]

print(f"가장 유사한 쌍: {labels[max_idx[0]]} & {labels[max_idx[1]]}")
print(f"유사도 점수: {max_similarity:.3f}")
print(f"문장1: '{sentences[max_idx[0]]}'")
print(f"문장2: '{sentences[max_idx[1]]}'")

 


Chroma (벡터 저장소)

  • 정의: 오픈소스 벡터 DB (Apache 2.0 라이선스)
  • 특징: 개발자 친화적, AI 네이티브, RAG용 벡터 인덱스 관리

Chroma가 뭔가요?

간단히 말하면 "의미를 이해하는 데이터베이스"입니다. 실리콘밸리의 두 개발자가 "AI 시대에 맞는 새로운 데이터베이스가 필요하다"며 만든 오픈소스 프로젝트가 Chroma입니다. 지금은 전 세계 개발자들이 사용하는 인기 벡터 데이터베이스가 되었어요!

일반 데이터베이스는 정확히 일치하는 단어만 찾지만, Chroma는 비슷한 의미의 텍스트를 찾아줍니다.

 

예시:

  • 일반 DB: "파이썬"으로 검색 → "파이썬"만 찾음
  • Chroma: "파이썬"으로 검색 → "Python", "프로그래밍 언어", "코딩" 등도 찾음

1. VectorStore 생성

  • Chroma.from_documents()
    • 문서 리스트 → 벡터 저장소 생성
    • 매개변수:
      • documents: 문서 리스트
      • embedding: 임베딩 함수
      • ids: 문서 ID 리스트 (기본 자동 생성)
      • collection_name: 컬렉션 이름
      • persist_directory: 저장 디렉토리 (없으면 메모리 임시 저장)
# 문서 객체에서 벡터 저장소 생성
vectorstore = Chroma.from_documents(
    documents=doc_list,           # Document 객체 리스트
    embedding=embeddings,         # 임베딩 함수
    ids=["doc1", "doc2"],        # 문서 ID (선택사항, 자동생성 가능)
    collection_name="my_docs",    # 컬렉션 이름
    persist_directory="./chroma"  # 저장 경로 (없으면 메모리 저장)
)
  • Chroma.from_texts()
    • 텍스트 리스트 → 벡터 저장소 생성
    • 메타데이터/ID 함께 지정 가능
    • 저장 시 UUID 자동 생성
from langchain.vectorstores import Chroma
from langchain.embeddings import OpenAIEmbeddings

# 1단계: 임베딩 준비 (텍스트를 숫자로 바꿔주는 도구)
embeddings = OpenAIEmbeddings()

# 2단계: 저장할 텍스트들 준비
texts = [
    "파이썬은 프로그래밍 언어입니다",
    "자바스크립트는 웹 개발에 사용됩니다", 
    "데이터베이스는 정보를 저장합니다"
]

# 3단계: 벡터 저장소 만들기
db = Chroma.from_texts(
    texts=texts,
    embedding=embeddings,
    persist_directory="./my_db"  # 컴퓨터에 저장
)

# 4단계: 검색하기
results = db.similarity_search("코딩 언어", k=2)
print(results)  # "파이썬은..." 이 가장 먼저 나올 것

# 그 외 
# 과제 참고자료 정리
# 논문, 웹사이트 등을 저장
references = [
    "딥러닝 논문: CNN의 구조와 특징",
    "블로그 포스트: 머신러닝 입문 가이드", 
    "교수님 강의자료: 인공신경망 기초"
]
db = Chroma.from_texts(references, embeddings)

 


2. 저장 & 로드

    • 저장 (persist_directory 지정) DB 파일로 저장해 재사용 가능
    • 로드 컬렉션 이름이 다르면 데이터 조회 불가
    • 중요한 주의사항
      • 1. 컬렉션 이름이 달라야 데이터 조회 불가
      • 2. 임베딩 함수도 동일해야 함
    • 실용적인 활용법

한 번 만들고 계속 사용:

DB_PATH = "./my_database"

# 처음 실행 - 데이터 생성 & 저장
if not os.path.exists(DB_PATH):
    db = Chroma.from_texts(texts, embeddings, persist_directory=DB_PATH)
else:
    # 이후 실행 - 기존 데이터 로드
    db = Chroma(persist_directory=DB_PATH, embedding_function=embeddings)

 

이렇게 하면 프로그램을 다시 실행해도 이전에 만든 벡터 데이터베이스를 그대로 사용할 수 있어요!

 


3. 주요 메서드

  • 유사도 검색 (similarity_search)
    • 입력 쿼리와 가장 비슷한 문서들을 찾아줍니다.
    • 매개변수: query, k(결과 수), filter(메타데이터 조건)
    • 반환: List[Document]
  • 문서 추가 (add_documents)
    • 새 문서 → 임베딩 후 DB 삽입/업데이트
    • 반환: 추가된 문서의 ID 리스트
  • 문서 삭제 (delete)
    • 특정 ID 문서 삭제
    • 반환: None
# 1. 초기 데이터베이스 생성
texts = ["Python은 프로그래밍 언어", "Java는 객체지향 언어"]
db = Chroma.from_texts(texts, embeddings, persist_directory="./db")

# 2. 검색해보기
results = db.similarity_search("코딩 언어", k=2)
print(f"검색 결과: {len(results)}개")

# 3. 새 문서 추가
new_doc = Document(page_content="JavaScript는 웹 개발 언어")
added_ids = db.add_documents([new_doc])
print(f"추가된 ID: {added_ids[0]}")

# 4. 다시 검색 (새 문서도 포함해서 검색됨)
results = db.similarity_search("프로그래밍", k=3)

# 5. 필요없는 문서 삭제
db.delete(ids=[added_ids[0]])

꿀팁

  • k값: 너무 크면 관련없는 문서까지 나옴 (보통 3-5개 적당)
  • filter: 날짜, 카테고리별로 검색 범위 제한 가능
  • 메타데이터: 문서 출처, 날짜 등 추가 정보 저장해두면 유용

FAISS (Facebook AI Similarity Search)

1. 정의

  • FAISSMeta(구 Facebook) AI Research에서 만든 벡터 검색 라이브러리입니다.
  • 역할: 수백만~수십억 개의 벡터 중에서, 주어진 쿼리 벡터와 가장 유사한 벡터(Nearest Neighbor)를 빠르게 찾아냄.
  • 수백만 개의 문서에서도 0.1초 만에 비슷한 텍스트를 찾아줌
  • 구글, 네이버 같은 대형 검색엔진에서 실제로 사용하는 기술
  • LangChain + FAISS = 간단해짐! 원래 FAISS는 복잡했지만, LangChain이 쉽게 만들어줬어요:

2. 특징

  • 고속 근사 최근접 탐색(ANN, Approximate Nearest Neighbor) 지원
  • CPU/GPU 최적화 → 대규모 데이터셋에서도 빠른 검색 가능
  • 다양한 인덱스 타입 제공: 상황에 맞는 검색 효율 선택 가능
  • 오픈소스 (C++로 구현, Python 바인딩 제공)

3. LangChain에서의 FAISS

    • LangChain은 FAISS를 VectorStore로 감싸서 쉽게 쓸 수 있게 해줌.
    • FAISS.from_texts(texts, embedding)
      → 텍스트 리스트를 임베딩 후, FAISS 인덱스로 바로 저장소 생성
    • 반환: FAISS VectorStore 인스턴스

4. 동작 방식

  1. 텍스트 → 임베딩 함수 → 벡터 변환
  2. 벡터와 메타데이터/ID를 묶어서 FAISS 인덱스에 저장
  3. 나중에 쿼리 벡터 입력 시, 최근접 이웃 검색(Nearest Neighbor Search) 수행
from langchain.vectorstores import FAISS
from langchain.embeddings import OpenAIEmbeddings

# 1단계: 임베딩 준비
embeddings = OpenAIEmbeddings()

# 2단계: 텍스트 준비
texts = [
    "파이썬은 프로그래밍 언어입니다",
    "자바스크립트는 웹 개발 언어입니다", 
    "HTML은 마크업 언어입니다"
]

# 3단계: 한 줄로 벡터 저장소 완성!
db = FAISS.from_texts(texts, embeddings)

# 4단계: 검색하기
results = db.similarity_search("코딩 언어", k=2)

 

5. 언제 FAISS를 쓸까요?

FAISS를 선택하세요: Chroma를 선택하세요:
✅ 문서가 10만개 이상
✅ 검색 속도가 중요함
✅ 메모리 절약이 필요함
✅ 실제 서비스에 배포할 때
✅ 개발/학습 단계
✅ 문서가 1만개 이하
✅ 데이터 저장이 중요함
✅ 간편함을 원할 때

6. FAISS의 장단점

장점

단점 

  • 오픈소스 + 무료
  • 로컬/온프레미스 환경에서 사용 가능
  • GPU 지원 → 대규모 데이터 처리에 적합
  • 다양한 인덱스 구조 제공 (Flat, IVFFlat, HNSW 등)
  • 메모리 기반 → 데이터가 커지면 메모리 부담 ↑
  • 분산 환경 지원은 직접 구현해야 함 (Pinecone 같은 SaaS 대비 부족)
  • 인덱스 선택/튜닝을 잘못하면 성능 저하 가능

 

알아두면 좋은 추가 개념

👉 지금 텍스트에는 안 나왔지만, 실무에서는 꼭 알아야 하는 부분이 있어요.

  1. 인덱스 타입
    • IndexFlatL2: 단순 유클리드 거리 기반, 정확하지만 속도 느림
    • IVFFlat: 코어세트(cluster) 기반 검색, 빠르지만 근사치 가능성 있음
    • HNSW: 그래프 기반, 대규모에서도 빠르고 효율적
  2. 유사도 측정 방식
    • L2 거리 (Euclidean)
    • Inner Product (Dot product)
    • Cosine Similarity
  3. 저장 & 로드
    • LangChain FAISS VectorStore → 디스크에 저장 가능 (save_local, load_local)
    • 재시작 시 DB 다시 만들 필요 없이 로드 가능

 


✅ 최종 요약

  • OpenAIEmbeddings: 텍스트/문서를 벡터로 변환 → 의미 기반 검색·분류 가능
  • Chroma(VectorStore): 벡터를 저장/검색하는 DB → RAG에서 인덱싱과 검색의 핵심
  • FAISS = 오픈소스 벡터 검색 엔진, 로컬에서 대규모 벡터 검색 가능 / LangChain 연동 = FAISS.from_texts()로 쉽게 벡터 저장소 생성.

👉 즉, 임베딩으로 의미를 숫자로 바꾸고, RAG DB에 저장해 인덱스를 만들어두면, 나중에 질문이 들어왔을 때 의미적으로 비슷한 문서를 빠르게 찾아낼 수 있다는 게 핵심입니다.

 

Split Text

A) CharacterTextSplitter (가장 단순)

  • 기본 분할 기준: "\n\n" (단일 문자 기준)
  • 길이 측정: 문자 수
  • 핵심 파라미터:
    • separator(기본 "\n\n"), chunk_size(예: 250자), chunk_overlap(예: 50자), length_function(예: len)
  • 용도: 규칙 단순/빠른 시도, 구조 약한 텍스트

B) RecursiveCharacterTextSplitter (권장 기본)

  • 분할 순서(기본 목록): ["\n\n", "\n", " ", ""]
    단락 → 문장 → 단어 순으로 최대한 의미 단위 유지
  • 길이 측정: 문자 수
  • 권장값 예시: chunk_size=250, chunk_overlap=50, length_function=len, is_separator_regex=False
  • 장점: 의미 보존이 잘 되고, 일반 문서에 무난

📊 Character vs Recursive Splitter 비교

구분
CharacterTextSplitter (그냥) RecursiveCharacterTextSplitter (리컬시브)
분할 기준 하나의 구분자만 사용 (기본 "\n\n") 구분자 리스트 순서대로 시도 (기본 ["\n\n", "\n", " ", ""])
방식 지정한 기준이 없으면 그냥 문자 수로 잘라냄 큰 단위 → 중간 단위 → 작은 단위 순으로 재귀적으로 분할
문맥 유지 문단·문장 단위 고려 부족 → 어색하게 끊길 수 있음 문단 → 문장 → 단어 순으로 유지 → 자연스러운 청크 생성
장점 단순하고 빠르다 의미 단위 보존, LLM이 이해하기 좋은 청크
단점 문맥 파괴 가능성 높음 조금 더 복잡하고 속도는 느릴 수 있음
적합한 경우 빠른 실험, 단순 데이터 일반 문서 처리, RAG 기본 추천

C) TokenTextSplitter (토큰 기준)

  • 이유: LLM 토큰 한도를 넘지 않게 안전하게 자르기
  • 토크나이저: 보통 tiktoken
  • from_tiktoken_encoder 주의:
    • CharacterTextSplitter.from_tiktoken_encoder: 분할은 캐릭터 기준, 병합 시 토큰 길이 참고 → 청크가 가끔 토큰 한도를 초과할 수 있음
    • RecursiveCharacterTextSplitter.from_tiktoken_encoder: 토큰 한도를 보장하도록 재귀 분할 → 권장
  • 토큰의 기준?
    • tiktoken 은 OpenAI가 만든 토큰화 라이브러리(Tokenizer) 의 이름
    • 📏 토큰의 크기 감각
      • 영어: 보통 1토큰 ≈ 4자(문자), 100토큰 ≈ 75단어 정도
      • 한국어/일본어/중국어: 단어가 길게 붙기 때문에 토큰 수가 더 적게 나오기도, 많게 나오기도 함

“단어를 중간에 잘라서 토큰으로 쪼개면, 모델이 그걸 어떻게 이해하지?”

  • 모델은 학습할 때부터 “토큰 시퀀스”를 보고 다음 토큰을 예측하는 방식으로 훈련됨 → 그래서 단어가 잘려도 문제 없음.

🔎 예시

문장: “안녕하세요”

  • tiktoken은 "안", "녕하세요" → 두 개 토큰으로 자름
  • 모델은 "안" + "녕하세요"라는 조합이 자주 함께 나옴을 학습 → “안녕하세요”라는 인사로 이해.

LLM은 토큰 단위로만 세상을 보고, 단어가 잘려도 연속된 토큰 패턴을 학습했기 때문에 “아, 이건 원래 이런 단어구나” 하고 이해할 수 있는 거예요.


D) SemanticChunker (의미론 기반, 실험적)

  • 아이디어: 문장을 자른 뒤, 임베딩 유사도로 이웃 문장들을 의미 그룹으로 묶음
    (예: 문장 3개씩 창으로 훑어 유사한 덩어리 병합 – Greg Kamradt 접근)
  • 장점: 주제/맥락에 맞는 자연스러운 청크
  • 주의: 추가 임베딩 계산 → 비용/시간↑
  SemanticChunker
분할 기준 임베딩(벡터 공간)에서 문장 의미 유사성
방식 먼저 문장을 나눈 뒤 → 임베딩으로 의미를 벡터화 → 비슷한 문장들을 묶어서 청크 생성
결과 크기는 다소 불규칙하지만, 의미적으로 응집력 높은 청크
장점 의미 단위 보존 ↑, 검색 정확도 ↑
단점 속도 느리고, 임베딩 비용 발생
적합한 경우 보고서·논문·블로그처럼 주제 맥락이 중요한 텍스트

✏️ 원문 (짧은 기사 예시)

애플은 새로운 아이폰 모델을 발표했다. 이번 모델은 카메라 성능이 크게 향상되었으며, 배터리 수명도 늘어났다. 
또한 가격은 이전 세대와 비슷하게 유지될 예정이다. 
삼성 역시 다음 달에 신제품을 공개할 것으로 알려졌다.
 

👉 문장 임베딩 후, 유사도 높은 것끼리 묶음

👉 이렇게 보면, SemanticChunker는 RAG에서 답변 정확도를 높이는 데 유리해요.
왜냐하면 “애플 관련 질문”이 들어왔을 때, 애플 관련 문장만 묶인 청크를 검색할 수 있으니까요.

  • 청크 1 (애플 관련):
    애플은 새로운 아이폰 모델을 발표했다. 이번 모델은 카메라 성능이 크게 향상되었으며, 배터리 수명도 늘어났다. 또한 가격은 이전 세대와 비슷하게 유지될 예정이다.
  • 청크 2 (삼성 관련):
    삼성 역시 다음 달에 신제품을 공개할 것으로 알려졌다.

🔎 특징: 같은 주제(애플) 문장들을 하나로 묶고, 다른 주제(삼성)는 따로 분리 → 검색·질의 시 더 자연스럽게 매칭됨


E) MarkdownHeaderTextSplitter (문서 구조 보존)

  • 기준: 마크다운 헤더 레벨(#, ##, ###…)별로 분할
  • 설정: headers_to_split_on=[("#", "H1"),("##","H2"), ...] 식으로 지정
  • 장점: 섹션 맥락 유지, 문서 구조를 그대로 RAG에 반영

+) 선택 가이드(현업 감각)

  • 처음/일반 문서: RecursiveCharacterTextSplitter(250/50 전후) → 가장 안전
  • 토큰 한도 엄격: RecursiveCharacterTextSplitter.from_tiktoken_encoder
  • 마크다운/보고서: MarkdownHeaderTextSplitter → 필요 시 이후 Character/Recursive로 2차 분할
  • 주제 경계가 뚜렷: SemanticChunker(의미 기반) 고려
  • 빠른 실험: CharacterTextSplitter로 감 잡기 → 품질 필요 시 위로 업그레이드

+) 실무 팁

  • 중복(Overlap) 10~20%로 시작(문장 경계 끊김 방지), 필요시 50자 등 조절
  • 길이 함수: 기본 len(문자) vs tiktoken(토큰). 검색/LLM 한도 생각하면 토큰 기준이 더 안전
  • 청크 크기: 200~800 토큰 권장(모델/도메인 따라 조정)
  • 메타데이터: source, title, page, section 등 꼭 넣기
  • load_and_split(): 로더 → 분할까지 한 번에. 간단 파이프라인에 유용

😁 텍스트 분할기(Text Splitter)를 공부하며 느낀 점

RAG 파이프라인을 준비하다 보면 “긴 문서를 어떻게 나눌까?”라는 고민이 꼭 따라옵니다. 이때 등장하는 것이 바로 텍스트 분할기(Text Splitter)입니다.

공부하면서 느낀 건, 분할에도 성격이 다 있다는 거예요.

  • CharacterTextSplitter는 말 그대로 단순하게 글자 수를 기준으로 자릅니다. 빠르고 단순하지만 문맥이 잘려나갈 위험이 있습니다.
  • RecursiveCharacterTextSplitter는 단락 → 문장 → 단어 순서로 점점 세밀하게 잘라서, 가능한 한 의미 단위를 유지하려는 점이 인상적이었습니다. 실제 문서를 다룰 때는 이 방식이 더 자연스럽게 느껴졌습니다.
  • TokenTextSplitter는 LLM이 이해할 수 있는 토큰 단위를 기준으로 자릅니다. “토큰 제한”이라는 현실적인 제약 때문에, 결국 토큰 기준 분할은 실무에서 꼭 필요한 요소라는 걸 알게 됐습니다.
  • SemanticChunker는 단순한 길이 기준이 아니라 문장의 의미를 기반으로 묶어줍니다. “아, 이건 진짜 사람이 읽듯이 문단을 만들어주는 거구나”라는 생각이 들어서 흥미로웠습니다.

이 과정을 정리하면서 느낀 점은, 텍스트를 자르는 기준이 단순한 기술적 선택이 아니라, 모델이 이해하는 문맥의 품질을 좌우한다는 점입니다. 결국 “잘 자른 텍스트”가 좋은 답변을 만드는 바탕이 되더군요.

앞으로는 문서의 성격에 따라 어떤 분할기를 선택할지 고민하는 과정 자체가 RAG 설계의 중요한 포인트가 될 것 같습니다.