티스토리 뷰

반응형

RAG 품질이 안 나오는 이유 — chunk 크기, overlap, k값, 프롬프트 문제를 어떻게 봐야 하나

첫 번째 RAG를 만들고 나면 보통 되게 비슷한 순간이 와요.

처음 질문 하나는 잘 맞습니다.
그래서 속으로 살짝 신나요.
“오… 됐다.”

근데 두 번째, 세 번째 질문부터 이상해집니다.

어떤 질문에는 진짜 그럴듯하게 답하는데,
어떤 질문에는 엉뚱한 chunk를 물고 오고,
가끔은 문서를 가져와 놓고도 답을 이상하게 하고,
또 어떤 경우엔 문서 안에 답이 있는데도 못 찾습니다.

이쯤 되면 많은 분이 바로 이렇게 생각해요.

“모델이 별론가?”
“임베딩 모델을 더 좋은 걸로 바꿔야 하나?”
“벡터DB를 바꿔야 하나?”

물론 그런 경우도 있습니다.
그런데 실제로는 그 전에 먼저 봐야 할 게 있어요.

RAG 품질 문제의 상당수는 chunk 크기, overlap, k값, 프롬프트 설계 같은 기본값에서 시작됩니다.

LangChain 문서는 generic text에는 RecursiveCharacterTextSplitter를 기본 추천으로 안내하고, knowledge base 튜토리얼 예시에서는 문서를 1000자 chunk, 200자 overlap으로 나눕니다. 그 overlap은 중요한 문맥이 경계에서 분리되는 걸 줄이기 위한 목적이라고 설명해요. 또 retriever는 비정형 쿼리를 받아 관련 문서를 반환하는 인터페이스이고, as_retriever(search_kwargs=...) 형태로 검색 파라미터를 조절할 수 있습니다. (LangChain Docs)


왜 RAG는 “만드는 것”보다 “튜닝하는 것”이 더 중요할까

솔직히 첫 번째 RAG를 만드는 건 생각보다 어렵지 않습니다.

  • 문서 넣고
  • 임베딩하고
  • retriever 만들고
  • 찾은 문서 넣고
  • 답하게 하면 되니까요

그런데 그 다음부터가 진짜예요.

RAG는 구조가 단순한 대신,
조금씩 어긋난 기본값이 전체 품질을 꽤 크게 흔듭니다.

예를 들어 이런 상황들요.

  • chunk가 너무 커서 질문과 무관한 내용이 같이 들어감
  • overlap이 너무 작아서 핵심 문장이 중간에서 잘림
  • k가 너무 커서 노이즈 문서가 섞임
  • 프롬프트가 느슨해서 문서 밖 내용을 아는 척함
  • 검색은 잘했는데 생성 단계에서 근거를 무시함

이걸 한 번에 못 보면,
계속 “RAG가 원래 이런가?” 싶은 상태가 됩니다.

근데 하나씩 뜯어보면 생각보다 명확해요.
그래서 이번 글은 “고급 기법”보다 어디서부터 의심해야 하는지를 잡는 글로 가겠습니다.


오늘 글에서 얻어갈 것

이번 글에서 꼭 잡아야 하는 건 딱 네 가지입니다.

  • chunk 크기를 어떻게 봐야 하는지
  • overlap이 왜 필요한지
  • k가 왜 양날의 검인지
  • 프롬프트가 retrieval 품질만큼 중요하다는 점

여기까지만 잡혀도,
RAG가 잘 안 될 때 어디를 손봐야 할지 감이 옵니다.


1. chunk 크기 문제 — 너무 크면 둔하고, 너무 작으면 맥락이 끊긴다

이게 제일 흔합니다.
그리고 제일 처음 봐야 합니다.

문서를 chunk로 나누는 이유는 간단하죠.
질문과 관련 있는 조각만 찾아오기 위해서요.

그런데 chunk가 너무 크면 문제가 생깁니다.

chunk가 너무 큰 경우

예를 들어 한 chunk 안에 이런 내용이 다 들어 있다고 해볼게요.

  • LangChain 소개
  • PromptTemplate 설명
  • Structured Output 설명
  • RAG 설명
  • Agent 설명

