컴퓨터공학 300 주제 시리즈의 276번째 글이다. 전체 지도는 여기.

한 줄 요약

RAG(retrieval-augmented generation, 검색 증강 생성)는 질문과 관련된 문서를 먼저 검색해 프롬프트에 넣고, 모델이 그 문서를 근거로 답을 생성하게 하는 구조다. 모델을 다시 학습시키지 않고 최신·사내 지식을 쓰게 하고, 출처를 댈 수 있게 한다.

왜 필요한가

LLM 의 지식에는 세 가지 한계가 있다.

  1. 시점: 학습 데이터를 모은 시점 이후의 일을 모른다.
  2. 범위: 사내 위키, 장애 보고서, 계약서처럼 공개되지 않은 자료는 본 적이 없다.
  3. 근거: 가중치에 녹아든 지식은 어디서 왔는지 댈 수 없고, 모르는 것을 그럴듯하게 지어내기도 한다.

재학습(미세조정)으로 지식을 넣을 수도 있지만 비싸고 느리며, 문서가 바뀔 때마다 다시 해야 한다. 미세조정은 지식 주입보다 형식·말투를 바꾸는 데 더 효과적이라는 것이 일반적인 경험이다. RAG 는 지식을 모델 밖에 두고 필요할 때 꺼내 읽힌다. 문서를 고치면 바로 반영되고, 답마다 출처를 붙일 수 있다. Lewis 외(2020)가 이 이름으로 체계화했다.

핵심 개념

전체 흐름

[색인 단계 — 미리]
문서 ─▶ 정제 ─▶ 조각내기(chunking) ─▶ 임베딩 / 역색인 ─▶ 저장소

[질의 단계 — 매 요청]
질문 ─▶ (질의 재작성) ─▶ 검색 상위 k ─▶ (재순위) ─▶ 프롬프트 조립 ─▶ LLM ─▶ 답 + 출처

조각내기

문서를 통째로 넣으면 컨텍스트가 넘치고 관련 없는 내용이 섞인다. 그래서 수백 토큰 정도의 조각으로 나눈다.

  • 너무 작으면 문맥이 끊긴다(“위 설정을 적용하면”의 “위”가 사라짐).
  • 너무 크면 검색 정확도가 떨어지고 토큰 비용이 는다.
  • 조각 사이를 조금 겹치게 하거나, 제목·절 구조를 따라 자르면 낫다. 조각마다 원문 제목·경로 같은 메타데이터를 붙여 두면 출처 표시와 필터링에 쓸 수 있다.

검색 — 희소와 밀집

방식 원리 강점 약점
희소(BM25 등) 단어 일치 + 희귀 단어 가중 고유명사, 오류 코드, 정확한 용어 동의어·바꿔 말하기
밀집(임베딩) 의미 벡터의 근접성 다른 표현의 같은 뜻 정확한 식별자, 새로운 용어
하이브리드 두 점수를 합치거나 순위 융합 둘의 장점 구성이 복잡

CrashLoopBackOff 같은 문자열은 BM25 가 확실히 찾고, “파드가 계속 재시작됨” 같은 서술은 임베딩이 더 잘 찾는다. 실무에서 하이브리드가 흔한 이유다. 밀집 검색의 대표 연구로 DPR(Karpukhin 외, 2020)이 있다.

BM25 점수는 질의 단어마다

IDF(w) · tf·(k1+1) / ( tf + k1·(1 − b + b·문서길이/평균길이) )

를 더한다. 드문 단어일수록(IDF), 많이 나올수록(tf, 단 포화됨) 점수가 높고, 긴 문서는 b 만큼 보정된다.

재순위와 프롬프트 조립

검색 상위 수십 개를 더 정밀한 모델(크로스 인코더 등)로 다시 매겨 상위 몇 개만 넣는 재순위가 품질을 많이 올린다. 프롬프트에는 다음을 명시한다.

  • 제공된 문서만 근거로 답할 것
  • 근거가 없으면 모른다고 할 것
  • 각 주장에 문서 ID 를 붙일 것

평가는 둘로 나눠서

RAG 가 틀렸을 때 원인은 둘 중 하나다. 검색이 맞는 문서를 못 가져왔거나, 가져왔는데 생성이 잘못 썼거나.

단계 지표
검색 recall@k (정답 문서가 상위 k 안에 있는가), MRR
생성 충실성(문서에 근거했는가), 답의 정확성, 출처 정확성

질문-정답 문서 쌍을 수십 개만 만들어 둬도 조각 크기, k, 검색 방식을 숫자로 비교할 수 있다.

직접 해 보기

문서 네 개에 BM25 를 직접 구현하고, 상위 문서로 프롬프트를 조립한다.

import math, re
from collections import Counter
docs = {
  "d1": "k3s 노드가 NotReady 가 되면 kubelet 로그와 디스크 압박 상태를 먼저 본다.",
  "d2": "파드가 CrashLoopBackOff 이면 이전 컨테이너 로그를 --previous 로 확인한다.",
  "d3": "인증서가 만료되면 API 서버 연결이 TLS 오류로 실패한다.",
  "d4": "디스크 사용률이 높으면 kubelet 이 이미지 가비지 컬렉션을 시작한다.",
}
tok = lambda s: re.findall(r"[\w-]+", s.lower())
D = {k: tok(v) for k, v in docs.items()}
N = len(D); avgdl = sum(map(len, D.values())) / N
df = Counter(w for t in D.values() for w in set(t))
def bm25(q, k1=1.2, b=0.75):
    out = {}
    for k, t in D.items():
        tf = Counter(t); s = 0.0
        for w in tok(q):
            if w not in tf: continue
            idf = math.log(1 + (N - df[w] + .5) / (df[w] + .5))
            s += idf * tf[w] * (k1 + 1) / (tf[w] + k1 * (1 - b + b * len(t) / avgdl))
        out[k] = round(s, 3)
    return sorted(out.items(), key=lambda kv: -kv[1])
