DM Log

[AI 프로젝트 #1] PDF Q&A #2: LangChain + MCP / FastAPI 기반 백엔드 서버 구현 (OpenAPI) 본문

PJT/AI PJT

[AI 프로젝트 #1] PDF Q&A #2: LangChain + MCP / FastAPI 기반 백엔드 서버 구현 (OpenAPI)

Dev. Dong 2025. 10. 19. 20:45

서론

  • PDF Q&A 서비스의 백엔드 핵심 구조를 구현
  • LangChain을 이용해 RAG(Retrieval-Augmented Generation) 을 구성하고, IDE용 MCP 서버와 웹용 FastAPI 서버 두 가지 형태로 동시에 제공

즉, IDE(CURSOR, Claude 등)에서도, React 프론트엔드에서도 동일한 모델과 벡터스토어를 사용하는 통합 AI 백엔드 구조 설계


벡엔드 디렉토리 구조

backend/
└── pdf_server/
    ├── mcp_server.py           # MCP 프로토콜 서버 (IDE용)
    ├── api_server.py           # FastAPI REST 서버 (웹용)
    └── app.py                  # 두 서버 동시 실행 (thread 병렬 실행)
 

환경 설정

필수 패키지 설치

cd backend
python -m venv venv
venv\Scripts\activate     # Windows
# source venv/bin/activate  # macOS/Linux

pip install fastapi uvicorn langchain langchain-openai langchain-community chromadb pypdf mcp python-dotenv

OpenAPI API 키 설정

  • backend/.env 파일 생성 (환경변수 자동 로드)
OPENAI_API_KEY=sk-xxxxxxxxxxxxxxxxxxxx
 

MCP 서버 (mcp_server.py)

  • IDE(CURSOR, Claude 등) 내에서 직접 호출 가능한 MCP Protocol 서버
import os
import logging
from mcp.server.fastmcp import FastMCP
from langchain_community.vectorstores import Chroma
from langchain_community.document_loaders import PyPDFLoader
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain.chains import RetrievalQA
from langchain_openai import ChatOpenAI, OpenAIEmbeddings
from dotenv import load_dotenv

load_dotenv()
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")

logging.basicConfig(level=logging.INFO)
mcp = FastMCP("PDF-RAG")

PDF_PATH = "./data/sample.pdf"
qa_chain = None

if os.path.exists(PDF_PATH):
    loader = PyPDFLoader(PDF_PATH)
    pages = loader.load()
    splitter = RecursiveCharacterTextSplitter(chunk_size=500, chunk_overlap=50)
    docs = splitter.split_documents(pages)

    embeddings = OpenAIEmbeddings(model="text-embedding-3-small", api_key=OPENAI_API_KEY)
    llm = ChatOpenAI(model="gpt-3.5-turbo", api_key=OPENAI_API_KEY)
    vectorstore = Chroma.from_documents(docs, embeddings)
    qa_chain = RetrievalQA.from_chain_type(llm=llm, retriever=vectorstore.as_retriever())

@mcp.tool()
def ask_pdf(query: str) -> str:
    """PDF 내용을 기반으로 질문에 답변합니다."""
    if qa_chain is None:
        return "PDF가 로드되지 않았습니다."
    return qa_chain.run(query)

def run_mcp():
    mcp.run(transport="stdio")

Fast API 서버 (api_server.py)

  • React 프론트엔드가 axios로 호출하는 REST API 서
from fastapi import FastAPI, File, UploadFile, Form
from fastapi.middleware.cores import CORSMiddleware
from lanchain_community.vectorstores import Chroma
from lanchain_community.document_loaders import PyPDFLoader
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain.chains import RetrievalQA
from langchain_openai import ChatOpenAI, OpenAIEmbeddings
import os
from dotenv import load_dotenv

# .env 로드
load_dotenv()
os.environ["OPENAI_API_KEY"] = os.getenv("OPENAI_API_KEY")

app = FastAPI()
# CORS 허용 (React 서버와 통신)
app.add_middleware(
    CORSMiddleware,
    allow_origins=["http://localhost:5173"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"]
)

# 글로벌 변수 (PDF 내용 저장)
vectorstore = None

@app.post('/upload')
async def upload_pdf(file: UploadFile = File(...)):
    global vectorstore
    file_path = f"./data/{file.filename}"
    os.makedirs("./data", exist_ok=True)

    with open(file_path, "wb") as f:
        f.write(await file.read())

    loader = PyPDFLoader(file_path)
    pages = loader.load()
    splitter = RecursiveCharacterTextSplitter(chunk_size=500, chunk_overlap=50)
    docs = splitter.split_documents(pages)

    embeddings = OpenAIEmbeddings()
    vectorstore = Chroma.from_documents(docs, embeddings)
    return {"status": "ok", "filename": file.filename}

@app.post("/ask")
async def ask_question(question: str = Form(...)):
    if vectorstore in None:
        return {"error": "먼저 PDF를 업로드해주세요"}
    
    # OPENAI_API_KEY 명칭 사용시 명시 안해도 자동 적용 (api_key 인자 전달 필요)
    # 예시 api_key=os.getenv("OPENAI_API_KEY") or "fallback-key"
    llm = ChatOpenAI(model="gpt-4o")
    qa_chain = RetrievalQA.from_chain_type(
        llm=llm, retriever=vectorstore.as_retriever()
    )
    answer = qa_chain.run(question)
    return {"answer": answer}

def run_fastapi():
    import uvicorn
    uvicorn.run(app, host="0.0.0.0", port=5000)

두 서버 동시 실행 (app.py)

import threading
from mcp_server import run_mcp
from api_server import run_fastapi

if __name__ == "__main__":
    # FastAPI 서버를 별도 스레드에서 실행
    api_thread = threading.Thread(target=run_fastapi)
    api_thread.start()

    # MCP 서버 실행
    run_mcp()

 


실행 방법

cd backend/pdf_server
venv\Scripts\activate
python app.py

저비용 운영 전략

  • 아래의 조합을 통해 OpenAI RAG 구조의 최소 비용 실행 셋업으로, 월 수 달러 수준의 요금으로도 개발·운영이 가능
항목 설정
임베딩 모델 text-embedding-3-small
LLM 모델 gpt-3.5-turbo
PDF 분할 chunk_size = 500, overlap = 50
비용 절감 팁 임베딩 크기 축소 / 캐시된 Chroma Vectorstore 재사용 / 필요 시 로컬 오픈웨이트 모델(GPT-OSS 20B) 대체
  • 2025년 8월 기준 Open AI에서 오픈웨이트 모델을 제공, API 비용없이 자체 RAG 서버를 구축 가능 단, GPU 자원 필요