도서 추천 서비스에서 일반 LLM만 쓰지 않고 RAG를 붙인 이유

LLM을 이용해 도서 추천 서비스를 처음 생각하면 구현 방법은 의외로 단순해 보인다.

사용자가 원하는 책을 자연어로 입력하고, 그 문장을 LLM에 전달한 뒤 추천 도서를 받아오면 된다.

예를 들어 사용자가 다음과 같이 질문한다고 해보자.

요즘 회사 생활이 너무 지치는데 가볍게 읽으면서 위로받을 수 있는 책을 추천해줘.

LLM은 이런 질문에 꽤 자연스럽게 답할 수 있다. 책 몇 권을 추천하고 각각의 이유도 그럴듯하게 설명할 수 있다.

문제는 내가 만들고 있던 서비스가 단순한 AI와의 도서 대화 서비스가 아니었다는 점이다.

실제 서비스에서는 사용자가 이용하는 도서관을 기준으로 검색해야 했고, 검색된 도서의 제목과 저자, ISBN, 출판 정보 같은 실제 데이터를 기반으로 결과를 만들어야 했다.

즉 내가 필요했던 것은:

그럴듯한 책을 알고 있는 AI

가 아니라,

현재 내가 가지고 있는 도서 데이터 안에서 조건에 맞는 책을 찾고, 그 결과를 사람이 이해하기 좋은 문장으로 설명해주는 시스템

이었다.

이 차이 때문에 일반 LLM 호출만으로는 부족했고 검색 시스템과 LLM을 연결하는 RAG 구조를 사용하게 됐다.


일반 LLM이 책을 잘 추천하는데도 부족했던 이유

LLM에게 책을 추천해 달라고 하면 답변 자체는 상당히 자연스럽다.

특히 유명한 책을 대상으로 하면 제목, 저자, 책의 특징까지 함께 설명하는 경우도 많다.

하지만 실제 검색 서비스를 만든다는 관점에서는 답변이 자연스러운 것과 서비스가 신뢰할 수 있는 데이터를 반환하는 것은 완전히 다른 문제였다.

내 서비스에서 필요한 결과에는 단순한 추천 문장만 있는 것이 아니었다.

검색 결과를 구성하려면 다음과 같은 실제 도서 정보가 필요했다.

책 제목
저자
ISBN
표지
출판연도
키워드
도서 설명
소장 도서관

그리고 그 정보를 이용해 다시 다음과 같은 문장을 만들어야 했다.

왜 이 책을 추천하는가

실제로 어떻게 활용할 수 있는가

이 책을 읽었을 때 어떤 점을 기대할 수 있는가

여기서 중요한 차이가 생긴다.

LLM은 추천 이유를 만들어내는 데는 강하지만, 우리 데이터베이스에 지금 어떤 책이 존재하는지 알지 못한다.

특정 도서관에 그 책이 있는지도 알 수 없다.

내부 DB에서 사용하는 정확한 ISBN이나 현재 저장되어 있는 메타데이터 역시 프롬프트만으로는 확인할 방법이 없다.

따라서 일반 LLM에게 처음부터 끝까지 맡기는 방식은 내가 만들고 싶은 서비스 구조와 맞지 않았다.


그래서 역할을 검색과 생성으로 분리했다

도서 추천을 하나의 AI 기능으로 생각하면 모든 일을 LLM에게 맡기기 쉽다.

하지만 실제 구현에서는 역할을 둘로 나누는 편이 훨씬 명확했다.

첫 번째는 어떤 책을 보여줄지 결정하는 검색 단계다.

두 번째는 검색된 책을 사용자에게 어떻게 설명할지 결정하는 생성 단계다.

내가 만든 구조를 단순하게 표현하면 다음과 같다.

사용자 질문
    ↓
검색어 분석
    ↓
Elasticsearch에서 실제 도서 검색
    ↓
관련 도서 후보
    ↓
검색된 도서 정보 + 사용자 질문
    ↓
LLM
    ↓
추천 이유와 설명 생성

