본문 바로가기
IT/AI

[AI] LlamaIndex RAG 시스템 구축 실패 사례: 흔한 문제와 디버깅 전략

by 수누다 2026. 6. 25.

[AI/LLM] LlamaIndex RAG 시스템 구축 실패 사례: 흔한 문제와 디버깅 전략

LlamaIndex RAG 시스템을 처음 붙일 때 많은 분들이 비슷한 지점에서 막히시더라고요. 문서는 잘 넣은 것 같은데 답이 엉뚱하게 나오고, 로그를 봐도 어디서 망가졌는지 감이 안 오고, 심지어 AI 에이전트(Agent) 문제인 줄 알았는데 알고 보니 검색 단계가 이미 틀어져 있던 경우도 많습니다. 저도 홈랩에서 이것저것 붙여 보면서 삽질 좀 했습니다 ㅎㅎ 처음엔 모델 탓인가 싶었는데, 실제로는 데이터 적재(Ingestion, 문서 수집 및 변환), 청킹(Chunking, 문서 분할), 검색(Retrieval, 관련 문맥 찾기), 응답 합성(Response Synthesis, 답변 생성) 중 하나가 어긋난 경우가 대부분이었습니다.

이번 글은 제품 홍보나 멋진 데모가 아니라, LlamaIndex RAG를 구성하다가 망했던 패턴을 정리한 실패 사례 중심 글입니다. 특히 RAG 시스템 문제, LlamaIndex 오류, RAG 디버깅, AI 에이전트 실패를 한 번에 점검할 수 있는 흐름으로 정리해 보겠습니다. 혹시 "문서는 넣었는데 왜 이렇게 못 찾지?" 같은 경험 있으신가요? 그거 생각보다 정상입니다. 중요한 건 감으로 고치는 게 아니라, 단계별로 끊어서 확인하는 겁니다.

LlamaIndex RAG 전체 아키텍처를 보여주는 홈랩 다이어그램

문서 수집부터 청킹, 벡터 저장, 검색, 응답 생성까지 흐름을 한눈에 보는 개요 이미지입니다.

LlamaIndex RAG를 쉽게 말하면 어디서 망가질까요?

쉽게 말해 RAG(Retrieval-Augmented Generation, 검색 증강 생성)는 "모델이 원래 알고 있는 것"에만 기대지 않고, 질문이 들어오면 관련 문서를 먼저 찾아서 그 문맥을 바탕으로 답하게 만드는 구조입니다. LlamaIndex는 이 과정을 구성하기 쉽게 만들어 주는 프레임워크 쪽에 가깝고요.

제가 직접 해보니, 실패 지점은 생각보다 단순했습니다.

  • 문서 적재 단계: 파일은 읽었는데 내용이 이상하게 잘렸습니다.
  • 인덱싱(Indexing, 검색용 구조화): 벡터 저장은 됐는데 실제 검색 품질이 낮았습니다.
  • 리트리버(Retriever, 관련 문맥 검색기): 질문과 맞는 조각을 못 가져왔습니다.
  • 응답 합성: 검색은 맞았는데 모델이 답변을 엉성하게 만들었습니다.
  • 영속화(Persistence, 디스크 저장): 재시작 후 이전 상태가 사라지거나 오래된 인덱스를 계속 읽었습니다.

여기서 중요한 포인트! LlamaIndex RAG는 한 덩어리처럼 보이지만, 실제 디버깅은 단계별 분리 진단이 핵심입니다. 검색이 틀렸는데 프롬프트만 계속 고치면 시간만 날립니다.

실패를 줄이는 기본 설계: 먼저 관측 가능한 구조로 만드세요

저는 예전엔 일단 돌아가게만 만들고 나중에 로그를 붙였었는데요, 그 방식은 RAG에서 진짜 비효율적이더라고요. 처음부터 "문서가 어떻게 잘렸는지", "질문에 대해 어떤 노드(Node, 검색 가능한 문서 조각)가 선택됐는지", "최종 답변이 어떤 근거를 썼는지"가 보여야 합니다.

