로컬 RAG 에이전트를 구축하며 배운 것들
요약
마크다운 문서를 기반으로 동작하는 로컬 RAG 에이전트 구축 경험과 아키텍처를 공유합니다. 데이터 수집(Ingestion)부터 임베딩(Embedding)까지 이어지는 5단계 파이프라인의 핵심 개념을 다룹니다.
핵심 포인트
- Watcher, Parser, Chunker로 구성된 데이터 수집 파이프라인 구축
- 문맥 유지를 위해 청크 간 중첩(Overlap) 적용의 중요성
- 텍스트를 수학적 벡터로 변환하는 임베딩 프로세스 이해
짧은 소개
저는 최근 마크다운 (Markdown) 파일로 저장된 수많은 문서를 읽고, 이를 바탕으로 일상적인 영어로 질문할 수 있는 로컬 RAG 에이전트를 구축했습니다. 이 에이전트는 모든 문서를 훑어보고 문서에 실제로 포함된 내용을 바탕으로 답변을 찾아냅니다.
아키텍처 (Architecture)
이 에이전트가 사용하는 5단계 파이프라인의 상위 수준 개요는 다음과 같습니다:
사용자의 마크다운 파일들
↓
[1. INGESTION (수집)] — 파일을 읽고, 작은 조각("chunks")으로 분할
...
제가 습득한 핵심 개념들
1. Ingestion (수집)
이 단계는 디스크에서 파일을 읽어 들여 AI가 이해할 수 있도록 **청크 (chunks)**라고 불리는 작고 의미 있는 조각들로 나누는 과정입니다.
다음의 3가지 부분으로 구성됩니다:
파트 1: Watcher (감시자)
이것은 폴더를 감시하며 무언가 변경될 때마다 이벤트를 발생시킵니다. 라이브러리가 OS (운영체제)의 신호를 듣고 있다가, 지정된 경로에서 파일 변경이 감지되면 다음과 같은 이벤트를 실행합니다:
FileEvent(path="notes/docker.md", event_type="created")
FileEvent(path="notes/docker.md", event_type="modified")
FileEvent(path="notes/docker.md", event_type="deleted")
파트 2: Parser (파서)
이것은 읽기 도구입니다. 가공되지 않은 파일 내용을 가져와 깨끗하고 사용 가능한 텍스트로 변환합니다.
다음과 같은 유용한 작업들을 수행합니다:
- 모든 마크다운 기호를 제거하여 (
**bold**→ bold,# Heading→ Heading) AI가 일반 텍스트를 받을 수 있도록 합니다. - YAML 프론트 매터 (YAML front matter, 일부 파일 상단의
---메타데이터 블록)를 추출합니다. - 헤딩 (heading) 레벨에 따라 문서를 섹션으로 나눕니다.
파트 3: Chunker (청커)
각 섹션을 가져와 약 512단어 크기의 청크로 자르며, 연속된 청크 사이에 64단어의 중첩 (overlap)을 둡니다.
왜 중첩이 필요할까요? 두 청크의 경계선에 딱 걸쳐 있는 문장을 상상해 보세요. 중첩이 없다면 그 문맥을 잃어버리게 될 것입니다. 중첩을 사용하면 두 청크 모두 인접한 청크의 내용을 조금씩 포함하게 되므로, 중요한 내용이 잘려 나가는 일이 없습니다.
한 줄 요약: Ingestion
폴더 감시자(Folder watcher)가 파일 변경을 감지하면 → 파서(Parser)가 텍스트를 읽고 정제하며 → 청커(Chunker)가 이를 변경 감지를 위한 태그와 함께 중첩되는 작은 조각(Bite-sized pieces)으로 나눕니다.
2. Embedding (임베딩)
목표: Ingestion (수집) 단계의 청크들을 가져와 각 청크에 벡터(숫자 리스트)를 부착하여, _의미(Meaning)_를 기반으로 저장하고 검색할 수 있도록 합니다.
마치 번역가와 같다고 생각하면 됩니다. 사람이 읽을 수 있는 텍스트를 가져와 데이터베이스가 실제로 비교할 수 있는 수학적 형태로 변환합니다.
임베딩은 두 부분으로 구성됩니다:
Part 1: The Embedder (임베더)
이것은 번역 데스크의 작업자입니다. 이들의 유일한 임무는 텍스트 문자열 리스트를 받아 로컬에서 실행되는 임베딩 모델(Embedding model)로 보내고, 벡터 리스트를 돌려받는 것입니다.
Part 2: The Smart Manager (스마트 매니저)
이것은 Embedder 상단에 위치하며, 임베딩은 속도가 느리고 연산 자원(Compute)이 소모되기 때문에 어떤 청크를 실제로 임베딩해야 할지 결정합니다.
이 기능 덕분에 반복 실행이 빨라집니다. 예를 들어, 이미 200개의 파일이 인덱싱된 폴더에 새로운 파일 하나를 추가하면, 새 파일의 청크만 처리됩니다.
Embedding flow (임베딩 흐름)
List[Chunk] (Phase 1로부터 전달됨)
↓
ChunkEmbedder.embed_chunks()
...
Embedding in one line (한 줄 요약)
스마트 매니저가 이미 확인된 청크는 건너뛰고 새로운 청크만 임베더로 보내면, 임베더가 로컬 모델을 호출하여 부동 소수점 벡터(Float vectors)를 생성하고, 저장이 가능한 EmbeddedChunk 객체를 출력합니다.
3. Storage (저장)
목표: 세션 사이에도 유지되고 나중에 검색할 수 있도록 EmbeddedChunk 객체(텍스트 + 벡터)를 데이터베이스에 저장합니다.
이를 위해 **벡터 데이터베이스 (Vector database)**라고 불리는 특수한 종류의 데이터베이스가 사용됩니다.
저장된 레코드는 다음과 같은 모습입니다:
| Field | Example |
|---------------|----------------------------------|
| id | "docker-guide:3" |
...
What's in the metadata? (메타데이터에는 무엇이 들어있나요?)
저장된 모든 청크는 벡터와 함께 다음과 같은 정보를 보유합니다:
{
"content": "Docker is a platform for running containers...",
"content_hash": "a3f9c2...",
...
스토리지(Storage) 요약
벡터 데이터베이스(Vector database)는 각 청크(chunk)를 디스크에 (벡터 + 메타데이터) 레코드로 저장하며, 빠른 유사도 검색(similarity lookups)을 지원합니다. 또한 시작 시 이미 인덱싱된 해시(hash) 목록을 반환하여 불필요한 재임베딩(re-embedding)이 발생하지 않도록 합니다.
4. 검색 (Retrieval)
목표: 질문을 던졌을 때, AI에게 무언가를 전달하기 전에 데이터베이스에서 가장 관련성이 높은 청크들을 찾아냅니다.
시맨틱 검색(Semantic search)의 작동 방식
이는 간단한 2단계 프로세스로 이루어집니다:
1단계: 질문 임베딩 (Embed the question)
사용자의 질문은 단순한 텍스트입니다. 저장된 벡터들과 비교하기 위해서는 질문 또한 벡터로 변환되어야 합니다:
"What is Docker?" → embed(["What is Docker?"]) → [0.18, -0.71, ...]
2단계: 가장 유사한 항목 찾기 (Find the closest matches)
해당 쿼리 벡터(query vector)를 코사인 유사도(cosine similarity)를 사용하여 저장된 모든 벡터와 비교함으로써, 가장 유사한 상위 K개(top-K)의 항목을 찾아냅니다:
"이 5개의 청크가 질문과 가장 유사합니다:"
→ docker-guide.md의 청크 (점수: 0.91)
→ docker-guide.md의 청크 (점수: 0.87)
...
각 결과에는 관련성 점수(relevance score)가 함께 반환됩니다:
RetrievedChunk:
chunk: Chunk # 전체 텍스트, 소스 경로, 메타데이터
score: float # 0.0 → 1.0 — 높을수록 관련성이 높음
검색(Retrieval) 요약
리트리버(Retriever)는 사용자의 질문을 벡터로 임베딩하고, 데이터베이스에 코사인 유사도 기반의 상위 K개(top-K) 가장 유사한 저장된 청크를 요청하며, 이를 점수별로 정렬하여 AI가 사용할 수 있도록 반환합니다.
5. 오케스트레이션 (Orchestration)
목표: 검색(Retrieval) 단계에서 가져온 관련 청크들을 사용하여, LLM(대규모 언어 모델)이 실제 사람이 읽을 수 있는 답변을 작성하도록 합니다.
이 단계는 전체 시스템의 두뇌 역할을 하며, 검색된 가공되지 않은 텍스트가 지능적인 응답으로 변환되는 곳입니다.
검색 단계에서 추출된 상위 K개(top-K)의 청크는 언어 모델(language model)의 컨텍스트(context)로 전달되며, 모델은 이를 사용하여 답변을 구성합니다.
여기서 얻을 수 있는 큰 이점은 환각 (hallucination)을 줄여준다는 것입니다. AI에게 집중된 청크 (chunks) 세트를 컨텍스트 (context)로 제공함으로써, 모델이 일반적인 학습 데이터로부터 내용을 지어내는 대신 사용자의 문서에 기반하여 답변하도록 유도합니다. 다만, top_k 값을 튜닝하는 것도 중요합니다. 청크가 너무 적으면 답변이 빈약해지고, 너무 많으면 모델이 혼란을 겪을 수 있습니다. 적절한 지점은 그 중간 어디쯤에 있습니다.
한 줄로 요약하는 오케스트레이션 (Orchestration)
쿼리 엔진 (query engine)은 검색 (retrieval)과 LLM을 하나로 묶어, 사용자의 질문을 근거 있고 컨텍스트를 인식하는 (context-aware) 답변으로 처리합니다.
마치며
이 에이전트를 구축하면서 RAG뿐만 아니라, 이러한 시스템들이 실제로 어떻게 '생각'하는지에 대해 기대 이상으로 많은 것을 배웠습니다.
5가지 단계 각각은 매우 구체적인 역할을 수행하며, 개별적으로는 모두 상당히 단순합니다. 마법은 이 단계들을 하나로 연결할 때 일어납니다. 파일 변경 → 청킹 (chunking) → 임베딩 (embedding) → 저장 (stored) → 검색 (retrieved) → 답변 (answered).
비슷한 것을 구축하려는 분들을 위해 몇 가지 강조하고 싶은 점이 있습니다:
- 청킹 (chunking) 시 오버랩 (overlap)은 생각보다 중요합니다. 오버랩이 없으면 경계 부분에서 컨텍스트를 잃게 되어 답변의 질이 떨어집니다.
- 임베딩 (embedding) 단계에서의 중복 제거 (deduplication)는 필수입니다. 매 실행마다 모든 것을 다시 임베딩하는 것은 느리고 낭비적입니다. 해시 (hash) 값을 추적하세요.
top_k를 튜닝하세요. 너무 작으면 AI가 작업할 충분한 정보가 없고, 너무 크면 과하게 생각하게 됩니다. 실제 질문으로 테스트해 보세요.- 로컬 모델 (local models)은 놀라울 정도로 유능합니다. 자신의 문서로부터 유용한 답변을 얻기 위해 항상 클라우드 API가 필요한 것은 아닙니다.
RAG나 로컬 AI에 관심이 있다면, 이러한 종류의 프로젝트는 아주 좋은 시작점입니다. 완전히 이해할 수 있을 만큼 규모가 작으면서도, 실제 핵심 원리들을 가르쳐줄 만큼 충분히 복잡하기 때문입니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기