
문서와 대화하기: LangChain, RAG, MCP, ChromaDB를 활용한 간편한 구현
요약
LangChain, RAG, MCP, ChromaDB를 활용하여 로컬 문서와 대화할 수 있는 Q&A 시스템 구축 방법을 안내합니다. 문서 인제스션 서버, MCP 도구 서버, LangChain 기반 에이전트로 구성된 모듈식 파이프라인 구현 과정을 다룹니다.
핵심 포인트
- RAG 아키텍처를 통한 LLM의 지식 한계 극복 방법 제시
- ChromaDB를 활용한 벡터 스토어 설정 및 데이터 인덱싱
- Model Context Protocol(MCP)을 통한 검색 레이어의 도구화
- LangChain 기반의 대화형 AI 에이전트 및 스트리밍 구현
내 컴퓨터에 쌓여 있는 문서, 스프레드시트, 또는 PDF 더미에 질문을 던지고 마법처럼 답변을 얻을 수 있기를 바란 적이 있나요? 이것은 마법이 아니며, 이 가이드에서 우리는 바로 그것을 구축할 것입니다: _AI 기술과 도구를 사용하여 여러분의 문서와 대화할 수 있는 로컬 Q&A 시스템_입니다.
우리는 **검색 증강 생성 (Retrieval Augmented Generation, RAG)**과 **모델 컨텍스트 프로토콜 (Model Context Protocol, MCP)**을 결합하여 모듈식의 프로덕션 준비가 된 파이프라인을 만들 것입니다. 이 글을 마칠 때쯤이면 문서 인제스션 (ingestion) 서버, MCP 도구 서버, 그리고 LangChain 기반의 대화형 AI 에이전트라는 세 가지 서비스로 구성된 작동 가능한 시스템을 갖게 될 것입니다.
우리가 구축할 것
- 문서로부터 강력한 Q&A 시스템 구축.
- RAG 아키텍처의 핵심 개념 이해.
- ChromaDB 벡터 스토어 (vector store) 설정 및 상호작용.
- LangChain 문서 로더 (document loaders)를 사용한 데이터 로딩 구현.
- 훌륭한 사용자 경험을 위한 응답 스트리밍 (streaming).
- 검색 증강 생성 (RAG) 통합.
자기소개
저는 기술 산업에서 10년 이상의 경력을 가진 풀스택 엔지니어 David Archanjo입니다. 저는 소프트웨어 개발과 AI 엔지니어링에 열정이 있으며, 개발자들이 실제 애플리케이션을 구축하는 데 도움이 되는 기술 아티클을 통해 실질적인 지식을 공유하는 것을 즐깁니다.
목차
- 배경 (Background)
- 왜 RAG와 MCP인가?
- 1단계: 환경 설정
- 2단계: ChromaDB에 문서 로드 및 인덱싱
- 3단계: MCP 도구 노출
- 4단계: RAG 체인 구축
- 5단계: Q&A 에이전트 실행 및 테스트
- 마무리
배경
대규모 언어 모델 (Large Language Models, LLMs)은 추론과 텍스트 생성에 뛰어나지만, 그 지식은 학습된 내용으로 제한됩니다. 이들은 우리의 개인 문서, 사내 데이터, 또는 학습에 포함되지 않은 정보에 접근할 수 없으며, 이로 인해 추가적인 컨텍스트(context) 없이는 우리의 지식 베이스에 관한 질문에 답할 수 없습니다.
검색 증강 생성 (Retrieval-Augmented Generation (RAG))은 쿼리 시점에 예를 들어 우리의 문서로부터 더 관련성 높은 정보를 검색하여 이를 LLM에 컨텍스트 (context)로 제공함으로써, 근거 있는 답변을 가능하게 하여 이러한 한계를 해결합니다. 이 가이드에서는 해당 검색 레이어를 Model Context Protocol (MCP) 도구로 노출하여, MCP 호환 에이전트나 애플리케이션이 활용할 수 있는 재사용 가능한 서비스를 구축합니다.
왜 RAG와 MCP인가?
RAG를 사용하는 이유
RAG가 없다면, LLM은 학습 과정에서 배운 내용만을 바탕으로 질문에 답할 수 있습니다.
다음과 같은 작업이 불가능합니다:
- 우리의 비공개 또는 독점 문서에 관한 질문에 답변하기.
- 학습 중단 시점 (training cutoff) 이후에 발생한 업데이트를 반영하기.
- 답변을 뒷받침할 특정 출처나 구절을 인용하기.
RAG는 시맨틱 검색 (semantic search)과 LLM의 추론 (reasoning)을 결합하여 이러한 문제들을 해결합니다. 우리의 문서는 먼저 작고 의미 있는 단위인 청크 (chunks)로 분할된 후, 임베딩 (embeddings) (의미를 벡터로 표현한 것)으로 변환되어 **벡터 데이터베이스 (vector database)**에 저장됩니다. 사용자가 질문을 제출하면, 쿼리는 동일한 벡터 공간으로 임베딩되며, 이를 통해 시스템은 키워드 매칭이 아닌 시맨틱 유사성 (semantic similarity)을 기반으로 가장 관련성 높은 청크를 검색할 수 있습니다. 검색된 구절들은 LLM의 프롬프트 (prompt)에 컨텍스트로 주입되어, 모델이 우리의 데이터에 근거하고 더 정확하며 환각 (hallucinations) 현상이 적은 답변을 생성할 수 있도록 합니다.
RAG의 주요 장점:
- 정확성 (Accuracy): 실제 문서를 바탕으로 답변을 제공합니다.
- 투명성 (Transparency): 모든 답변을 특정 출처로 추적할 수 있습니다.
- 비용 효율성 (Cost efficiency): 질문과 관련 있는 정보만 처리합니다.
- 최신성 (Freshness): 모델을 재학습시키지 않고도 언제든 새로운 문서를 추가할 수 있습니다.
MCP를 사용하는 이유
**Model Context Protocol (MCP)**는 AI 에이전트가 외부 도구를 발견하고 호출하는 방식을 정의하는 개방형 표준입니다.
우리의 문서 검색 기능을 MCP 도구로 노출함으로써, 다음과 같은 몇 가지 아키텍처 측면의 이점을 얻을 수 있습니다:
- 모듈성 (Modularity): 검색 서비스가 에이전트(Agent)로부터 완전히 분리됩니다.
- 재사용성 (Reusability): 동일한 MCP 서버가 여러 에이전트나 애플리케이션에 동시에 서비스를 제공할 수 있습니다.
- 발견 가능성 (Discoverability): 에이전트가 런타임(Runtime) 시점에 사용 가능한 도구와 그 설명을 자동으로 발견할 수 있습니다.
- 표준화 (Standardization): 개방형 프로토콜을 통해 통합 과정을 특정 프레임워크에 종속되지 않게 함으로써, 벤더 종속성(Vendor lock-in)을 줄이고 장기적인 상호 운용성(Interoperability)을 보장합니다.
RAG와 MCP를 결합하면 두 방식의 장점을 모두 얻을 수 있습니다. 즉, 검색 증강 생성 (RAG)의 정확성과 문맥적 근거 제시 능력, 그리고 도구 및 데이터 소스 통합을 위한 표준화된 프로토콜의 유연성, 상호 운용성, 유지보수성을 동시에 확보할 수 있습니다.
1단계: 환경 설정
코드를 작성하기 전에, 프로젝트 구조를 잡고 필요한 의존성(Dependencies)을 설치하겠습니다.
프로젝트 구조
.
├── .env # 환경 변수
├── .db/ # ChromaDB 영구 저장소
...
참고: 이 샘플 문서들은 함께 제공되는 GitHub 저장소에서 찾을 수 있습니다.
우리 프로젝트는 명확한 책임을 가진 세 가지 별도의 서비스로 구성됩니다:
| 서비스 | 파일 | 책임 |
|---|---|---|
| Ingestion Server | doc_ingestion_server.py | 문서 로드, 청킹 (Chunking), 임베딩 (Embedding) 및 저장 |
| ... |
의존성 설치
- 가상 환경을 생성하고 활성화합니다:
python -m venv venv
source venv/bin/activate # Windows의 경우: venv\Scripts\activate
requirements.txt파일을 생성하고 다음 내용을 추가합니다:
fastapi>=0.139.2
fastmcp>=3.4.4
langchain>=1.3.14
...
- 의존성을 설치합니다:
pip install -r requirements.txt
환경 설정
프로젝트 루트에 다음 내용이 담긴 .env 파일을 생성합니다:
DOCS_DIR="./docs"
CHROMA_DIR="./.db"
CHROMA_COLLECTION_NAME="documents"
...
각 변수 상세 설명:
| 변수 | 용도 |
|---|---|
DOCS_DIR | 업로드된 문서가 저장되는 디렉토리 |
| ... |
로컬 OpenAI 호환 API 서비스
이 글에서는 OpenAI 호환 API를 통해 언어 모델 (Language Model) 및 임베딩 모델 (Embedding Model)과 상호작용할 것입니다. LangChain의 통합 기능 (Integrations)은 이 인터페이스를 구현하는 모든 서버와 통신할 수 있습니다. 다행히 이 API는 공식 OpenAI 서비스를 필요로 하지 않으므로, 동일한 HTTP 인터페이스를 구현하는 어떤 서버든 투명하게 사용할 수 있습니다.
우리는 채팅 모델 (Chat Model)과 임베딩 모델 (Embedding Model)을 모두 로컬에서 실행할 것입니다. 저는 llamafile 사용을 권장합니다. llamafile은 llama.cpp의 독립형 배포판으로, 이 방식을 사용하면 애플리케이션 코드를 공식 OpenAI API용으로 작성할 때와 동일하게 유지하면서 전체 스택을 우리 자신의 머신에서 실행할 수 있습니다.
다음의 llamafile 모델들을 추천합니다:
| 용도 | Llamafile 모델 |
|---|---|
| 채팅 모델 (Chat model) | Qwen3.5-2B-Q8_0.llamafile |
| 임베딩 모델 (Embedding model) | mxbai-embed-large-v1-f16.llamafile |
임베딩 모델을 시작합니다:
./mxbai-embed-large-v1-f16.llamafile \
--server \
--host 0.0.0.0 \
...
다른 터미널에서 채팅 모델을 시작합니다:
./Qwen3.5-2B-Q8_0.llamafile \
--server \
--host 0.0.0.0 \
...
--alias gpt-4o 옵션은 우리의 로컬 언어 모델을 gpt-4o라는 이름으로 노출합니다. LangChain의 관점에서는 마치 OpenAI GPT-4o 모델에 연결된 것과 정확히 동일하게 동작하므로, 동일한 LLM 초기화 설정을 사용하여 작업할 수 있습니다.
참고 (NOTE): 만약 공식 OpenAI 서비스를 사용하는 것을 선호한다면, 단순히
LLM_API_KEY를 본인의 API 키로 교체하고, 임베딩 (Embedding) 서비스가 다른 곳에서 실행 중이라면EMBEDDING_MODEL_BASE_URL을 업데이트하면 됩니다. 만약 OpenAI 임베딩을 사용한다면EMBEDDING_MODEL_BASE_URL을 제거하세요. 그 외의 다른 변경 사항은 필요하지 않습니다.왜 llamafile인가요? 저는 개인적으로 llamafile을 추천합니다. 왜냐하면 llamafile은 llama.cpp, 모델 가중치 (model weights), 그리고 필요한 모든 런타임 구성 요소를 단일 실행 파일로 패키징하기 때문입니다. 설치할 데몬 서비스, 패키지 매니저, 또는 유지 관리해야 할 추가적인 Python 환경이 전혀 없습니다. 우리는 단순히 파일을 다운로드하고, 실행 권한을 부여한 뒤 실행하기만 하면 됩니다. llama.cpp를 기반으로 구축되었기 때문에, 뛰어난 성능을 상속받으면서도 Linux, macOS, Windows 전반에 걸쳐 휴대성 (portability)을 유지합니다.
설정 클래스 (The Configuration Class)
# config.py
from pydantic_settings import BaseSettings
...
config.py 파일은 pydantic-settings의 BaseSettings 클래스를 확장하여 .env 파일로부터 모든 환경 변수를 자동으로 로드합니다. 이와 같이 설정을 중앙 집중화함으로써, 모든 서비스(즉, doc_ingestion_server.py, doc_mcp_server.py, agent.py)가 동일한 EnvironmentConfiguration 클래스를 임포트(import)하고 일관되며 검증된 설정을 받게 됩니다.
2단계: ChromaDB에 문서 로드 및 인덱싱하기
수집 (Ingestion) 파이프라인은 우리 RAG 시스템의 기반입니다. 이는 원본 문서를 읽고, 이를 청크 (chunks)로 분할하며, 해당 청크를 벡터 임베딩 (vector embeddings)으로 변환하고, 나중에 MCP 서버가 쿼리할 수 있도록 모든 것을 ChromaDB에 영구 저장하는 역할을 담당합니다.
전체 구현은 doc_ingestion_server.py에 작성될 예정입니다. 이 파일은 FastAPI 웹 서버로 구축되어, 서버를 재시작할 필요 없이 런타임 중에 API 요청을 통해 새로운 문서를 업로드할 수 있습니다.
"""
문서 수집 파이프라인 및 서버.
...
코드 분석
- 허용된 파일 유형
ALLOWED_FILE_EXTENSIONS = {".csv", ".txt", ".md", ".pdf"}
우리는 데이터 수집 (ingestion)을 네 가지 파일 형식으로 제한합니다. 각 형식은 해당 구조를 이해하는 전용 LangChain 커뮤니티 로더 (community loader)를 사용합니다.
CSVLoader: 행 (rows)과 열 (columns)을 파싱합니다.PyPDFLoader: 페이지별로 텍스트를 추출합니다.TextLoader: 일반 텍스트 및 Markdown 파일을 처리합니다.
- 임베딩 (Embeddings) 초기화
embeddings = OpenAIEmbeddings(
model=config.EMBEDDING_MODEL_NAME,
base_url=config.EMBEDDING_MODEL_BASE_URL,
...
우리는 로컬에 호스팅된 임베딩 모델(EMBEDDING_MODEL_BASE_URL을 통해 설정됨)을 가리키는 OpenAIEmbeddings 인스턴스를 초기화합니다. check_embedding_ctx_length=False 설정이 되어 있음에 주목하세요. 이 특정 설정은 토큰 길이 가드 (token-length guard)를 비활성화하는데, 이는 OpenAI API와 동일한 방식으로 컨텍스트 제한 (context limits)을 보고하지 않을 수 있는 로컬 서비스 모델을 사용할 때 필요합니다.
split_documents함수
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=config.CHUNK_SIZE,
chunk_overlap=config.CHUNK_OVERLAP,
...
이것이 청킹 (chunking) 전략의 핵심입니다. RecursiveCharacterTextSplitter는 자연스러운 경계에서 텍스트를 분할하려고 시도합니다. 즉, 먼저 이중 줄바꿈 (문단 구분)에서, 그다음 단일 줄바꿈에서, 그다음 문장 경계 순으로 구분자 목록을 따라 내려가며 결과 청크 (chunk)가 설정된 CHUNK_SIZE (기본값 500) 이내가 될 때까지 작업합니다.
CHUNK_OVERLAP (기본값 50자)은 청크 경계에서 문맥 (context)이 손실되지 않도록 보장합니다. 만약 문장이 두 개의 청크에 걸쳐 있다면, 첫 번째 청크의 뒷부분이 두 번째 청크의 앞부분에 나타나므로, 검색 (retrieval) 시 항상 완전한 생각을 반환할 수 있습니다.
embed_and_write함수
Chroma.from_documents(
documents=chunks,
embedding=embeddings,
...
Chroma.from_documents는 청크 (chunks) 리스트를 받아 임베딩 모델 (embedding model)을 호출하여 각 청크를 밀집 벡터 (dense vector)로 변환하고, 해당 벡터와 원본 텍스트를 모두 CHROMA_DIR 아래 디스크에 기록합니다. ChromaDB는 이 데이터를 자동으로 영속화 (persist)하므로, MCP 서버는 나중에 아무것도 다시 임베딩하지 않고도 이를 로드할 수 있습니다.
청크 리스트가 비어 있는 경우 (시작 시 문서를 찾지 못한 경우), 폴백 (fallback) 용도로 빈 ChromaDB 컬렉션 (collection)을 초기화합니다. 이는 문서가 업로드되기 전에 MCP 서버가 연결을 시도할 때 발생할 수 있는 다운스트림 (downstream) 오류를 방지합니다.
ingest함수
def ingest():
# Text and Markdown
loader = DirectoryLoader(config.DOCS_DIR, glob=["**/*.txt", "**/*.md"], ...)
...
ingest 함수는 세 번의 별도 DirectoryLoader 패스 (pass)를 실행합니다. 각 파일 유형마다 서로 다른 로더 클래스 (loader class)가 필요하기 때문에 문서 유형별로 한 번씩 실행됩니다. 생성된 모든 청크는 ChromaDB에 한 번에 배치 (batch)로 기록되기 전에 하나의 리스트로 수집됩니다. 이는 적어도 이 가이드의 목적상, 각 파일 유형을 별도로 기록하는 것보다 더 효율적입니다.
- FastAPI 라이프스팬 (Lifespan) 및 업로드 엔드포인트 (Endpoint)
@asynccontextmanager
async def lifespan(app: FastAPI):
Path(config.DOCS_DIR).mkdir(parents=True, exist_ok=True)
...
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기