사용자 질문이 “Structured Output이 뭐야?”인데,
retriever는 그 chunk를 통째로 가져옵니다.

그럼 모델 입장에서는 관련 내용도 있지만,
관련 없는 내용도 잔뜩 같이 보게 돼요.
그러면 답이 흐려질 수 있습니다.

반대로 chunk가 너무 작아도 문제예요.

chunk가 너무 작은 경우

예를 들어 한 문장이 chunk 하나라고 해봅시다.

  • “RAG는 외부 문서를 검색해서”
  • “질문과 관련 있는 문맥을”
  • “모델에 함께 넣어”
  • “답변 품질을 높이는 방식이다”

이렇게 잘리면,
각 chunk 하나하나는 의미가 어정쩡해집니다.
retriever가 하나만 가져오면 설명이 덜 되고,
모델도 맥락을 충분히 못 봅니다.

LangChain은 generic text splitting에는 RecursiveCharacterTextSplitter를 권장하고, knowledge base 예시에서는 1000 characters와 200 overlap을 기본 예제로 사용합니다. 이건 절대 정답이라기보다, “처음 출발하기 좋은 무난한 기본값”에 가까워요. (LangChain Docs)

제가 느끼기엔 이거예요.

chunk는 “질문 하나에 답할 수 있는 최소 의미 단위” 정도로 잡는 게 좋습니다.

너무 큰 문단 뭉치도 아니고,
너무 잘게 찢긴 파편도 아니고요.


2. overlap 문제 — 문서 경계에서 생각보다 자주 망가진다

반응형

이건 처음엔 별거 아닌 옵션처럼 보입니다.
근데 생각보다 중요해요.

LangChain 문서도 overlap이 중요한 이유를 분명하게 말합니다.
문맥이 chunk 경계에서 분리돼 버릴 수 있기 때문에, 일부 겹치게 나눠서 그 문제를 완화하는 거예요. (LangChain Docs)

예를 들어 이런 문장이 있다고 해봅시다.

“Structured Output은 모델 응답을 JSON이나 Pydantic 스키마처럼 예측 가능한 형식으로 다루게 해준다.”

근데 chunk 경계가 딱 중간에 걸리면,

  • chunk A: “Structured Output은 모델 응답을 JSON이나”
  • chunk B: “Pydantic 스키마처럼 예측 가능한 형식으로 다루게 해준다.”

이렇게 갈릴 수 있어요.

질문이 “Structured Output이 왜 필요한가?”일 때
A만 가져오면 설명이 어색하고,
B만 가져오면 주어가 사라집니다.

그래서 overlap이 필요한 거예요.
일부 내용을 다음 chunk에도 겹치게 남겨두면,
중요한 문장이 경계에서 끊겨도 retrieval이 훨씬 덜 망가집니다. (LangChain Docs)

다만 overlap도 너무 크면 또 문제입니다.

  • 저장량이 늘고
  • 중복 chunk가 많아지고
  • 검색 결과가 비슷한 조각으로 도배될 수 있어요

그래서 보통은 chunk size의 10~20% 전후에서 시작해보는 편이 무난합니다.
물론 문서 종류에 따라 달라집니다.


3. k값 문제 — 많이 가져온다고 항상 좋아지지 않는다

이것도 진짜 자주 하는 실수예요.

처음엔 이렇게 생각하기 쉽습니다.

“더 많이 가져오면 놓칠 확률이 줄겠지?”

맞는 말 같죠.
근데 절반만 맞습니다.

LangChain의 retriever/vector store 예시들은 as_retriever(search_kwargs={"k": ...})처럼 k를 조정하게 해줍니다. 이건 결국 “몇 개의 후보 문서를 모델에 보여줄지”를 조절하는 거예요. (LangChain Docs)

k가 너무 작은 경우

  • 정답 chunk를 놓칠 수 있음
  • 답이 빈약하거나 틀릴 수 있음

k가 너무 큰 경우

  • 관련 없는 문서까지 같이 들어옴
  • 모델이 노이즈에 휘둘릴 수 있음
  • 프롬프트 길이와 비용이 증가함
  • 서로 비슷한 chunk가 여러 개 들어와서 오히려 산만해짐

