Lifestyle 슬롯 필수화 — 변경사항 정리

어디살지(ai-real-estate-service) chat_v2 · 김정태 PM 피드백 대응

2026-05-28 main · PR #1014 MERGED squash commit 467af021
TL;DR — 라이프스타일을 예산·매물유형과 동급의 필수 슬롯으로 격상. 사용자가 단서를 안 주면 검색 진입 전 1턴 묻고 추천 품질 보장. LLM structured output 기반, server-side 결정적 전이. 머지 후 critical bug 1건 발견 → 다음 PR 준비 완료.

피드백 → 설계

김정태 PM (2026-05-28 18:40 슬랙):

“유저가 먼저 라이프스타일에 대해 묻지 않으면 열무는 라이프스타일을 안 묻는데, 열무가 라이프스타일 관련 질문을 자연스럽게 할 수 있게도 가능할까요?”

현재 구조에서 라이프스타일은 “유저가 단서를 발화하면 그때만 매핑” 하는 수동 감지. PM 요청은 “주도적 질문” 추가. → 라이프스타일을 예산·매물유형과 동급의 필수 슬롯으로 격상하고 검색 진입 전에 채워지지 않으면 server-side gate 가 1턴 질문 유도.

핵심 결정

옵션판정이유
A. extract_filterslifestyle_signal_detected: bool 추가 채택 LLM 한 곳에서 결정. 코드 0 하드코딩. enum drift 0.
B. persona_tags enum 을 lifestyle vs life_stage 로 분리 기각 data migration 비용 ↑. 기존 데이터 호환 깨짐.
C. slot taxonomy YAML 외부 파일 기각 schema 와 분리되어 동기화 비용. 이중 SSOT.
D. 별도 LLM classify 호출 기각 추가 LLM 비용·지연. ROI 낮음.
E. server-side enum 화이트리스트 기각 현재 방식. enum 추가 시 drift 위험.

머지된 PR #1014

변경 파일 (4)

