LangChain 없이 RAG를 만들면 무엇이 불편할까?

RAG를 직접 연결할 때 생기는 반복 코드와 LangChain의 Loader·Splitter·Retriever·Runnable 추상화가 줄여주는 일, 그 대가를 쉽게 설명합니다.

LangChain으로 사내 문서 RAG 챗봇 만들기 4/9

문서를 준비하는 단계부터 검색, 대화 화면, 배포와 평가까지 하나씩 연결하는 학습 기록

이전 글 — Chroma에 문서를 넣으면 AI가 바로 답할 수 있을까?

학교 축제 방송을 준비한다고 해봅시다. 안내문을 읽고, 질문과 가까운 문단을 찾고, 그 문단을 방송 원고에 붙인 뒤, AI에게 자연스러운 답을 부탁해야 합니다. 이 모든 작업을 직접 만들 수는 있습니다.

문제는 두 번째 문서와 두 번째 모델이 들어올 때 시작됩니다. LangChain 없이도 RAG는 만들 수 있지만, 서로 다른 입력과 출력을 맞추는 반복 코드가 빠르게 늘어납니다. LangChain은 그 연결부에 공통 모양을 제공해 부품을 바꾸고 관찰하기 쉽게 만듭니다.

그렇다고 LangChain이 검색 품질을 자동으로 높이는 것은 아닙니다. 추상화는 배관을 정리할 뿐, 어떤 문서를 나눌지와 어떤 결과가 정답인지는 개발자가 결정해야 합니다.

이 글은 시리즈 순서에 맞춰 2026년 5월 28일에 배치했습니다. API 설명은 2026년 8월 16일 LangChain 공식 문서를 기준으로 확인했습니다. 아래 코드는 이 Astro 저장소에서 실행한 결과가 아닌 학습 예제입니다.

직접 만들면 어떤 코드가 필요할까?

Framework 없이 RAG를 만든다고 해서 특별한 마법이 필요한 것은 아닙니다. HTTP 요청과 일반 Python 코드만으로도 다음 일을 연결할 수 있습니다.

  1. 파일을 읽습니다.
  2. 긴 글을 작은 조각으로 나눕니다.
  3. 각 조각을 Embedding API로 보냅니다.
  4. Vector Database에 벡터와 원문을 저장합니다.
  5. 질문을 Embedding하고 가까운 조각을 검색합니다.
  6. 검색 결과를 Prompt 문자열에 넣습니다.
  7. Chat Model API를 호출합니다.

처음 한 번은 어렵지 않아 보입니다. 하지만 제공자마다 인증 방식과 요청 JSON, 응답 JSON, 재시도 규칙이 다릅니다. 문서 조각을 dict로 쓸지 객체로 쓸지도 팀이 정해야 합니다.

모델을 교체하면 응답에서 본문을 꺼내는 위치가 달라질 수 있습니다. Vector Store를 바꾸면 검색 필터와 점수 해석도 달라질 수 있습니다. 결국 업무 코드보다 변환 코드가 더 눈에 띄게 됩니다.

불편함의 정체는 연결부다

휴대전화 충전기를 떠올려봅시다. 전기가 부족한 것이 아니라 기기마다 단자가 다르면 어댑터가 계속 필요합니다. RAG에서도 각 부품의 입력과 출력 모양이 다르면 중간 변환기가 늘어납니다.

연결 지점 직접 처리할 일 놓치기 쉬운 문제
파일 → 문서 인코딩·metadata 정리 출처와 버전 손실
문서 → 조각 분할 규칙·ID 생성 의미 경계 단절
조각 → Embedding API 요청·batch 처리 일부 실패와 중복 적재
질문 → 검색 필터·Top-K 구성 권한 없는 문서 검색
검색 → Prompt 길이 제한·출처 표시 Context 누락과 잡음
모델 → 화면 응답 파싱·Streaming 중간 취소와 오류 처리