여기서 LLM은 존재하지 않는 책을 자유롭게 고르는 역할을 하지 않는다.

먼저 검색 시스템에서 실제 도서 후보를 확보하고, LLM이 판단할 수 있는 범위를 그 후보 안으로 좁힌다.

이 구조가 내가 도서 서비스에서 RAG를 사용하는 가장 중요한 이유였다.


RAG가 필요한 이유는 LLM에게 더 많은 지식을 넣기 위해서만은 아니었다

RAG를 설명하는 글을 보면 흔히 다음과 같이 이야기한다.

LLM이 모르는 최신 정보를 검색해서 제공한다.

물론 중요한 기능이다.

하지만 실제 서비스를 개발하면서 느낀 RAG의 장점은 조금 달랐다.

내 입장에서는 LLM이 답변할 수 있는 범위를 통제할 수 있다는 점이 더 중요했다.

예를 들어 현재 검색된 책이 다음 세 권이라고 하자.

도서 A
도서 B
도서 C

LLM에게 이 데이터와 사용자 질문을 같이 전달하면 내가 원하는 것은 다음과 같다.

A, B, C 중에서 사용자의 질문에 가장 적합한 책을 설명해줘.

반면 검색 과정 없이 질문만 전달하면 의미가 달라진다.

네가 알고 있는 모든 책 중에서 적합한 책을 찾아줘.

둘 다 자연스러운 답변을 만들 수 있지만 서비스 개발자 입장에서는 첫 번째 방식이 훨씬 관리하기 쉽다.

어떤 데이터가 모델에게 전달됐는지 확인할 수 있고, 왜 특정 책이 답변에 등장했는지도 어느 정도 역추적할 수 있기 때문이다.


특정 도서관의 책만 추천해야 하는 문제가 있었다

내가 만든 검색 서비스에서는 도서관을 구분하는 값이 중요했다.

사용자가 특정 도서관에서 책을 찾고 있다면 검색 결과 역시 그 도서관이 보유한 도서를 대상으로 해야 한다.

그래서 검색 데이터에는 도서관을 구분하기 위한 libCode가 존재했고 검색 단계에서 이 조건을 이용했다.

이 문제는 일반 LLM만으로 해결하기 어렵다.

예를 들어 사용자가:

내가 이용하는 도서관에서 빌릴 수 있는 파이썬 입문 책을 추천해줘.

라고 입력했다고 하자.

여기에서 중요한 것은 ‘파이썬 입문 책’을 아는 것이 아니다.

실제 서비스는 먼저:

  1. 사용자가 어느 도서관을 이용하는지 확인하고
  2. 해당 도서관의 데이터만 대상으로 검색하고
  3. 그중 파이썬 입문에 적합한 책을 찾고
  4. 검색된 책을 근거로 추천해야 한다.

내 검색 구조에서는 이 역할을 Elasticsearch가 담당한다.

LLM은 이 결과를 받은 뒤 추천 이유나 사용자가 이해하기 쉬운 설명을 만드는 쪽에 집중한다.


자연어 질문 때문에 벡터 검색도 필요했다

도서 검색은 ISBN이나 제목만 입력받는다면 비교적 단순하다.

하지만 내가 만들고 싶었던 기능은 사용자가 자연어로 질문할 수 있는 검색이었다.

예를 들어:

최근에 아무것도 하기 싫고 의욕이 없는데 너무 무겁지 않은 책을 읽고 싶어.

이런 질문에는 정확한 도서명이 없다.

저자도 없다.

사용자는 자기 상태와 원하는 느낌만 이야기하고 있다.

기존 키워드 검색으로도 의욕, 무기력, 위로 같은 단어를 이용해 후보를 찾을 수 있지만 책 메타데이터에서 사용하는 표현과 사용자의 표현이 정확히 일치한다는 보장은 없다.

그래서 검색 단계에서 텍스트 검색과 함께 embedding을 이용한 벡터 검색도 사용했다.

전체 흐름은 다음에 가깝다.

사용자 자연어 질문
        ↓
텍스트 검색 + 의미 기반 검색
        ↓