구간 자주 보이는 증상 실제 원인 후보 우선 확인할 것
문서 로딩 파일은 읽히는데 답변이 비어 있음 본문 추출 실패, 인코딩 문제, 예상과 다른 폴더 로드된 문서 수와 본문 샘플
청킹 검색 결과가 너무 짧거나 문맥이 끊김 chunk_size 과소, overlap 부족 분할된 노드 샘플
검색 관련 없는 문서가 자주 나옴 질문-문서 표현 불일치, 메타데이터 필터 오류 retriever 결과 목록
응답 생성 찾아놓고도 엉뚱한 답변 프롬프트 제약, 컨텍스트 과다/과소 source nodes와 최종 응답 비교
저장/재로딩 재실행 후 결과가 달라짐 인메모리 상태 의존, persist 누락 저장 경로와 로딩 경로

LlamaIndex RAG 실전 구현: 최소 구성부터 시작하기

처음부터 벡터 DB(Vector Database, 벡터 저장소)까지 크게 벌이지 말고, 최소한의 로컬 흐름으로 시작하는 걸 추천드립니다. 이유는 간단합니다. 실패 지점을 줄여야 하거든요.

  1. 문서를 읽는다.
  2. 인덱스를 만든다.
  3. 리트리버와 쿼리 엔진을 분리해서 테스트한다.
  4. 결과가 괜찮으면 그다음에 영속화와 외부 저장소를 붙인다.

1. 설치와 기본 실행

python -m venv .venv
source .venv/bin/activate
pip install llama-index

여기서는 버전 핀을 일부러 박지 않았습니다. 글의 목적이 특정 릴리스 사용기가 아니라, 디버깅 흐름 자체에 있기 때문입니다.

2. 가장 단순한 인덱스 생성

from llama_index.core import SimpleDirectoryReader, VectorStoreIndex

# data/ 폴더 아래 문서를 읽습니다.
documents = SimpleDirectoryReader("data").load_data()

# 가장 단순한 형태의 인덱스를 만듭니다.
index = VectorStoreIndex.from_documents(documents)

# 검색과 응답 생성을 분리해서 확인할 수 있도록 둘 다 만듭니다.
retriever = index.as_retriever()
query_engine = index.as_query_engine()

question = "장애 대응 절차를 요약해줘"
retrieved_nodes = retriever.retrieve(question)
response = query_engine.query(question)

print("[RETRIEVED NODES]")
for i, node in enumerate(retrieved_nodes, start=1):
    print(f"--- node {i} ---")
    print(node.text[:300])

print("[FINAL RESPONSE]")
print(response)

이 코드에서 핵심은 retriever와 query_engine을 따로 본다는 점입니다. 처음엔 이게 뭔가 싶었는데, 실제로 써보니까 이 분리가 디버깅 시간을 확 줄여주더라고요.

3. 청킹을 직접 통제하는 적재 파이프라인

from llama_index.core import Document
from llama_index.core.ingestion import IngestionPipeline
from llama_index.core.node_parser import SentenceSplitter
from llama_index.core.extractors import TitleExtractor

sample_docs = [
    Document(text="장애 조치 문서 예시입니다. 원인 분석, 임시 조치, 영구 조치가 포함됩니다."),
]

pipeline = IngestionPipeline(
    transformations=[
        SentenceSplitter(chunk_size=256, chunk_overlap=32),
        TitleExtractor(),
    ]
)

nodes = pipeline.run(documents=sample_docs)

for i, node in enumerate(nodes, start=1):
    print(f"NODE {i}")
    print(node.text)
    print(node.metadata)

청킹은 진짜 중요합니다. 저는 처음에 chunk_size를 너무 작게 잡아서 문서 문맥이 다 잘려 나갔었는데, 검색 결과는 그럴듯하게 보여도 실제 답변은 자꾸 뜬구름 잡더라고요.

LlamaIndex RAG 문서 청킹과 리트리버 동작을 설명하는 이미지

문서가 여러 조각으로 분할되고 질문에 따라 관련 노드가 선택되는 구조를 설명하는 이미지입니다.

4. 캐시와 저장 경로를 명시하기

from llama_index.core import StorageContext, load_index_from_storage

# 인덱스를 저장합니다.
index.storage_context.persist(persist_dir="./storage")

# 저장된 인덱스를 다시 로드합니다.
storage_context = StorageContext.from_defaults(persist_dir="./storage")
loaded_index = load_index_from_storage(storage_context)

