📋 요약 한 줄 요약 RAG는 질문과 관련된 자료를 검색하고, 그 내용을 LLM에 함께 전달해 답변의 근거로 활용하는 방식이다. 벡터 검색으로 자료를 찾는 과정과, 찾은 자료를 바탕으로 답변을 만드는 과정을 나누어 이해하면 흐름이 보인다.
벡터와 코사인 유사도를 공부하면서 문장도 벡터로 바꾸면 관련된 내용을 찾을 수 있다는 것을 알게 됐다. 여기서 한 단계 더 나아가, 검색한 자료가 어떻게 답변으로 이어지는지 RAG의 기본 흐름을 정리한다.
예시는 가상의 문서 관리 서비스 NOTE를 대상으로 한다. 설치 방법과 로그인 안내 등 아래 문서와 정책은 개념을 설명하기 위한 가정이다.
1. RAG는 무엇인가
RAG(Retrieval-Augmented Generation, 검색 증강 생성)는 질문에 필요한 자료를 검색하고, 검색한 내용을 질문과 함께 LLM에 전달해 답변을 생성하는 방식이다.
사용자가 “NOTE 비밀번호를 잊었는데 어떻게 해야 하나요?”라고 물었다고 해보자. 서비스는 비밀번호 재설정 안내를 찾아 질문과 함께 전달한다. LLM은 제공된 안내를 참고해 사용자에게 답한다.
사용자 질문
↓
관련 자료 검색 — Retrieval
↓
질문에 검색한 본문을 함께 전달 — Augmentation
↓
LLM이 자료를 참고해 답변 작성 — Generation
검색한 문서를 그대로 보여주는 기능부터 만들 수도 있다. 여기에 자료를 근거로 답변을 작성하는 생성 단계를 연결하면 RAG의 흐름이 된다. LangChain4j 공식 문서
RAG의 검색에는 키워드 검색이나 벡터 검색 등 여러 방법을 사용할 수 있다. 이 글에서는 지금까지 공부한 벡터 검색을 사용하는 방식을 다룬다.
2. 질문을 받기 전에 자료를 준비한다
검색할 자료는 미리 준비해 둔다. 문서를 읽고, 검색에 사용할 단위로 나누고, 벡터로 변환해 저장하는 과정을 인덱싱(Indexing)이라고 한다.
매뉴얼·FAQ
↓
검색할 단위로 나누기
↓
제품명·출처 같은 정보 붙이기
↓
임베딩 모델로 벡터 생성
↓
벡터를 본문·메타데이터와 연결해서 저장
청크는 검색할 설명의 단위다
긴 매뉴얼에는 설치, 로그인, 이용 방법처럼 여러 주제가 섞여 있다. 이를 검색하기 좋은 단위로 나누는 작업이 청킹(Chunking)이고, 나눈 조각 하나가 청크(Chunk)다.
예를 들어 매뉴얼에서 설치 안내와 비밀번호 재설정 안내를 각각 독립적으로 이해할 수 있는 단위로 나눈다. 반면 짧은 FAQ라면 질문과 답변을 함께 한 청크로 사용해도 된다.
질문: NOTE 비밀번호를 잊었어요.
답변: 사내 관리자에게 비밀번호 재설정을 요청하세요.
청크만 읽어도 설명을 이해할 수 있어야 한다. 질문과 답변, 증상과 조치 방법이 서로 떨어지면 검색한 조각에 필요한 맥락이 빠질 수 있다. 문서의 구조와 길이를 보고 나누는 기준을 정한다. 청크를 나누는 이유와 고려 사항
메타데이터는 자료를 구분하고 출처를 연결한다
메타데이터(Metadata)는 본문에 붙이는 추가 정보다. 어떤 제품의 문서인지, 원본은 무엇인지, 언제 수정됐는지 등을 담을 수 있다.
| 자료 | product | sourceId | errorCode |
|---|---|---|---|
| NOTE 비밀번호 재설정 안내 | NOTE | faq-login-001 | 없음 |
| NOTE 설치 안내 | NOTE | guide-install-001 | 없음 |
| NOTE 오류 안내 | NOTE | faq-error-001 | “E001” |
product, sourceId, errorCode는 서비스에서 정한 항목 이름이다. 일반적인 백엔드 개발에서 데이터 필드를 설계하는 것과 비슷하다.
errorCode는 오류 문서에만 붙이면 된다. 설치 방법과 로그인 안내까지 모두 오류 코드가 있어야 하는 구조로 만들 필요는 없다. sourceId는 검색한 청크를 원본 자료로 연결하고, 출처를 표시하거나 갱신할 때 사용할 수 있다.
임베딩 모델이 본문을 벡터로 바꾼다
임베딩(Embedding)은 텍스트를 숫자 목록인 벡터로 변환하는 것이다. 실제 변환은 임베딩 모델이 수행한다.
여기서 사용하는 모델의 역할을 구분하면 다음과 같다.
| 모델 | 입력 | 출력 |
|---|---|---|
| 임베딩 모델 | 청크의 본문 또는 사용자 질문 | 검색에 사용할 벡터 |
| 답변 생성 모델(LLM) | 질문과 검색한 자료 | 사용자에게 전달할 답변 |
Spring AI는 임베딩 모델을 연결하고 호출하는 API를 제공한다. 사용할 모델은 외부 API로 호출하거나 직접 실행하는 방식으로 연결할 수 있다. 모델과 연결 설정을 준비하면 애플리케이션에서 텍스트를 보내 벡터를 받는다. Spring AI Embeddings Model API
생성한 벡터는 청크의 본문과 연결해서 보관한다. 벡터 저장소는 유사한 자료를 찾는 데 쓰이고, 검색 후에는 그 벡터에 연결된 본문을 가져온다. 본문을 같은 저장소에 둘 수도 있고, 원문을 가리키는 식별자로 연결할 수도 있다.
문서가 바뀌지 않았다면 저장된 벡터를 재사용한다. 문서를 수정하면 해당 청크와 벡터를 갱신해야 한다. 질문과 문서의 벡터는 서로 비교할 수 있는 같은 임베딩 공간에 있어야 하므로, 사용하는 모델과 설정도 맞춰야 한다.
3. 질문에 맞는 청크를 찾는다
사용자가 화면에서 제품 NOTE를 선택하고 “비밀번호를 까먹었어요”라고 입력했다고 해보자.
검색은 제품으로 범위를 좁힌 뒤, 그 안에서 질문과 관련된 내용을 찾는 흐름으로 이해할 수 있다.
전체 자료
↓ product = "NOTE" 조건 적용
NOTE 관련 자료
↓ 질문을 벡터로 바꾸고 청크 벡터와 유사도 비교
비밀번호 재설정과 관련된 청크
사용자가 선택한 것은 제품이다. 구체적으로 어떤 청크가 질문에 필요한지는 시스템이 검색한다. 제품을 선택하는 화면 없이 자연어 질문만 받는다면, 질문에서 제품을 식별하거나 추가로 확인하는 처리가 필요하다.
벡터의 유사도를 비교하는 방법 중 하나가 코사인 유사도(Cosine Similarity)다. 질문과 청크의 벡터가 얼마나 비슷한 방향을 가리키는지 점수로 비교한다. 계산 원리는 정규화와 코사인 유사도에서 다뤘다.
임계값과 topK로 가져올 결과를 정한다
임계값(Threshold)은 결과로 허용할 유사도 기준이고, topK는 가져올 결과의 최대 개수다.
아래는 조건의 역할을 설명하기 위한 가상 점수다. 검색 점수는 높을수록 유사하다고 가정한다.
검색 조건
- product: NOTE
- 유사도 임계값: 0.7
- topK: 3
| 청크 | 제품 | 유사도 | 결과 |
|---|---|---|---|
| 다른 제품의 로그인 안내 | OTHER | 0.96 | 제품 조건에서 제외 |
| NOTE 비밀번호 재설정 안내 | NOTE | 0.91 | 표시 |
| NOTE 계정 잠금 안내 | NOTE | 0.78 | 표시 |
| NOTE 설치 안내 | NOTE | 0.43 | 유사도 기준에서 제외 |
이 경우 결과는 두 개다. topK = 3이어도 기준을 통과하지 못한 자료를 넣어 세 개를 채우지는 않는다. 조건에 맞는 자료가 많으면 유사도가 높은 결과를 최대 세 개까지 가져온다.
0.7은 예시일 뿐이다. 모델과 저장소의 점수 체계에 맞춰 실제 질문으로 조절해야 한다. 기준을 높이면 덜 관련된 결과를 줄일 수 있지만 필요한 자료도 빠질 수 있고, 낮추면 덜 관련된 자료가 섞일 수 있다.
Spring AI 코드로 연결하기
메타데이터 필터와 유사도 검색을 지원하는 vectorStore에 임베딩 모델이 연결되어 있고, 앞의 자료도 저장되어 있다고 가정한다. 검색 조건은 다음처럼 작성할 수 있다.
import org.springframework.ai.vectorstore.SearchRequest
val question = "비밀번호를 까먹었어요."
val request = SearchRequest.builder()
.query(question)
.filterExpression("product == 'NOTE'")
.similarityThreshold(0.7)
.topK(3)
.build()
val documents = vectorStore.similaritySearch(request)
request는 검색 조건을 담고, similaritySearch에서 실제 검색을 실행한다. 연결된 임베딩 모델을 사용하는 저장소 구현이 질문을 벡터로 변환하고 검색을 수행한다. 결과의 각 Document에는 찾은 본문과 메타데이터 등이 담긴다. 이 코드는 검색 부분의 예시이며, 모델·저장소 설정은 별도로 필요하다. Spring AI Vector Databases
4. 찾아낸 본문을 질문과 함께 LLM에 전달한다
관련 청크를 찾았다면 답변 생성에 필요한 것은 그 청크의 본문이다. 서버는 사용자 질문과 본문을 함께 LLM에 전달한다.
사용자 질문:
NOTE 비밀번호를 까먹었는데 어떻게 해야 하나요?
찾아낸 자료:
NOTE 비밀번호를 잊은 경우,
사내 관리자에게 비밀번호 재설정을 요청한다.
답변 요청:
제공된 자료를 근거로 안내한다.
자료로 확인되지 않는 내용은 확인이 필요하다고 알린다.
이를 바탕으로 다음과 같은 답변을 작성할 수 있다.
사내 관리자에게 NOTE 비밀번호 재설정을 요청해 주세요.
서비스는 답변과 함께 원본 자료의 제목이나 링크를 표시해 사용자가 근거를 확인할 수 있게 할 수 있다. 원본 출처는 검색 결과의 메타데이터에서 가져온다.
벡터는 자료를 찾는 데 사용하고, 본문은 답변의 근거로 사용한다. Spring AI의 RAG 구성 요소도 검색한 문서 내용을 질문에 함께 제공하는 흐름을 지원한다. Spring AI RAG
5. 유사도가 높아도 답할 근거가 없을 수 있다
사용자가 “NOTE를 맥에 설치할 수 있나요?”라고 물었는데, 검색된 문서는 윈도우 설치 안내뿐인 상황을 가정해보자.
둘 다 NOTE 설치에 관한 내용이므로 유사도가 높게 나올 수 있다. 하지만 문서에 맥 지원 여부가 적혀 있지 않다면, 해당 문서만으로는 설치가 가능한지 판단할 수 없다.
이때 필요한 안내는 다음과 같다.
현재 확인된 자료에는 맥 지원 여부가 나와 있지 않습니다. 추가 확인이 필요합니다.
“유사도가 높다”는 말은 “정답일 확률이 높다”와 같지 않다. 질문과 관련 있는 자료인지, 그 자료에 질문의 답이 있는지는 따로 확인해야 한다.
LLM 없이 검색 결과만 보여주는 서비스라면 임계값 미만을 제외하고, 통과한 문서를 관련 자료로 표시할 수 있다. 그러나 점수만으로 본문에 답이 있는지까지 판단할 수는 없다. 구조화된 데이터나 별도의 규칙으로 답할 수 있는 경우도 있지만, 자유로운 질문과 문서를 읽어 설명하는 역할은 답변 생성 모델에 맡길 수 있다.
자료에 근거해서 답하도록 요청해도 LLM이 항상 지키는 것은 아니다. 문서를 잘못 해석하거나 자료에 없는 내용을 덧붙일 수 있으므로, 검색 결과와 생성 답변을 각각 확인해야 한다.
6. 검색과 답변을 나누어 확인한다
서비스가 잘 동작하는지 보려면 실제로 들어올 법한 질문과 기대 결과를 준비한다. 앞의 가상 문서를 기준으로 한 테스트 예시는 다음과 같다.
| 질문 | 검색에서 확인할 것 | 답변에서 확인할 것 |
|---|---|---|
| NOTE 비밀번호를 잊었어요 | 비밀번호 재설정 안내를 찾았는가 | 관리자에게 재설정을 요청하라는 핵심 내용이 있는가 |
| NOTE 설치 방법을 알려주세요 | 해당 제품의 설치 안내를 찾았는가 | 문서에 있는 절차를 정확히 안내하는가 |
| NOTE를 맥에 설치할 수 있나요 | 검색한 자료에 맥 지원 정보가 있는가 | 정보가 없는 상황에서 지원 여부를 단정하지 않는가 |
비밀번호 질문에 설치 안내를 가져왔다면 검색 과정을 살펴본다. 재설정 안내를 찾았는데 답변이 “프로그램을 재설치하세요”라면 자료를 활용해 답변을 만드는 과정을 살펴본다.
답변 문장이 미리 적어둔 문장과 완전히 같을 필요는 없다. 필요한 내용이 맞는지, 근거가 없는 내용을 덧붙이지 않았는지가 기준이다. 이 질문 목록은 임계값이나 모델을 바꾼 뒤에도 다시 확인하는 데 사용할 수 있다.