어디살지 챗봇 — 라이프스타일 추천질문 개선안

"룰베이스 강제 X · 추천칩으로 자연 노출 O" 라는 요청 조건을 만족하는 최소 변경 설계

대상: ai-real-estate-service (어디살지) 봇: 열무 작성: 2026-05-24
목차
  1. 1. 현상
  2. 2. 근본 원인 3가지
  3. 3. 개선 설계 (L0~L3)
  4. 4. 코드 변경 (파일·라인·diff)
  5. 5. 측정 & 롤아웃
  6. 6. 리스크 & 대안
TL;DR. 추천질문은 LLM(Groq gpt-oss-120b)이 매 턴 4개 카테고리 1개씩 강제로 만들고 있다 (add_condition · change_filter · persona · reject). 문제는 ① persona 설명이 한 줄·예시 1개에 그쳐서 LLM이 다양한 라이프스타일 갈래를 못 만들고, ② 사용자가 라이프스타일 단서를 줘도 그걸 deepen 하는 후속 로직이 없으며, ③ 프론트엔드가 category 필드를 무시하고 균등 렌더라 시각적 우선순위도 없다는 점. 프롬프트 보강(L1)·deepen 가드(L2)·시각 차별화(L3) 3계층으로 풀면 본문 답변 흐름은 안 건드리고 추천칩에서만 라이프스타일 가시성을 올릴 수 있다.

1. 현상 — 사용자 캡처 케이스

본문 답변이 "예산 먼저" 단일 트랙으로 갇혀 있고, 사용자가 라이프스타일 단서(대중교통 편한 동네 원해요)를 줘도 후속이 예산으로 회귀. 추천칩 자체가 화면에 거의 안 보임.

열무
안녕, 나 열무예요. 어디서 살지 한 줄 던져줘요.
예산이 빠듯해요.
열무
월세나 전세·보증금 기준으로 대략 어느 정도 금액을 생각하고 계세요?
대중교통 편한 동네 원해요
열무
월세·전세·보증금 중 어느 정도 예산을 잡고 계신가요?

여기서 사용자 요청은 명확:

2. 근본 원인 3가지

① 프롬프트 빈약

prompts.py:526-540SUGGEST_REPLIES_INSTRUCTION 에서 persona 카테고리 설명이 단 한 줄: "페르소나·라이프스타일 단서 — 예: '학원가 가까운 곳?'". 예시가 1개뿐이라 LLM이 학원가류로 편향됨.

② deepen 로직 부재

adapter.py:165-241 _suggest_replies_search() 는 마지막 발화를 통째로 LLM에 넘기지만, 라이프스타일 단서가 이미 들어왔을 때 "그 갈래를 더 깊게 파라" 는 가드가 없음. reject 누락만 fallback 추가.

③ FE가 category 무시

QuickButtons.tsx:49-104type·value 만 보고 스타일링. 백엔드가 보내는 category enum 4개를 무시해서, persona 칩이 reject 칩과 동일 styling. 시각적 우선순위 없음.

(보조) 통계 부재

카테고리별 노출·클릭 추적이 FE/BE 어디에도 없음. 개선 후 효과 측정이 불가능하므로 이벤트 1줄 추가가 필수.

3. 개선 설계 — L0~L3 계층

설계 원칙. 본문 답변(LLM이 자연어로 응답하는 reply) 의 프롬프트는 건드리지 않는다. 사용자 요청대로 룰베이스 강제를 피하고, 오직 suggest_replies 파이프라인과 FE 렌더링만 손본다.

P0L1 · 프롬프트 보강 (persona 카테고리 다양화)

persona 한 줄 설명을 라이프스타일 sub-도메인 6개로 펼치고, "직전 사용자 발화에 라이프스타일 단서가 있으면 그 sub-도메인을 deepen 하는 chip 1개 포함" 가이드 추가.

  persona       : 페르소나·라이프스타일 단서 — 예: '학원가 가까운 곳?'  persona       : 라이프스타일 단서 — 다음 sub-도메인 중 컨텍스트에 맞는 1개:     · 교통       — 예: '도보 10분 내 지하철?', '버스 노선 다양해?'     · 편의시설   — 예: '카페 많은 동네?', '대형마트 가까워?'     · 학군/교육  — 예: '학원가 가까운 곳?', '초품아 있어?'     · 자연/공원  — 예: '공원 산책 좋은 동네?'     · 동네분위기 — 예: '조용한 주거지?', '젊은 분위기?'     · 출퇴근     — 예: '강남 출퇴근 30분 내?'     ※ 사용자 마지막 발화에 라이프스타일 단서 포함 시 그 sub-도메인을 deepen.

P0L2 · deepen 가드 (라이프스타일 단서 감지)

사용자 마지막 발화에서 NEARBY_INFRA·라이프스타일 키워드를 정규식 하나로 감지해, LLM 프롬프트에 hint block 으로 주입. LLM 자유도는 유지하면서 명시적 nudging.

# adapter.py _suggest_replies_search() 안