LangChain은 이 연결 지점에 Document, Text Splitter, Embedding Model, Vector Store, Retriever와 Chat Model 같은 공통 역할을 둡니다. 구현체가 완전히 같아지는 것은 아니지만, 애플리케이션이 바라보는 모양이 비슷해집니다.

LangChain이 묶어주는 흐름

흩어진 부품이 하나의 RAG 흐름이 되기까지

각 부품은 맡은 일만 하고, LangChain의 공통 인터페이스가 다음 단계로 결과를 전달합니다.

1문서 읽기Loader가 원문과 metadata를 Document로 정리
2문서 나누기Splitter가 검색 가능한 Chunk 생성
3의미 표현Embedding Model이 숫자 벡터 생성
4저장과 검색Vector Store와 Retriever가 관련 조각 반환
5Prompt 구성질문과 검색 Context를 같은 입력에 배치
6답변 생성Chat Model이 전달받은 근거로 문장 생성

부품 교체 지점을 구분한 RAG연결 코드는 줄어들지만 문서 품질·권한·평가 책임은 애플리케이션에 남습니다.

현재 단계: 문서 읽기

학교 축제 안내문을 처리하는 가상 흐름입니다. 애니메이션은 역할의 연결을 설명하며 실제 모델 처리 속도를 측정한 결과가 아닙니다.

그림에서 중요한 점은 LangChain이 모든 일을 한 덩어리로 숨기지 않는다는 것입니다. Retriever만 직접 호출해 검색 결과를 확인할 수 있고, Chat Model만 별도로 호출할 수도 있습니다.

문제가 생겼을 때 단계를 분리해 보면 “모델이 이상하다”는 큰 추측 대신 “검색 결과에 정답 조각이 없다”는 작은 문제로 바꿀 수 있습니다.

Runnable은 무엇을 줄여줄까?

LangChain의 여러 구성 요소는 invoke, stream, batch처럼 비슷한 실행 방식을 따릅니다. 이 공통 실행 규칙을 Runnable 인터페이스라고 부릅니다.

Python의 | 연산자로 앞 단계의 출력을 다음 단계의 입력으로 연결하는 방식을 LangChain Expression Language, 줄여서 LCEL이라고 부릅니다. 예를 들어 Prompt와 Chat Model의 연결은 다음처럼 보입니다.

from langchain_core.prompts import ChatPromptTemplate
from langchain_openai import ChatOpenAI

prompt = ChatPromptTemplate.from_messages([
    (
        "system",
        "제공된 Context만 참고해 답하세요. "
        "근거가 없으면 확인할 수 없다고 말하세요.\n\n"
        "Context:\n{context}",
    ),
    ("human", "질문: {question}"),
])

model = ChatOpenAI(model="<사용 가능한 모델 이름>")
answer_chain = prompt | model

response = answer_chain.invoke({
    "context": "<Retriever가 반환한 문서 조각>",
    "question": "축제 음식 판매는 몇 시까지야?",
})

이 코드는 Prompt의 출력 Message를 Model 입력으로 넘기는 배관을 직접 작성하지 않습니다. 하지만 {context}를 실제로 채우는 검색과 권한 검사는 별도로 필요합니다.

추상화가 주는 이점

첫째, 부품을 역할 단위로 바꿀 수 있습니다. Chroma를 Pinecone으로 바꿔도 애플리케이션은 여전히 Vector Store와 Retriever라는 역할을 바라볼 수 있습니다.

둘째, 테스트 경계가 선명해집니다. Retriever가 기대 문서를 반환하는지와 Model이 Context를 잘 사용하는지를 따로 확인할 수 있습니다.

셋째, Streaming과 batch 같은 실행 방식을 구성 요소마다 새로 발명할 필요가 줄어듭니다. 공통 설정과 관측 정보도 Runnable 실행 단위에 연결할 수 있습니다.

넷째, 코드를 읽는 사람이 “이 객체가 어떤 제공자인가”보다 “이 단계가 무슨 역할인가”부터 이해할 수 있습니다.

추상화의 비용도 있다

