Hun-Bot

On-The-Block 서비스 개발기 05 : 추천 시스템 & 챗 봇

recommendation AI on-the-block chat-bot

추천 시스템 개발

이번 글에서는 추천 시스템 개발에 대한 내용을 공유하려고 합니다. 저희 서비스 같은 경우는 주류 추천, 장소 추천, 주류 설명, 주류와 장소에 대한 질문 답변 등이 존재합니다.

동시에 사용자가 앱의 범위를 벗어난 질문을 할 수도 있기 때문에, 이를 방지하기 위해 Rule-based 방식으로 질문 범위를 먼저 제한하는 구조를 선택했습니다.

지금 만든 것은 “LLM이 알아서 추천하는 챗봇”이 아니라, PostgreSQL을 기준으로 재현 가능한 추천 결과를 만들고, 그 결과를 나중에 ML 모델로 개선할 수 있게 만드는 추천 서비스다.

즉, 사용자의 모든 입력을 바로 LLM에 넘기는 것이 아니라, 먼저 질문이 ONTHEBLOCK 서비스 범위에 포함되는지 확인합니다.

예를 들어 “내 취향에 맞는 술 추천해줘”, “근처에 내 취향의 술 파는 곳 있어?”, “이 술은 어떤 맛이야?” 같은 질문은 서비스 범위 안에 포함됩니다. 반면 “주식 추천해줘”, “오늘 정치 뉴스 알려줘”, “의학적으로 술을 얼마나 마셔도 돼?” 같은 질문은 서비스 범위를 벗어난 것으로 판단하고 답변을 제한합니다.

규칙

저희가 설정한 기본 원칙은 다음과 같습니다.

1. 앱 범위 안의 질문인지 먼저 판단한다.
2. 범위 밖 질문이면 거절하거나 안내한다.
3. 범위 안 질문이면 추천 시스템과 지도 데이터를 조회한다.
4. 조회된 근거만 SLM에게 전달한다.
5. SLM은 추천을 새로 만들지 않고, 근거를 자연어로 설명한다.

그렇게 정해둔 원칙에 따라서 만들어진 내용은 다음과 같습니다.

  • 사용자의 설문 결과에서 파생된 취향 프로필을 사용한다.
  • 술/음료 카탈로그와 flavor vector를 기반으로 음료를 추천한다.
  • 지도/장소 서비스가 제공한 스냅샷을 기반으로 거리, 가격, 재고/가용성, freshness를 반영해 장소를 추천한다.
  • 추천 요청, 결과, 설명, 사용자 interaction을 PostgreSQL에 남긴다.
  • Qdrant는 빠른 vector 검색을 위한 파생 인덱스로만 사용한다.
  • 추천 결과는 reason code와 score breakdown으로 설명 가능해야 한다.
  • 챗봇은 추천 순위를 직접 만들지 않고 recommendation-service의 deterministic result를 호출해야 한다.

예를 들어 사용자가 다음과 같이 질문했다고 가정합니다.

내 취향에 맞는 술을 추천해줘.

이 경우 챗봇은 먼저 이 질문을 recommend_beverage intent로 분류합니다. 이후 recommendation-service에서 사용자의 taste profile을 기준으로 술 후보를 가져옵니다.

이때 SLM이 직접 술을 고르는 것이 아니라, recommendation-service가 이미 계산한 결과를 사용합니다.

응답에 사용되는 데이터는 대략 다음과 같은 형태입니다.

{
  "intent": "recommend_beverage",
  "profile_status": "active",
  "preferred_keywords": ["vanilla_caramel", "nutty", "oak_woody"],
  "recommendations": [
    {
      "rank": 1,
      "name": "Example Bourbon",
      "score": 0.91,
      "reason_codes": [
        "MATCHES_VANILLA_CARAMEL",
        "BEGINNER_FRIENDLY",
        "WITHIN_BUDGET"
      ]
    }
  ]
}

SLM은 이 context를 기반으로 사용자의 위치 정보가 있다면 다음과 같이 답변합니다.

당신의 취향에는 바닐라·캐러멜 향과 고소한 느낌이 있는 술이 잘 맞는 편이에요. 
그 기준으로는 [실제 바, 펍 장소]을 먼저 추천드릴 수 있습니다. 취향 적합도가 높고, 예산 범위에도 들어와 부담 없이 선택하기 좋은 편입니다.