관련성이 높은 실제 도서 후보
        ↓
도서 메타데이터 확보
        ↓
LLM에 컨텍스트로 전달
        ↓
사용자 질문에 맞춘 설명

이 구조에서 검색과 생성은 서로 역할이 다르다.

Elasticsearch가 무슨 책을 근거로 사용할 것인지를 결정하고,

LLM은 그 책이 사용자에게 왜 적합한지를 설명한다.


실제로는 검색 결과가 나쁘면 LLM도 살릴 수 없었다

RAG를 붙였다고 해서 답변 품질이 자동으로 좋아지는 것은 아니었다.

이 구조를 개발하면서 가장 중요하게 느낀 부분 중 하나다.

LLM에 아무리 좋은 프롬프트를 사용해도 앞 단계에서 엉뚱한 문서가 검색되면 최종 답변 역시 제한을 받는다.

예를 들어 사용자가:

파이썬을 처음 공부하는 사람이 읽을 책

을 검색했는데 검색 단계에서 고급 머신러닝 서적이나 Python 내부 구현에 관한 책이 상위 후보로 들어왔다고 하자.

LLM에게 그 책들만 전달한 상태에서:

초보자에게 가장 적합한 책을 골라 설명해줘.

라고 요구한다고 갑자기 좋은 입문서가 생겨나지는 않는다.

LLM은 제공된 후보 안에서 답을 만들기 때문이다.

그래서 RAG 답변이 이상하게 나왔을 때 처음부터 프롬프트만 수정하는 방식은 효율적이지 않았다.

먼저 확인해야 할 것은:

LLM에게 어떤 검색 결과를 전달했는가?

였다.


RAG 문제를 세 단계로 나눠서 보기 시작했다

이후에는 답변이 마음에 들지 않을 때 문제를 크게 세 단계로 나눠봤다.

1. Retrieval 문제

먼저 검색이 제대로 됐는지 본다.

사용자 질문

↓

검색 결과 TOP N

여기에서 이미 관련 없는 책이 들어왔다면 검색 단계부터 조정해야 한다.

예를 들어 확인할 수 있는 것은:

  • 텍스트 검색 필드
  • title 가중치
  • keywords
  • description
  • embedding
  • KNN 설정
  • 검색 필터
  • 검색 후보 수

등이다.


2. Context 문제

검색 결과는 적절하지만 LLM에 전달하는 정보가 부족할 수도 있다.

책 제목만 넘기는 것과:

title

다음 정보를 같이 넘기는 것은 차이가 크다.

title
author
keywords
description
publication year

하지만 반대로 필요 이상으로 많은 데이터를 한 번에 넣는다고 항상 좋은 것도 아니다.

관련 없는 정보를 많이 전달하면 모델이 어떤 정보에 집중해야 할지 어려워질 수 있고 입력 토큰도 증가한다.

그래서 검색 결과에서 생성에 필요한 정보가 무엇인지를 따로 결정해야 했다.


3. Generation 문제

검색 결과도 좋고 전달한 정보도 충분한데 설명이 이상하다면 그때 생성 단계를 본다.

예를 들어 프롬프트에서:

  • 추천 이유를 어떻게 작성할지
  • 검색 결과 밖의 책을 언급해도 되는지
  • 어떤 말투를 사용할지
  • 사실과 추천 표현을 어떻게 구분할지

같은 조건을 조정한다.

이렇게 나눠보니 RAG 문제를 전부 프롬프트 문제라고 생각하는 것보다 원인을 찾기가 훨씬 쉬웠다.


일반 LLM 방식과 RAG 방식에서 가장 크게 다른 부분

내가 실제 서비스 관점에서 두 방식을 비교할 때 가장 중요한 차이는 다음이었다.

일반 LLM

질문:

초등학생이 과학에 흥미를 가질 만한 책 추천해줘.

LLM은 자신의 학습 과정에서 알고 있는 정보를 바탕으로 책을 추천할 수 있다.

답변이 자연스러울 수 있고 실제 존재하는 유명 도서를 추천할 수도 있다.