특히 주니어 때 흔한 실수가
품질이 안 나오면 k부터 8, 10, 20으로 키워버리는 거예요.

근데 그러면 retrieval recall은 늘어날 수 있어도,
generation 단계 품질은 오히려 내려갈 수 있습니다.
왜냐면 모델은 “많은 문서”보다 “적절한 문서”를 봐야 하거든요.

제 경험상 처음엔 k=2~4 정도에서 시작해보고,
문서 성격에 따라 조정하는 게 제일 낫습니다.
FAQ처럼 짧고 명확한 문서면 2도 충분할 수 있고,
긴 정책 문서라면 4~5가 필요할 수도 있어요.


4. 프롬프트 문제 — 검색은 맞았는데 답이 틀리는 경우

이건 진짜 자주 놓칩니다.

검색은 잘됐어요.
retriever가 관련 문서를 제대로 가져왔어요.
근데 답이 이상합니다.

이럴 땐 많은 분이 retrieval부터 다시 의심하는데,
사실 generation 프롬프트 문제일 때가 많아요.

예를 들어 프롬프트가 너무 느슨하면 모델은 이렇게 행동할 수 있습니다.

  • 문서에 없는 일반 지식을 섞음
  • 문서를 대충 참고만 하고 자기식으로 풀어씀
  • 모르는 부분도 아는 척함
  • 출처 없는 문장을 확신 있게 말함

그래서 시스템 메시지에 이런 제약을 넣는 게 중요해요.

  • 제공된 문서에 근거해서만 답해라
  • 문서에 없는 정보는 추측하지 마라
  • 확실하지 않으면 모른다고 말해라
  • 가능하면 답변과 출처를 함께 제시해라

LangChain의 RAG/retrieval 가이드가 보여주는 기본 구조도 결국 검색 문맥을 프롬프트에 넣고, 그 문맥을 바탕으로 답하게 하는 방식입니다. retriever는 문서를 찾아줄 뿐이고, 최종 답변 태도는 프롬프트 설계에 크게 좌우됩니다. (LangChain Docs)

이게 중요해요.
RAG 품질은 retrieval만의 문제가 아니라, retrieval과 generation의 합입니다.


5. “검색이 문제인지, 생성이 문제인지” 나눠서 봐야 한다

이건 정말 개발자다운 습관인데,
RAG 디버깅할 때 가장 중요합니다.

답이 이상하면 바로 이렇게 나눠보세요.

1단계. retriever가 맞는 문서를 가져왔는가?

  • 질문과 관련 있는 chunk였는가?
  • 정답이 실제로 그 안에 있었는가?
  • 노이즈가 너무 많지 않았는가?

2단계. 모델이 그 문서를 제대로 사용했는가?

  • 문서 근거를 무시했는가?
  • 문서에는 있는데 답에서 빠졌는가?
  • 문서 밖 내용을 섞었는가?

이걸 안 나누면
모든 문제를 그냥 “RAG 품질이 안 좋다”로 뭉개게 됩니다.

근데 실제로는 두 문제는 다릅니다.

  • retrieval 문제면 chunk / overlap / k / embeddings / metadata 쪽
  • generation 문제면 prompt / answer format / grounding rule 쪽

이 분리가 되면 훨씬 빨리 개선됩니다.


6. 실제로 자주 터지는 패턴들

여기부터는 좀 현실적으로 적어볼게요.

패턴 1. “문서에 답이 있는데 못 찾음”

보통 이런 경우입니다.

  • chunk가 너무 커서 관련 정보가 묻힘
  • chunk가 너무 작아서 문맥이 끊김
  • overlap이 부족해서 핵심 문장이 찢김
  • 질문 표현과 문서 표현 차이가 큼

이럴 땐 retrieval을 먼저 봐야 합니다.

패턴 2. “검색 결과는 맞는데 답이 이상함”

보통 이런 경우입니다.

  • 프롬프트가 근거 기반 답변을 강하게 요구하지 않음
  • 모델이 일반 지식을 섞음
  • 답변 형식 제약이 약함

