이 글은 “왜 Python인가 — 2026년 데이터로 보는 AI 시대의 필수 언어” 편의 후속입니다. 전편에서 Python의 위상과 이론을 다뤘다면, 이번엔 손으로 직접 만드는 시간입니다.
📌 난이도: 중급 (Python 기초 문법은 알고 있다고 가정) ⏱️ 읽는 시간: 약 12분 / 실습 시간: 약 2~3시간 🛠️ 완성물: PDF를 업로드하면 그 내용으로 대답하는 AI 챗봇 API
“ChatGPT 같은 거 만들 수 있어요?”
Python을 배운 사람들이 가장 먼저 묻는 질문입니다. 그리고 2026년 현재, 대답은 “네, 오늘 만들 수 있습니다” 입니다.
오늘은 RAG(Retrieval-Augmented Generation) 라는 기술을 써서 나만의 AI 챗봇을 처음부터 배포까지 만들어보겠습니다. 회사 내부 문서에 대해 답변하는 챗봇, PDF 기반 Q&A 시스템, 고객지원 봇 — 이 모든 것의 핵심이 바로 RAG입니다.
📊 목차
- RAG가 뭔지 3분 만에 이해하기
- 왜 일반 ChatGPT API로는 안 되는가
- 오늘 만들 것의 전체 구조
- 환경 세팅 — 5분이면 끝
- 단계별 코드 구현 (핵심!)
- FastAPI로 API 서버 만들기
- 로컬에서 테스트하기
- 클라우드 배포 — Railway로 5분 만에 올리기
- 다음 단계: 더 나아가고 싶다면
1. RAG가 뭔지 3분 만에 이해하기
RAG는 Retrieval-Augmented Generation의 약자입니다. 직역하면 “검색으로 보강된 생성”인데, 쉽게 말하면 이렇습니다.
AI가 답변하기 전에, 먼저 관련 문서를 검색해서 읽고 나서 대답하게 만드는 기술
일반적인 LLM(ChatGPT, Claude 등)은 학습된 지식만으로 대답합니다. 그래서 회사 내부 자료나 최신 정보, 개인적인 문서에 대해서는 엉터리로 지어냅니다(이걸 환각, Hallucination이라고 합니다).
RAG는 이 문제를 해결합니다.
[사용자 질문] ↓[관련 문서 검색] ← 내 문서(PDF, 텍스트 등)에서 의미적으로 유사한 부분 찾기 ↓[검색 결과 + 질문을 함께 LLM에 전달] ↓[LLM이 제공된 문서를 바탕으로 정확하게 대답]
이게 전부입니다. 구현하면 마법 같지만, 원리는 단순합니다.
2. 왜 일반 ChatGPT API로는 안 되는가
솔직하게 비교해봅시다.
| 방식 | 장점 | 단점 |
|---|---|---|
| 일반 LLM API | 빠르고 간단 | 내 문서 모름, 환각 발생, 비용 폭탄 가능 |
| 파인튜닝 | 모델 자체를 학습 | 비용 수백만원+, 데이터 업데이트할 때마다 재학습 |
| RAG | 정확하고 비용 효율적 | 초기 구현이 필요 |
RAG는 모델을 재학습하지 않아도 됩니다. 문서가 바뀌면 벡터 DB만 업데이트하면 됩니다. 그래서 2026년 현재 기업 AI 프로젝트의 표준 접근법이 됐습니다.
💡 RAG의 핵심 강점: 프라이빗 데이터(사내 문서, 개인 노트, 고객 데이터)를 LLM에 안전하게 연결할 수 있다는 점입니다. 문서가 외부 API로 전송되지 않고 로컬에서 검색해 필요한 부분만 컨텍스트로 전달합니다.
3. 오늘 만들 것의 전체 구조
오늘 완성할 시스템의 아키텍처입니다.
📄 PDF 업로드 ↓[문서 분할(Chunking)] → 긴 문서를 작은 조각으로 나눔 ↓[임베딩(Embedding)] → 각 조각을 숫자 벡터로 변환 ↓[벡터 DB 저장] → ChromaDB (로컬, 무료) ↓━━━━━━━━━━━━━━━━━━━━━━━━━━📨 사용자 질문 입력 ↓[질문 임베딩] → 질문도 벡터로 변환 ↓[유사도 검색] → 벡터 DB에서 가장 관련 있는 조각 3~5개 검색 ↓[LLM 호출] → "이 문서들을 참고해서 질문에 답해줘" ↓✅ 정확한 답변 반환
사용할 기술 스택:
- LangChain — 파이프라인 조립
- ChromaDB — 벡터 데이터베이스 (무료, 로컬)
- Anthropic Claude — LLM
- FastAPI — API 서버
- PyMuPDF — PDF 파싱
4. 환경 세팅 — 5분이면 끝
패키지 설치
bash
pip install langchain langchain-anthropic langchain-community \ chromadb fastapi uvicorn python-dotenv \ pymupdf sentence-transformers
폴더 구조 만들기
my-rag-chatbot/├── .env # API 키 (절대 깃헙에 올리지 마세요!)├── main.py # FastAPI 서버├── rag_pipeline.py # RAG 핵심 로직├── uploads/ # PDF 업로드 폴더 (자동 생성)└── vector_store/ # ChromaDB 저장 폴더 (자동 생성)
.env 파일 작성
bash
ANTHROPIC_API_KEY=your-api-key-here
5. 단계별 코드 구현 (핵심!)
Step 1 — PDF 로드 & 문서 분할
python
# rag_pipeline.pyfrom langchain_community.document_loaders import PyMuPDFLoaderfrom langchain.text_splitter import RecursiveCharacterTextSplitterdef load_and_split_pdf(file_path: str): """ PDF를 로드하고 작은 청크(chunk)로 나눕니다. 청크가 너무 크면 LLM 컨텍스트 한도를 초과하고, 너무 작으면 문맥이 끊겨버립니다. 500자가 적당한 시작점입니다. """ # 1. PDF 로드 loader = PyMuPDFLoader(file_path) documents = loader.load() # 2. 텍스트 분할 splitter = RecursiveCharacterTextSplitter( chunk_size=500, # 한 청크당 최대 500자 chunk_overlap=50, # 청크 간 50자 겹침 (문맥 유지) length_function=len, ) chunks = splitter.split_documents(documents) print(f"✅ 총 {len(documents)}페이지 → {len(chunks)}개 청크로 분할 완료") return chunks
Step 2 — 임베딩 & 벡터 DB 저장
python
# rag_pipeline.py (이어서)from langchain_community.vectorstores import Chromafrom langchain_community.embeddings import HuggingFaceEmbeddings# 임베딩 모델 초기화 (무료, 로컬 실행)# 처음 실행 시 모델 다운로드 (~90MB)embedding_model = HuggingFaceEmbeddings( model_name="sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2" # 한국어 포함 다국어 지원 모델)VECTOR_STORE_PATH = "./vector_store"def save_to_vector_db(chunks, collection_name: str = "my_docs"): """ 청크를 벡터로 변환해서 ChromaDB에 저장합니다. 같은 collection_name이면 기존 데이터에 추가됩니다. """ vector_db = Chroma.from_documents( documents=chunks, embedding=embedding_model, persist_directory=VECTOR_STORE_PATH, collection_name=collection_name, ) vector_db.persist() print(f"✅ {len(chunks)}개 청크 → 벡터 DB 저장 완료") return vector_dbdef load_vector_db(collection_name: str = "my_docs"): """기존 벡터 DB를 불러옵니다.""" return Chroma( persist_directory=VECTOR_STORE_PATH, embedding_function=embedding_model, collection_name=collection_name, )
💡 임베딩 모델 선택 팁 위에서 사용한
paraphrase-multilingual-MiniLM-L12-v2는 무료이고 한국어를 잘 지원합니다. 더 정확한 결과가 필요하다면 Anthropic의 임베딩 API를 쓰거나, 영어 전용이라면all-MiniLM-L6-v2로 교체해도 됩니다.
Step 3 — RAG 체인 구성 (핵심 로직)
python
# rag_pipeline.py (이어서)from langchain_anthropic import ChatAnthropicfrom langchain.prompts import ChatPromptTemplatefrom langchain.schema.runnable import RunnablePassthroughfrom langchain.schema.output_parser import StrOutputParserimport osdef create_rag_chain(vector_db): """ RAG 파이프라인을 조립합니다. 질문 → 문서 검색 → LLM 답변의 흐름을 하나의 체인으로 연결합니다. """ # 1. 리트리버: 유사한 청크 상위 4개 검색 retriever = vector_db.as_retriever( search_type="similarity", search_kwargs={"k": 4} ) # 2. 프롬프트 템플릿 prompt = ChatPromptTemplate.from_template("""당신은 주어진 문서를 기반으로 질문에 정확하게 답변하는 AI 어시스턴트입니다.[참고 문서]{context}[질문]{question}위 문서를 바탕으로 질문에 답변해주세요. 문서에 없는 내용은 "제공된 문서에서 해당 정보를 찾을 수 없습니다"라고 답하세요.절대 지어내지 마세요.""") # 3. LLM 설정 llm = ChatAnthropic( model="claude-sonnet-4-20250514", api_key=os.getenv("ANTHROPIC_API_KEY"), temperature=0, # 0 = 가장 일관된 답변, 환각 최소화 max_tokens=1024 ) # 4. 체인 조립 (LangChain Expression Language) def format_docs(docs): return "\n\n---\n\n".join( f"[출처: {doc.metadata.get('source', '알 수 없음')}, " f"페이지: {doc.metadata.get('page', '?')}]\n{doc.page_content}" for doc in docs ) rag_chain = ( {"context": retriever | format_docs, "question": RunnablePassthrough()} | prompt | llm | StrOutputParser() ) return rag_chaindef ask_question(question: str, collection_name: str = "my_docs") -> dict: """ 질문을 받아 RAG 파이프라인으로 답변을 생성합니다. """ vector_db = load_vector_db(collection_name) chain = create_rag_chain(vector_db) # 검색된 문서도 함께 반환 (출처 확인용) retriever = vector_db.as_retriever(search_kwargs={"k": 4}) source_docs = retriever.invoke(question) answer = chain.invoke(question) return { "answer": answer, "sources": [ { "page": doc.metadata.get("page", "?"), "source": doc.metadata.get("source", "알 수 없음"), "snippet": doc.page_content[:200] + "..." } for doc in source_docs ] }
6. FastAPI로 API 서버 만들기
python
# main.pyfrom fastapi import FastAPI, UploadFile, File, HTTPExceptionfrom fastapi.middleware.cors import CORSMiddlewarefrom pydantic import BaseModelfrom pathlib import Pathimport shutilimport osfrom dotenv import load_dotenvfrom rag_pipeline import load_and_split_pdf, save_to_vector_db, ask_questionload_dotenv()app = FastAPI( title="나만의 RAG 챗봇 API", description="PDF를 업로드하고 내용에 대해 질문하세요", version="1.0.0")# CORS 설정 (프론트엔드와 연결할 경우 필요)app.add_middleware( CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"],)UPLOAD_DIR = Path("./uploads")UPLOAD_DIR.mkdir(exist_ok=True)# ── 요청/응답 모델 ──────────────────────────────class QuestionRequest(BaseModel): question: str collection_name: str = "my_docs"class QuestionResponse(BaseModel): answer: str sources: list# ── 엔드포인트 ──────────────────────────────────@app.get("/")async def root(): return {"message": "RAG 챗봇 API가 실행 중입니다 🚀"}@app.post("/upload")async def upload_pdf( file: UploadFile = File(...), collection_name: str = "my_docs"): """ PDF 파일을 업로드하고 벡터 DB에 저장합니다. """ # 1. PDF만 허용 if not file.filename.endswith(".pdf"): raise HTTPException(status_code=400, detail="PDF 파일만 업로드 가능합니다.") # 2. 파일 저장 file_path = UPLOAD_DIR / file.filename with open(file_path, "wb") as f: shutil.copyfileobj(file.file, f) # 3. RAG 파이프라인 실행 try: chunks = load_and_split_pdf(str(file_path)) save_to_vector_db(chunks, collection_name) except Exception as e: raise HTTPException(status_code=500, detail=f"처리 중 오류 발생: {str(e)}") return { "message": f"✅ '{file.filename}' 업로드 완료", "chunks_created": len(chunks), "collection": collection_name }@app.post("/ask", response_model=QuestionResponse)async def ask(request: QuestionRequest): """ 질문을 받아 업로드된 문서를 기반으로 답변합니다. """ if not request.question.strip(): raise HTTPException(status_code=400, detail="질문을 입력해주세요.") try: result = ask_question(request.question, request.collection_name) return QuestionResponse( answer=result["answer"], sources=result["sources"] ) except Exception as e: raise HTTPException(status_code=500, detail=f"답변 생성 중 오류: {str(e)}")@app.delete("/collection/{collection_name}")async def delete_collection(collection_name: str): """컬렉션(문서 그룹)을 삭제합니다.""" import shutil store_path = f"./vector_store/{collection_name}" if os.path.exists(store_path): shutil.rmtree(store_path) return {"message": f"'{collection_name}' 컬렉션 삭제 완료"} raise HTTPException(status_code=404, detail="해당 컬렉션이 없습니다.")
7. 로컬에서 테스트하기
서버 실행
bash
uvicorn main:app --reload --port 8000
서버가 뜨면 브라우저에서 http://localhost:8000/docs를 열면 자동 생성된 API 문서(Swagger UI) 가 보입니다. 여기서 바로 테스트할 수 있습니다.
터미널에서 테스트 (curl)
bash
# 1. PDF 업로드curl -X POST "http://localhost:8000/upload" \ -F "file=@./my_document.pdf" \ -F "collection_name=my_docs"# 응답 예시:# {"message": "✅ 'my_document.pdf' 업로드 완료", "chunks_created": 47, "collection": "my_docs"}# 2. 질문하기curl -X POST "http://localhost:8000/ask" \ -H "Content-Type: application/json" \ -d '{"question": "이 문서의 핵심 내용은 무엇인가요?", "collection_name": "my_docs"}'# 응답 예시:# {# "answer": "이 문서는 ...",# "sources": [# {"page": 3, "source": "my_document.pdf", "snippet": "..."}# ]# }
Python으로 테스트
python
import requestsBASE_URL = "http://localhost:8000"# PDF 업로드with open("my_document.pdf", "rb") as f: response = requests.post( f"{BASE_URL}/upload", files={"file": f}, data={"collection_name": "test"} )print(response.json())# 질문response = requests.post( f"{BASE_URL}/ask", json={"question": "주요 결론은?", "collection_name": "test"})result = response.json()print("답변:", result["answer"])print("\n출처:")for src in result["sources"]: print(f" - 페이지 {src['page']}: {src['snippet'][:80]}...")
8. 클라우드 배포 — Railway로 5분 만에 올리기
로컬에서 잘 돌아간다면 이제 외부에서 접근 가능하게 배포할 차례입니다. Railway는 무료 플랜이 있고 GitHub 연동 한 번으로 자동 배포가 됩니다.
준비 파일
requirements.txt
langchain==0.3.19langchain-anthropiclangchain-communitychromadbfastapiuvicornpython-dotenvpymupdfsentence-transformers
Procfile
web: uvicorn main:app --host 0.0.0.0 --port $PORT
.gitignore (반드시!)
.envuploads/vector_store/__pycache__/*.pyc
배포 순서
bash
# 1. Git 초기화 및 GitHub 푸시git initgit add .git commit -m "RAG 챗봇 초기 배포"git remote add origin https://github.com/your-username/my-rag-chatbot.gitgit push -u origin main# 2. railway.app 접속 → "New Project" → "Deploy from GitHub"# 3. 저장소 선택# 4. Settings → Environment Variables에서 ANTHROPIC_API_KEY 추가# 5. 자동 배포 완료! 제공된 URL로 접근 가능
⚠️ 배포 시 주의사항
.env파일은 절대 GitHub에 올리지 마세요. Railway 환경변수로 따로 설정합니다.vector_store/폴더는 서버 재시작 시 초기화됩니다. 프로덕션에서는 Pinecone, Supabase pgvector 등 영구 벡터 DB를 사용하세요.- 무료 플랜은 월 $5 크레딧이 포함됩니다 (개인 프로젝트 충분).
9. 다음 단계: 더 나아가고 싶다면
오늘 만든 것은 RAG의 가장 기본적인 형태입니다. 실제 프로덕션 수준으로 끌어올리려면 아래를 고려해보세요.
성능 향상
| 방법 | 효과 | 난이도 |
|---|---|---|
| Hybrid Search (키워드 + 벡터) | 검색 정확도 30~50% 향상 | ★★★ |
| Re-ranking (Cohere Rerank 등) | 최종 답변 품질 향상 | ★★★ |
| 청크 사이즈 튜닝 | 도메인에 따라 최적값 다름 | ★★ |
| Multi-query 검색 | 다양한 표현으로 검색해 재현율 향상 | ★★ |
기능 확장
python
# 대화 히스토리 유지 예시 (멀티턴 대화)from langchain.memory import ConversationBufferWindowMemorymemory = ConversationBufferWindowMemory( k=5, # 최근 5턴만 기억 return_messages=True, memory_key="chat_history")
영구 벡터 DB로 마이그레이션
python
# ChromaDB → Pinecone (프로덕션 권장)import pineconefrom langchain_community.vectorstores import Pineconepinecone.init(api_key="your-pinecone-key", environment="us-east-1")vector_db = Pinecone.from_documents( documents=chunks, embedding=embedding_model, index_name="my-rag-index")
마치며 — 만들어봐야 보이는 것들
RAG를 직접 구현해보면 처음엔 당연해 보이는 것들이 사실 얼마나 정교하게 설계됐는지 알게 됩니다.
- 청크를 어떻게 나눠야 할까?
- 임베딩 모델은 어떤 걸 써야 할까?
- 검색된 문서가 너무 많으면? 너무 적으면?
- 프롬프트는 어떻게 써야 환각이 줄어들까?
이 질문들에 답하는 과정이 바로 AI 엔지니어가 되는 과정입니다.
오늘 만든 챗봇을 토대로, 다음 편에서는 LangGraph로 멀티 에이전트 시스템 구축하기 — 검색 에이전트, 요약 에이전트, 검증 에이전트가 서로 협력해 더 정확한 답변을 만드는 방법을 다뤄볼 예정입니다.
코드를 실행하다 막히는 부분이 있으면 댓글로 남겨주세요. 최대한 빠르게 답변드리겠습니다 🙌
🔖 이 시리즈의 다른 글
- 1편: 왜 Python이 2026년에도 압도적 1위인가
- 2편: 나만의 AI 챗봇 만들기 — RAG 처음부터 배포까지 ← 지금 여기
- 3편: LangGraph로 멀티 에이전트 시스템 구축하기 (예정)
태그: #Python #RAG #LangChain #FastAPI #AI챗봇 #ChromaDB #딥러닝 #개발튜토리얼 #AI개발 #2026
데이터 출처: LangChain Docs · FastAPI Docs · ChromaDB Docs · DataCamp RAG Tutorial · Dev.to LangGraph Guide 2026