[내일배움캠프 Spring 3기] ForPets AI 도전 기능 1 - RAG 파이프라인

jiiim_ni·2026년 6월 9일

ForPets RAG 파이프라인 구축기: 리뷰 데이터를 AI 추천 근거로 활용하기

ForPets 프로젝트에서 AI 도전 기능으로 RAG 파이프라인을 구현했다.

기존 AI 시터 추천 챗봇은 시터의 기본 정보, 리뷰 요약, 스케줄 정보를 기반으로 추천을 제공했다. 이번 작업에서는 여기서 한 단계 더 나아가, 실제 보호자 리뷰 원문을 벡터화해서 검색 가능한 지식 베이스로 만들고, 사용자의 자연어 질문과 의미적으로 유사한 리뷰를 찾아 AI 추천 응답의 근거로 활용하도록 확장했다.

즉, LLM이 그냥 “그럴듯하게” 추천하는 것이 아니라 실제 리뷰 데이터를 기반으로 응답하도록 만드는 것이 목표였다.

보호자 리뷰 데이터 조회
→ 리뷰 원문 임베딩 생성
→ Qdrant Vector DB 저장
→ 사용자 자연어 질문 임베딩
→ 유사 리뷰 시맨틱 검색
→ AI 추천 응답에 리뷰 근거 포함

왜 RAG가 필요했나?

시터 추천에서 중요한 건 단순 조건 검색만이 아니었다.

예를 들어 사용자는 이렇게 물어볼 수 있다.

분리불안 있는 말티즈를 차분하게 돌봐줄 시터 추천해줘
노령견을 천천히 산책시켜주는 시터 찾아줘
낯가림 있는 아이를 잘 기다려주는 시터 있어?

이런 요청은 단순히 지역, 가격, 경력만으로 판단하기 어렵다. 실제 보호자 리뷰에 “분리불안”, “천천히 산책”, “억지로 만지지 않고 기다려줌” 같은 경험이 남아 있다면, 그 리뷰를 추천 근거로 활용하는 게 훨씬 설득력 있다.

그래서 리뷰 원문을 벡터화하고, 사용자 질문과 의미적으로 가까운 리뷰를 찾아 AI 추천에 붙이는 구조를 만들었다.


사용 기술

이번 RAG 구현에는 아래 기술을 사용했다.

구분사용 기술이유
EmbeddingGemini gemini-embedding-001무료 테스트가 가능하고 리뷰 문장 임베딩에 사용 가능
Vector DBQdrantDocker로 로컬 실행이 쉽고 payload 기반 출처 저장이 편함
BackendSpring Boot기존 ForPets 백엔드 구조 유지
HTTP ClientSpring RestClientGemini API, Qdrant API 직접 호출
SimilarityCosine Similarity자연어 의미 유사도 비교에 적합

전체 구조

구현한 흐름은 크게 세 단계다.

1. 리뷰 인덱싱
   리뷰 원문 → Gemini Embedding → Qdrant 저장

2. RAG 검색
   사용자 질문 → Gemini Embedding → Qdrant 유사 리뷰 검색

3. AI 추천 챗봇 결합
   Tool Calling 추천 결과 + RAG 리뷰 근거 → 최종 응답

1. 리뷰 데이터 확인

RAG 인덱싱 대상은 COMPLETED 예약에 연결된 보호자 리뷰다.

리뷰는 실제 케어가 완료된 예약에 대해서만 작성될 수 있기 때문에, 추천 근거로 사용하기에 적합하다고 판단했다.

테스트용으로 시터 리뷰 데이터를 조회했다.

GET /api/reviews/sitters/13?page=0&size=10&sort=createdAt&direction=desc

응답 일부는 다음과 같다.

{
  "id": 130,
  "reservationId": 135,
  "reviewerId": 3,
  "revieweeId": 16,
  "reviewComment": "노령견이라 걱정했는데 이동 속도에 맞춰 천천히 산책해주시고 물 먹는 양도 확인해주셨습니다.",
  "rating": 2
}

이 리뷰 원문을 임베딩해서 Qdrant에 저장한다.


2. RAG 인덱싱 구현

인덱싱 API는 다음과 같다.

POST /api/ai/rag/reviews/index

처리 흐름은 다음과 같다.

