13년차의 서버실

서버, 인프라, 홈랩 기술 블로그

[태그:] LangChain

  • [AI] LangChain RAG 오류: 임베딩 문제와 해결 전략

    [AI] LangChain RAG 오류: 임베딩 문제와 해결 전략

    LangChain RAG 오류: 임베딩 문제와 해결 전략

    LangChain RAG 오류를 처음 겪으면 꽤 당황스럽습니다. 문서는 분명 들어갔는데 검색 결과가 엉뚱하게 나오고, 어떤 날은 벡터 저장소에서 차원 불일치 에러가 터지고, 또 어떤 날은 임베딩 단계에서 조용히 일부 문서만 빠지기도 하거든요. 저도 문서 검색용 RAG(Retrieval-Augmented Generation, 검색 증강 생성) 파이프라인을 여러 번 손보면서 같은 문제를 반복해서 봤는데, 막상 파고 들어가 보니 원인은 모델 하나가 아니라 문서 전처리, 청크(chunk, 문서 분할 단위), 메타데이터, 패키지 구성, 임베딩 모델 선택이 얽혀 있더라고요.

    이번 글에서는 LangChain RAG 오류 중에서도 특히 자주 마주치는 LangChain 임베딩 문제를 중심으로, 실제 디버깅할 때 먼저 보는 체크포인트와 해결 전략을 정리해보겠습니다. 단순히 따라 하는 설정이 아니라 왜 이런 문제가 생기는지까지 같이 보면, 이후 LLM 애플리케이션 개발에서도 훨씬 덜 헤매게 됩니다.

    LangChain RAG 오류 흐름과 임베딩 문제 지점을 보여주는 아키텍처 다이어그램

    문서 로더, 텍스트 분할기, 임베딩 모델, 벡터 저장소, 검색기, LLM 응답 생성까지 이어지는 흐름과 오류가 자주 발생하는 지점을 설명하는 이미지입니다.

    왜 LangChain RAG 오류가 임베딩 단계에서 자주 생길까요?

    쉽게 말해 임베딩(Embedding, 텍스트를 숫자 벡터로 바꾸는 과정)은 RAG의 바닥 공사입니다. 여기서 조금만 어긋나도 이후 검색 품질이 크게 흔들립니다. 생성 모델은 멀쩡한데 답변이 이상한 경우, 의외로 원인은 검색 단계 이전인 임베딩 쪽에 있는 경우가 많더라고요.

    제가 직접 겪어보니 특히 아래 네 가지가 반복적으로 문제를 만들었습니다.

    • 임베딩 모델을 바꿨는데 기존 벡터 저장소를 그대로 재사용한 경우
    • 문서 청크가 비어 있거나 너무 짧거나 너무 긴 경우
    • 질문과 문서의 언어 특성이 다른데 모델 특성을 고려하지 않은 경우
    • 배치 처리(batch processing)와 API 제한 때문에 중간에 누락이 생기는 경우

    여기서 중요한 포인트가 하나 있습니다. 오류가 눈에 보이게 터지면 오히려 낫습니다. 진짜 까다로운 건 에러 없이 검색 품질만 나빠지는 상황입니다. 이건 로그만 봐서는 잘 안 보여서, 검증 루틴을 따로 만들어두는 게 진짜 편하더라고요.

    LangChain RAG 오류를 이해하는 핵심 개념

    임베딩 벡터 차원(Dimension, 벡터 길이)

    각 임베딩 모델은 고정된 형태의 벡터를 만듭니다. 벡터 저장소는 이 차원 정보를 기준으로 데이터를 저장하는데, 나중에 다른 임베딩 모델로 바꾸면서 기존 인덱스를 그대로 읽어오면 차원 불일치가 발생할 수 있습니다. 같은 텍스트라도 숫자 표현 방식 자체가 다르기 때문에, 서로 다른 모델에서 만든 벡터를 같은 인덱스에 섞어 쓰면 안 됩니다.

    청크 전략(Chunking Strategy, 문서 분할 방식)

    RAG 디버깅을 하다 보면 임베딩 모델보다 청크 전략이 더 큰 영향을 주는 경우가 많습니다. 문서를 너무 크게 자르면 검색 결과가 뭉뚱그려지고, 너무 잘게 자르면 문맥이 끊깁니다. 특히 FAQ, 기술 문서, 설정 가이드처럼 구조가 뚜렷한 문서는 헤더 기준 분할이 꽤 중요합니다.

    검색 품질과 임베딩 모델 선택

    임베딩 모델 선택은 단순히 성능 좋은 모델을 고르는 문제가 아닙니다. 문서 언어, 도메인, 비용, 지연 시간, 운영 방식까지 같이 봐야 합니다. 예를 들어 한국어 문서 비중이 높은데 영어 중심 평가만 믿고 가면 검색 적중률이 기대보다 낮게 나올 수 있습니다. 반대로 내부 문서가 짧은 영어 설정 파일 위주라면 과한 모델이 꼭 필요한 것도 아니고요.

    실전 구현: 안정적인 LangChain RAG 기본 골격

    아래 예시는 가장 기본적인 구조입니다. 현재 LangChain 생태계는 패키지가 분리되어 있어서, 문서 로더는 <code>langchain-community, OpenAI 임베딩은 langchain-openai, 텍스트 분할기는 langchain-text-splitters를 함께 설치하는 쪽이 안전합니다. 핵심 흐름은 문서를 읽고, 청크를 만들고, 임베딩한 뒤, 벡터 저장소에 넣고, 검색기로 바로 검증하는 겁니다.

    1. 문서를 로드합니다.
    2. 청크 크기와 겹침(overlap)을 정합니다.
    3. 임베딩 모델명을 명시적으로 고정합니다.
    4. 벡터 저장소를 새로 생성하거나, 메타데이터와 함께 관리합니다.
    5. 질문 샘플로 검색 결과를 직접 검증합니다.
    python -m venv .venv
    source .venv/bin/activate
    pip install -U langchain langchain-community langchain-openai langchain-text-splitters faiss-cpu pypdf

    환경 변수도 먼저 분리해두는 게 좋습니다. 운영하다 보면 테스트 키와 운영 키가 섞여서 엉뚱한 장애를 만들기도 하거든요.

    export OPENAI_API_KEY="your_api_key"
    export SOURCE_DOCS_DIR="./docs"
    export VECTOR_DIR="./vector_store"

    이 코드는 PDF 문서를 로드해 청크를 나누고, 빈 청크를 제거한 뒤, 임베딩을 생성해 FAISS 인덱스로 저장하는 예시입니다. 실무에서는 OpenAIEmbeddings()를 기본값으로 두기보다 모델명을 명시해두는 편이 추적하기 쉽습니다.

    import os
    from pathlib import Path
    
    from langchain_community.document_loaders import PyPDFDirectoryLoader
    from langchain_community.vectorstores import FAISS
    from langchain_openai import OpenAIEmbeddings
    from langchain_text_splitters import RecursiveCharacterTextSplitter
    
    DOCS_DIR = Path(os.environ.get("SOURCE_DOCS_DIR", "./docs"))
    VECTOR_DIR = os.environ.get("VECTOR_DIR", "./vector_store")
    
    loader = PyPDFDirectoryLoader(str(DOCS_DIR))
    docs = loader.load()
    
    splitter = RecursiveCharacterTextSplitter(
        chunk_size=800,
        chunk_overlap=120,
        separators=["\n\n", "\n", ". ", " ", ""]
    )
    chunks = splitter.split_documents(docs)
    chunks = [chunk for chunk in chunks if chunk.page_content and chunk.page_content.strip()]
    
    embeddings = OpenAIEmbeddings(model="text-embedding-3-large")
    vectorstore = FAISS.from_documents(chunks, embeddings)
    vectorstore.save_local(VECTOR_DIR)
    
    print(f"documents={len(docs)}")
    print(f"chunks={len(chunks)}")

    여기서 제가 꼭 넣는 줄이 있습니다. 바로 빈 청크 제거입니다. 이거 하나만 넣어도 나중에 검색 이상 현상이 생겼을 때 원인 추적이 훨씬 쉬워집니다.

    LangChain 임베딩 문제를 줄이기 위한 문서 로딩과 청크 분할 구성 다이어그램

    실전 구현 단계에서 어떤 순서로 데이터가 이동하는지, 그리고 어느 단계에서 검증 로그를 남겨야 하는지 시각적으로 보여주는 이미지입니다.

    LangChain RAG 오류를 줄이는 검증 코드

    RAG 디버깅은 “만들었다”로 끝나지 않습니다. 인덱스를 만든 직후 검색 결과를 바로 확인해야 합니다. 저는 보통 아래처럼 샘플 질의를 몇 개 정해서 바로 돌려봅니다.

    한 가지 주의할 점도 있습니다. FAISS.load_local(..., allow_dangerous_deserialization=True)는 로컬에 저장한 신뢰 가능한 인덱스를 다시 읽을 때만 쓰는 옵션입니다. 외부에서 받은 인덱스 파일에 이 옵션을 켜는 건 위험합니다.

    from langchain_community.vectorstores import FAISS
    from langchain_openai import OpenAIEmbeddings
    
    embeddings = OpenAIEmbeddings(model="text-embedding-3-large")
    vectorstore = FAISS.load_local(
        "./vector_store",
        embeddings,
        allow_dangerous_deserialization=True
    )
    
    queries = [
        "이 문서는 어떤 시스템 구성에 대한 설명인가?",
        "장애 대응 절차는 어떻게 정리되어 있나?",
        "설정 파일 경로와 관련된 설명을 찾아줘"
    ]
    
    for query in queries:
        print(f"\n[QUERY] {query}")
        results = vectorstore.similarity_search(query, k=3)
        for idx, doc in enumerate(results, start=1):
            preview = doc.page_content[:180].replace("\n", " ")
            print(f"{idx}. {preview}")

    이 검증을 해보면 생각보다 빨리 감이 옵니다. 질문은 맞는데 본문 일부가 계속 헛나오면 청크 문제일 가능성이 높고, 전혀 다른 문서가 튀어나오면 임베딩 모델 적합성이나 메타데이터 필터 조건을 먼저 의심해보면 됩니다.

    LangChain 임베딩 문제, 이런 식으로 터집니다

    이 섹션이 핵심입니다. 실제로 많이 마주치는 증상과 원인을 표로 먼저 정리해보겠습니다.

    증상 가능한 원인 우선 조치
    차원 불일치 오류 기존 인덱스를 다른 임베딩 모델로 재사용 벡터 저장소를 새로 생성
    에러는 없는데 검색 품질이 낮음 청크 전략 부적절, 언어 특성 불일치 청크 크기/겹침 조정, 샘플 질의 검증
    임베딩 중간 실패 또는 누락 배치 크기 과다, API 제한, 재시도 부족 배치 축소, 로깅 추가, 재시도 적용
    유사도 검색 결과가 비어 있음 문서 로드 실패, 빈 청크 저장 문서 수와 청크 수를 먼저 출력

    1. 임베딩 모델을 바꿨는데 기존 인덱스를 그대로 쓴 경우

    이건 정말 자주 나옵니다. 테스트 단계에서는 모델을 자주 바꿔보게 되거든요. 그런데 FAISS 같은 벡터 저장소는 기존 벡터 차원을 기준으로 인덱스를 유지하므로, 모델을 바꿨다면 거의 항상 재생성이 안전합니다.

    해결 전략은 단순합니다. 벡터 저장소 디렉터리에 메타 파일을 두고, 어떤 임베딩 모델과 청크 설정으로 생성했는지 기록하세요.

    import json
    from pathlib import Path
    
    meta = {
        "embedding_provider": "openai",
        "embedding_model": "text-embedding-3-large",
        "chunk_size": 800,
        "chunk_overlap": 120,
    }
    
    Path("./vector_store").mkdir(exist_ok=True)
    Path("./vector_store/index_meta.json").write_text(
        json.dumps(meta, ensure_ascii=False, indent=2),
        encoding="utf-8"
    )

    2. 문서는 들어갔는데 검색이 너무 엉뚱한 경우

    처음엔 이게 뭔가 싶었는데, 실제로는 청크 설계 문제가 원인인 경우가 많았습니다. 설정 문서와 장애 대응 문서를 한 덩어리로 넣으면 질문 의도와 상관없이 공통 단어에 끌려가더라고요. 특히 한국어 문서는 줄바꿈, 제목, 코드 블록이 의미 단위를 나누는 데 중요해서, 무작정 문자 수만 기준으로 자르면 손해를 보기 쉽습니다.

    해결 전략은 아래 순서로 잡으면 편합니다.

    • 문서 유형별로 분리합니다. 예: 매뉴얼, 장애일지, 설정 예제
    • 헤더 단위 분할을 우선 고려합니다.
    • 청크 미리보기 로그를 남깁니다.
    • 질문 5개 정도를 고정해 회귀 테스트처럼 돌립니다.

    3. 임베딩 호출은 되는데 일부 문서가 빠지는 경우

    이건 조용히 지나가는 경우가 있어서 더 위험합니다. 배치 처리 중 일부 실패, 비정상 문서 인코딩, 너무 긴 텍스트 같은 이유로 누락될 수 있습니다. 저도 나중에 청크 개수와 저장 결과를 대조하다가 발견한 적이 있었는데, 그때 로그가 없었으면 원인 찾는 데 훨씬 오래 걸렸을 겁니다.

    해결 전략은 임베딩 전후 카운트를 반드시 비교하는 겁니다.

    expected_chunks = len(chunks)
    vectorstore = FAISS.from_documents(chunks, embeddings)
    
    print(f"expected_chunks={expected_chunks}")
    print("index build completed")

    단순해 보여도 이 출력 하나가 삽질 시간을 꽤 줄여줍니다.

    4. 한국어 문서 검색이 유독 약한 경우

    여기서는 문서와 질문의 언어 분포를 먼저 봐야 합니다. 내부 문서는 한국어인데 질의 테스트를 영어 예문으로만 돌리면 평가가 왜곡됩니다. 반대로 영어 설정 파일을 한국어로만 묻는 상황도 마찬가지고요. 결국 임베딩 모델 선택은 데이터 특성과 함께 봐야 합니다.

    저는 보통 아래 기준으로 판단합니다.

    • 문서 언어가 한국어 중심인지
    • 질의 언어가 문서와 같은지
    • 코드, 로그, 설정 키가 많은지
    • 짧은 답을 찾는지, 긴 문맥을 찾는지
    LangChain RAG 오류 대표 사례와 임베딩 문제 해결 도식

    실무에서 자주 겪는 임베딩 오류 유형과 각각의 원인, 우선 확인해야 할 체크포인트를 한눈에 정리한 이미지입니다.

    운영 관점에서 추천하는 LangChain RAG 오류 디버깅 체크리스트

    RAG 디버깅은 감으로 하면 오래 갑니다. 체크리스트로 고정해두면 훨씬 수월합니다.

    1. 문서 수와 청크 수를 출력합니다.
    2. 청크 샘플 3~5개를 직접 눈으로 봅니다.
    3. 사용한 임베딩 모델 정보와 청크 설정을 메타데이터로 남깁니다.
    4. 인덱스 생성 직후 고정 질의 세트를 돌립니다.
    5. 검색 결과 상위 3개를 로그로 저장합니다.
    6. 임베딩 모델 변경 시 기존 인덱스를 재사용하지 않습니다.
    7. 문서 인코딩과 특수문자 깨짐 여부를 확인합니다.

    이 정도만 지켜도 대부분의 LangChain RAG 오류는 꽤 빨리 좁혀집니다. 특히 4번과 5번은 꼭 해보세요. 검색 결과를 눈으로 안 보면, 문제를 LLM 탓으로 착각하기 쉽습니다.

    검증 결과는 어떻게 봐야 할까요?

    검증 단계에서는 정답 문장 하나를 맞췄는지보다, 관련 있는 문서를 안정적으로 가져오느냐를 먼저 봐야 합니다. RAG는 1차 검색이 맞아야 2차 생성도 좋아집니다. 저는 보통 아래 기준으로 확인합니다.

    • 같은 질문을 여러 번 해도 상위 결과가 크게 흔들리지 않는지
    • 문서 제목 또는 섹션 단위로 관련성이 보이는지
    • 질문 표현을 조금 바꿔도 비슷한 결과가 나오는지
    • 한국어 질의와 영어 키워드 혼합 질의에서 성능 차이가 과한지

    상위 3개 문서가 납득 가능한 수준으로 꾸준히 나오기 시작하면, 그다음부터는 프롬프트(prompt, 모델 지시문) 튜닝으로 넘어가면 됩니다. 이 지점이 오면 한숨 돌려도 됩니다. 이거 진짜 체감이 확 오더라고요.

    LangChain RAG 오류 검증을 위한 검색 결과 대시보드와 RAG 디버깅 시각화

    샘플 질의를 기준으로 검색 결과의 일관성과 관련성을 확인하는 검증 화면을 표현한 이미지입니다.

    임베딩 모델 선택 시 현실적인 기준

    모델 이름만 비교하면 판단이 오히려 어려워집니다. 운영 조건까지 같이 적어두면 의사결정이 훨씬 쉬워집니다.

    기준 질문 판단 포인트
    언어 한국어 문서 비중이 높은가? 다국어 대응 여부를 우선 확인
    문서 형태 코드/로그/설정 파일이 많은가? 토큰 분포와 특수문자 처리 성향 확인
    운영 비용 대량 재색인이 자주 필요한가? 배치 처리 비용과 속도 고려
    지연 시간 실시간 임베딩이 필요한가? 오프라인 색인과 온라인 질의 경로 분리

    모델을 바꾸면 다 해결될 줄 알았는데, 실제 원인은 청크 설정이었던 경우가 생각보다 많습니다. 저도 처음엔 모델만 계속 갈아타다가 시간을 꽤 썼습니다. 운영에서는 좋은 모델 하나보다 일관된 파이프라인이 더 중요하더라고요.

    이전 글에서 다룬 문서 청크 설계 글도 함께 읽어보시면 흐름이 더 잘 잡힙니다. 내부 링크를 걸어두면 독자 입장에서도 다음 액션이 분명해서 체류 시간 관리에 도움이 됩니다.

    정리: LangChain 임베딩 문제는 로그와 검증 루틴으로 잡아야 합니다

    오늘 내용만 딱 정리하면 이렇습니다.

    • 차원 불일치가 보이면 임베딩 모델 변경 여부부터 확인합니다.
    • 검색 품질 저하는 청크 전략과 문서 구조를 먼저 봅니다.
    • 조용한 실패를 막으려면 문서 수, 청크 수, 샘플 질의 결과를 반드시 기록합니다.
    • 임베딩 모델 선택은 언어, 문서 유형, 운영 비용까지 같이 판단합니다.

    다음 글에서는 RAG 디버깅을 좀 더 확장해서, 재순위화(Reranking, 검색 결과 재정렬)와 하이브리드 검색(Hybrid Search, 키워드+벡터 검색 결합) 기준도 다뤄볼 예정입니다. 이전 글의 문서 청크 설계 내용과 함께 보면 이해가 더 빨라집니다.

    문서 로딩, 청크 분할, 임베딩 생성, 인덱스 재생성, 검색 검증까지의 해결 흐름을 한 장으로 요약한 이미지입니다.

    자주 묻는 질문

    Q1. 에러가 없는데도 답변 품질이 낮으면 어디부터 봐야 하나요?

    A. LLM보다 먼저 검색 결과를 보셔야 합니다. 상위 문서 3개가 질문과 관련이 없으면, 거의 항상 임베딩이나 청크 쪽 문제입니다.

    Q2. 벡터 저장소는 언제 다시 만들어야 하나요?

    A. 임베딩 모델을 바꿨거나, 청크 전략을 크게 바꿨거나, 문서 구조가 크게 바뀌었다면 재생성이 안전합니다.

    Q3. RAG 디버깅에서 가장 먼저 자동화할 것은 뭔가요?

    A. 고정 질의 세트 기반의 회귀 테스트입니다. 같은 질문에 대한 검색 결과를 저장해두면 변경 영향이 바로 보입니다.

  • [AI] LangChain으로 AI 에이전트 구축: 복잡한 작업 자동화 실전 가이드

    [AI] LangChain으로 AI 에이전트 구축: 복잡한 작업 자동화 실전 가이드

    LangChain으로 AI 에이전트 구축: 복잡한 작업 자동화 실전 가이드

    왜 LangChain AI 에이전트가 필요할까요?

    안녕하세요, 13년차 서버실 지킴이입니다. 인프라 엔지니어로 일하면서 수많은 반복 작업과 복잡한 문제 해결에 시간과 에너지를 쏟았던 경험, 혹시 여러분도 있으신가요? 매번 똑같은 정보를 찾아보고, 여러 시스템을 오가며 데이터를 조합하고, 때로는 예측 불가능한 변수까지 고려해야 하는 상황들 말이죠. 저는 이런 작업들을 보면서 "이걸 좀 더 스마트하게 자동화할 수는 없을까?" 하는 고민을 늘 했었습니다. 특히 최근 LLM(Large Language Model, 대규모 언어 모델)의 발전은 이런 고민에 한 줄기 빛이 되어주었죠. 단순 반복을 넘어, 어느 정도의 추론과 판단까지 가능한 자동화 말입니다.

    그래서 제가 홈랩에서 직접 실험해본 결과 알게 된 게 바로 LangChain AI 에이전트의 강력함이었어요. 처음엔 이게 뭔가 싶었는데, 막상 써보니까 AI가 스스로 판단해서 필요한 도구(Tool)를 찾아 쓰고, 심지어 이전 대화 맥락(Memory)까지 기억하면서 복잡한 작업을 척척 해내더라고요. 정말 놀랐습니다. 오늘 이 글에서는 저의 삽질 경험을 바탕으로, 여러분도 LangChain을 활용해 나만의 AI 에이전트를 구축하고 복잡한 작업을 자동화하는 방법을 실전 가이드 형식으로 알려드리려고 합니다. 우리 함께 AI 자동화의 세계로 떠나볼까요? 🎉

    LangChain AI 에이전트가 어떻게 동작하는지 한눈에 보여주는 개념도입니다. LLM을 중심으로 Tools, Memory, Agent Executor가 유기적으로 연결되어 복잡한 작업을 수행합니다.

    LangChain AI 에이전트, 도대체 뭘까요?

    쉽게 말해, LangChain AI 에이전트는 LLM(대규모 언어 모델)을 ‘두뇌’ 삼아 다양한 ‘도구’를 사용하고, ‘기억’까지 하면서 사람처럼 생각하고 행동하는 AI 시스템을 말해요. 기존 LLM이 단순히 주어진 질문에 답만 했다면, 에이전트는 한 발 더 나아가 스스로 계획을 세우고, 필요한 정보를 찾아오고, 외부 시스템과 상호작용하면서 목표를 달성하는 거죠. 이 모든 과정을 쉽게 구현할 수 있도록 도와주는 프레임워크가 바로 LangChain(랭체인)이에요.

    • Agent (에이전트): LLM이 어떤 도구를 사용할지, 어떤 순서로 사용할지 결정하는 ‘두뇌’ 역할을 합니다. 사용자의 요청을 이해하고, 목표 달성을 위한 최적의 경로를 탐색하죠.
    • Tools (도구): 에이전트가 외부 세계와 상호작용하는 수단이에요. 예를 들어, 인터넷 검색(Web Search), 계산기(Calculator), 외부 API 호출(API Call), 코드 실행(Code Interpreter) 등이 될 수 있어요. LangChain은 다양한 기본 도구를 제공하고, 커스텀 도구를 만들기도 정말 쉽습니다.
    • Memory (메모리): 에이전트가 이전 대화 내용을 기억해서 컨텍스트(Context)를 유지하는 역할이에요. 덕분에 여러 번의 질문과 답변 속에서도 일관성 있는 행동을 할 수 있습니다. 마치 사람처럼 대화를 기억하는 거죠.
    • LangChain (랭체인): 이런 에이전트를 쉽게 만들고, 구성하고, 배포할 수 있도록 도와주는 파이썬(Python) 라이브러리이자 프레임워크예요. 다양한 LLM과 도구를 연결하고, 복잡한 워크플로우를 체인(Chain) 형태로 만들 수 있게 해줍니다.

    결국 LangChain AI 에이전트는 마치 유능한 비서처럼, 우리가 시키는 복잡한 일들을 스스로 판단하고 여러 도구를 활용해서 처리해주는 시스템이라고 생각하시면 돼요. 진짜 편하더라고요!

    LangChain으로 나만의 AI 에이전트 만들기 실전!

    자, 이제 직접 코드를 만져보면서 LangChain AI 에이전트를 만들어볼 시간입니다. 저는 간단한 정보 검색 에이전트를 만들어 볼 건데요, 이 에이전트는 사용자의 질문을 받으면 인터넷을 검색해서 최신 정보를 찾아오는 역할을 할 겁니다.

    준비물 챙기기: 개발 환경 설정

    Python 3.8+ 환경이 필요해요. 먼저 필요한 라이브러리들을 설치해볼까요?

    pip install langchain langchain-openai langchain-community python-dotenv
    
    • langchain: LangChain 프레임워크의 핵심 라이브러리예요.
    • langchain-openai: OpenAI의 LLM을 사용하기 위한 통합 라이브러리입니다. (다른 LLM을 사용한다면 해당 프로바이더 라이브러리를 설치하세요.)
    • langchain-community: 검색 도구(Tool)와 같은 커뮤니티 기반의 유용한 기능들을 담고 있어요.
    • python-dotenv: 환경 변수를 .env 파일에서 로드하기 위해 사용합니다. API 키 등을 코드에 직접 노출하지 않고 안전하게 관리할 수 있어요.

    다음으로, OpenAI API 키를 설정해야 해요. 프로젝트 루트 폴더에 .env 파일을 만들고 아래 내용을 추가해주세요.

    OPENAI_API_KEY="YOUR_OPENAI_API_KEY"
    

    YOUR_OPENAI_API_KEY 부분에 여러분의 OpenAI API 키를 입력하시면 돼요. ⚠️ 절대 이 파일을 GitHub 같은 공개 저장소에 올리시면 안 됩니다!

    첫 번째 에이전트: 간단한 정보 검색 에이전트

    이제 agent_app.py라는 파일을 만들고 코드를 작성해봅시다. 이 에이전트는 DuckDuckGo Search 도구를 사용해서 웹 검색을 수행할 겁니다.

    import os
    from dotenv import load_dotenv
    from langchain_openai import ChatOpenAI
    from langchain.agents import AgentExecutor, create_react_agent
    from langchain_community.tools import DuckDuckGoSearchRun
    from langchain import hub
    
    # .env 파일에서 환경 변수 로드
    load_dotenv()
    
    # 1. LLM(Large Language Model) 설정
    # gpt-3.5-turbo 모델을 사용하고, 창의성을 낮추기 위해 temperature는 0으로 설정했어요.
    llm = ChatOpenAI(model="gpt-3.5-turbo", temperature=0)
    
    # 2. 도구(Tools) 정의
    # 웹 검색을 위한 DuckDuckGoSearchRun 도구를 사용합니다.
    # 이 도구는 별도의 API 키 없이 바로 사용할 수 있어서 편리해요.
    search_tool = DuckDuckGoSearchRun()
    tools = [search_tool]
    
    # 3. 에이전트의 프롬프트(Prompt) 로드
    # LangChain Hub에서 ReAct(Reasoning and Acting) 프롬프트를 가져옵니다.
    # ReAct는 LLM이 추론(Reason)하고 행동(Act)하는 과정을 반복하며 목표를 달성하도록 돕는 강력한 패턴이에요.
    prompt = hub.pull("hwchase17/react")
    
    # 4. 에이전트 생성
    # create_react_agent 함수는 LLM, Tools, Prompt를 받아서 에이전트의 로직을 생성합니다.
    agent = create_react_agent(llm, tools, prompt)
    
    # 5. Agent Executor 생성 및 실행
    # AgentExecutor는 에이전트를 실행하고, 에이전트가 도구를 사용하는 과정을 관리합니다.
    # verbose=True로 설정하면 에이전트의 생각(Thought) 과정과 도구 사용 내역을 자세히 볼 수 있어요.
    agent_executor = AgentExecutor(agent=agent, tools=tools, verbose=True, handle_parsing_errors=True)
    
    # 에이전트 실행 예시
    print("\n--- 에이전트 실행 시작 ---")
    response = agent_executor.invoke({"input": "2024년 파리 올림픽에 대한 최신 정보를 알려주고, 한국 선수단에 대한 내용도 포함해줘."})
    print("\n--- 에이전트 최종 답변 ---")
    print(response["output"])
    print("\n--- 에이전트 실행 종료 ---")
    
    # 또 다른 질문
    print("\n--- 두 번째 질문 실행 ---")
    response = agent_executor.invoke({"input": "AI 에이전트 개발 시 가장 중요한 고려사항은 무엇일까요?"})
    print("\n--- 에이전트 최종 답변 ---")
    print(response["output"])
    print("\n--- 두 번째 질문 종료 ---")
    

    코드를 저장하고 실행해보세요.

    python agent_app.py
    

    verbose=True 덕분에 에이전트가 어떤 생각(Thought)을 하고, 어떤 도구(Tool)를 사용하며 정보를 찾아나가는지 자세히 볼 수 있을 겁니다. 제가 처음 이걸 봤을 때, “와, 진짜 AI가 생각하고 있네!” 싶더라고요. LLM이 단순히 답변만 하는 게 아니라, 스스로 계획하고 실행하는 모습을 보니 정말 신기했어요.

    직접 작성한 LangChain 에이전트 코드와 그 실행 결과 스크린샷입니다. 에이전트의 추론 과정이 자세히 출력되는 것을 볼 수 있습니다.

    삽질 경험: 저도 이걸로 꽤나 고생했습니다 ⚠️

    LangChain AI 에이전트가 만능처럼 보이지만, 저도 처음부터 술술 만들었던 건 아니었어요. 여러 삽질을 거치면서 배운 점들이 많더라고요. 여러분은 저 같은 시행착오를 겪지 않으시길 바라며 몇 가지 주의사항과 트러블슈팅 팁을 공유해봅니다.

    • API Rate Limit (API 호출 제한): LLM API는 호출 횟수나 토큰 수에 제한이 있어요. 에이전트가 무한정 검색하거나 반복 작업을 수행하면 금세 제한에 걸리곤 하더라고요. 저도 테스트하다가 갑자기 API 에러를 만나서 당황했던 적이 많았어요. 💡 팁: 개발 단계에서는 temperature를 낮춰서 불필요한 추론을 줄이거나, 프롬프트 설계를 통해 에이전트의 ‘생각’ 과정을 효율적으로 유도하는 것이 중요합니다. 그리고 비용 모니터링은 필수죠!

    • Tool Selection (도구 선택의 어려움): 에이전트에게 너무 많은 도구를 주거나, 역할에 맞지 않는 도구를 주면 엉뚱한 방향으로 흘러가는 경우가 있어요. 처음엔 에이전트가 “계산기” 도구가 있는데도 굳이 검색으로 답을 찾으려고 해서 답답했던 적도 있거든요. 💡 팁: 에이전트의 목적에 맞는 최소한의 도구만 제공하고, 각 도구의 description을 명확하게 작성해서 LLM이 언제 어떤 도구를 써야 할지 정확히 알 수 있도록 도와줘야 합니다.

    • Prompt Engineering (프롬프트 설계의 중요성): 에이전트의 성능은 프롬프트에 크게 좌우돼요. ReAct 프롬프트처럼 잘 설계된 프롬프트는 에이전트가 훨씬 효율적으로 작동하게 만들어요. 하지만 잘못된 지시나 모호한 프롬프트는 에이전트를 길 잃게 만들 수 있어요. 💡 팁: LangChain Hub에서 제공하는 검증된 프롬프트를 참고하고, 에이전트의 역할과 목표를 명확하게 지시하는 것이 중요합니다.

    • Memory Management (메모리 관리): 장기적인 대화에서는 메모리 관리가 중요해요. 너무 많은 대화 기록을 메모리에 담으면 토큰 제한에 걸리거나 불필요한 비용이 발생할 수 있어요. 💡 팁: ConversationBufferWindowMemory 같은 특정 길이만 기억하는 메모리 타입을 사용하거나, 요약(Summarization) 기능을 활용해서 필요한 핵심 정보만 기억하도록 하는 것이 좋습니다.

    • Parsing Errors (파싱 에러): LLM이 도구 사용 형식을 제대로 지키지 않아서 발생하는 에러예요. create_react_agent 함수에 handle_parsing_errors=True 옵션을 주면 에러를 좀 더 부드럽게 처리할 수 있어요. 저도 이 옵션 덕분에 스트레스를 많이 줄일 수 있었네요.

    드디어! 에이전트가 똑똑하게 일합니다 🎉

    위의 삽질과정을 거쳐 에이전트가 제대로 작동하는 모습을 보면 정말 뿌듯해요. 실제로 “2024년 파리 올림픽에 대한 최신 정보를 알려주고, 한국 선수단에 대한 내용도 포함해줘.” 같은 복잡한 질의를 던졌을 때, 에이전트는 다음과 같은 과정을 거쳐 답변을 생성합니다.

    1. 사용자의 질문을 이해하고, “파리 올림픽”과 “한국 선수단”이라는 키워드를 추출합니다.
    2. 인터넷 검색 도구(DuckDuckGo Search)를 사용하기로 결정합니다.
    3. “2024 파리 올림픽 최신 정보”를 검색합니다.
    4. 검색 결과에서 주요 정보를 추출하고, 다시 “2024 파리 올림픽 한국 선수단”을 검색합니다.
    5. 두 가지 검색 결과를 종합하여 사용자에게 최신 정보와 한국 선수단 관련 내용을 포함한 답변을 제공합니다.

    제가 기대했던 것 이상으로 잘 해내더라고요. 특히 여러 단계에 걸쳐 정보를 수집하고 조합하는 능력은 기존 LLM 단독으로는 어려웠던 부분이라 더욱 인상 깊었어요. 이런 방식으로 단순 정보 검색을 넘어, 특정 문서 요약, 데이터 분석, 심지어는 간단한 코드 생성까지 다양한 작업을 자동화할 수 있습니다.

    LangChain 에이전트가 여러 도구를 활용하여 복합적인 질문에 답하는 모습입니다. 마치 사람이 생각하듯 정보를 수집하고 가공하여 최종 답변을 도출합니다.

    마무리하며: AI 자동화, 이제 시작입니다.

    오늘은 LangChain을 활용해서 AI 에이전트를 구축하고 복잡한 작업을 자동화하는 방법에 대해 저의 경험을 바탕으로 이야기해봤어요. 13년차 인프라 엔지니어로서 늘 어떻게 하면 더 효율적으로 일할 수 있을까 고민했는데, LangChain AI 에이전트가 그 해답 중 하나가 될 수 있다는 확신을 얻었어요. 여러분도 저처럼 홈랩에서 직접 만들어보시면 좋겠네요. 직접 코드를 만져보고 에이전트가 ‘생각’하는 과정을 지켜보는 것만으로도 정말 값진 경험이 될 겁니다.

    LangChain은 AI 에이전트 개발을 위한 강력하고 유연한 프레임워크예요. 오늘 다룬 내용은 아주 기초적인 시작에 불과해요. 앞으로는 커스텀 도구를 만들어서 사내 시스템과 연동하거나, 더 복잡한 추론 체인(Chain)을 구성해서 고도화된 자동화 시스템을 구축할 수도 있어요. AI 자동화의 시대는 이제 막 시작되었고, 그 가능성은 무궁무진합니다.

    다음 글에서는 더 복잡한 멀티모달(Multimodal) 에이전트나, 에이전트 간의 협업(Agent Collaboration)을 통해 더욱 강력한 자동화 시스템을 만드는 방법에 대해 다뤄볼 예정입니다. 기대해주세요! 궁금한 점이나 여러분의 삽질 경험이 있다면 댓글로 공유해주세요. 우리 함께 배우고 성장해나가요. 😊

    LangChain 에이전트의 핵심 기능과 다양한 활용 방안을 시각적으로 정리한 요약본입니다. 복잡한 작업 자동화를 위한 강력한 도구임을 보여줍니다.

  • [AI] LangChain RAG 시스템 구축: 임베딩과 검색 증강 생성 실전 가이드

    LLM이 모르는 정보를 물어보면 어떻게 될까요?

    GPT나 Claude 같은 LLM(Large Language Model, 대형 언어 모델)을 써보신 분들은 한 번쯤 이런 경험 있으실 거예요. 회사 내부 문서나 최신 정보를 물어봤더니 “저는 그 정보를 가지고 있지 않습니다”라고 하거나, 아예 그럴싸한 거짓말을 늘어놓는 경우 말이죠. 이게 바로 LLM의 고질적인 한계인 지식 컷오프(Knowledge Cutoff)와 할루시네이션(Hallucination, 환각) 문제입니다.

    저도 처음에 사내 기술 문서 기반 Q&A 시스템을 만들어보려고 했을 때 이 벽에 딱 부딪혔거든요. 그때 찾은 해답이 바로 RAG(Retrieval-Augmented Generation, 검색 증강 생성)였습니다. LangChain RAG 조합을 실제로 구축해보면서 생각보다 강력하다는 걸 느꼈고, 오늘은 그 경험을 처음부터 차근차근 공유해드리려고 합니다.

    이 글은 RAG 시스템을 처음 접하시는 분부터, 개념은 알지만 실제 구현에서 막히는 분들까지 모두 도움이 될 수 있도록 개념 설명부터 실제 코드까지 담았습니다.

    ▲ RAG 시스템의 전체 파이프라인 — 문서 수집부터 임베딩, 벡터 저장소, 검색, 그리고 LLM 응답 생성까지의 흐름

    RAG가 뭔지 쉽게 이해해보기

    검색 증강 생성(RAG)이란?

    쉽게 말해, LLM한테 “오픈북 시험”을 보게 해주는 방식이에요. 기존 LLM은 학습된 데이터만으로 답을 내야 하는 “클로즈드 북” 방식이었다면, RAG는 질문이 들어왔을 때 관련 문서를 먼저 찾아서 LLM에게 “이 자료 참고해서 답해봐”라고 넘겨주는 거거든요.

    RAG의 핵심 흐름은 크게 두 단계로 나뉩니다.

    1. 인덱싱(Indexing) 단계: 문서를 잘게 쪼개고(Chunking), 임베딩(Embedding, 텍스트를 숫자 벡터로 변환)해서 벡터 데이터베이스에 저장
    2. 검색 및 생성(Retrieval & Generation) 단계: 사용자 질문을 임베딩하고, 유사한 문서 조각을 찾아서 LLM에게 컨텍스트로 전달

    임베딩(Embedding)이 RAG의 핵심입니다

    임베딩은 RAG에서 가장 중요한 개념이에요. 텍스트를 수백~수천 차원의 숫자 벡터로 변환하는 건데, 의미가 비슷한 문장은 벡터 공간에서 가까이 위치하게 됩니다. 예를 들어 “강아지”와 “개”는 벡터 공간에서 서로 가깝고, “강아지”와 “자동차”는 멀리 떨어져 있는 식이죠.

    이 거리를 기반으로 질문과 가장 관련 있는 문서 조각을 찾아내는 게 RAG의 검색 로직입니다. 결국 임베딩 품질이 곧 RAG 시스템의 성능을 좌우한다고 봐도 됩니다.

    방식 장점 단점 적합한 상황
    순수 LLM 구현 간단, 빠름 지식 컷오프, 할루시네이션 일반적인 지식 질의
    Fine-tuning 모델 자체가 지식 보유 비용 높음, 업데이트 어려움 특정 도메인 전문화
    RAG 최신 정보 반영, 유연함 검색 품질에 의존 사내 문서, 최신 정보 Q&A

    환경 준비 — 시작 전에 챙겨야 할 것들

    저는 Python 3.11 환경을 기준으로 구성했습니다. 로컬이든 클라우드든 동일하게 적용되는 내용이에요.

    필요한 패키지 설치

    # 가상환경 생성 및 활성화
    python -m venv rag-env
    source rag-env/bin/activate  # Windows: rag-env\Scripts\activate
    
    # 핵심 패키지 설치
    pip install langchain langchain-community langchain-openai
    pip install chromadb  # 벡터 데이터베이스
    pip install tiktoken  # 토큰 카운팅
    pip install pypdf     # PDF 파일 처리
    pip install python-dotenv  # 환경변수 관리

    💡 팁: OpenAI API 대신 로컬 LLM을 쓰고 싶다면 ollama와 langchain-ollama를 추가로 설치하면 됩니다. 이 부분은 다음 글에서 자세히 다룰 예정이에요.

    환경변수 설정

    # .env 파일 생성
    cat > .env << 'EOF'
    OPENAI_API_KEY=sk-your-api-key-here
    EOF

    LangChain RAG 실전 구현 — 단계별 가이드

    이제 진짜 구현 단계입니다. 저는 기술 문서 몇 개를 PDF로 준비해서 테스트했는데요, 예제에서는 텍스트 파일을 기준으로 설명드릴게요. 흐름만 이해하면 PDF든 웹페이지든 동일하게 적용됩니다.

    1단계: 문서 로드(Document Loading)

    from langchain_community.document_loaders import TextLoader, DirectoryLoader
    from langchain_community.document_loaders import PyPDFLoader
    
    # 단일 텍스트 파일 로드
    loader = TextLoader("./docs/my_document.txt", encoding="utf-8")
    documents = loader.load()
    
    # 폴더 내 모든 PDF 파일 로드
    # loader = DirectoryLoader("./docs", glob="**/*.pdf", loader_cls=PyPDFLoader)
    # documents = loader.load()
    
    print(f"로드된 문서 수: {len(documents)}")
    print(f"첫 번째 문서 미리보기: {documents[0].page_content[:200]}")

    2단계: 문서 청킹(Text Splitting)

    문서를 그대로 넣으면 너무 길어서 LLM이 처리하기 어렵거든요. 적당한 크기로 잘라줘야 합니다. chunk_size와 chunk_overlap 설정이 은근히 중요한데, 저도 처음엔 이 값을 어떻게 잡아야 하나 고민 많이 했습니다.

    from langchain.text_splitter import RecursiveCharacterTextSplitter
    
    text_splitter = RecursiveCharacterTextSplitter(
        chunk_size=1000,      # 각 청크의 최대 문자 수
        chunk_overlap=200,    # 청크 간 겹치는 문자 수 (문맥 연속성 유지)
        length_function=len,
        separators=["\n\n", "\n", " ", ""]  # 분할 우선순위
    )
    
    chunks = text_splitter.split_documents(documents)
    print(f"생성된 청크 수: {len(chunks)}")
    print(f"첫 번째 청크: {chunks[0].page_content}")

    ⚠️ 주의: chunk_overlap을 너무 크게 잡으면 중복 내용이 많아져서 검색 결과가 편향될 수 있어요. 보통 chunk_size의 10~20% 정도가 적당합니다.

    3단계: 임베딩 생성 및 벡터 저장소 구축

    이제 진짜 핵심인 임베딩 단계입니다. 텍스트를 벡터로 변환해서 ChromaDB(로컬 벡터 데이터베이스)에 저장합니다. ChromaDB는 가볍고 설정이 간단해서 개인 프로젝트나 중소 규모 시스템에 딱 맞아요.

    import os
    from dotenv import load_dotenv
    from langchain_openai import OpenAIEmbeddings
    from langchain_community.vectorstores import Chroma
    
    load_dotenv()
    
    # 임베딩 모델 초기화
    embeddings = OpenAIEmbeddings(
        model="text-embedding-3-small"  # 비용 효율적인 임베딩 모델
    )
    
    # 벡터 저장소 생성 (청크를 임베딩해서 저장)
    vectorstore = Chroma.from_documents(
        documents=chunks,
        embedding=embeddings,
        persist_directory="./chroma_db"  # 로컬에 영구 저장
    )
    
    print("✅ 벡터 저장소 생성 완료!")
    print(f"저장된 벡터 수: {vectorstore._collection.count()}")

    ▲ 텍스트 청크가 임베딩 벡터로 변환되어 벡터 데이터베이스에 저장되는 과정 — 유사도 기반 검색의 핵심 메커니즘

    4단계: 검색기(Retriever) 설정

    # 기존에 저장된 벡터 저장소 불러오기 (재시작 시)
    vectorstore = Chroma(
        persist_directory="./chroma_db",
        embedding_function=embeddings
    )
    
    # 검색기 생성 — 질문과 유사한 상위 4개 청크 반환
    retriever = vectorstore.as_retriever(
        search_type="similarity",  # 유사도 기반 검색
        search_kwargs={"k": 4}     # 상위 4개 문서 반환
    )
    
    # 검색 테스트
    test_query = "RAG 시스템의 장점은 무엇인가요?"
    results = retriever.invoke(test_query)
    for i, doc in enumerate(results):
        print(f"\n--- 검색 결과 {i+1} ---")
        print(doc.page_content[:200])

    5단계: RAG 체인(Chain) 완성

    드디어 마지막 단계입니다! 검색기와 LLM을 연결해서 실제로 질문에 답하는 RAG 체인을 만들어볼게요. LangChain의 LCEL(LangChain Expression Language) 문법을 사용하면 복잡한 파이프라인도 간단하게 구성할 수 있습니다.

    from langchain_openai import ChatOpenAI
    from langchain_core.prompts import ChatPromptTemplate
    from langchain_core.runnables import RunnablePassthrough
    from langchain_core.output_parsers import StrOutputParser
    
    # LLM 초기화
    llm = ChatOpenAI(
        model="gpt-4o-mini",
        temperature=0  # 일관된 답변을 위해 temperature를 낮게
    )
    
    # 프롬프트 템플릿 — 이 부분이 RAG 품질을 좌우합니다
    prompt_template = """
    당신은 주어진 컨텍스트를 바탕으로 질문에 답하는 전문 어시스턴트입니다.
    
    컨텍스트:
    {context}
    
    질문: {question}
    
    답변 지침:
    - 반드시 제공된 컨텍스트에 기반하여 답변하세요.
    - 컨텍스트에 없는 내용은 "제공된 문서에서 해당 정보를 찾을 수 없습니다"라고 명시하세요.
    - 명확하고 구체적으로 답변하세요.
    
    답변:
    """
    
    prompt = ChatPromptTemplate.from_template(prompt_template)
    
    # 문서 포맷팅 함수
    def format_docs(docs):
        return "\n\n".join(doc.page_content for doc in docs)
    
    # RAG 체인 구성 (LCEL 방식)
    rag_chain = (
        {
            "context": retriever | format_docs,
            "question": RunnablePassthrough()
        }
        | prompt
        | llm
        | StrOutputParser()
    )
    
    # 실제 질문 테스트
    question = "RAG 시스템에서 임베딩의 역할은 무엇인가요?"
    response = rag_chain.invoke(question)
    
    print("\n🎉 RAG 응답:")
    print(response)

    ⚠️ 삽질 기록 — 실제로 겪었던 문제들

    솔직히 말씀드리면, 처음 구현할 때 꽤 헤맸습니다. 비슷한 상황에서 도움이 될 것 같아서 공유드릴게요.

    문제 1: 한국어 문서 청킹이 이상하게 됨

    영어 문서는 잘 되는데 한국어 문서를 넣었더니 청킹이 이상하게 잘리더라고요. RecursiveCharacterTextSplitter의 기본 separators가 영어 기준이라서 그런 거였어요. 한국어에는 문장 끝 기준을 추가해주면 훨씬 나아집니다.

    # 한국어에 최적화된 청킹 설정
    text_splitter = RecursiveCharacterTextSplitter(
        chunk_size=800,
        chunk_overlap=150,
        separators=["\n\n", "\n", "。", ".", "! ", "? ", " ", ""]  # 한국어 구분자 추가
    )

    문제 2: 검색 결과가 관련 없는 내용을 가져옴

    질문과 전혀 관련 없는 청크가 상위에 올라오는 경우가 있었는데요. 이때는 MMR(Maximal Marginal Relevance) 검색 방식을 써보세요. 다양성과 관련성을 동시에 고려해줘서 훨씬 나은 결과가 나오더라고요.

    # MMR 방식으로 검색기 설정
    retriever = vectorstore.as_retriever(
        search_type="mmr",
        search_kwargs={
            "k": 4,           # 최종 반환 문서 수
            "fetch_k": 20,    # 후보로 가져올 문서 수
            "lambda_mult": 0.5  # 0: 다양성 최대, 1: 관련성 최대
        }
    )

    문제 3: ChromaDB 재시작 후 데이터 날아감

    처음에 persist_directory를 안 지정했다가 서버 재시작하고 나서 임베딩 데이터가 다 사라졌었어요. 정말 황당했죠. 반드시 영구 저장 경로를 지정하세요. 위 코드에는 이미 반영되어 있으니 그대로 따라하시면 됩니다.

    결과 검증 — 실제로 잘 동작하는지 확인하기

    구현이 끝났으면 제대로 동작하는지 확인해봐야죠. 간단한 평가 코드를 만들어서 테스트해봤습니다.

    # 다양한 질문으로 RAG 시스템 테스트
    test_questions = [
        "문서의 주요 내용을 요약해주세요.",
        "구체적인 설정 방법을 알려주세요.",
        "문서에 없는 내용을 물어보면?"  # 할루시네이션 방지 테스트
    ]
    
    print("=" * 60)
    print("RAG 시스템 검증 테스트")
    print("=" * 60)
    
    for q in test_questions:
        print(f"\n❓ 질문: {q}")
        print("-" * 40)
        
        # 어떤 청크가 검색됐는지도 확인
        retrieved_docs = retriever.invoke(q)
        print(f"📚 검색된 청크 수: {len(retrieved_docs)}")
        
        response = rag_chain.invoke(q)
        print(f"💬 답변: {response}")
        print("=" * 60)

    ▲ RAG 시스템 테스트 결과 화면 — 각 질문에 대해 검색된 청크와 생성된 응답을 확인할 수 있습니다

    세 번째 테스트 질문처럼 문서에 없는 내용을 물어봤을 때, 잘 구성된 RAG 시스템은 "해당 정보를 문서에서 찾을 수 없습니다"라고 답해야 합니다. 이게 안 되면 프롬프트 템플릿을 좀 더 강하게 제약해줘야 해요.

    더 나아가기 — RAG 품질을 높이는 방법들

    기본 RAG는 이제 동작하는데, 실제 프로덕션 환경에서 쓰려면 몇 가지 더 고려해야 할 것들이 있습니다.

    • 청크 크기 최적화: 문서 종류에 따라 최적 chunk_size가 달라요. 코드 문서는 크게, FAQ 형태는 작게
    • Re-ranking(재순위화): 검색된 문서를 다시 한번 관련성 순으로 정렬하는 기법. 검색 품질이 크게 향상됩니다
    • 하이브리드 검색: 벡터 유사도 검색과 키워드 기반 BM25 검색을 함께 사용하면 더 정확해져요
    • 메타데이터 필터링: 문서 출처, 날짜, 카테고리 등 메타데이터를 활용해 검색 범위를 좁힐 수 있습니다
    • 대화 기록 관리: 멀티턴 대화를 위해 ConversationalRetrievalChain 활용 고려

    ▲ 기본 RAG와 고도화된 RAG 파이프라인 비교 — Re-ranking, 하이브리드 검색 등 품질 향상 기법들

    자주 묻는 질문 (FAQ)

    Q. OpenAI API 없이 로컬에서만 RAG를 구축할 수 있나요?

    네, 가능합니다. 임베딩은 sentence-transformers 라이브러리의 오픈소스 모델을, LLM은 Ollama로 로컬 모델을 사용하면 돼요. 비용 없이 완전히 로컬에서 돌릴 수 있어요. 이 부분은 다음 글에서 자세히 다룰 예정입니다.

    Q. 문서가 수만 개인데 ChromaDB로 충분한가요?

    소규모~중규모는 ChromaDB로 충분합니다. 수십만 개 이상의 대규모 문서라면 Pinecone, Weaviate, pgvector 같은 전용 벡터 데이터베이스를 고려해보세요.

    Q. 임베딩 모델을 바꾸면 기존 벡터 데이터도 다시 만들어야 하나요?

    네, 맞습니다. 임베딩 모델이 달라지면 벡터 공간이 달라지기 때문에 기존 데이터를 새 모델로 전부 다시 임베딩해야 합니다. 처음 모델 선택을 신중하게 하는 게 좋아요.

    마무리 — 오늘 배운 것 정리

    오늘 LangChain RAG 시스템을 처음부터 구축해봤는데요, 정리하면 이렇습니다.

    1. 문서를 로드하고 적절한 크기로 청킹
    2. 임베딩 모델로 텍스트를 벡터로 변환해서 벡터 저장소에 저장
    3. 사용자 질문을 임베딩해서 유사 문서 검색
    4. 검색된 문서를 컨텍스트로 LLM에게 전달해서 응답 생성

    처음엔 개념이 복잡해 보여도, 막상 코드로 짜보면 생각보다 직관적이에요. LangChain이 복잡한 파이프라인을 상당히 추상화해줘서 핵심 로직에 집중할 수 있거든요.

    다음 글에서는 OpenAI 없이 Ollama로 완전 로컬 RAG 시스템을 구축하는 방법을 다룰 예정입니다. API 비용 걱정 없이 사내 문서를 분석하고 싶으신 분들께 도움이 될 거예요. 궁금하신 점은 댓글로 남겨주세요! 🎉

  • [AI] RAG 실전 구현 가이드: LLM 환각 현상 줄이고 최신 정보 활용하기

    [AI] RAG 실전 구현 가이드: LLM 환각 현상 줄이고 최신 정보 활용하기

    RAG 실전 구현 가이드: LLM 환각 현상 줄이고 최신 정보 활용하기

    안녕하세요, 13년차 서버실의 인프라 엔지니어입니다. 요즘 인공지능(AI) 분야는 정말 눈 깜짝할 사이에 발전하고 있죠. 특히 대규모 언어 모델(Large Language Model, LLM)은 놀라운 성능으로 우리의 삶과 업무 방식을 바꾸고 있습니다. 그런데 말입니다. 아무리 똑똑한 LLM이라도 가끔은 엉뚱한 소리를 하거나, 오래된 정보만을 바탕으로 답변할 때가 있어요. 일명 LLM의 환각(Hallucination) 현상인데요. 혹시 이런 경험, 직접 해보신 적 있으신가요?

    저도 홈랩에서 LLM을 이것저것 만져보면서 느낀 건데, 최신 정보를 반영하거나 특정 도메인의 깊이 있는 지식을 묻기에는 한계가 명확하더라고요. 마치 최신 뉴스를 전혀 모르는 옛날 사람에게 질문하는 느낌이랄까요? 그래서 오늘은 이 문제를 해결하고 LLM의 답변 정확도를 높이는 Retrieval Augmented Generation(RAG) 기술을 직접 구현해보는 가이드를 준비했습니다.

    RAG는 LLM이 답변을 생성하기 전에 외부 지식 소스에서 관련 정보를 검색하여 이를 바탕으로 답변하도록 하는 기술이에요. 쉽게 말해, LLM에게 “책을 보고 답하세요!”라고 가이드하는 것과 같죠. 이 글을 통해 RAG가 뭔지, 왜 필요한지, 그리고 LangChain과 벡터 데이터베이스를 활용하여 어떻게 실전 구현하는지 단계별로 알아보겠습니다. 자, 그럼 시작해 볼까요?

    RAG 시스템의 전체적인 흐름을 보여주는 아키텍처 다이어그램입니다.

    1. LLM 환각 현상, 왜 발생할까요? 그리고 RAG가 답입니다!

    LLM은 방대한 텍스트 데이터를 학습하여 언어 패턴을 익히고 이를 기반으로 텍스트를 생성합니다. 하지만 학습 데이터는 특정 시점에 고정되기 때문에 최신 정보를 알지 못하죠. 또한, 학습 데이터에 없는 특정 분야의 전문 지식이나 개인화된 정보에 대한 답변은 어렵습니다. 이러한 문제들이 복합적으로 작용하여 LLM의 환각 현상이 발생하게 됩니다.

    환각 현상(Hallucination)은 LLM이 사실이 아니거나 학습 데이터에 근거하지 않은 정보를 마치 사실인 것처럼 그럴듯하게 만들어내는 현상을 뜻해요. 예를 들어, 존재하지 않는 논문을 인용하거나, 잘못된 통계를 제시하는 식이죠. 이는 LLM의 신뢰도를 크게 떨어뜨리는 요인이 됩니다.

    여기서 Retrieval Augmented Generation(RAG)이 등장합니다. RAG는 LLM의 이러한 한계를 극복하기 위한 강력한 솔루션이에요. RAG는 다음과 같은 두 가지 핵심 과정을 통해 작동합니다:

    • Retrieval (검색): 사용자의 질문과 관련된 정보를 외부 지식 소스(문서, 데이터베이스 등)에서 검색합니다.
    • Augmentation (증강): 검색된 정보를 사용자의 질문과 함께 LLM의 입력(프롬프트)으로 제공합니다.

    LLM은 이렇게 제공된 문맥(Context)을 바탕으로 답변을 생성하기 때문에, 훨씬 더 정확하고 최신 정보를 반영한 답변을 할 수 있게 되죠. 마치 똑똑한 AI 비서에게 필요한 자료를 미리 찾아주고 질문하는 것과 같다고 생각하시면 됩니다. 🎉

    2. RAG 구현을 위한 핵심 요소: LangChain과 벡터 데이터베이스

    RAG 시스템을 구축하기 위해서는 몇 가지 핵심 기술 요소가 필요해요. 제가 이번에 사용해 본 툴들은 다음과 같습니다.

    • LangChain: LLM 애플리케이션 개발을 위한 프레임워크예요. LLM과의 연동, 데이터 처리, 외부 도구 연결 등을 쉽게 할 수 있도록 다양한 모듈과 추상화를 제공합니다. RAG 파이프라인 구축에 필수적인 역할을 하죠.
    • 벡터 데이터베이스 (Vector Database): 텍스트 데이터를 벡터(수치형 배열)로 변환하여 저장하고, 유사도 검색을 효율적으로 수행하는 데이터베이스예요. LangChain RAG의 ‘Retrieval’ 단계에서 질문과 관련된 문서를 빠르게 찾아오는 데 핵심적인 역할을 합니다. ChromaDB, FAISS, Pinecone 등이 대표적인데, 저는 이번에 ChromaDB를 사용해봤어요. 설치와 사용이 정말 간편하더라고요.
    • 임베딩 모델 (Embedding Model): 텍스트를 벡터로 변환하는 역할을 해요. OpenAI의 text-embedding-ada-002나 Hugging Face의 다양한 오픈소스 모델 등을 사용할 수 있습니다.

    이 요소들을 조합하면 RAG 파이프라인을 효과적으로 구축할 수 있어요. LangChain은 이러한 각 구성 요소를 연결하고 조율하는 역할을 담당하며, 벡터 데이터베이스는 방대한 지식 소스에서 필요한 정보를 신속하게 찾아오는 ‘창고’ 역할을 하는 셈이죠.

    LangChain을 이용하여 ChromaDB와 LLM을 연결하는 RAG 파이프라인의 구성도입니다.

    3. RAG 실전 구현: 단계별 가이드 (Python & LangChain)

    이제 실제로 LLM RAG 시스템을 구현해 봅시다. 저는 개인적으로 자주 사용하는 Python 환경에서 LangChain 라이브러리를 이용할 예정입니다. 몇 가지 예제 문서를 준비해서 진행해 볼 테니까요. (실제로는 여러분의 문서나 데이터를 사용하시면 됩니다.)

    3.1. 필요한 라이브러리 설치

    먼저 필요한 라이브러리들을 설치합니다. 저는 langchain, chromadb, openai (API 키가 필요합니다), tiktoken 등을 사용해요.

    pip install langchain openai chromadb tiktoken python-dotenv
    

    3.2. 환경 설정 (API 키 로드)

    OpenAI API 키를 환경 변수로 설정하는 게 안전합니다. .env 파일을 생성하고 API 키를 저장해두세요.

    # .env 파일 내용
    OPENAI_API_KEY=your_openai_api_key_here
    

    Python 코드에서는 dotenv 라이브러리를 사용해 이 키를 로드합니다.

    import os
    from dotenv import load_dotenv
    
    load_dotenv()  # .env 파일에서 환경 변수 로드
    
    # 이제 os.getenv("OPENAI_API_KEY") 등으로 API 키에 접근 가능합니다.
    

    3.3. 문서 로드 및 분할 (Document Loading & Splitting)

    RAG의 첫 단계는 외부 지식 소스를 LLM이 이해할 수 있는 형태로 준비하는 거예요. LangChain은 다양한 포맷의 문서를 로드하는 기능을 제공합니다. 저는 간단한 텍스트 파일(.txt)을 사용해 보겠습니다.

    문서를 불러온 후에는 LLM이 처리하기 좋은 크기로 분할(Chunking)해야 해요. 너무 크면 문맥을 놓칠 수 있고, 너무 작으면 정보의 맥락이 끊어질 수 있거든요. RecursiveCharacterTextSplitter를 주로 사용하는데, 재귀적으로 텍스트를 분할하면서 지정된 크기를 유지하도록 도와줍니다.

    from langchain.document_loaders import TextLoader
    from langchain.text_splitter import RecursiveCharacterTextSplitter
    
    # 1. 문서 로드
    loader = TextLoader("path/to/your/document.txt", encoding="utf-8")
    documents = loader.load()
    
    # 2. 문서 분할
    text_splitter = RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=200)
    chunks = text_splitter.split_documents(documents)
    
    print(f"총 {len(chunks)}개의 청크로 분할되었습니다.")
    

    3.4. 벡터 데이터베이스 설정 및 데이터 임베딩

    분할된 텍스트 청크를 벡터 데이터베이스에 저장해야 해요. 이 과정에서 임베딩 모델을 사용하여 각 텍스트 청크를 벡터로 변환합니다. 저는 OpenAI의 임베딩 모델을 사용하겠습니다.

    ChromaDB는 로컬에서 쉽게 사용할 수 있는 좋은 옵션이에요. Chroma.from_documents() 함수를 사용하면 문서 로딩, 임베딩, 벡터 DB 저장까지 한 번에 처리할 수 있어서 매우 편리합니다.

    from langchain_openai import OpenAIEmbeddings
    from langchain_community.vectorstores import Chroma
    
    # 임베딩 모델 설정 (OpenAI 사용 시 API 키 필요)
    embeddings = OpenAIEmbeddings()
    
    # ChromaDB 벡터 스토어 생성 및 문서 저장
    # persist_directory는 데이터를 디스크에 저장할 경로입니다.
    vectorstore = Chroma.from_documents(
        chunks,
        embeddings,
        persist_directory="./chroma_db"
    )
    
    print("벡터 데이터베이스에 문서가 성공적으로 저장되었습니다!")
    

    텍스트 문서를 임베딩하여 벡터로 변환 후 ChromaDB에 저장하는 과정입니다.

    3.5. RAG 체인 구성 및 질문 실행

    이제 마지막 단계예요. 검색기(Retriever)를 생성하고, 이를 LLM과 연결하여 RAG 체인을 완성합니다. LangChain의 create_stuff_documents_chain과 create_retrieval_chain을 사용하면 이 과정을 쉽게 구현할 수 있거든요.

    Retriever는 벡터 데이터베이스에서 질문과 가장 유사한 청크들을 검색하는 역할을 해요. vectorstore.as_retriever()로 간단하게 생성할 수 있습니다.

    from langchain_openai import ChatOpenAI
    from langchain.chains import create_retrieval_chain
    from langchain.chains.combine_documents import create_stuff_documents_chain
    
    # LLM 모델 설정 (GPT-3.5 Turbo 사용 예시)
    llm = ChatOpenAI(model="gpt-3.5-turbo", temperature=0.7)
    
    # RAG 프롬프트 템플릿
    from langchain_core.prompts import ChatPromptTemplate
    
    prompt = ChatPromptTemplate.from_template(
        """
        다음은 관련 정보를 검색한 내용입니다.
        이 정보를 바탕으로 질문에 답해주세요.
        만약 정보를 찾을 수 없다면, 정보가 없다고 말해주세요.
    
        컨텍스트:
        {context}
    
        질문:
        {input}
        """
    )
    
    # 문서 체인 생성
    document_chain = create_stuff_documents_chain(llm, prompt)
    
    # 검색기(Retriever) 생성
    retriever = vectorstore.as_retriever()
    
    # 검색 및 답변 체인 생성
    retrieval_chain = create_retrieval_chain(retriever, document_chain)
    
    # 질문 실행
    question = "RAG 기술의 주요 목적은 무엇인가요?"
    response = retrieval_chain.invoke({"input": question})
    
    print(f"질문: {question}")
    print(f"답변: {response['answer']}")
    

    코드를 실행하면, 준비된 문서에서 관련 정보를 검색하여 LLM이 답변을 생성하는 것을 볼 수 있어요. 제가 준비한 문서에는 RAG의 목적에 대한 내용이 포함되어 있었기 때문에, LLM이 정확하게 답변해 줄 겁니다. 와, 드디어 됐다! 🎉

    4. 주의사항 및 트러블슈팅 ⚠️

    RAG 구현은 생각보다 간단해 보이지만, 실제 운영 환경에서는 몇 가지 주의해야 할 점들이 있어요. 저도 몇 번의 삽질을 통해 배운 내용들을 공유해 드릴게요.

    • 청크 크기 및 오버랩: 청크 크기(chunk_size)와 오버랩(chunk_overlap) 설정은 검색 성능에 큰 영향을 미칩니다. 너무 작으면 문맥이 끊기고, 너무 크면 관련 없는 정보가 많이 포함될 수 있어요. 다양한 값을 테스트하며 최적의 값을 찾아야 합니다. 제 경험상 500~1000 토큰 사이의 chunk_size와 100~200 토큰 사이의 chunk_overlap이 무난하더라고요.
    • 임베딩 모델 선택: 어떤 임베딩 모델을 사용하느냐에 따라 검색 정확도가 달라집니다. OpenAI 모델이 성능이 좋지만 비용이 발생하죠. Hugging Face의 오픈소스 모델 중에서도 좋은 성능을 내는 모델들이 많으니, 필요에 따라 비교해보는 게 좋습니다.
    • 벡터 데이터베이스 선택: ChromaDB는 로컬 테스트에 좋지만, 대규모 서비스에서는 확장성이나 성능 면에서 FAISS, Milvus, Pinecone 같은 전문 벡터 DB를 고려해야 할 수 있어요.
    • 프롬프트 엔지니어링: LLM에게 어떤 지시를 내리느냐에 따라 답변의 품질이 크게 달라집니다. 컨텍스트를 어떻게 활용하고, 어떤 상황에서 “모른다”고 답해야 하는지 등을 명확하게 지시하는 프롬프트 작성 능력이 중요해요.
    • 성능 저하: 문서의 양이 많아질수록 검색 속도가 느려질 수 있어요. 이 경우, 벡터 DB의 인덱싱 전략을 최적화하거나, 더 효율적인 검색 알고리즘을 도입하는 등의 성능 개선 작업이 필요합니다.

    가장 흔하게 겪는 문제는 “왜 관련 없는 정보가 검색될까?” 또는 “내가 찾는 정보가 안 나와!” 하는 경우인데요. 이때는 보통 임베딩 모델이나 청크 크기 설정을 의심해 봐야 합니다. 저도 처음에 이걸 몰라서 한참 헤맸다니까요. 😂

    RAG 시스템의 질문 답변 정확도, 검색 속도 등을 모니터링하는 대시보드 예시입니다.

    5. 검증 및 결과 확인

    구현한 RAG 시스템의 성능을 확인하는 건 매우 중요해요. 단순히 질문에 답변이 나오는지 확인하는 것을 넘어, 얼마나 정확하고 관련성 높은 답변을 생성하는지 평가해야 합니다.

    가장 간단한 방법은 다양한 질문을 던져보고 답변의 품질을 육안으로 평가하는 거예요. 하지만 더 체계적인 평가를 위해서는 다음과 같은 지표들을 고려할 수 있습니다.

    • 정확도 (Accuracy): 생성된 답변이 사실에 부합하는가?
    • 관련성 (Relevance): 생성된 답변이 질문과 얼마나 관련이 있는가?
    • 최신성 (Freshness): 답변이 최신 정보를 반영하고 있는가?
    • 환각 방지 (Hallucination Reduction): 사실이 아닌 정보를 생성하지 않는가?

    LangChain에서는 이러한 평가를 자동화하기 위한 라이브러리들도 제공하고 있어요. 예를 들어, 특정 질문에 대해 예상되는 답변과 실제 생성된 답변을 비교하거나, 검색된 문서의 관련성을 평가하는 등의 방식으로 시스템을 검증할 수 있습니다.

    직접 테스트해보니, RAG를 적용했을 때 LLM의 답변이 훨씬 더 구체적이고 신뢰할 수 있게 되었어요. 특히 전문 분야나 최신 정보에 대한 질문에서 그 차이가 두드러지더라고요. 👍

    6. 마무리하며: RAG, LLM 활용의 새로운 지평을 열다

    오늘은 LLM의 환각 현상을 줄이고 최신 정보를 활용하기 위한 RAG(Retrieval Augmented Generation) 기술에 대해 알아봤습니다. LangChain과 벡터 데이터베이스를 활용하여 RAG 파이프라인을 직접 구현해보는 과정을 통해, LLM의 한계를 극복하고 더욱 강력하고 신뢰할 수 있는 AI 애플리케이션을 만들 수 있다는 걸 확인했어요.

    RAG는 단순히 LLM의 성능을 보완하는 것을 넘어, LLM이 특정 도메인의 전문 지식을 갖추고 실시간 정보를 반영하도록 만드는 핵심 기술이에요. 앞으로 RAG는 고객 지원 챗봇, 내부 문서 검색 시스템, 개인 맞춤형 정보 제공 등 정말 다양한 분야에서 활용될 거예요. 저도 이 기술을 활용해서 홈랩 프로젝트에 적용해볼 계획이거든요. 기대되지 않으신가요?

    RAG 기술이 적용될 수 있는 다양한 산업 분야와 서비스 예시입니다.

    이번 글이 RAG 기술에 대한 이해를 높이고, 직접 구현해보는 데 도움이 되었기를 바랍니다. 다음 글에서는 RAG 성능을 더욱 향상시킬 수 있는 고급 기법들에 대해 다뤄볼 예정이니, 많은 기대 부탁드립니다!

    핵심 요약:

    • LLM 환각 현상: LLM이 사실이 아닌 정보를 생성하는 문제
    • RAG (Retrieval Augmented Generation): 외부 정보 검색 후 LLM 답변 생성
    • 핵심 도구: LangChain (프레임워크), 벡터 DB (ChromaDB 등), 임베딩 모델
    • 구현 절차: 문서 로드/분할 → 임베딩/벡터 DB 저장 → RAG 체인 구성 → 질문 실행
    • 주의사항: 청크 크기, 임베딩 모델, 프롬프트 엔지니어링 등

    궁금한 점이 있다면 언제든지 댓글로 남겨주세요!