이때 SLM은 context에 없는 술을 새로 만들거나, 순위를 바꾸거나, 점수를 임의로 수정하지 않습니다.

recommendation-service API 문서에서도 추천 응답은 rank, target_id, scores, reason_codes, explanation 같은 설명 가능한 메타데이터를 포함하는 방향으로 정의되어 있습니다.

장소 추천의 경우도 마찬가지입니다. 사용자가 다음과 같이 질문할 수 있습니다.

근처에 내 취향의 술 파는 곳 있어?

이 경우 챗봇은 사용자의 위치 정보와 취향 정보를 함께 사용합니다. 다만 장소, 가격, 재고, 거리 정보는 SLM이 판단하지 않습니다. recommendation-service가 map-service의 read model 또는 snapshot을 기반으로 장소 후보를 가져오고, SLM은 그 결과를 설명합니다. 추천 시스템 문서에서도 장소 추천은 venue_snapshots, venue_inventory_snapshots 같은 read model을 사용하며, nearest_reasonable, best_price, balanced_best처럼 구매 선택지를 구분하는 구조로 정의되어 있습니다.

예를 들어 context가 다음과 같다면:

{
  "intent": "find_nearby_venue",
  "recommendations": [
    {
      "option_type": "balanced_best",
      "venue_name": "A Bottle Shop",
      "beverage_name": "Example Bourbon",
      "distance_m": 180,
      "price_krw": 8500,
      "availability_status": "IN_STOCK"
    },
    {
      "option_type": "best_price",
      "venue_name": "B Liquor Store",
      "beverage_name": "Example Bourbon",
      "distance_m": 720,
      "price_krw": 7900,
      "availability_status": "IN_STOCK"
    },
    {
      "option_type": "nearest_reasonable",
      "venue_name": "C Bar",
      "beverage_name": "Example Highball",
      "distance_m": 90,
      "price_krw": 9000,
      "availability_status": "UNKNOWN"
    }
  ]
}

SLM은 다음처럼 답변합니다.

근처 기준으로는 A Bottle Shop이 가장 균형이 좋아 보여요. 약 180m 거리이고, 네 취향에 맞는 Example Bourbon을 8,500원에 구매할 수 있습니다.

가격만 보면 B Liquor Store가 7,900원으로 더 저렴하지만, 약 720m 정도 떨어져 있어요. 가장 가까운 선택지는 C Bar이지만 재고 정보가 확실하지 않아 방문 전 확인이 필요합니다.

SLM이 “그럴듯한 말”을 만드는 것이 아니라, 이미 검증된 추천 결과와 장소 데이터를 기반으로 답변하기 때문에 할루시네이션 가능성을 크게 줄일 수 있다고 생각했습니다.

즉, 검색된 근거가 없으면 답변하지 않습니다. 예를 들어 사용자가 “근처 위스키바 추천해줘”라고 물었지만 위치 정보가 없거나, 현재 DB에서 신뢰할 수 있는 장소를 찾지 못했다면 다음처럼 답합니다.

현재 위치 정보가 없어 근처 장소를 정확히 추천드리기 어렵습니다. 위치 권한을 허용해 주시면 주변에서 취향에 맞는 장소를 다시 찾아드릴게요.

또는:

현재 ONTHEBLOCK 데이터에서 신뢰할 수 있는 장소 정보를 찾지 못했습니다.

이러한 답변 정책은 단순 prompt engineering만으로는 부족합니다.

ChatBot

그래서 저희는 rule-based guardrail, RAG, verifier, 그리고 fine-tuned SLM을 함께 사용하는 구조를 고려하고 있습니다.

fine-tuning까지 경험해보는 것이 이번 프로젝트의 목적이였지만, 시간이 부족해 경험하지 못했고 방학을 활용해서 다른 어플리케이션을 만들면서 제작할 계획입니다.

전체적으로는 다음 네 단계로 나눌 수 있습니다. 뭔가 말이 거창하지만, 과정되지 않고 사용할 수 있는 다른 좋은 표현이 없는 것 같아서 그대로 사용하려고 합니다.

1. Rule-based filtering (O)
2. Deterministic recommendation (O)
3. Grounded context generation (O)
4. Fine-tuned SLM response (X)

첫 번째 단계인 Rule-based filtering은 사용자의 질문이 서비스 범위 안에 있는지 판단합니다. 질문이 주류 추천, 장소 추천, 취향 설명, 가격·거리 비교, 재고 확인, 주류 설명과 관련되어 있으면 통과시키고, 그렇지 않으면 거절합니다.