1. COMPLETED 예약의 활성 리뷰 조회
2. 리뷰별 RagDocument 생성
3. Gemini Embedding API 호출
4. 리뷰 원문을 벡터로 변환
5. Qdrant collection 초기화
6. Qdrant에 vector + payload upsert
7. 성공/실패 개수 반환

Qdrant payload에는 검색 결과의 출처를 표시하기 위해 다음 정보를 저장했다.

필드설명
sourceTypeREVIEW / REVIEW_SUMMARY
sourceId원본 데이터 ID
reviewId리뷰 ID
sitterId시터 프로필 ID
rating리뷰 평점
content리뷰 원문

테스트 결과는 다음과 같았다.

{
  "success": true,
  "data": {
    "indexedCount": 100,
    "failedCount": 30
  },
  "error": null
}

총 130개 대상 중 100개가 인덱싱되었고, 30개는 실패 처리되었다. 실패한 문서가 있어도 전체 인덱싱을 중단하지 않고 가능한 문서만 우선 저장하도록 했다.


3. RAG 검색 구현

검색 API는 다음과 같다.

POST /api/ai/rag/search

요청 예시:

{
  "query": "노령견을 천천히 산책시켜주는 시터"
}

처리 흐름은 다음과 같다.

1. 사용자 자연어 질문 수신
2. 질문을 Gemini Embedding API로 벡터화
3. Qdrant에서 cosine similarity 기반 유사 리뷰 검색
4. score_threshold 이상인 결과만 반환
5. sourceId, reviewId, sitterId, snippet, score 포함

응답 예시:

{
  "sourceType": "REVIEW",
  "sourceId": 100,
  "reviewId": 100,
  "sitterId": 10,
  "rating": 5,
  "snippet": "노령견이라 걱정했는데 이동 속도에 맞춰 천천히 산책해주시고 물 먹는 양도 확인해주셨습니다.",
  "score": 0.8516915
}

“노령견을 천천히 산책시켜주는 시터”라는 질문에 대해 실제 리뷰 원문 중 “노령견”, “천천히 산책”, “물 먹는 양 확인”과 의미적으로 가까운 리뷰가 검색되는 것을 확인했다.


4. AI 추천 챗봇과 RAG 결합

기존 AI 추천 챗봇에도 RAG 검색 결과를 연결했다.

POST /api/ai/chat

요청:

{
  "message": "분리불안 있는 말티즈를 차분하게 돌봐줄 시터 추천해줘"
}

응답에는 추천 시터 목록과 함께 RAG 검색 출처가 포함된다.

{
  "sourceType": "REVIEW",
  "sourceId": 61,
  "reviewId": 61,
  "sitterId": 7,
  "rating": 5,
  "snippet": "말티즈가 낯을 많이 가리는데 천천히 적응시켜주시고 사진도 자주 보내주셔서 안심됐습니다.",
  "score": 0.8104445
}

AI 답변에도 실제 리뷰 근거가 포함되도록 했다.

리뷰 근거: 리뷰 #61 / 시터 #7 / "말티즈가 낯을 많이 가리는데 천천히 적응시켜주시고 사진도 자주 보내주셔서 안심됐습니다."

이렇게 하면 사용자는 추천 결과가 어떤 리뷰를 근거로 만들어졌는지 확인할 수 있다.


5. 검색 결과 없음 fallback

ForPets 도메인과 관련 없는 질문도 테스트했다.

요청:

{
  "query": "자동차 엔진오일을 잘 교체하는 시터"
}

응답:

{
  "success": true,
  "data": {
    "results": []
  },
  "error": null
}

관련 없는 질문에 대해 억지로 리뷰를 끼워 맞추지 않고 빈 결과를 반환했다. 환각 방어 관점에서 이 동작이 중요하다고 봤다.


6. Qdrant 장애 fallback

Qdrant 장애 상황도 고려했다.

RAG는 추천 품질을 높이는 보조 기능이지, 핵심 추천 기능 자체가 아니다. 따라서 Qdrant 검색에 실패해도 챗봇 전체가 죽으면 안 된다.

Qdrant 검색 실패
→ RAG sources 빈 리스트 반환
→ Tool Calling 기반 추천은 계속 진행

장애 상황에서도 응답 형태는 유지되도록 처리했다.

{
  "success": true,
  "data": {
    "results": []
  },
  "error": null
}


트러블슈팅 1. Gemini Embedding 모델 오류