하지만 서비스 입장에서는 다음을 바로 보장할 수 없다.

우리 DB에 있는 책인가?

해당 도서관이 가지고 있는가?

현재 저장된 ISBN과 일치하는가?

우리 검색 조건에 맞는가?

RAG

같은 질문을 먼저 검색 시스템으로 보낸다.

질문
↓
도서 검색
↓
실제 데이터에 있는 후보
↓
후보 정보를 LLM에 전달
↓
추천 설명 생성

이 경우 적어도 답변의 출발점은 내가 관리하고 있는 데이터가 된다.

나에게는 이 차이가 RAG를 사용하는 가장 현실적인 이유였다.


RAG를 붙인다고 환각이 사라지는 것은 아니다

RAG를 사용하면 환각 문제가 완전히 해결된다고 생각하기 쉽다.

하지만 그렇게 보지는 않는다.

검색된 데이터를 제공해도 LLM은 여전히 생성 모델이다.

전달한 정보를 잘못 해석하거나, 입력에 없는 내용을 추가하거나, 지나치게 확신하는 문장을 만들 가능성은 남아 있다.

그래서 서비스에서는 RAG를:

LLM에게 정답을 알려주는 기능

이라기보다,

LLM이 참고해야 할 근거와 답변 범위를 최대한 명확하게 만들어주는 구조

로 보는 편이 더 맞다고 생각한다.

이 관점으로 보면 검색 결과 자체를 로그로 남기는 것도 중요해진다.

최종 답변만 저장하면 나중에 문제가 생겼을 때:

왜 이런 답변이 나왔지?

를 확인하기 어렵다.

반대로:

사용자 검색어
검색된 책
검색 방식
최종 답변

을 함께 확인할 수 있다면 문제를 검색과 생성 단계로 나눠서 분석할 수 있다.


실제 서비스에서는 답변보다 추적 가능성이 중요했다

개발 초반에는 사용자가 보는 최종 문장이 가장 중요해 보였다.

추천 문장이 자연스러운지, 질문에 잘 답했는지에 집중했다.

하지만 운영을 생각하면 왜 그 답변이 나왔는지를 다시 확인할 수 있는 구조도 중요했다.

예를 들어 사용자가:

왜 이런 책을 추천했지?

라고 느낄 만한 결과가 나왔다고 하자.

일반 LLM 호출만 저장했다면 확인할 수 있는 정보가 많지 않다.

하지만 RAG 구조라면 최소한 다음 순서로 추적할 수 있다.

당시 사용자가 입력한 검색어
        ↓
Elasticsearch가 반환한 도서
        ↓
LLM에 전달한 컨텍스트
        ↓
최종 추천 문장

검색된 책 자체가 이상했다면 검색 품질 문제다.

책은 적절한데 설명만 이상했다면 생성 문제에 가깝다.

이 차이는 기능을 처음 만들 때보다 운영하면서 더 중요하게 느껴졌다.


그래서 검색 로그를 따로 남기게 됐다

검색·추천 서비스를 만들면서 검색과 생성 관련 데이터를 별도로 기록하는 이유도 여기에 있다.

사용자가 어떤 검색어를 입력했는지, 어떤 검색 결과가 나왔는지, 어떤 추천 문장이 생성됐는지를 나중에 다시 확인해야 검색 품질을 개선할 수 있기 때문이다.

단순히:

요청 성공
HTTP 200

만 남겨서는 AI 검색 품질을 분석하기 어렵다.

오히려 다음 정보가 더 유용할 수 있다.

사용자가 입력한 검색어

사용한 검색 방식

검색된 도서

추천 문장

응답시간

오류 여부

서비스를 운영하면서 이런 데이터를 쌓으면 특정 검색어에서 반복적으로 좋지 않은 결과가 나오는지 확인할 수 있다.

그리고 그때 비로소 검색 알고리즘이나 프롬프트를 수정할 근거가 생긴다.


일반 LLM이 필요 없는 것은 아니다

그렇다고 일반 LLM 호출 자체가 의미 없다는 뜻은 아니다.

