들어가며
LLM과 RAG의 정의만 읽으면 실제 서비스에서 어떤 일이 일어나는지 잘 보이지 않을 수 있습니다. 그래서 이 글에서는 가상의 반품 FAQ를 예로 들어 질문이 들어온 뒤 자료를 찾고 답변을 만드는 과정을 차례대로 살펴봅니다.
이 글의 FAQ와 설정은 흐름을 설명하기 위해 만든 예시입니다. 실제 쇼핑몰 정책이나 구현·성능 측정 결과가 아닙니다. 다음 실습 글에서는 선택한 기술과 실행 결과를 따로 기록하겠습니다.
먼저 LLM이 답하는 방식을 보자
LLM(Large Language Model)은 입력된 문맥을 바탕으로 다음 토큰을 하나씩 생성해 답변을 이어갑니다. 토큰은 단어 전체일 수도 있고 단어 일부나 기호일 수도 있습니다. 모델은 입력 문장을 토큰 단위로 처리하고, 앞에 나온 내용에 이어질 토큰의 확률 분포를 계산합니다. 그중 하나를 선택하고 같은 과정을 반복해 문장을 만듭니다.
따라서 LLM의 출력은 데이터베이스에서 정답 행을 찾아 그대로 반환하는 것과 다릅니다. 모델은 질문과 문맥에 어울리는 답을 생성하지만, 그 답이 사실인지 자동으로 보증하지는 않습니다. 학습 자료에 없거나 시점이 지난 정보에 대해서도 자연스러운 문장을 만들 수 있습니다.
생성 설정 중 temperature는 다음 토큰을 선택할 때의 무작위성에 영향을 줍니다. 낮추면 대체로 출력이 덜 다양해지지만, 사실 검증 기능이 생기는 것은 아닙니다. 답변의 정확성은 입력으로 준 근거, 모델의 해석, 검색 품질과 함께 판단해야 합니다.
RAG는 LLM 앞에 검색 단계를 둔다
RAG(Retrieval-Augmented Generation)는 사용자 질문에 필요한 외부 자료를 먼저 검색하고, 그 결과를 질문과 함께 LLM에 전달하는 방식입니다. 문서를 모델의 파라미터 안에 다시 학습시키는 작업과 다릅니다. 문서 변경을 검색 인덱스에 반영하면, 다음 질문부터 변경된 자료를 가져올 수 있습니다.
일반적인 RAG는 크게 두 시점으로 나뉩니다. 질문이 오기 전에 문서를 정리해 인덱스를 만드는 색인 단계, 그리고 질문마다 관련 내용을 찾고 답변하는 질의 단계입니다.
[색인 단계: 문서가 추가·변경될 때]
원본 문서 → 텍스트 추출·정리 → 청크 분할 → 임베딩 생성 → 검색 인덱스 저장
[질의 단계: 사용자가 질문할 때]
질문 → 질문 임베딩 → 관련 청크 검색 → (선택) 재정렬
→ 검색 결과와 질문을 LLM에 전달 → 답변과 출처 반환
가상의 반품 FAQ로 끝까지 따라가기
예시 문서에 다음 정책이 있다고 가정하겠습니다.
반품 안내
상품을 받은 날부터 7일 이내이며 사용하지 않은 경우 반품을 접수할 수 있습니다. 단순 변심으로 반품하면 반품 배송비는 구매자가 부담합니다. 상품 불량으로 반품하면 판매자가 배송비를 부담합니다.
사용자 질문은 다음과 같습니다.
“상품을 받은 지 5일 됐고 아직 사용하지 않았어요. 단순 변심으로 반품하면 배송비는 누가 내나요?”
LLM에 질문만 보내면 모델이 일반적인 관행이나 학습 중 접한 내용을 바탕으로 답을 만들 수 있습니다. 이 예시에서는 답의 근거가 될 문서를 실제로 검색해 함께 제공하는 것이 RAG의 역할입니다.
1. 문서를 추출하고 청크로 나눈다
PDF나 웹페이지에서 텍스트를 가져왔다면 메뉴·푸터·중복 문구와 같은 잡음을 정리합니다. 그 다음 검색할 단위인 청크(chunk)로 나눕니다. 문서 전체를 하나의 청크로 저장하면 검색 결과에 불필요한 내용이 많이 포함될 수 있고, 반대로 문장을 지나치게 잘게 나누면 예외 조건과 본문이 떨어져 답의 의미를 잃을 수 있습니다.
이번 FAQ는 짧으므로 제목과 정책 문장을 한 청크로 유지하는 편이 자연스럽습니다.
문서 제목: 반품 안내
청크 내용:
상품 수령일로부터 7일 이내이고 미사용 상태인 경우 반품 접수 가능.
단순 변심 반품 배송비는 구매자 부담.
상품 불량 반품 배송비는 판매자 부담.
긴 문서에서는 글자 수나 토큰 수를 기준으로 자를 수 있지만, 숫자 하나를 모든 문서에 적용할 수는 없습니다. 표·제목·항목 번호처럼 문서 구조를 활용하고, 앞뒤 청크에 일부 문맥을 겹치는 오버랩도 검토합니다. 오버랩이 너무 크면 같은 내용이 반복 저장되어 검색 결과와 비용이 낭비될 수 있습니다.
청크 텍스트와 함께 문서 ID, 제목, URL 또는 페이지, 갱신 시각, 문서 분류와 접근 권한 같은 메타데이터를 보관하는 것이 좋습니다. 나중에 출처를 표시하고 오래된 문서를 제외하거나 사용 권한을 적용할 때 필요합니다.
2. 임베딩은 문장의 의미를 검색용 벡터로 바꾼다
임베딩 모델은 텍스트를 숫자 배열인 벡터로 변환합니다. 벡터 공간에서 의미가 비슷한 텍스트는 대체로 가까운 방향에 놓이도록 학습됩니다. 그래서 질문의 표현이 문서와 정확히 일치하지 않아도 관련 문서를 찾을 가능성이 있습니다.
예를 들어 FAQ에는 “상품 수령일로부터 7일 이내”라고 쓰여 있고 질문에는 “받은 지 5일”이라고 표현되어 있어도, 의미 기반 검색이 관련 청크를 찾을 수 있습니다. 다만 임베딩 검색은 의미가 비슷한 표현을 찾는 기술이지, 모든 숫자·상품명·정책 조건을 완벽히 일치시키는 기술은 아닙니다.
색인 단계에서는 각 청크의 벡터와 원문, 메타데이터를 함께 저장합니다. 질문이 들어왔을 때도 같은 임베딩 모델로 질문 벡터를 만들어 비교해야 합니다. 색인에 사용한 모델을 바꾼다면 기존 벡터와 새 벡터의 공간이 호환되는지 확인하고, 보통은 인덱스를 다시 생성하는 작업을 계획해야 합니다.
3. 질문과 가까운 청크를 검색한다
검색 단계에서는 질문 벡터와 문서 벡터 사이의 유사도를 계산해 상위 결과를 가져옵니다. 코사인 유사도를 쓸 수 있지만, 점수를 확률이나 답변의 정확도로 해석하면 안 됩니다. 점수는 선택한 임베딩 모델과 데이터·검색 방식 안에서 상대적으로 비교하는 신호입니다.
가져올 문서 수를 Top-K라고 부릅니다. K가 너무 작으면 답에 필요한 예외나 조건을 놓칠 수 있고, 너무 크면 무관한 문장이 프롬프트에 섞여 모델이 혼란스러워지거나 입력 토큰과 지연 시간이 늘어날 수 있습니다. 적절한 K는 실제 질문 모음으로 검색 결과를 확인해 정해야 합니다.
벡터 검색은 의미가 가까운 내용을 찾는 데 강점이 있지만, 주문번호·제품 코드·정확한 용어처럼 문자열 일치가 중요한 질문은 키워드 검색이 더 나을 수 있습니다. 그래서 키워드 검색과 벡터 검색을 결합하는 하이브리드 검색을 사용하기도 합니다. 후보가 많으면 별도의 재정렬(reranking) 모델로 질문과 각 후보의 관련성을 다시 평가해 LLM에 전달할 결과를 좁힐 수 있습니다.
검색 전에 사용자 권한으로 문서 범위를 필터링해야 합니다. 검색한 뒤에 답변만 가리는 방식은 권한 없는 자료가 모델 입력에 이미 들어갈 수 있어 안전하지 않습니다.
4. 검색 결과를 프롬프트의 근거로 전달한다
검색된 청크는 지시문, 질문과 구분해 프롬프트에 넣습니다. 예를 들면 다음과 같습니다.
역할: 제공된 참고 자료를 바탕으로 고객 질문에 답한다.
규칙:
- 참고 자료에 없는 정책을 만들어내지 않는다.
- 근거가 부족하면 필요한 정보가 없다고 말한다.
- 답변 뒤에 근거 문서 제목과 항목을 표시한다.
참고 자료 [반품 안내, 반품 배송비]:
단순 변심 반품 배송비는 구매자 부담.
상품 불량 반품 배송비는 판매자 부담.
질문:
상품을 받은 지 5일 됐고 사용하지 않았습니다.
단순 변심으로 반품하면 배송비는 누가 내나요?
이 예시에서 기대하는 답은 “가상의 안내 기준으로는 미사용 상품을 수령 후 7일 이내에 반품 접수할 수 있습니다. 단순 변심의 반품 배송비는 구매자 부담입니다. 근거: 반품 안내의 반품 조건 및 배송비 항목.”입니다.
실제 서비스에서는 답변의 출처를 모델에게 문장으로만 만들게 하기보다, 검색 결과의 문서 ID와 위치 정보를 애플리케이션이 함께 보관해 연결하는 편이 추적에 유리합니다. 모델이 출처를 잘못 지어내거나 인용 문구를 바꿀 수 있기 때문입니다. 검색된 문서 내용도 명령이 아니라 참고 데이터로 취급하도록 프롬프트를 구성해야 합니다.
5. 백엔드에서는 어떤 구성으로 볼 수 있을까?
Java 백엔드에서 RAG를 구현한다면 역할을 대략 다음처럼 나눌 수 있습니다. 특정 프레임워크나 라이브러리에 종속되지 않은 개념 구조입니다.
- Controller: 질문과 사용자 인증 정보를 받고 응답 형식을 반환합니다.
- RagService: 질문 임베딩, 검색, 프롬프트 구성, LLM 호출의 순서를 조정합니다.
- EmbeddingClient: 문서와 질문을 벡터로 변환합니다.
- DocumentRetriever: 사용자 권한 범위를 적용해 관련 청크를 검색합니다.
- LlmClient: 질문과 검색 근거를 전달하고 생성된 답을 받습니다.
- SourceMapper: 답변과 원문 문서·페이지 정보를 연결해 화면에 출처를 표시합니다.
// 개념을 보여주기 위한 의사 코드입니다. 실제 라이브러리 API가 아닙니다.
Answer ask(String question, User user) {
Vector query = embeddingClient.embed(question);
List<Chunk> chunks = retriever.search(
query,
user.allowedDocumentScopes(),
topK
);
if (chunks.isEmpty()) {
return Answer.noEvidence();
}
Prompt prompt = promptFactory.create(question, chunks);
String text = llmClient.generate(prompt);
return sourceMapper.attachSources(text, chunks);
}
여기서 중요한 점은 검색 호출과 LLM 호출을 한 메서드 안에 모두 넣는 것이 아니라, 각 단계의 입력·출력과 실패를 관찰할 수 있게 나누는 것입니다. 그래야 검색 결과가 틀렸는지, 프롬프트가 근거를 전달하지 못했는지, 생성 단계가 잘못 해석했는지를 구분하기 쉽습니다.
RAG가 틀릴 때는 어디를 봐야 할까?
- 관련 청크가 검색되지 않음: 문서 추출, 청크 분할, 임베딩 모델, 검색 필터와 질문 표현을 확인합니다.
- 관련 없는 청크가 위에 나타남: Top-K, 키워드 결합, 메타데이터 필터, 재정렬 적용 여부를 점검합니다.
- 청크는 맞지만 답이 틀림: 모델이 어떤 근거를 받았는지, 프롬프트가 충돌하는 지시는 없는지, 답에 필요한 조건이 한 청크 안에 있는지 확인합니다.
- 근거가 없는데 단정함: 검색 결과가 부족할 때 답변을 보류하는 기준과 프롬프트, 후처리 로직을 확인합니다.
- 다른 사용자의 자료가 섞임: 검색 시점의 권한 필터와 문서 갱신·삭제 시 인덱스 동기화 절차를 확인합니다.
따라서 “답이 맞았는가?”만 보면 원인을 찾기 어렵습니다. 검색 단계와 생성 단계를 각각 기록하고 평가해야 합니다.
검색 품질과 답변 품질을 따로 평가하기
먼저 대표 질문과 각 질문에 필요한 정답 문서(또는 청크)를 작은 평가 세트로 정합니다. 예를 들어 “단순 변심 배송비”, “불량 반품 배송비”, “수령 후 10일이 지난 경우”처럼 조건이 다른 질문을 포함합니다. 이 질문들은 예시 데이터에 기반한 테스트 케이스이며 실제 측정 결과는 아닙니다.
- 검색 평가: 필요한 문서가 상위 K개에 포함되는지(Recall@K), 관련 문서가 얼마나 앞쪽에 나타나는지 확인합니다.
- 답변 평가: 답이 검색 근거와 일치하는지, 질문의 조건을 빠뜨리지 않았는지, 출처가 실제 검색 문서와 연결되는지 확인합니다.
- 보류 평가: 자료에 답이 없는 질문에 모델이 추측하지 않고 정보 부족을 알리는지 확인합니다.
- 운영 평가: 응답 지연 시간, 토큰 사용량, 검색·모델 호출 실패율도 함께 살펴봅니다.
설정 하나를 바꿀 때는 같은 질문 세트로 다시 비교합니다. 청크 크기나 Top-K를 바꿨는데 답변만 읽어보면, 검색이 좋아졌는지 우연히 문장이 달라졌는지 구별하기 어렵습니다.
RAG를 붙이면 해결되는 것은 아니다
RAG는 모델의 지식을 완벽하게 최신화하거나 환각을 없애는 만능 기능이 아닙니다. 원문이 틀리면 답도 틀릴 수 있고, 검색이 실패하면 좋은 근거를 전달하지 못합니다. 검색 결과가 맞아도 모델이 조건을 빠뜨리거나 서로 다른 문서를 섞어 해석할 수 있습니다.
또한 문서 권한, 삭제·갱신 동기화, 개인정보 마스킹, 검색 로그 보관 같은 운영 문제가 생깁니다. 사용자별 접근 권한이 필요한 서비스에서는 문서 적재부터 검색과 출처 표시까지 권한 규칙이 일관되게 적용되는지 확인해야 합니다.
정리
RAG의 핵심은 “문서를 벡터 DB에 넣는다”에 그치지 않습니다. 문서를 의미가 유지되는 단위로 나누고, 질문에 맞는 근거를 검색하고, 권한을 적용한 뒤, 모델이 근거 범위 안에서 답하도록 지시하고, 검색과 답변을 각각 평가하는 전체 흐름입니다.
이 글에서는 가상의 FAQ로 구조를 설명했습니다. 아직 특정 모델·벡터 저장소를 사용해 구현하거나 성능을 측정한 것은 아닙니다. 다음 실습 글에서는 작은 공개 문서 모음으로 실제 인덱스를 만들고, 질문별 검색 결과와 답변을 기록해 설계 설명과 구현 결과를 구분해서 다루겠습니다.
'AI·LLM' 카테고리의 다른 글
| [RAG 기초] 코사인 유사도 — 질문과 문서의 관련성을 숫자로 비교하는 방법 (0) | 2026.10.17 |
|---|---|
| [LLM 기초] Token·Embedding·Context Window — Java 개발자가 이해하는 LLM의 입력 처리 (0) | 2026.10.14 |