q = "노드 디스크 kubelet 문제"
ranked = bm25(q); print("검색 순위:", ranked)
ctx = "\n".join(f"[{k}] {docs[k]}" for k, s in ranked[:2] if s > 0)
print("--- 생성 모델에 넘길 프롬프트 ---")
print(f"아래 문서만 근거로 답하고, 문장 끝에 [문서id] 를 붙여라. 근거가 없으면 모른다고 답하라.\n{ctx}\n질문: {q}")

실행 결과:

검색 순위: [('d4', 1.417), ('d1', 1.252), ('d2', 0.0), ('d3', 0.0)]
--- 생성 모델에 넘길 프롬프트 ---
아래 문서만 근거로 답하고, 문장 끝에 [문서id] 를 붙여라. 근거가 없으면 모른다고 답하라.
[d4] 디스크 사용률이 높으면 kubelet 이 이미지 가비지 컬렉션을 시작한다.
[d1] k3s 노드가 NotReady 가 되면 kubelet 로그와 디스크 압박 상태를 먼저 본다.
질문: 노드 디스크 kubelet 문제

질문의 “디스크”, “kubelet” 이 들어간 d4, d1 이 위로 왔다. 둘 다 맞은 단어는 두 개로 같은데 d1 이 낮다. d1 이 더 길어서 길이 보정(b)으로 점수가 깎였기 때문이다. 질문의 “노드” 는 d1 에 “노드가” 로 들어 있어 아예 일치하지 않았다. 조사 하나 때문에 놓친 것이다. d2, d3 은 겹치는 단어가 없어 0 점이고, s > 0 조건으로 프롬프트에서 빠졌다. 관련 없는 문서를 억지로 채워 넣지 않는 것도 중요하다. 모델은 넣어 준 것을 쓰려고 하기 때문이다.

이 예제에는 한국어 형태소 분석이 없다. “문제”와 “문제를”을 다른 단어로 본다. 실제 한국어 검색에서는 형태소 분석기나 n-gram 토큰화, 또는 밀집 검색을 함께 써야 한다.

현업에서는

  • 사내 문서 Q&A: 가장 흔한 LLM 도입 사례다. 실패의 대부분은 모델이 아니라 문서 쪽에서 온다. 오래된 문서, 중복, PDF 표가 깨진 추출, 권한 없는 문서의 노출.
  • 권한 필터링은 검색 단계에서. 사용자가 볼 수 없는 문서가 검색 결과에 들어가 프롬프트로 새면 그대로 정보 유출이다. 메타데이터로 권한을 먼저 거른다.
  • 간접 프롬프트 인젝션: 검색된 문서 안에 악의적 지시가 들어 있을 수 있다. 외부에서 들어온 문서를 색인할 때 특히 주의한다.
  • 운영 지식 베이스: 홈랩 같은 작은 환경에서도 과거 장애 기록과 런북을 조각내 색인해 두면, “이 증상 전에 본 적 있나”를 묻는 도구가 된다. 단, 답의 출처 링크를 반드시 확인하는 습관을 함께 들여야 한다.

확인 문제

  1. RAG 가 미세조정보다 사내 지식 반영에 유리한 점 두 가지는?
  2. 조각이 너무 작을 때와 너무 클 때의 문제는?
  3. 오류 코드 E1234 로 검색할 때 BM25 와 임베딩 검색 중 어느 쪽이 더 믿을 만한가?
  4. RAG 의 답이 틀렸을 때 원인을 나누어 진단하는 방법은?
  5. 예제에서 d1 과 d4 는 맞은 단어 수가 같은데 d1 이 낮다. 이유는? 그리고 질문의 “노드” 가 d1 과 일치하지 않은 이유는?

풀이

  1. 문서를 바꾸면 재학습 없이 바로 반영되고, 답에 출처를 붙여 검증할 수 있다. (비용이 싸다는 점도 있다.)
  2. 너무 작으면 문맥이 끊겨 의미를 잃고, 너무 크면 검색이 부정확해지고 컨텍스트·비용을 낭비한다.
  3. BM25. 정확한 문자열 일치를 보며, 드문 토큰이라 IDF 가중이 크다. 임베딩은 이런 식별자를 비슷한 다른 코드와 섞기 쉽다.
  4. 정답 문서가 검색 상위 k 에 들어왔는지(recall@k)를 먼저 보고, 들어왔다면 생성 단계의 충실성 문제로, 안 들어왔다면 검색 문제로 본다.
  5. d1 이 더 길어 길이 정규화로 점수가 깎였다. “노드” 는 d1 에 “노드가” 라는 다른 토큰으로 들어 있어, 형태소 분석 없는 공백·기호 기준 토큰화로는 일치하지 않는다.

더 읽을거리 (References)

  • P. Lewis 외, “Retrieval-Augmented Generation for Knowledge-Intensive NLP Tasks”, 2020. arXiv:2005.11401
  • V. Karpukhin 외, “Dense Passage Retrieval for Open-Domain Question Answering”, 2020. arXiv:2004.04906
  • Y. Gao 외, “Retrieval-Augmented Generation for Large Language Models: A Survey”, 2023. arXiv:2312.10997
  • S. Robertson, H. Zaragoza, “The Probabilistic Relevance Framework: BM25 and Beyond”, Foundations and Trends in Information Retrieval 3(4), 2009. (서지 정보)