이럴 땐 generation을 먼저 봐야 합니다.

패턴 3. “질문마다 들쭉날쭉함”

이건 보통 복합 문제예요.

  • 어떤 질문은 검색이 쉬움
  • 어떤 질문은 표현 차이가 커서 retrieval이 흔들림
  • 어떤 질문은 k가 부족함
  • 어떤 질문은 너무 많은 문서가 들어가 노이즈가 커짐

이 경우엔 테스트 질문 세트를 만들어서 비교해보는 게 좋습니다.

LangSmith 쪽 평가 튜토리얼도 RAG 애플리케이션은 중간 단계와 최종 답변을 함께 평가하는 식으로 접근할 수 있다고 설명합니다. (LangChain Docs)


7. 처음 튜닝할 때 추천하는 순서

이건 제가 꽤 추천하는 순서입니다.

먼저 1. 검색 결과부터 눈으로 확인하기

질문마다 top-k 문서를 출력해보세요.
생각보다 여기서 바로 문제가 보입니다.

그다음 2. chunk 크기 바꿔보기

너무 크면 줄이고,
너무 잘게 찢겼으면 키워봅니다.

그다음 3. overlap 조정하기

경계가 자주 끊기면 overlap을 늘려봅니다.

그다음 4. k 조정하기

2, 3, 4 정도를 비교해보세요.
무조건 크게 하는 건 별로입니다.

마지막 5. 프롬프트 강화하기

검색된 문서만 근거로 답하라고 더 분명하게 적습니다.

이 순서가 좋은 이유는
문제를 점점 분리해서 보기 쉽기 때문입니다.


8. 직접 해볼 수 있는 비교 실험

이건 블로그 글에 넣어도 좋고, 직접 해봐도 진짜 도움이 됩니다.

같은 문서셋으로 아래를 비교해보세요.

실험 A

  • chunk_size=300
  • overlap=0
  • k=2

실험 B

  • chunk_size=300
  • overlap=50
  • k=2

실험 C

  • chunk_size=800
  • overlap=100
  • k=2

실험 D

  • chunk_size=800
  • overlap=100
  • k=4

그리고 같은 질문 세트를 던져보는 거예요.

  • “RAG가 뭐야?”
  • “문서 검색 기반 답변이 왜 필요해?”
  • “Structured Output은 왜 필요한가?”
  • “PromptTemplate이 그냥 f-string보다 나은 이유는?”

이렇게 해보면 진짜 재밌습니다.
같은 모델인데도 결과가 꽤 달라져요.

그 순간 보입니다.
“아, RAG는 모델보다 retrieval 설계가 생각보다 훨씬 중요하구나.”


9. 코드로 아주 간단한 튜닝 포인트만 정리해보면

예를 들어 splitter와 retriever는 보통 이렇게 바뀝니다.

from langchain_text_splitters import RecursiveCharacterTextSplitter

splitter = RecursiveCharacterTextSplitter(
    chunk_size=800,
    chunk_overlap=100,
)

LangChain은 generic text에 이 splitter를 권장하고, recursive splitting이 문단과 문장 경계를 최대한 보존하려고 시도한다고 설명합니다. (LangChain Docs)

retriever = vectorstore.as_retriever(
    search_kwargs={"k": 3}
)

retriever는 search_kwargs로 top-k 같은 검색 파라미터를 조절할 수 있습니다. (LangChain Docs)

그리고 프롬프트는 이런 식으로 더 분명하게 적을 수 있어요.

from langchain_core.prompts import ChatPromptTemplate

prompt = ChatPromptTemplate.from_messages(
    [
        (
            "system",
            "너는 문서 기반 Q&A 도우미다. "
            "반드시 제공된 문서 내용에 근거해서만 답하고, "
            "문서에 없는 내용은 추측하지 말고 모른다고 말해라. "
            "답변 마지막에는 참고한 문서 출처를 정리해라."
        ),
        (
            "human",
            "문서 내용:\n{context}\n\n질문: {question}"
        ),
    ]
)

이런 식으로 retrieval 품질과 generation 제약을 같이 잡아줘야,
비로소 RAG가 좀 안정적으로 보이기 시작합니다.


