
RAG를 정말 제대로 사용하기 위해, 처리 단계별로 우수한 기법을 정리해 보았다
요약
RAG 시스템 구축 시 단계별 핵심 기법과 주요 OSS 프레임워크의 특징을 정리한 가이드입니다. 단순 검색을 넘어 데이터 수집, 파싱, 청킹 등 워크플로우 설계의 중요성을 강조합니다.
핵심 포인트
- RAG의 핵심은 Retrieval 이전에 데이터 파싱과 청킹 단계의 품질에 있음
- Dify와 같은 도구를 통해 RAG를 단일 기능이 아닌 워크플로우로 설계해야 함
- 데이터 수집(Ingest) 단계에서 실패 관리 및 차분 업데이트 설계가 필수적임
- GraphRAG나 Agentic RAG 도입 전 기초적인 문서 이해와 입도 조절이 우선되어야 함
최근 RAG의 구현 사례가 상당히 늘어났습니다.
Dify, RAGFlow, LlamaIndex, LangChain, GraphRAG, LightRAG, Docling, Marker, Unstructured, RAG-Anything, ColPali, PageIndex, Ragas, AutoRAG, DSPy 등 우수한 OSS / 프레임워크 / 연구 구현이 많이 있습니다.
다만, 이것들을 「제품별」로 바라보면 결국 자신의 RAG에 어디를 도입해야 할지 알기 어렵습니다.
그래서 이번에는 RAG를 다음 처리 단계로 분해하여, 각 단계에서 업계의 우수한 기법을 어떻게 도입할지를 정리합니다.
1. Data Source / Workflow
2. Parser / Document Understanding
3. Chunking
...
대상 독자는 실제로 RAG를 구축·운용하고 싶은 기술자입니다.
단순한 개요가 아니라, 구현으로 옮길 때의 판단 포인트, 코드 예시, 실수하기 쉬운 부분도 작성합니다.
이번 정리에서는 직접 조사한 RAG 관련 OSS 조사 카탈로그도 참조하고 있습니다.
결론부터 말씀드리면, RAG에서 효과가 나타나는 순서는 대략 다음과 같습니다.
먼저 효과가 나타나는 것:
Parser / Document Understanding
Chunking
...
갑자기 Agentic RAG나 GraphRAG로 가기보다, 우선은 문서를 파괴하지 않고 가져오며, 검색 가능한 입도(granularity)로 chunk를 만들고, 근거를 바탕으로 답변하게 하는 부분을 다지는 것이 안정적입니다.
Dify는 workflow / 운용 측면, RAGFlow는 document understanding / citation 측면, Docling・Marker・Unstructured는 parser 측면, LlamaIndex는 RAG 부품의 재조합, GraphRAG・LightRAG는 관계 검색, ColPali는 시각 검색, PageIndex는 tree search, Ragas・AutoRAG・DSPy는 평가와 개선 루프에 강점이 있습니다. Dify는 AI workflow, RAG pipeline, agent, model management, observability를 통합적으로 다루는 LLM 애플리케이션 개발 기반으로 설명되어 있습니다. (GitHub)
RAG의 첫 번째 문제는 사실 retrieval이 아닙니다.
먼저, 데이터가 어디에서 오는지 관리할 필요가 있습니다.
- PDF
- Word / PowerPoint / Excel
- HTML
...
이 단계에서 중요한 것은 단순히 파일을 업로드할 수 있는 것이 아니라, 가져오기(ingest) workflow를 가시화하고, 실패·재실행·차분 업데이트·권한 관리까지 다룰 수 있는 것입니다.
Dify의 좋은 점은 RAG를 단일 기능으로서가 아니라, workflow의 일부로서 다루고 있다는 점입니다. 공식 README에서도 visual workflow, RAG pipeline, agent capabilities, model management, observability를 통합하는 개발 기반으로 설명되어 있습니다. (GitHub)
Dify 방식의 설계에서 도입해야 할 점은 다음과 같습니다.
| 관점 | 도입해야 할 설계 |
|---|---|
| Workflow | ingest, 검색, 답변, tool call을 화면에서 볼 수 있게 한다 |
| ... |
Dify를 로컬에서 테스트하는 경우는 Docker Compose가 가장 간단합니다.
git clone https://github.com/langgenius/dify.git
cd dify/docker
cp .env.example .env
...
이 단계에서는 Dify를 그대로 사용할지 여부보다, RAG를 workflow로서 설계한다는 사고방식을 도입하는 것이 중요합니다.
직접 만드는 경우에도 처음부터 ingest job을 상태 관리하는 것이 좋습니다.
from enum import Enum
from dataclasses import dataclass
from datetime import datetime
...
RAG에서는 실패한 document를 「어쩌다 보니 실패함」 식으로 취급하면 나중에 곤란해집니다.
PARSING_FAILED,
OCR_FAILED,
UNSUPPORTED_FORMAT
、INDEX_FAILED
와 같이, 어느 단계에서 실패했는지를 남겨야 합니다.
RAG의 품질은 parser (파서)에 의해 크게 결정됩니다.
흔히 발생하는 실패 사례입니다.
- PDF의 표가 깨짐
- 2단 구성의 읽기 순서가 뒤섞임
- 그림의 caption (캡션)이 사라짐
...
LLM에 전달하기 전에 문서 구조가 깨져 있다면, 아무리 좋은 embedding (임베딩) / rerank (리랭크) / generation (생성) 기법을 도입하더라도 한계가 있습니다.
RAGFlow는 이 단계의 철학이 상당히 명확합니다. README에서는 deep document understanding (심층 문서 이해), template-based chunking (템플릿 기반 청킹), grounded citations (근거 기반 인용), multiple recall with fused re-ranking (결합된 리랭킹을 통한 다중 검색) 등이 특징으로 설명되어 있습니다. (GitHub)
RAGFlow로부터 배워야 할 점은, RAG의 입력을 단순한 plain text (일반 텍스트)로 취급하지 않는 것입니다.
document
├─ page
├─ section
...
특히 뛰어난 점은, document understanding (문서 이해)과 citation (인용)을 처음부터 연결해 두었다는 점입니다.
parser (파서) 단계에서 page (페이지) / bbox (경계 상자) / section (섹션) / table (표) 정보를 가지고 있다면, 답변 시 "이 문장은 어느 페이지의 어느 chunk (청크)에 기반하는가"를 반환할 수 있습니다.
Docling은 문서를 Docling document로 변환한 뒤, Markdown (마크다운)이나 다양한 workflow (워크플로우)에 사용할 수 있는 형태로 만듭니다. 공식 문서에서는 DocumentConverter로 source (소스)를 변환하고, Docling document를 workflow에 사용하는 흐름을 소개하고 있습니다. (Docling Project)
from docling.document_converter import DocumentConverter
source = "docs/report.pdf"
converter = DocumentConverter()
...
Docling이 적합한 케이스입니다.
| 케이스 | 이유 |
|---|---|
| PDF / Office 문서가 많음 | document model (문서 모델)을 중심으로 다룰 수 있음 |
| ... |
Marker는 PDF, 이미지, PPTX, DOCX, XLSX, HTML, EPUB을 Markdown, JSON, chunks, HTML로 변환할 수 있습니다. README에서는 표, 수식, 링크, 참조, 코드 블록, 이미지 저장, structured extraction (구조화된 추출), LLM을 통한 정확도 향상 등이 설명되어 있습니다. (GitHub)
from marker.converters.pdf import PdfConverter
from marker.models import create_model_dict
from marker.output import text_from_rendered
...
Marker로부터 취해야 할 핵심 아이디어는, Markdown뿐만 아니라 JSON / chunks / HTML도 출력한다는 점입니다.
Markdown만 있다면 인간이 읽기에는 좋지만, RAG의 후속 단계에서는 다음과 같은 정보가 필요합니다.
{
"page": 3,
"block_type": "table",
...
Unstructured는 복잡한 문서를 LLM용 구조화 데이터로 변환하는 ETL (추출·변환·적재)적인 위치를 차지합니다. README에서는 복잡한 문서를 clean (정제된) / structured format (구조화된 형식)으로 변환하는 OSS (오픈 소스 소프트웨어)로 설명되어 있습니다. (GitHub)
from unstructured.partition.auto import partition
elements = partition("docs/report.pdf")
for element in elements[:10]:
...
Unstructured는 이메일, HTML, Word, PDF 등이 혼재된 환경에서 입구를 통일하기 쉽습니다.
이 부분이 가장 중요합니다.
Docling, Marker, Unstructured, RAGFlow 스타일의 parser (파서)를 사용하더라도, 그 출력을 그대로 후속 단계로 흘려보내면 운영이 힘들어집니다.
반드시 자신만의 RAG용 schema (스키마)로 변환해야 합니다.
from dataclasses import dataclass, field
from typing import Any
@dataclass
...
Parser adapter (파서 어댑터)는 다음과 같은 형태가 됩니다.
class ParserAdapter:
name: str
def parse(self, file_path: str) -> list[DocumentElement]:
...
Unstructured adapter (Unstructured 어댑터)의 예시입니다.
class UnstructuredAdapter(ParserAdapter):
name = "unstructured"
def parse(self, file_path: str) -> list[DocumentElement]:
...
이 단계에서 최소한으로 남겨두어야 할 metadata (메타데이터)입니다.
- parser_backend
- source_file
- page_number
...
engchina/No.1-PdfParser-Free와 같은 PDF → page image (페이지 이미지) → VLM/OCR → Markdown (마크다운) 흐름은, 스캔된 PDF나 복잡한 레이아웃의 PDF에 효과적입니다.
중요한 것은, VLM OCR을 일반적인 parser (파서) 뒤에 대충 배치하는 것이 아니라, **preprocess profile (전처리 프로필)**로서 명시적으로 다루는 것입니다.
import fitz
from pathlib import Path
def pdf_to_page_images(pdf_path: str, output_dir: str, zoom: float = 2.0) -> list[Path]:
...
VLM OCR 측은 parser adapter (파서 어댑터)로 분리합니다.
def vlm_ocr_page(image_path: str, vlm_client) -> str:
prompt = """
이 페이지 이미지를 RAG용 Markdown (마크다운)으로 변환해 주세요.
...
"""
주의할 점으로, PyMuPDF 등의 라이선스는 반드시 확인해야 합니다. 상업적 이용 시 parser (파서) / OCR 주변의 라이선스를 경시하면 나중에 곤란해질 수 있습니다.
Chunking (청킹)은 RAG 내에서 눈에 띄지 않지만, 상당히 중요합니다.
잘못된 chunking (청킹)은 다음과 같습니다.
- 헤딩 (Heading, 제목)을 가로지름
- 표 (Table)를 중간에 자름
- 캡션 (Caption)과 피규어 (Figure, 도표)가 서로 다른 chunk (청크)가 됨
...
흔히 볼 수 있는 구현 방식입니다.
def naive_chunk(text: str, size: int = 1000, overlap: int = 100) -> list[str]:
chunks = []
start = 0
...
이 방식은 간단하지만, 업무용 문서에서는 쉽게 무너집니다.
표나 그림, 수식, 코드, 헤딩 (Heading) 구조를 무시해 버리기 때문입니다.
RAGFlow가 template-based chunking (템플릿 기반 청킹)을 강조하는 이유는 이 문제에 대한 현실적인 해답이기 때문입니다. 문서 종류별로 chunking template (청킹 템플릿)을 변경함으로써, PDF, 표, QA 문서, 매뉴얼을 모두 동일한 방식으로 자르지 않도록 설계할 수 있습니다. (GitHub)
manual:
heading-aware chunking
table-heavy PDF:
...
LlamaIndex는 quickstart (퀵스타트)가 간편하여, SimpleDirectoryReader와 VectorStoreIndex를 통해 바로 테스트해 볼 수 있습니다. 공식 문서에서도 document loading (문서 로딩), indexing (인덱싱), query engine (쿼리 엔진)의 기본 형태가 소개되어 있습니다. (Developer Documentation)
from llama_index.core import VectorStoreIndex, SimpleDirectoryReader
documents = SimpleDirectoryReader("data").load_data()
index = VectorStoreIndex.from_documents(documents)
...
LlamaIndex로부터 도입해야 할 설계는, chunking (청킹) / node parsing (노드 파싱)을 교체 가능한 component (컴포넌트)로 취급하는 것입니다.
Parser (파서)를 통해 얻은 DocumentElement
를 사용하여, 헤딩(heading)이나 content type (콘텐츠 타입)을 확인하면서 chunk (청크)를 만듭니다.
from dataclasses import dataclass, field
@dataclass
class RagChunk:
...
def structure_aware_chunk(
elements: list[DocumentElement],
max_chars: int = 1800,
...
이 방식의 장점입니다.
- 헤딩을 가로지르지 않음
- 표나 그림을 중간에 자르지 않음
- citation (인용)이 element (엘리먼트)로 돌아갈 수 있음
...
AutoRAG는 parsing (파싱), chunking (청킹), QA 생성, 평가, dashboard (대시보드), 최적 pipeline (파이프라인) 탐색을 통합적으로 다루는 프레임워크입니다. README에서도 parser (파서), chunker (청커), evaluator (평가기)를 개별적으로 실행하는 흐름이 소개되어 있습니다. (GitHub)
이러한 사고방식은 매우 중요합니다.
즉, chunking (청킹)은 "한 번 결정하면 끝"인 것이 아니라, 평가 대상 중 하나로 삼아야 합니다.
chunking_experiments:
- name: recursive_800
strategy: recursive
...
평가에서는 다음을 확인합니다.
- retrieval_recall (검색 재현율)
- context_precision (문맥 정밀도)
- table_qa_accuracy (표 QA 정확도)
...
Embedding (임베딩) / Index (인덱스) 단계에서 중요한 것은 단순히 vector (벡터)를 만드는 것만이 아닙니다.
중요한 것은, 재 임베딩 (re-embedding) 하더라도 잃고 싶지 않은 정보를 별도로 저장하는 것입니다.
Embedding (임베딩)은 다시 만들 수 있습니다.
하지만 parser (파서)가 추출한 page (페이지), bbox (경계 상자), table cell (표 셀), section path (섹션 경로)는 잃어버리면 복원이 어렵습니다.
CREATE TABLE rag_chunks (
chunk_id VARCHAR2(64) PRIMARY KEY,
document_id VARCHAR2(64) NOT NULL,
...
metadata_json에는 다음을 넣습니다.
{
"parser_backend": "docling",
"chunk_strategy": "structure_aware",
...
Vector search (벡터 검색)의 예시입니다.
SELECT
chunk_id,
document_id,
...
LightRAG는 knowledge graph (지식 그래프)와 vector embeddings (벡터 임베딩)의 dual-layer architecture (이중 계층 구조)를 가지며, local / global / hybrid / naive / mix의 query mode (쿼리 모드)를 전환할 수 있는 설계입니다. 또한, 최근의 README에서는 reranker (리랭커), citation (인용), multimodal (멀티모달), RAGAS 평가 등도 통합되어 있습니다. (GitHub)
여기서 배울 수 있는 점은 index (인덱스)를 한 종류로 고정하지 않는 것입니다.
dense vector index (밀집 벡터 인덱스)
sparse keyword index (희소 키워드 인덱스)
metadata index (메타데이터 인덱스)
...
이와 같이 여러 개의 index (인덱스)를 갖는다는 전제로 하면, 나중에 retrieval routing (검색 라우팅)을 수행하기가 용이해집니다.
Retrieval (검색)에서는 단순히 embedding (임베딩) 유사도의 top-k를 가져오는 것만으로는 불충분합니다.
흔히 발생하는 문제입니다.
- 에러 코드나 모델 번호에 취약함
- 표의 열 이름에 취약함
- 고유 명사에 취약함
...
RAGFlow는 multiple recall (다중 검색)과 fused re-ranking (융합 재순위화)을 특징으로 내세우고 있습니다. (GitHub)
이는 매우 실용적입니다.
dense vector search (밀집 벡터 검색)
+ keyword search (키워드 검색)
+ metadata filter (메타데이터 필터)
...
from collections import defaultdict
def reciprocal_rank_fusion(
ranked_lists: list[list[dict]],
...
모든 query에 동일한 retrieval을 사용할 필요는 없습니다.
def classify_query(query: str) -> str:
if any(word in query for word in ["関係", "依存", "全体像", "比較"]):
return "graph"
...
def route_retrieval(query: str) -> list[dict]:
route = classify_query(query)
if route == "graph":
...
Retrieval의 목적은 단순히 '비슷한 chunk'을 가져오는 것이 아닙니다.
답변에 필요한 evidence를 가져오는 것입니다.
Retrieval의 top-k에 정답이 포함되어 있어도, 순서가 좋지 않을 수 있습니다.
또한, 비슷한데 답변에는 불필요한 chunk이 섞일 수도 있습니다.
이 단계에서는 다음을 수행합니다.
-
ereank
- deduplication
- context compression
...
RAGFlow는 fused re-ranking과 grounded citations를 중시합니다. LightRAG도 README 상에서 reranker support나 citation functionality의 추가를 설명하고 있습니다. (GitHub) (GitHub)
이 단계의 본질은 LLM에 전달할 context를 줄이면서, 근거로서의 질을 높이는 것입니다.
CRAG는 Corrective Retrieval Augmented Generation의 개념으로, retrieval evaluator가 검색 결과의 신뢰도를 평가하고, 불충분할 경우 query rewrite나 fallback을 수행합니다. 논문에서는 retrieval evaluator와 corrective action에 의해, 취득 문서의 품질을 보면서 답변으로 나아가는 구성을 설명하고 있습니다. (Self-RAG)
구현 이미지는 다음과 같습니다.
def grade_context(query: str, chunks: list[dict], llm) -> dict:
context = "\n\n".join(
f"[{i}] {chunk['text'][:800]}"
...
def corrective_retrieve(query: str) -> list[dict]:
hits = retrieve(query)
grade = grade_context(query, hits, llm)
...
CRAG적인 생각은 특히 다음 상황에서 효과적입니다.
- top-k가 빈약할 때
- query가 모호할 때
- 문서에 답이 없을 가능성이 있을 때
...
여기서는 일반적인 vector search만으로는 가져오기 어려운 정보를 다룹니다.
GraphRAG는 비정형 텍스트에서 graph를 구축하고, local/global search를 구분하여 사용하는 구성입니다. 공식 Getting Started에서도 index 생성 후에 global search나 local search를 실행하는 흐름이 소개되었습니다. (Microsoft GitHub)
효과적인 query는 다음과 같습니다.
- A와 B의 관계는?
- 이 문서들의 주요 주제는?
- 부서 간의 의존관계를 정리해서
...
GraphRAG의 quickstart 이미지는 다음과 같습니다.
-m pip install graphrag
mkdir graphrag_quickstart
cd graphrag_quickstart
...
GraphRAG는 강력하지만, indexing cost가 무거워지기 쉽습니다.
LightRAG는 지식 그래프 (knowledge graph)와 벡터 임베딩 (vector embeddings)을 결합한 경량화된 방향성을 지향합니다. README에서는 local / global / hybrid / naive / mix 등의 쿼리 모드 (query modes), 리랭커 (reranker), 인용 (citation), 멀티모달 통합 (multimodal integration) 등에 대해 설명하고 있습니다. (GitHub)
git clone https://github.com/HKUDS/LightRAG.git
cd LightRAG
cp env.example .env
...
사용 구분은 다음과 같습니다.
| mode | 사용 상황 |
|---|---|
| naive | 일반적인 청크 검색 (chunk retrieval) |
| ... |
그래프 (Graph) 계열은 모든 쿼리에 사용하는 것이 아니라, 관계성 쿼리 (relationship query)로 라우팅 (route)하는 것이 현실적입니다.
ColPali는 VLM을 사용한 시각적 문서 검색 (visual document retrieval) 방향입니다. README에서는 페이지 이미지를 멀티 벡터 임베딩 (multi-vector embedding)으로 취급하여, OCR 파이프라인 (OCR pipeline)에 의존하지 않고 시각적 공간 (visual space)에서 검색하는 개념을 설명합니다. (GitHub)
효과적인 상황입니다.
- 스캔된 PDF
- 도표 중심의 자료
- 다단 레이아웃
...
구현 이미지입니다.
from PIL import Image
import torch
# 실제 import는 colpali-engine 버전에 맞춰 조정하십시오
...
주의할 점으로, ColPali는 검색 (retrieval)에는 강하지만, 최종 답변에는 텍스트 근거 (text evidence)가 필요합니다.
따라서 실무에서는 다음과 같이 조합합니다.
1. ColPali로 페이지 히트 (page hit)를 확보한다
2. 히트된 페이지를 OCR / VLM 캡션 (caption) / 파서 (parser) 출력으로 변환한다
3. 텍스트 청크 (text chunk)와 퓨전 (fusion)한다
...
PageIndex는 벡터리스 / 추론 기반 RAG (vectorless / reasoning-based RAG)를 표방하며, No Vector DB, No Chunking, 트리 검색 (tree search), 추적 가능한 검색 (traceable retrieval)을 특징으로 합니다. README에서는 유사도 (similarity)가 아닌 관련성 (relevance)을 확보하기 위해 계층적 트리 (hierarchical tree)를 사용하는 방향성을 설명합니다. (GitHub)
이는 긴 전문 문서에 효과적입니다.
- 규정
- 계약서
- 재무 보고서
...
PageIndex 방식의 트리는 다음과 같은 형태입니다.
{
"title": "Financial Stability",
"node_id": "0006",
...
트리 검색 (tree search)의 구현 이미지입니다.
def tree_search(node: dict, query: str, llm) -> list[dict]:
decision = llm.json(f"""
Query:
...
PageIndex 타입의 장점은 검색 경로 (retrieval path)를 설명할 수 있다는 점입니다.
Root
-> Chapter 2
-> Section 2.3
...
단, LLM 호출 (LLM call)이 증가하므로 모든 쿼리에 사용하는 것이 아니라, 긴 전문 문서에만 라우팅 (route)하는 것이 좋습니다.
RAG-Anything은 문서 파싱 (document parsing), 콘텐츠 분석 (content analysis), 지식 그래프 (knowledge graph), 지능형 검색 (intelligent retrieval)을 포함하는 멀티 스테이지 파이프라인 (multi-stage pipeline)으로, 텍스트 / 이미지 / 표 / 수식을 다루는 올인원 멀티모달 프레임워크 (all-in-one multimodal framework)로 설명됩니다. (GitHub)
중요한 것은 멀티모달 콘텐츠 (multimodal content)를 텍스트로 뭉개지 않는 것입니다.
content_list = [
{
"type": "text",
...
이를 자체 RAG에 도입하려면, content_kind를 퍼스트 클래스 (first-class)로 취급해야 합니다.
@dataclass
class MultimodalElement:
element_id: str
...
Agentic RAG에서 하고자 하는 것은 LLM에게 모든 것을 맡기는 것이 아닙니다.
해야 할 일은 query (질의)에 따라 다음을 판단하는 것입니다.
- 검색이 필요한가
- query rewrite (질의 재작성)가 필요한가
- sub-question decomposition (하위 질문 분해)가 필요한가
...
HyDE는 query로부터 hypothetical document (가상 문서)를 생성하고, 그 embedding (임베딩)으로 검색하는 기법입니다. 논문에서는 relevance labels (관련성 레이블) 없이 hypothetical document를 생성하여 dense retrieval (밀집 검색)에 사용하는 개념이 제시되어 있습니다. (Self-RAG)
짧고 모호한 query에 효과적입니다.
def hyde_retrieve(query: str, llm, embed_fn, vector_search):
hypothetical_doc = llm.generate(f"""
다음 질문에 답하는 문서가 존재한다고 가정하고,
...
주의사항입니다.
- LLM call (LLM 호출)이 증가함
- 가설 문서가 틀리면 검색도 어긋남
- 감사 용도로 hypothetical_doc을 trace (추적)할 것
Self-RAG는 retrieve (검색) / generate (생성) / critique (비판)를 self-reflection (자기 성찰)으로 제어하는 개념입니다. 공식 페이지에서는 필요에 따라 retrieval을 수행하고, 생성 결과를 critique 하는 framework (프레임워크)로 설명되어 있습니다. (Self-RAG)
실무에서는 전용 모델을 학습시키지 않더라도, decision layer (의사결정 계층)로서 사용할 수 있습니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Qiita AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기