loaded_query_engine = loaded_index.as_query_engine()
print(loaded_query_engine.query("장애 대응 절차를 요약해줘"))

여기서 많이 터집니다. LlamaIndex는 기본적으로 인메모리(in-memory, 메모리 상주) 상태를 많이 쓰기 때문에, 재시작 후에도 같은 동작을 기대한다면 저장과 로딩 경로를 명확히 관리해야 합니다.

⚠️ 흔한 LlamaIndex 오류와 RAG 디버깅 전략

이제부터는 제가 실제로 자주 밟았던 실패 패턴입니다. 증상만 보면 비슷한데 원인은 꽤 다릅니다.

문제 1. 문서는 읽었는데 답변이 너무 일반적입니다

이 경우 많은 분이 모델 성능부터 의심하시는데요, 사실은 검색 단계에서 충분한 근거가 안 들어간 경우가 많습니다.

  • 문서가 너무 잘게 쪼개져 핵심 문맥이 끊겼는지 봅니다.
  • 질문과 문서 표현이 다르면 검색 품질이 확 떨어집니다.
  • 리트리버가 뽑은 노드 본문을 직접 읽어 봅니다.

제가 직접 해보니, 최종 답변보다 retrieved_nodes 출력을 먼저 보는 습관이 정말 중요했습니다. 답이 이상하면 먼저 "뭘 찾았는가"를 봐야 합니다.

문제 2. 재색인했는데 결과가 안 바뀝니다

이건 캐시나 저장 디렉터리 때문에 생기는 경우가 많습니다. 특히 테스트하면서 같은 경로를 계속 재사용하면 오래된 데이터가 남아 있을 수 있습니다.

rm -rf ./storage
rm -rf ./pipeline_storage

물론 운영 환경에서는 이렇게 단순 삭제하면 안 됩니다. 다만 로컬 검증 단계에서는 "정말 새로 인덱싱된 게 맞나?"를 확인하는 용도로 한 번쯤 초기화가 필요하더라고요.

문제 3. 검색은 맞는데 최종 답변이 엉뚱합니다

이건 응답 합성 단계 문제일 가능성이 높습니다. 쉽게 말해 모델이 가져온 컨텍스트를 잘 못 쓰는 거죠.

  • 너무 많은 컨텍스트를 넣어 핵심이 묻히지 않았는지 봅니다.
  • 프롬프트에서 근거 기반 답변을 강하게 요구하는지 봅니다.
  • 답변과 함께 source nodes를 출력해 비교합니다.

근데 여기서 중요한 포인트가 하나 있습니다. 검색 품질이 80인데 생성 품질이 20이면 프롬프트 손봐야 하고, 검색 품질이 20인데 생성만 만지면 계속 헛바퀴 돕니다.

문제 4. AI 에이전트 실패처럼 보이는데 사실 RAG 실패입니다

도구 호출(Tool Calling, 외부 기능 호출)이나 에이전트 라우팅이 문제인 줄 알았는데, 막상 까보면 검색된 문맥이 비어 있는 경우가 있습니다. 저도 처음엔 에이전트 흐름도를 한참 봤었는데요, 결국 원인은 리트리버였습니다. 그래서 저는 에이전트를 붙이기 전에 항상 아래 순서로 검증합니다.

  1. retriever.retrieve() 결과를 먼저 확인
  2. query_engine.query() 결과와 비교
  3. 그 다음에만 agent/tool 레이어 확인

문제 5. 운영 환경에서만 이상합니다

개발 환경에서는 한글 문서가 잘 검색됐는데 운영에서만 품질이 달라지는 경우도 있었습니다. 이런 건 대개 데이터셋 차이, 적재 타이밍 차이, 저장소 경로 차이, 혹은 문서 전처리 차이로 이어집니다. 이럴 때는 "운영 모델이 다르다" 같은 큰 가설보다, 실제로 어떤 문서가 들어갔는지를 먼저 비교하는 게 낫습니다.

검증 방법: 정답률보다 먼저 파이프라인을 눈으로 확인하세요