10. 결국 중요한 건 “고정 정답값”이 아니라 “문서 성격에 맞는 값”이다

이건 꼭 말하고 싶어요.

많은 분이 “정답 chunk size가 뭐냐”, “RAG는 k를 몇으로 해야 하냐”를 묻는데,
사실 그런 만능 정답은 없습니다.

문서가 다르면 달라져요.

  • FAQ처럼 짧고 선명한 문서
  • 긴 정책 문서
  • 코드 문서
  • 블로그 글
  • 회의록
  • API 레퍼런스

이건 전부 다릅니다.

LangChain 문서가 1000/200 같은 값을 예시로 보여주는 것도 “시작점”이지, 절대 규칙은 아니에요. generic text에 좋은 기본값과 원리를 제시하는 쪽에 가깝습니다. (LangChain Docs)

그래서 중요한 건 숫자 자체보다 왜 그 값을 쓰는지 설명할 수 있는가입니다.


오늘 글에서 꼭 가져가야 할 한 문장

오늘 내용을 한 줄로 줄이면 이겁니다.

RAG 품질이 안 나올 때는 “모델이 별로다”보다 먼저, 문서를 어떻게 자르고 얼마나 겹치고 몇 개를 가져오며 어떤 규칙으로 답하게 했는지부터 봐야 한다.

이 감각이 생기면,
RAG가 갑자기 덜 운빨처럼 느껴집니다.

왜냐면 그때부터는 개선 포인트가 보이거든요.


마무리

저는 RAG를 처음 만들었을 때,
잘 되는 날은 괜찮고 안 되는 날은 괜히 다 엉망인 느낌이었어요.

근데 조금 지나고 나서 알았습니다.
그게 랜덤이 아니라,
제가 retrieval을 너무 대충 보고 있었던 거더라고요.

문서를 어떻게 자르는지,
어디를 겹치는지,
몇 개를 가져오는지,
모델에게 뭘 강하게 요구하는지.

이런 기본값들이 생각보다 훨씬 큰 차이를 만들었습니다.

RAG는 멋진 구조이기도 하지만,
조금 더 솔직하게 말하면
검색과 프롬프트의 디테일 싸움이기도 합니다.

그걸 알게 되면,
이제부터는 진짜 개발처럼 보이기 시작해요.


다음 글 예고

다음 글에서는
Tool Calling이 진짜 중요한 이유 — LLM이 답변만 하는 걸 넘어서 외부 기능을 쓰게 만드는 방법
으로 이어가겠습니다.

이제부터는 RAG 다음 단계로 넘어갑니다.
문서를 읽는 것에서 끝나지 않고, 계산기, 검색, API 조회 같은 외부 도구를 LLM이 직접 쓰게 만드는 흐름으로 들어갈 거예요.


출처

  • LangChain RecursiveCharacterTextSplitter 가이드: generic text에 추천되는 splitter이며, 기본 구분자 목록을 순서대로 시도해 문단/문장/단어 경계를 최대한 보존하려고 한다고 설명합니다. (LangChain Docs)
  • LangChain knowledge base 튜토리얼: retrieval용 지식베이스 예시에서 1000 characters chunk와 200 characters overlap을 사용하며, overlap이 문맥 분리 가능성을 줄인다고 설명합니다. (LangChain Docs)
  • LangChain Retrieval 문서: retriever는 비정형 쿼리를 받아 관련 문서를 반환하는 인터페이스라고 설명합니다. (LangChain Docs)
  • LangChain vector store / retriever 예시: as_retriever(search_kwargs={"k": ...}) 형태로 top-k 같은 검색 파라미터를 조정할 수 있음을 보여줍니다. (LangChain Docs)
  • LangSmith RAG evaluation 튜토리얼: RAG 애플리케이션은 retrieval과 generation의 중간 단계까지 포함해 평가할 수 있다고 설명합니다. (LangChain Docs)


LangChain, RAG, Chunking, Overlap, Retriever, Semantic Search, 생성형AI, LLM개발, RAG튜닝, 주니어개발자

※ 이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다.
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2026/04   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30
글 보관함
반응형