두 번째 단계는 recommendation-service가 담당합니다. 추천 시스템은 사용자의 derived taste profile, beverage vector, 장소 snapshot, 가격, 거리, 재고 상태 등을 기반으로 후보를 계산합니다.

세 번째 단계는 chatbot-service가 담당합니다. recommendation-service가 반환한 후보, 점수, reason code, 장소 정보, 가격 정보, 재고 정보 등을 모아 SLM에게 전달할 grounded context를 만듭니다. 이 context는 SLM이 답변에 사용할 수 있는 유일한 정보입니다.

네 번째 단계에서 fine-tuned SLM이 동작합니다. 저희가 생각하는 SLM은 Qwen/Qwen2.5-7B-Instruct 같은 open model을 기반으로, ONTHEBLOCK 도메인에 맞게 QLoRA fine-tuning한 작은 언어 모델입니다. 이 모델은 GPT급 거대 모델처럼 모든 질문에 답하는 모델이 아니라, 저희 서비스의 추천 결과를 한국어로 설명하는 역할을 합니다.

마지막 단계를 추후 다른 프로젝트를 통해 완성시켜보려고 합니다.

SLM이 하는 일은 다음과 같습니다.

- 추천 결과를 자연스러운 한국어로 설명
- 가격/거리/재고/분위기 trade-off를 문장화
- 사용자 질문에 맞춰 짧고 정중하게 답변
- 정보가 부족하면 모른다고 답변

반대로 SLM이 하면 안 되는 일은 다음과 같습니다.

- 술 추천 후보를 새로 만들기
- 장소 후보를 새로 만들기
- 추천 순위 바꾸기
- 가격, 거리, 재고를 지어내기
- 사용자 survey/profile을 직접 해석하기
- map DB나 recommendation DB를 직접 조회하기

사용자의 취향 정보도 SLM이 직접 해석하지 않습니다. survey-service는 raw survey answer를 소유하고, recommendation-service는 그 응답을 기반으로 taste profile을 생성합니다. 따라서, SLM이 받는 정보는 raw survey가 아니라 다음과 같이 가공된 profile summary입니다.

{
  "profile_status": "active",
  "preferred_categories": ["whiskey", "cocktail"],
  "preferred_keywords": ["vanilla_caramel", "nutty", "oak_woody"],
  "budget_range": "30000_100000",
  "experience_level": "beginner"
}

이렇게 하면 사용자 개인정보나 raw survey 구조를 모델에 직접 노출하지 않으면서도, 취향 기반 설명은 가능해집니다.

마치며

동작하는 챗 봇의 형태는 누구나 쉽게 만들 수 있지만, 이걸 다른 서비스와 엮었을 때 추가적으로 발생하는 문제라던가 비용적인 부분들이 어떻게 발생하는지 배울 수 있엇던 좋은 프로젝트였다고 생각합니다. 챗 봇을 만들었다 라고는 말하기는 어려울 것 같지만, 아마 이런 형태가 챗 봇의 초기 형태이지 않을까하는 생각이 있습니다.

추천 서비스와 결합된 챗 봇은 설게해야할 영역이 더 커지며, 이 선호 조사 데이터에 따른 추천이라는 것이 사용자에게 얼마나 크게 작용하는지를 배웠습니다. 또한, 실시간 응답성이 매우 중요한 서비스들이기에 인프라 구축에 대한 지식을 많이 공부해야겠다 라는 느낌을 받았습니다.

이번에 개발한 팀 프로젝트가 MSA였기에 DB에 담겨 있는 데이터들이 서로 왔다 갔다 해야하는 경우가 있었고 이걸 메모리에 올려둬서 서브스가 시작할 때 미리 불러오는 설게를 했었는데, 이 부분 또한 더 많은 공부가 필요하다는 것을 느꼈습니다.

정리하자면, 인프라쪽 공부나 DB에 대한 공부가 부족하다는 것을 느낄 수 있었던 프로젝트였으습니다…

이번 글에서는 늘어지지 않게 여기까지만 작성하고 다른 포스팅에서 추천 엔진에 대한 이론적 배경과 논문을 다뤄보도록 하겠습니다. 다음 글은 아마 발생했던 버그들에 대해서 얘기 할 것 같습니다.

on-the-block 9 / 10

목차

댓글