저는 RAG를 검증할 때 처음부터 화려한 평가 지표를 붙이지 않습니다. 물론 평가(Evaluation, 성능 측정)는 중요하지만, 초기에는 육안 검증이 훨씬 빠릅니다.

  1. 질문 10개를 만든다.
  2. 각 질문마다 검색된 노드 상위 결과를 저장한다.
  3. 최종 답변과 근거 문장을 같이 본다.
  4. 틀린 답변은 검색 실패인지 생성 실패인지 분류한다.
test_questions = [
    "장애 보고 절차는 어떻게 되나요?",
    "임시 조치와 영구 조치의 차이는 무엇인가요?",
    "점검 창구는 어디인가요?",
]

for question in test_questions:
    nodes = retriever.retrieve(question)
    answer = query_engine.query(question)

    print("=" * 40)
    print("Q:", question)
    print("[TOP NODES]")
    for node in nodes[:3]:
        print(node.text[:200])
    print("[ANSWER]")
    print(answer)

실제로 써보니까, 이 정도만 해도 어디가 문제인지 금방 보입니다. 드디어 됐다! 싶은 순간도 보통 여기서 오더라고요.

LlamaIndex RAG 검증 과정에서 검색 노드와 답변을 비교하는 대시보드 이미지

질문, 검색된 문서 조각, 최종 응답을 나란히 놓고 비교하는 검증 화면 예시입니다.

실패를 줄이는 운영 체크리스트

LlamaIndex RAG를 조금 오래 운영하다 보면, 결국 반복 확인 항목이 정해집니다. 저는 아래 체크리스트를 배포 전 마지막 관문처럼 씁니다.

  • 문서 수와 예상 파일 수가 맞는가
  • 청크 샘플을 3개 이상 직접 읽어 봤는가
  • 검색 결과 상위 노드가 질문 의도와 맞는가
  • 재시작 후에도 같은 인덱스를 로드하는가
  • 캐시와 저장 디렉터리 충돌이 없는가
  • 에이전트 문제로 보기 전에 검색 결과를 확인했는가
실패 유형 대응 우선순위 빠른 처방
답변이 너무 일반적 높음 청킹과 검색 결과부터 확인
재색인 후 결과 동일 높음 persist 경로와 캐시 정리 확인
운영에서만 품질 저하 중간 실제 적재 문서와 경로 비교
AI 에이전트 실패처럼 보임 높음 agent 이전에 retriever 단독 테스트

자주 묻는 질문: RAG 시스템 문제를 어디서부터 봐야 하나요?

Q1. LlamaIndex 오류가 나지 않아도 시스템이 틀릴 수 있나요?

네, 그게 더 흔합니다. 에러 없이도 잘못된 문맥을 검색하면 결과는 충분히 틀릴 수 있습니다. 그래서 RAG 디버깅은 예외 메시지보다 중간 산출물 확인이 더 중요합니다.

Q2. 청킹만 잘하면 해결되나요?

아닙니다. 청킹은 출발점일 뿐이고, 메타데이터, 저장 경로, 검색기 설정, 응답 합성까지 같이 봐야 합니다.

Q3. AI 에이전트 실패와 RAG 실패는 어떻게 구분하나요?

에이전트를 떼고 retriever와 query_engine을 각각 테스트해 보시면 됩니다. 에이전트를 제거했는데도 답이 틀리면 대개 RAG 쪽입니다.

마무리: 화려한 튜닝보다 관측 가능한 구조가 먼저입니다

이번 글에서 말씀드리고 싶었던 건 딱 하나입니다. LlamaIndex RAG는 잘 만들면 강력하지만, 실패했을 때는 "어디서 틀어졌는지 보이게" 설계해야 한다는 점입니다. 저도 처음엔 모델만 계속 바꿔 봤었는데, 결국 해결은 대부분 로그, 노드 출력, 저장 경로 확인에서 나왔습니다. 사실 이런 건 멋이 없거든요. 근데 운영에서는 이런 기본기가 제일 셉니다.

다음 글에서는 검색 품질이 낮을 때 메타데이터 필터링과 질문 정규화 전략을 어떻게 붙이는지 다뤄볼 예정입니다. 이전 글 참고 형식으로 이어서 보시면 흐름 잡기 편하실 겁니다.

LlamaIndex RAG 디버깅 체크리스트와 실패 유형 요약 이미지

실패 유형별 점검 순서와 대응 우선순위를 한 장으로 정리한 요약 이미지입니다.

참고한 공식 문서