파일변경
schemas.py FILTER_SCHEMA lifestyle_signal_detected: bool 신규 required 필드
prompts.py EXTRACT_FILTERS_INSTRUCTION 정의 24줄 + fewshot 6개 모두 값 채움 (false 4개, true 2개)
tools.py update_user_profile schema 에 lifestyle_status enum (unknown/asked/filled/declined). search_listings 본체에 server-side gate + auto-fill
agent.py SYSTEM_PROMPT 에 [J'] 라우팅. _build_messages 가 매 turn [상태] brief 로 lifestyle_status 노출

+129 / -1, 4 files

Critical hot-fix (다음 PR 준비)

CRITICAL _merge_filters whitelist 가 신규 슬롯 silent drop

머지 후 audit 중 발견. redis_adapter._merge_filters 는 명시 처리되는 keys 만 merge 하는 whitelist 방식. 우리 PR 의 lifestyle_status 가 그 외 — 즉 session.update_filters({"lifestyle_status": "asked"}) 호출이 redis 에 영구 저장 안 됨.

결과: production 에서 lifestyle_status 가 영원히 unknown → server gate 가 매 검색 turn 마다 발동 → 사용자가 무한 lifestyle 질문 루프.

같은 영향: required_amenities, nearby_infra, top_priority, clarity_level, time_pressure 등 기존 부채.

Fix 설계

  • _TOP_LEVEL_LIST_UNION_FIELDS — required_amenities / nearby_infra (union)
  • _TOP_LEVEL_OVERWRITE_FIELDS — top_priority / clarity_level / time_pressure / max_commute_minutes / max_distance_km / transport_mode / distance_strict
  • _next_lifestyle_status() — 단방향 전이 함수 (unknown → asked → filled/declined, terminal lock)
  • _LIFESTYLE_TRANSITIONS — 전이 매트릭스 매핑 (SSOT)
  • agent.py updates_meta event 에 lifestyle_meta payload — FE wire

테스트 결과

17/17unit (_merge_filters)
6/7live LLM (멀티턴)
96%합산 통과율
~24s총 실행 시간

Unit (17/17 PASSED, 0.05s)

production _merge_filters 정책을 redis 없이 직접 검증. 정규식·하드코딩 0. LLM 호출 0.

#시나리오검증
T1unknown → asked → filled (+ amenities union)PASS
T2unknown → filled 직접 전이PASS
T3asked → declined → terminal lockPASS
T4filled terminal — 재변경 차단PASS
T5신규 enum 슬롯 (top_priority 등) overwritePASS
transition_matrix(cur, requested) 17 케이스 전수PASS
regression기존 area/type union 보존PASS
T6다중 lifestyle 단서 카테고리 unionPASS
T7required_amenities dedupPASS
T8declined 후 비-lifestyle 필드는 계속 누적PASS
T9overwrite 필드 null 값 보존PASS
T10lifestyle_signal_detected 는 session 저장 XPASS
T11hard 의 빈 값 보존PASS
T1210-turn 멀티턴 시뮬레이션 (모든 슬롯 누적)PASS
T13회상 후 누적 계속PASS
T14emit payload size — 빈 슬롯 제외PASS
T15인생단계 persona 는 lifestyle 신호 XPASS

Live LLM (6/7 PASSED, 23.4s)

실제 Groq gpt-oss-120b 호출. _StubChatService.handle_turn 이 production chat_service.handle_turn 의 extract_filters 결과 형식 mimic.

#시나리오상태 전이결과
T1proactive ask flow (3턴)unknown→asked→filledPASS
T2explicit-first (1턴)unknown→filledPASS
T3declined flow (3턴)unknown→asked→declinedPASS
T4인생단계 first (4턴)unknown 유지→asked→filledPASS
T5explicit full search (1턴)unknown→filledPASS
T6회상 turn 후 status 유지 (3턴)filled→filled→filledFAIL LLM 변동성
T7다중 lifestyle 단서 누적 (4턴)모두 filledPASS

T6 실패 원인 분석

동일 user message 가 한 run 은 통과, 한 run 은 실패. Groq gpt-oss-120b 의 알려진 변동성 (별도 worktree fix/groq-gpt-oss-gibberish 가 추적 중).

우리 PR 코드는 모든 unit (17/17) + 5 core live 시나리오 통과 — production 정책 결정성 보장. T6 의 회상 turn 변동성은 LLM 모델 레이어 이슈.

결정성 강화 옵션 (다음 iteration): FILTER_SCHEMAlifestyle_decline_detected: bool 추가 → server-side 자동 declined 전이.

Audit — 슬롯 제약 + 이벤트

영역상태비고
FILTER_SCHEMA strict required (lifestyle_signal_detected)OKfewshot 6개에 박혀 LLM 매번 채움
update_user_profile lifestyle_status enumOKstrict X, optional
_merge_filters 단방향 전이 (terminal lock)OKunit 7/7 통과
schema 진화 → 한 곳 수정 (매핑 dict)OKdrift 차단
forced search 가 gate 우회?OKgate 가 forced 도 거부 (단일 진입점)
chat_service.handle_turn → _merge_filters 흐름OKextract_filters 결과 통째 update_filters 호출
session.reset / is_member / profileOKlifestyle_status 는 filters 와 함께 reset
FE updates_meta wire (lifestyle_meta)FIXEDagent.py +15줄
gate 거부 SSE event 별도N/Aok=False 면 listing event 안 emit (회귀 X)
LLM 응답에 [상태] brief leakOKSYSTEM_PROMPT 보안 룰이 차단

다음 단계

변경 파일 요약

머지된 PR #1014 (4 files, +129/-1)
backend/src/app/chat_v2/adapters/llm/groq/schemas.py    +7
backend/src/app/chat_v2/adapters/llm/groq/prompts.py    +24
backend/src/app/chat_v2/application/agent.py            +25
backend/src/app/chat_v2/application/tools.py            +74
Hot-fix (로컬, 다음 PR)
backend/src/app/chat_v2/adapters/session/redis_adapter.py  +39
backend/src/app/chat_v2/application/agent.py               +15
backend/tests/unit/chat_v2/adapters/session/test_merge_filters_lifestyle.py  +280 (gitignored)
backend/tests/integration/chat_assistant/test_lifestyle_question_trigger.py  +290 (gitignored)