모든 질문이 반드시 검색 데이터를 필요로 하는 것은 아니다.

예를 들어:

독서 습관을 만들려면 어떻게 시작하는 게 좋을까?

같은 질문이라면 특정 도서 데이터 없이도 일반적인 답변을 제공할 수 있다.

반면:

우리 도서관에서 빌릴 수 있는 책 중에서 경제를 처음 공부하기 좋은 책을 추천해줘.

는 검색 데이터가 핵심이다.

그래서 앞으로 AI 서비스를 설계한다면 모든 요청을 무조건 RAG로 보내기보다 **이 질문에 외부 데이터가 필요한가?**를 먼저 생각하는 편이 좋다고 본다.

검색이 필요하지 않은 질문까지 매번 Elasticsearch를 거치게 하면 시스템 구조와 비용만 복잡해질 수도 있다.


내가 RAG를 사용하는 기준

현재는 다음과 같은 질문에 RAG가 특히 필요하다고 본다.

서비스가 보유한 데이터에서 답해야 할 때

우리 도서관의 책
우리 서비스에 등록된 책
특정 조건에 해당하는 도서

데이터의 정확한 값을 사용해야 할 때

ISBN
저자
출판연도
도서관 코드

자연어 질문을 실제 검색 결과와 연결해야 할 때

취업 준비하면서 자신감이 떨어졌을 때 읽을 책

나중에 결과의 근거를 추적해야 할 때

왜 이 책이 추천됐는가?

반대로 외부 데이터가 필요하지 않은 일반적인 대화라면 반드시 RAG가 필요한 것은 아니다.


이번 서비스를 만들면서 RAG를 바라보는 방식이 달라졌다

처음에는 RAG를 주로:

LLM에 검색 기능을 붙이는 기술

정도로 생각했다.

실제 서비스를 만들면서는 조금 다르게 보게 됐다.

RAG의 핵심은 단순히 검색 결과를 프롬프트에 넣는 것이 아니라 서비스가 신뢰하는 데이터와 LLM의 생성 능력 사이에 경계를 만드는 것에 더 가까웠다.

내 도서 검색 서비스에서는:

어떤 책이 존재하는가
어떤 도서관에 있는가
책의 실제 정보는 무엇인가

같은 부분은 검색과 데이터가 담당한다.

반대로:

왜 이 책이 사용자에게 맞는가
어떻게 설명하면 이해하기 쉬운가
어떤 방식으로 추천 이유를 표현할 것인가

같은 부분은 LLM이 담당한다.

이렇게 역할을 나누고 나니 LLM에게 모든 것을 맡기는 것보다 시스템 전체를 이해하고 수정하기가 훨씬 쉬워졌다.


마무리

도서 추천 기능만 놓고 보면 일반 LLM도 상당히 좋은 답변을 만든다.

하지만 실제 서비스를 만들 때 내가 필요했던 것은 좋은 문장을 만드는 AI 하나가 아니었다.

특정 도서관의 실제 데이터에서 책을 찾고, 그 책의 정확한 정보를 유지하면서, 사용자의 자연어 질문에 맞는 추천 설명까지 생성해야 했다.

그래서 검색과 생성을 분리했다.

Elasticsearch는 실제 도서를 찾는다.

RAG는 검색된 정보를 LLM에 전달한다.

LLM은 그 범위 안에서 사용자가 이해하기 쉬운 설명을 만든다.

그리고 문제가 생겼을 때:

검색이 잘못됐는가?
컨텍스트가 잘못됐는가?
생성이 잘못됐는가?

를 따로 확인한다.

지금은 이것이 일반 LLM과 RAG의 가장 큰 차이라고 생각한다.

RAG를 사용했다고 해서 LLM이 갑자기 더 똑똑해지는 것은 아니다.

대신 실제 서비스가 가지고 있는 데이터를 근거로 답하게 만들고, 답변이 만들어진 과정을 추적할 수 있게 해준다.

도서 검색·추천 서비스를 운영하는 입장에서는 바로 그 점이 RAG를 붙일 가장 큰 이유였다.