공통 인터페이스 뒤에는 실제 제공자 API가 있습니다. 오류 메시지가 여러 계층을 거치면 원래 HTTP 응답을 찾기 어려울 수 있습니다. 패키지가 여러 개로 분리되어 호환 버전을 확인해야 하는 일도 생깁니다.

추상화가 모든 제공자 기능을 똑같이 만들지도 않습니다. 어떤 Model은 Tool Calling을 지원하고 어떤 Model은 지원하지 않을 수 있습니다. Vector Store마다 metadata filter 문법과 운영 방식도 다릅니다.

그래서 다음 습관이 필요합니다.

  • 개발 중에는 Retriever 결과와 metadata를 별도로 확인합니다.
  • 예외의 가장 안쪽 원인과 제공자 응답을 민감정보 없이 기록합니다.
  • 패키지 버전과 모델 이름을 배포 설정에 명시합니다.
  • 한 번에 여러 부품을 바꾸지 않고 평가 질문으로 전후를 비교합니다.
  • LangChain 공식 문서와 실제 제공자 문서를 함께 봅니다.

직접 구현이 더 나은 때도 있을까?

요청 하나를 한 제공자에게 보내는 작은 도구라면 직접 HTTP 호출이 더 단순할 수 있습니다. 의존성을 줄이고 제공자 고유 기능을 빠르게 사용할 수 있기 때문입니다.

반대로 문서 Loader, 여러 Vector Store, 대화 기록, Streaming과 평가를 함께 연결한다면 공통 추상화의 이점이 커집니다. 중요한 것은 Framework 사용 여부가 아니라 변경과 실패를 어디에서 관찰할지입니다.

상황 먼저 검토할 선택
제공자 API 하나를 짧게 실험 직접 SDK 또는 HTTP
여러 RAG 부품을 교체하며 비교 LangChain 추상화
제공자 고유 기능이 핵심 공식 SDK와 LangChain 지원 범위 비교
운영 장애 원인 추적이 중요 단계별 로그·Trace·평가 경계 우선 설계

자주 생기는 오해

LangChain을 쓰면 검색이 더 정확해지나요?

자동으로 그렇지 않습니다. 연결 코드는 줄지만 Chunking, Embedding, 검색 조건과 문서 품질은 직접 검증해야 합니다.

prompt | model이면 RAG가 완성되나요?

아닙니다. 검색된 Context를 준비하고 Prompt에 넣는 단계가 있어야 합니다. 권한 검사와 최신 문서 관리도 별도입니다.

추상화가 있으면 제공자를 설정 하나로 완벽히 교체할 수 있나요?

공통 호출 코드는 유지할 수 있지만 인증, 지원 기능, 비용, 응답 metadata와 운영 제한은 다시 확인해야 합니다.

직접 구현은 항상 나쁜 선택인가요?

아닙니다. 범위가 작고 제공자 기능을 깊게 써야 한다면 더 명확할 수 있습니다. 앞으로의 변경 비용까지 비교해 결정해야 합니다.

세 줄 요약

  • LangChain 없이도 RAG를 만들 수 있지만 문서·검색·Prompt·모델 사이의 변환과 운영 코드가 반복됩니다.
  • LangChain은 공통 역할과 Runnable 실행 방식을 제공해 부품을 연결하고 교체하며 테스트하기 쉽게 만듭니다.
  • 추상화는 검색 품질과 보안을 자동으로 해결하지 않으므로 실제 제공자 동작과 단계별 결과를 계속 확인해야 합니다.

다음 편 예고

공통 인터페이스가 있으면 Chroma 대신 다른 Vector Database를 연결하기 쉬워집니다. 그러나 코드 한 줄을 바꾸는 것과 데이터를 안전하게 옮기는 것은 다른 문제입니다.

다음 편 — Chroma에서 Pinecone으로 바꾸면 무엇이 달라질까?에서 로컬 저장소와 관리형 Vector Database의 경계를 비교합니다.

공식 문서