처음에는 text-embedding-004 모델을 사용했다.

그런데 Gemini API 호출 시 다음 오류가 발생했다.

models/text-embedding-004 is not found for API version v1beta

현재 호출 방식에서는 해당 모델을 사용할 수 없었다.

그래서 embedding model을 다음과 같이 변경했다.

forpets:
  ai:
    gemini:
      embedding-model: gemini-embedding-001

트러블슈팅 2. Qdrant Collection 중복 생성

인덱싱 API를 여러 번 호출하니 Qdrant에서 collection이 이미 존재한다는 오류가 발생했다.

Collection `forpets_reviews` already exists

인덱싱 API는 여러 번 호출해도 안전해야 한다. 그래서 collection이 이미 존재하면 실패로 보지 않고 재사용하도록 처리했다.

collection 없음 → 새로 생성
collection 있음 → 재사용

트러블슈팅 3. Vector Dimension 불일치

가장 오래 잡아먹은 문제는 벡터 차원 불일치였다.

Gemini gemini-embedding-001은 3072차원 벡터를 반환하는데, Qdrant collection은 처음에 768차원으로 생성되어 있었다.

그 결과 다음 오류가 발생했다.

Vector dimension error: expected dim: 768, got 3072

해결 방법은 두 가지였다.

먼저 설정을 3072로 변경했다.

forpets:
  ai:
    rag:
      vector-size: 3072

그리고 기존 collection이 이미 생성되어 있는 경우를 위해, collection의 vector size를 확인한 뒤 설정값과 다르면 RAG 전용 collection을 삭제 후 재생성하도록 처리했다.

collection 있음 + vector size 일치 → 재사용
collection 있음 + vector size 불일치 → 삭제 후 재생성

이렇게 해서 로컬 테스트 중에도 이전 설정으로 만들어진 collection 때문에 계속 실패하는 문제를 해결했다.


Postman 테스트 결과

최종적으로 Postman에서 6개 시나리오를 테스트했다.

번호테스트 항목결과
1리뷰 데이터 확인시터별 리뷰 정상 조회
2RAG 인덱싱indexedCount=100, failedCount=30
3RAG 검색자연어 질문과 유사한 리뷰 검색 성공
4AI 챗봇 + RAG 근거 확인추천 응답에 sources와 리뷰 근거 포함
5검색 결과 없음 fallback도메인과 무관한 질문에 빈 결과 반환
6Qdrant 장애 fallback장애 시 빈 결과 반환, 서비스 응답 정상 유지

과제 요구사항 매핑

요구사항구현 내용
도메인 데이터를 임베딩하여 벡터 DB에 저장보호자 리뷰 원문을 Gemini Embedding으로 벡터화 후 Qdrant 저장
자연어 질의 기반 시맨틱 검색사용자 질문을 임베딩하여 Qdrant에서 유사 리뷰 검색
검색 결과 출처 포함sourceId, reviewId, sitterId, snippet, score 반환
유사도 임계값 설정score-threshold: 0.7 적용
검색 결과 없음 fallback관련 없는 질문에 results: [] 반환
RAG + Tool Calling 결합AI 추천 챗봇 응답에 RAG sources 포함
장애 fallbackQdrant 실패 시 빈 sources 반환, 추천 기능은 유지

마무리

이번 RAG 작업을 통해 ForPets의 AI 추천 기능은 단순 조건 검색 기반 추천에서 한 단계 확장되었다.

최종 구조는 다음과 같다.

리뷰 원문
→ Gemini Embedding
→ Qdrant 저장
→ 자연어 질문 임베딩
→ 유사 리뷰 검색
→ AI 추천 응답에 리뷰 근거 포함

이제 사용자는 “분리불안 있는 말티즈”, “노령견 산책”, “낯가림 있는 아이”처럼 정확한 키워드가 아니더라도, 실제 리뷰 원문을 기반으로 더 신뢰할 수 있는 시터 추천을 받을 수 있다.

가장 크게 느낀 점은 RAG는 단순히 “벡터 DB 붙이기”가 아니라는 것이다. 모델의 임베딩 차원, Vector DB collection 설정, 검색 threshold, 출처 payload, 장애 fallback까지 함께 맞아야 실제 서비스 기능으로 쓸 수 있었다.

Postman 테스트 링크

0개의 댓글