LIFESTYLE_PATTERN = re.compile(
    r"(지하철|버스|교통|출퇴근|"
    r"카페|마트|편의점|병원|"
    r"학원|학교|초품아|학군|"
    r"공원|산책|조용|시끌|분위기|"
    r"반려|강아지|고양이)"
)
hint = ""
if LIFESTYLE_PATTERN.search(last_user_message):
    hit = LIFESTYLE_PATTERN.search(last_user_message).group(1)
    hint = (
        f"\n\n[힌트] 사용자가 라이프스타일 단서 '{hit}' 를 제시함. "
        f"persona 카테고리는 반드시 이 갈래를 deepen 하는 후속으로 작성."
    )
content = SUGGEST_REPLIES_INSTRUCTION + ... + listings_block + hint

P1L3 · FE 카테고리 시각화 (persona 칩 우선순위)

QuickButtons.tsxcategory 를 받아서 persona 칩에 약한 강조(연한 그린 보더) 부여 + 정렬 시 persona 를 두 번째 슬롯에 고정.

// QuickButtons.tsx
const CATEGORY_STYLE: Record<Category, string> = {
  add_condition: 'border-neutral-200 bg-white',
  change_filter: 'border-amber-200 bg-amber-50',
  persona:       'border-green-200 bg-green-50',  // 라이프스타일 약강조
  reject:        'border-neutral-200 bg-neutral-50 text-neutral-500',
};
const ORDER: Category[] = ['add_condition', 'persona', 'change_filter', 'reject'];
const sorted = [...buttons].sort((a,b) => ORDER.indexOf(a.category) - ORDER.indexOf(b.category));

P2L0 · 측정 인프라 (없으면 효과 검증 불가)

FE 에서 chip impression + click 을 category 와 함께 기존 분석 파이프라인(예: posthog/segment 또는 backend log 엔드포인트)으로 1줄 송신. backend 는 suggest_replies 호출 시 카테고리 분포만 카운트 로깅.

4. 코드 변경 — 파일·라인·diff

#파일라인변경 요지예상 LOC
1backend/.../prompts.py526-540persona 설명 sub-도메인 6개로 확장+8 / -1
2backend/.../adapter.py165-241LIFESTYLE_PATTERN + hint 주입+15
3frontend/.../QuickButtons.tsx49-104category 기반 스타일 + 정렬+10 / -3
4frontend/.../QuickButtons.tsxonClickimpression/click 이벤트 송신+8
5backend/.../adapter.pyreturn 직전카테고리 분포 로그 1줄+2

총 ~46줄, 단일 PR 권장. DB 마이그레이션·schema 변경 없음 (카테고리 enum은 이미 존재).

주의. prompts.py 변경은 토큰 비용에 직접 영향. 추가 ~120 토큰 × 매 추천질문 호출. Groq gpt-oss-120b 단가가 낮아 무시할 만하지만, 측정 후 효과 없으면 L1 만 롤백 가능하도록 git 분리 커밋.

5. 측정 & 롤아웃

지표현재목표 (2주차)측정 방법
persona 카테고리 노출률~25% (이론상 1/4)≥25% 유지backend 카운터
persona 칩 클릭률 (CTR)미측정측정 가능 + ≥10%FE 이벤트
라이프스타일 단서 발화 후 persona 칩 deepen 율0%≥70%backend log 매칭
전체 칩 클릭률미측정baseline 측정FE 이벤트

롤아웃 순서

  1. Day 0 · L0(측정) 머지 — baseline 1주 수집
  2. Day 7 · L1+L2(프롬프트+deepen) 머지 — 효과 측정 1주
  3. Day 14 · L3(FE 시각화) 머지 — CTR 변화 측정
  4. Day 21 · 효과 없으면 L1 롤백, 효과 있으면 sub-도메인 풀 확장

6. 리스크 & 대안

R1 · LLM 토큰 증가

persona 설명 +120 토큰. Groq 가격 기준 무시 수준이지만 호출량 폭증 시 재검토. 대안: sub-도메인 풀을 enums.py 로 빼고 프롬프트는 슬롯만 남기기.

R2 · 정규식 false negative

LIFESTYLE_PATTERN 이 못 잡는 표현("아이 키우기 좋은") 다수. 임베딩 기반 분류로 업그레이드 옵션 (cost 증가). 1차는 keyword 로 시작 + 운영 중 patterns.json 외부화.

R3 · persona 편향

LLM이 매번 "지하철" 만 뽑을 수 있음. temperature 0.3 → 0.5 로 올리거나, sub-도메인 ring buffer 로 강제 순환. 측정 후 결정.

R4 · 추천칩 자체 미노출

이미지에서 추천칩이 화면에 안 보였음. 별도 이슈일 가능성 — FE QuickButtons 가 본문 응답에서만 렌더되는데 본문이 짧으면 잘리는지 확인 필요. L3 머지 전 점검.

다음 액션

  1. 이 설계로 진행 OK 한 번 확인
  2. worktree-pr 스킬로 어디살지 레포에 단일 PR 생성 (L0+L1+L2+L3 한 번에 또는 단계별)
  3. baseline 측정용 L0 만 먼저 머지 권장