RAG가 거짓말을 할 때, 범인은 대개 LLM이 아니다: 정확도를 높이기 전에 읽어야 할 '검색 측 사건 기록부' [RAG 사건 기록부 #1]
요약
RAG 시스템에서 답변의 오류(환각)는 LLM 자체의 문제라기보다, 검색 및 데이터 처리 단계에서 발생하는 경우가 훨씬 많습니다. 본 기사는 RAG의 정확도를 떨어뜨리는 5가지 전형적인 원인과 재발 방지책을 Python 코드를 포함하여 제시합니다.
핵심 포인트
- RAG 오류의 주범은 LLM이 아닌 '검색' 및 '데이터' 단계에 있다.
- 청킹(chunking) 시 구조를 고려한 분할 방법이 필요하다.
- 벡터 검색만으로는 부족하며, BM25와 같은 하이브리드 검색을 사용해야 한다.
- 권한 필터는 반드시 '검색 전'에 적용되어야 정확도를 높일 수 있다.
LLM은 아마도 오판일 것입니다.
사내 규정이나 제품 매뉴얼을 학습시킨 RAG 챗봇이 자신만만하게 잘못된 것을 말합니다. 그러자 팀원 중 누군가 이렇게 말하죠. '역시 LLM은 환각(hallucination)을 일으키네', '더 똑똑한 모델로 바꿔야 할까?'
그 기분, 정말 이해합니다. 하지만 RAG의 결함을 셀 수 없이 추적해 온 입장에서 말씀드리자면. RAG가 거짓말을 했을 때, LLM이 주범인 경우는 체감상 3할도 안 됩니다. 나머지 7할 이상은 LLM에게 전달되기 전 단계, 즉 '검색'과 '데이터' 측에서 사건이 발생하고 있습니다.
본 기사에서는 RAG의 답변 정확도를 떨어뜨리는 전형적인 원인을 '사건'으로 5가지 다루며, 각각의 수법과 재발 방지책을 Python 코드를 포함하여 설명합니다. 모델을 바꾸기 전에, 비용을 늘리기 전에, 꼭 한번 이 사건 기록부를 펼쳐보시길 바랍니다.
본 기사에서 알게 될 것들
- RAG의 '환각' 중 상당수가 검색 측 문제인 이유
- 각주가 사라지는 청킹(chunking)의 함정과 구조를 의식한 분할 방법
- 벡터 검색이 제품 번호나 조문 번호에 약한 이유와 BM25와의 하이브리드 검색 (RRF)
- 오래된 버전 문서와 새로운 버전 문서가 섞이는 문제 대처법
- 권한 필터를 '검색 전'에 적용해야 하는 이유
- 검색과 생성을 분리하여 평가하는 방법 (Recall@k / MRR)
- '답변하지 않음'을 설계에 포함시키는 방법
대상 독자는 RAG를 일단 구동해 봤지만 '정확도가 영 시원치 않아' 멈춰 있는 분들입니다. LangChain이나 LlamaIndex를 사용하든 안 하든 읽을 수 있도록, 코드는 순수 Python 중심으로 작성했습니다.
2026년 9월은 새로운 모델 발표가 정말 끊이지 않았습니다. 매주 '이전 세대보다 똑똑하다', '더 저렴하다'는 뉴스가 흘러와서, 솔직히 따라가는 것만으로도 숨이 넘어갈 지경입니다.
그런 시기에 VentureBeat에 실린 한 기사의 제목이 눈에 띄었습니다.
Companies can build RAG in days. Making it reliable enough to run the business is much harder
(RAG는 며칠 만에 만들 수 있다. 하지만 비즈니스 운영에 충분히 신뢰할 수 있게 만드는 것은 훨씬 어렵다)
— VentureBeat, 2026년 9월 27일
기사에서 필자는 기업 데이터의 품질, 청킹, 벡터 검색만으로는 부족한 이유, 검색과 생성을 분리한 평가, 권한 관리, 인덱스의 신선도, 비용과 지연 시간(latency)의 트레이드오프를 언급했습니다. 그리고 이렇게 썼습니다.
The model is only one part of the system.
(모델은 시스템의 일부에 지나지 않는다)
읽으면서 몇 번이나 고개를 끄덕였습니다. 모델이 매달 똑똑해져도, 검색이 전달하는 자료가 틀리면 나오는 답변 역시 잘못된 채로 남습니다. 아무리 명탐정이라도 가짜 증거를 받으면 추리를 오가립니다.
셜록 홈즈는 『보헤미아의 추문』에서 왓슨에게 이렇게 말했습니다.
It is a capital mistake to theorize before one has data.
(데이터를 얻기 전에 이론을 세우는 것은 중대한 실수다)
'LLM 탓이다'라는 추리 역시, 데이터를 보지 않고 세우면 대부분 틀립니다. 그렇다면 실제 현장에는 어떤 사건들이 벌어지고 있을까요? 순서대로 살펴보겠습니다.
설명을 위해 가상의 회사 '냐~ 상사'의 사내 규정 Q&A 봇을 예로 들겠습니다. 직원이 '유급휴가는 언제까지 신청해야 하나요?'라고 물으면, 취업 규칙을 검색하여 답변해 주는 지극히 평범한 RAG입니다.
구조도 아주 일반적입니다.
질문 → 임베딩 → 벡터 DB에서 유사 검색(top-k) → 상위 청크를 프롬프트에 채움 → LLM이 답변
이 '지극히 평범한' 구조 속에 사건의 씨앗들이 여러 개 숨어 있습니다.
직원 A가 질문했습니다.
Q. 아이가 갑자기 열이 났어요. 오늘 유급휴가를 지금 신청할 수 있나요?
봇의 답변은 이렇습니다.
A. 유급 휴가는 취득일로부터 3영업일 전까지 신청해야 합니다. 당일 신청은 할 수 없습니다.
A는 어쩔 수 없이 출근했습니다. 그런데, 취업 규칙 원문에는 이렇게 쓰여 있었습니다.
제18조 (연차 유급 휴가 신청)
1. 연차 유급 휴가를 취득하는 경우, 취득일로부터 3영업일 전까지 정해진 양식에 따라 신청해야 한다.
2. 전항의 규정에 불구하고, 질병, 가족 간호 등 부득이한 사유가 있는 경우,
...
당일에도 신청할 수 있습니다. 게다가, 바로 A님의 사례입니다.
로그를 확인해 보니, 검색으로 가져온 것은 제1항을 포함한 청크뿐이었습니다. 원인은 글자 수로 기계적으로 자르는 고정 길이 청킹(fixed-length chunking) 때문입니다. 예를 들어 500글자마다 자르면, 제1항의 끝에서 정확히 잘리면서 제2항이 다음 청크로 넘어가 버리는 경우가 있습니다.
더 골치 아픈 것은, 제2항만 있는 청크에는 '유급'이라는 단어가 한 번도 등장하지 않는다는 것입니다. 그저 '전 항의 규정에 불구하고'라고 쓰여 있을 뿐이라, 질문과의 유사도가 높아지지 않아 상위에 올라가지 못합니다.
LLM은 전달받은 자료를 충실하게 읽었을 뿐입니다. 자료에 단서가 포함되어 있지 않았기 때문에 '당일은 신청할 수 없다'고 답변하는 것은 오히려 성실한 행동이라고까지 할 수 있습니다. 이것이 바로 오판 그 자체입니다.
일본어 규정이나 매뉴얼에는 보통 '장・조・항'이나 제목 같은 구조가 있습니다. 이를 무시하고 글자 수로 자르는 것은, 책을 읽을 때 눈을 감고 페이지를 찢는 것과 같습니다.
구조를 의식한 분할의 최소 구현은 다음과 같습니다.
import re
from dataclasses import dataclass, field
ARTICLE_PATTERN = re.compile(r"^(第[0-90-9一二三四五六七八九十百]+条)((.+?))", re.MULTILINE)
...
포인트는 두 가지가 있습니다.
의미 덩어리(조)를 단위로 자른다. '원칙'과 '예외'는 같은 조 안에 쓰여 있는 경우가 많으므로, 조별로 자르면 양쪽 모두 검색 결과에 함께 들어옵니다. -
모든 청크의 시작 부분에 제목을 붙인다. 긴 조를 부득이하게 분할한 경우에도, 후반부 청크에는 '제18조(연차 유급 휴가 신청)'이라는 제목이 붙기 때문에, '전 항의 규정에 불구하고'만 쓰여 있는 청크라도 '유급'이라는 질문에 걸리게 됩니다.
두 번째는 사소하지만 효과는 큽니다. 저는 이것을 멋대로 '청크에 명찰을 다는 것'이라고 부릅니다. 명찰이 없는 사람에게 '유급 담당자 계신가요?'라고 불러도 아무도 돌아보지 않으니까요.
PDF에서 추출한 텍스트에는 주의할 것
PDF에서 텍스트를 뽑아내면, 줄 바꿈 위치가 흐트러지거나 헤더・푸터('대외비', '3/42페이지' 등)가 본문에 섞이게 됩니다. 정규 표현식으로 조를 추출하기 전에, 추출 결과를 반드시 사람의 눈으로 몇 페이지 분량 확인해 주세요. 청킹의 품질은 그보다 앞선 추출의 품질을 초월할 수 없습니다. Garbage in, garbage out입니다.
다음은 제품 매뉴얼 봇에 대한 내용입니다. 지원 담당자가 이렇게 물었습니다.
Q. KX-2041 필터 교체 주기는요?
봇의 답변은 다음과 같았습니다.
A. 필터는 6개월 간격으로 교체를 권장합니다.
실제로는 이것이 KX-2014 (숫자가 바뀐 다른 기종)의 정보였습니다. KX-2041의 정확한 교체 주기는 3개월입니다.
임베딩 벡터를 이용한 검색은, '유급'과 '연휴'와 '휴가를 갖다'가 비슷한 의미라는 것을 알아주는 매우 영리한 시스템입니다. 반면에, KX-2041 와 KX-2014 처럼 글자 하나 차이 나는 식별자를 구별하는 것은 서툽니다. 임베딩 모델의 관점에서 보면, 둘 다 'KX라는 모델 번호의 무언가'여서 벡터 공간에서는 거의 같은 위치에 오게 됩니다.
조문 번호(제12조와 제21조), 에러 코드(E-503과 E-305), 사내 시스템명, 사람 이름 등도 마찬가지입니다. 업무에서 사용하는 질문일수록, 이런 '철자가 일치하지 않으면 곤란한' 단어가 많이 포함됩니다.
대책의 정석은, 키워드 검색(BM25)과 벡터 검색을 모두 실행하고 결과를 통합하는 **하이브리드 검색(hybrid search)**입니다.
통합하는 방법은 여러 가지가 있지만, 제가 먼저 시도하는 것은 **RRF (Reciprocal Rank Fusion)**입니다. 이유는 간단해서, 점수를 정규화할 필요가 없기 때문입니다. BM25의 점수와 코사인 유사도는 스케일이 완전히 달라서, 더하려고 하면 가중치 조정의 늪에 빠지게 됩니다. RRF는 순위만 사용하기 때문에, 그 늪을 처음부터 피할 수 있습니다.
$$
ext{RRF}(d) = rac{1}{k} imes rac{1}{ ext{rank}_r(d)} + rac{1}{k} imes rac{1}{ ext{rank}_s(d)} + ... $$ (수식 표기 오류 수정)
$k$는 관례적으로 60이 자주 사용됩니다. 수식대로, '어떤 검색에서도 어느 정도 상위에 오는 문서'가 최종적으로 위에 떠오르게 됩니다.
일본어로 BM25를 사용할 경우, 본래는 형태소 분석기(形態素解析器)로 토큰화하는 것이 좋지만, 우선 의존성을 늘리지 않고 문자 bigram으로 시작하는 것을 추천합니다. 품번 같은 미지어에도 강하고 사전을 유지보낼 필요가 없습니다.
import numpy as np
from rank_bm25 import BM25Okapi
from sentence_transformers import SentenceTransformer
...
e5 계열 모델의 '접두사'를 잊지 마세요
intfloat/multilingual-e5
계열의 모델은 학습 시 질문 측에 query:
, 문서 측에 passage:
을 붙입니다. 이것을 빠뜨려도 작동하지만, 정확도가 눈에 띄게 떨어집니다. 에러가 나지 않기 때문에 알아차리기 어려운, 가장 조용한 버그 중 하나입니다. 사용하는 모델의 모델 카드를 끝까지 읽어봅시다.
이 하이브리드화만으로 품번이나 조문 번호를 포함하는 질문에서 누락되는 경우가 상당히 줄어듭니다. 더 높은 정확도를 원한다면, 통합된 상위 20~50건을 Cross-Encoder(재순위 지정기, re-ranker)로 다시 정렬하는 2단계 구성을 합니다. 다만, 재순위 지정기는 레이턴시와 비용이 증가하므로, 우선 RRF까지 평가를 진행하고, 그래도 부족할 때 추가하는 순서를 추천합니다.
서두르다 완주하기 쉽습니다. 갑자기 모든 것을 다 넣으면 어떤 부분이 효과가 있었는지 알 수 없게 됩니다.
Q. 재택근무 수당은 얼마인가요?
A. 재택근무 수당은 월 3,000엔입니다. 다만, 월 5,000엔으로 하는 규정도 있어 조건에 따라 다를 수 있습니다.
언뜻 보면 정중한 답변입니다. 하지만 실제로는 '2024년판에서는 3,000엔, 2026년 4월 개정으로 5,000엔'이라는 내용이며, 조건에 따라 다른 것이 아닙니다. 오래된 버전의 규정과 새로운 버전의 규정이 모두 인덱스에 남아 있었던 것입니다.
벡터 DB에게 있어 2024년판과 2026년판의 규정은 그저 '비슷한 두 문서'일 뿐입니다. 어느 것이 최신인지 알 수 있는 단서가 없으면, 둘 다 같은 가중치로 반환합니다.
모순되는 증언을 두 개 받은 LLM은 어떻게든 둘 다 모순 없이 설명하려고 '조건에 따라 다를 수 있습니다'라고 정리했습니다. 말하자면, 기민한 탐정이 존재하지 않는 제3의 설을 만들어낸 것과 같습니다. 이것을 LLM의 거짓말이라고 부르기는 조금 가여운 느낌이 듭니다.
할 일은 도서관 사서의 업무와 비슷합니다.
모든 청크에 '문서 ID・버전・시행일・폐지일'을 메타데이터로 포함시키기-
검색 시 '현재 유효한 버전'으로만 필터링하기(과거 버전을 물었을 때만 제외)
문서가 업데이트되면, 변경된 부분만 인덱싱 다시 하기
세 번째는 문서 내용에서 해시(hash)를 가져두면 쉽게 구현할 수 있습니다.
import hashlib
from datetime import date
def content_hash(text: str) -> str:
...
store
부분은 사용하시는 벡터 DB에 맞춰 바꿔주세요. 많은 벡터 DB가 메타데이터 필터링을 지원하므로, 검색 시 active == True
조건만 추가해도 두 명의 증인 문제는 거의 해결됩니다.
또 하나, 은근히 효과적인 방법이 있습니다. 프롬프트에 전달하는 청크에 시행일을 적어두는 것입니다.
[자료 1] 취업규칙 제32조(재택근무 수당) / 2026년 4월 1일 시행
재택근무를 하는 직원에게는 월 5,000엔의 재택근무 수당을 지급한다.
만약 필터링을 빠져서 오래된 버전이 섞여 들어와도, LLM이 날짜를 보고 '새로운 것을 우선시하는' 판단을 하기 쉬워집니다. 올바른 판단에 필요한 정보는 아끼지 말고 전달해야 합니다.
이것은 정확도라기보다 보안의 문제입니다만, RAG를 업무에 사용한다면 피할 수 없으므로 다룹니다.
Q. 이번 분기 승진률은 어느 정도인가요?
A. 죄송하지만, 그 정보에는 답변드릴 수 없습니다.
언뜻 보면 제대로 지키고 있는 것처럼 보입니다. 그런데 로그를 보니 검색 결과에 이사회 자료의 '승진 재원 배분안'이 확실히 포함되어 있었습니다. LLM이 시스템 프롬프트의 '기밀 정보는 답하지 말 것'을 따랐기 때문에, 우연히 새어나가지 않은 것뿐입니다.
'LLM에게
VentureBeat 기사에서도 접근 제어는 모델에 도달하기 전에 적용해야 한다고 강조했습니다. 저도 완전히 같은 생각입니다.
def retrieve_for_user(retriever, query: str, user) -> list[dict]:
# 사용자(user)가 열람 가능한 그룹을 검색 조건 자체에 포함시킵니다
allowed = set(user.groups) | {"all_employees"}
...
여기서 중요한 것은, **검색한 후에 권한으로 필터링하는 것(후단 필터)**이 아니라, **검색 시점에 필터링하는 것(사전 필터)**입니다. 후단에서 필터링하면, 상위 5건이 모두 열람 불가 문서였을 경우 결과가 0건이 되어, '무언가를 숨기고 있다'는 인상을 줄 수 있습니다. 게다가 로그나 중간 처리 과정에 기밀 문서가 남을 위험도 있습니다.
그리고 권한 정보를 원래 시스템(파일 서버나 SharePoint 등)에서 주기적으로 동기화하는 메커니즘도 잊지 마세요. 전근이나 퇴사로 그룹이 바뀌었는데 RAG의 권한 정보만 오래된 상태인 경우는 정말 흔합니다.
마지막으로, 가장 눈치채기 어려운 사건에 대해 말씀드리겠습니다.
평가용으로 만든 질문 세트로 답변을 체크해 보니, '통근 수당 상한액은?'이라는 질문에 봇은 정확히 '월 5만 원입니다'라고 답했습니다. 평가는 '정답'.
그런데 검색 결과를 보면, 가져온 것은 출장 여비 규정의 '교통비 정산 상한액' 청크였습니다. 우연히 같은 5만 원이었을 뿐, 근거는 완전히 다른 문서였던 것입니다.
최종 답변의 정오 여부만 보고 있으면, 이런 '우연적 적중(まぐれ当たり)'이 정답으로 계산되어 버립니다. 반대로, 검색은 완벽한데 생성에서 실패하는 경우 역시 검색 문제와 구분하기 어렵습니다.
에드거 카달(Edgar Dijkstra)의 유명한 말이 있습니다.
Program testing can be used to show the presence of bugs, but never to show their absence!
(테스트는 버그가 있다는 것을 보여줄 수는 있어도, 버그가 없다는 것은 결코 보여줄 수 없다!)
최종 답변만 평가하는 것은, 말 그대로 '버그가 없음'을 보여주려 하는 평가입니다.
평가 데이터에 '질문'과 '정답'뿐만 아니라, **'정답의 근거가 되는 청크 ID'**도 포함시켜야 합니다. 이렇게 하면 검색 성능을 단독으로 측정할 수 있습니다.
from statistics import mean
def recall_at_k(retrieved: list[str], relevant: set[str], k: int) -> float:
"""상위 k건에, 정답 청크가 얼마나 포함되어 있는지"""
...
평가 데이터의 한 줄은 이런 형태입니다.
{
"question": "통근 수당 상한액은?",
"answer": "월 5만 원",
...
이 평가를 돌려보면, 개선할 지점이 명확하게 나뉩니다.
| 검색 (Recall@k) | 생성 (답변의 정오) | 의심해야 할 곳 |
|---|---|---|
| 낮음 | 낮음 | 청킹・검색 방식・데이터의 질 |
| ... | 우연적 적중. 가장 위험 | |
| 높음 | 높음 | 다음은 응답 속도와 비용을 재검토할 차례 |
3번째 줄, 즉 '검색은 실패했는데, 답변은 맞는다'가 이번 사건의 본질입니다. 이런 상태의 평가 점수는 실제 운영 환경에서 어떤 계기로 한순간에 무너집니다.
그리고 가장 중요한 것을 말씀드리겠습니다. complete_misses
즉, 단 하나도 정답을 건지지 못한 질문 목록은 반드시 사람의 눈으로 한 개씩 읽어봐야 합니다. 숫자는 '얼마나 나쁜지'를 알려줄 뿐, '왜 나쁜지'는 알려주지 않습니다. 왜 나쁜지는 대부분 질문과 청크를 나란히 놓고 바라보고 있다 보면 보입니다.
Rob Pike의 '프로그래밍의 5가지 규칙' 중 두 번째에도 이렇게 나와 있습니다.
Measure. Don't tune for speed until you've measured, and even then don't unless one part of the code overwhelms the rest.
(측정하라. 측정하기 전까지는 속도에 맞춰 조정하지 마라. 그리고 설령 측정했더라도, 코드의 한 부분이 나머지를 압도하지 않는 한 하지 마라.)
이것은 속도에 관한 말이지만, RAG의 정확도에도 그대로 적용된다고 생각합니다. 측정하지 않고 청크 크기를 바꾸거나 모델을 교체하는 것은 안개 속에서 핸들을 조작하는 것과 같습니다.
5가지 사건을 살펴보았지만, 마지막으로 모든 사건에 공통되는 이야기를 하나 더 하겠습니다.
RAG 평가에서 자주 간과되는 것이 바로 '답변해서는 안 되는 질문에 답변해 버리는' 실패입니다. 자료에 쓰여 있지 않은 것을 물었을 때, 그럴듯한 일반론으로 채워버립니다. 사내 규정 챗봇에서 이런 일이 발생하면 상당히 곤란합니다. 왜냐하면 '일반적으로는 ~입니다'라는 답변을 들은 직원은 그것이 자사의 규칙이라고 착각해 버리기 때문입니다.
논어에 다음과 같은 구절이 있습니다.
知るを知ると為し、知らざるを知らずと為す。是れ知るなり。
(알고 있는 것은 알고 있다고 하고, 모르는 것은 모른다고 한다. 그것이 진정으로 안다는 것이다.)
2500년 전 공자가 이미 RAG의 설계 사상을 이야기하고 있었다고 생각하니 조금 유쾌합니다.
구현 측면으로는 다음 두 가지를 조합합니다.
1. 검색 점수(Search Score)가 너무 낮은 경우에는 LLM을 호출하지 않고 중단하기
NO_ANSWER = (
"질문에 해당하는 규정이 발견되지 않았습니다."
"번거로우시겠지만, 인사부(내선 1234)로 문의해 주십시오."
...
min_score
값은 감으로 정하지 않고 평가 데이터에서 결정합니다. '자료에 답이 없는 질문'도 평가 세트에 섞어 놓고, 정답이 있는 질문과 없는 질문의 점수 분포를 나란히 놓고 경계를 그립니다. 여기 역시 결국 측정입니다.
2. 프롬프트로 '답변하지 않음'을 명확하게 허가하기
당신은 냐~상사(にゃー商事)의 사내 규정에 대해 답변하는 어시스턴트입니다.
아래 [자료]에 쓰여 있는 내용만을 근거로 답변해 주십시오.
- 자료에 답이 쓰여 있지 않은 경우에는, 추측이나 일반론으로 보충하지 마시고,
...
'답변할 필요가 없다'고 명시적으로 적어두지 않으면 LLM은 어떻게든 도움이 되려고 합니다. 그 성실함이 환각(Hallucination)의 정체 중 하나입니다. '모른다'고 말하는 것을 실패가 아니라 올바른 행동으로 정의해 주는 것. 이것만으로도 답변의 신뢰성은 상당히 달라집니다.
저는 평소 RAG 챗봇 구축을 도울 때 '정답률 98%'라는 수치를 기준으로 말씀드립니다. 다만, 이 숫자에는 제대로 된 단서 조항(但し書き)을 붙이고 싶습니다. 사라진 단서 조항의 사건을 쓴 직후니까요.
이 98%는 해당 프로젝트를 위해 만든 평가 세트 상에서의 수치입니다. 평가 세트를 만드는 방식에 따라 정답률은 얼마든지 높게, 낮게 보이도록 만들 수 있습니다. 쉬운 질문만 모으면 99%는 어렵지 않고, 까다로운 질문을 모으면 50%까지 떨어집니다.
그래서 제가 중요하게 생각하는 것은 숫자 그 자체보다 다음 네 가지입니다.
- 실제 이용자가 물어볼 만한 질문을 이용자 본인으로부터 수집하는 것
- '자료에 답이 없는 질문'을 반드시 섞는 것
- 검색과 생성을 분리해서 측정하는 것
- 틀린 질문을 하나씩 직접 읽어보는 것
98%는 이 꾸준한 작업을 거친 결과로 따라오는 숫자에 불과합니다. 숫자만 혼자 돌아다니면, 그것이야말로 '정답했는데도 부정확'해져 버립니다.
마지막으로 이번 사건을 목록으로 정리하겠습니다. RAG의 정확도에 고민이 될 때 체크리스트로 사용해 주십시오.
| No. | 사건 | 진범 | 재발 방지책 |
|---|---|---|---|
| 1 | 사라진 단서 조항 | 고정 길이 청킹(Fixed-length chunking) | 구조(조/제목)로 분할하고, 청크에 제목 명찰을 붙인다 |
| ... | |||
| 이렇게 나열해 보면, LLM 자체가 진범이었던 사건은 하나도 없습니다. 등대 아래 어둠 속에서 빛을 보는 격입니다. 범인은 늘 우리가 준비한 데이터와 파이프라인 속에 숨어 있었습니다. |
물론, LLM이 실제로 환각(Hallucination)을 일으키는 경우도 있습니다. 하지만 그것을 의심하는 것은 검색 측의 용의점을 모두 털어낸 후에 해도 늦지 않습니다. 그 편이 훨씬 저렴하고, 훨씬 확실합니다.
브라이언 카니한은 『The Elements of Programming Style』에서 다음과 같이 썼습니다.
Everyone knows that debugging is twice as hard as writing a program in the first place. So if you're as clever as you can be when you write it, how will you ever debug it?
(디버깅은 프로그램을 작성하는 것의 두 배로 어렵다는 것은 누구나 안다. 그렇다면, 작성할 때 최대한 똑똑하게 써버렸다면, 도대체 어떻게 디버깅을 할 수 있겠는가?)
RAG도 마찬가지라고 생각합니다. 처음부터 모든 똑똑한 부품을 다 넣기보다는, 먼저 소박한 구성으로 측정할 수 있는 상태를 만드는 것이 중요합니다. '똑똑함'은 범인을 잡고 난 후에 조금씩 추가해 나가면 됩니다.
이번 글에서 여러 번 '평가 세트(evaluation set)'라는 단어를 사용했습니다. 하지만 사실 RAG 개선에 가장 많은 시간이 걸리고, 가장 차이가 나는 부분은 바로 이 평가 세트를 만드는 것입니다.
- 질문은 몇 개나 있어야 충분한가. 30개일까, 100개일까, 1,000개일까?
- 현업 담당자에게 '질문을 생각해 주세요'라고 부탁하면 왜 도움이 안 되는 질문들만 모이는가?
- LLM에게 평가용 질문을 만들게 하면 어떤 편향이 생기는가?
- 정답의 근거 청크 ID를 쉽고, 하지만 정확하게 붙이는 방법
다음 【RAG 사건 기록부 #2】에서는 이 '증거물 수집 방법'에 대해 깊이 파헤쳐 보겠습니다. 솔직히 말해서, 여기는 꽤 지저분한 이야기(muddy talk)가 될 겁니다. 하지만 바로 그런 지저분한 곳에 다른 글에는 쓰여 있지 않은 통찰력이 숨어 있는 법입니다.
그럼, 다음 사건 현장에서 만나요.
이 글이 도움이 되셨다면, 좋아요나 스토리를 해주시면 다음 글을 쓰는 힘이 됩니다. '우리 회사에서는 이런 사건이 있었다'는 댓글도 환영합니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Qiita AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기