Lifestyle 슬롯 필수화 — 변경사항 정리
어디살지(ai-real-estate-service) chat_v2 · 김정태 PM 피드백 대응
피드백 → 설계
김정태 PM (2026-05-28 18:40 슬랙):
“유저가 먼저 라이프스타일에 대해 묻지 않으면 열무는 라이프스타일을 안 묻는데, 열무가 라이프스타일 관련 질문을 자연스럽게 할 수 있게도 가능할까요?”
현재 구조에서 라이프스타일은 “유저가 단서를 발화하면 그때만 매핑” 하는 수동 감지. PM 요청은 “주도적 질문” 추가. → 라이프스타일을 예산·매물유형과 동급의 필수 슬롯으로 격상하고 검색 진입 전에 채워지지 않으면 server-side gate 가 1턴 질문 유도.
핵심 결정
| 옵션 | 판정 | 이유 |
|---|---|---|
A. extract_filters 에 lifestyle_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.pyupdates_meta event 에lifestyle_metapayload — FE wire
테스트 결과
Unit (17/17 PASSED, 0.05s)
production _merge_filters 정책을 redis 없이 직접 검증. 정규식·하드코딩 0. LLM 호출 0.
| # | 시나리오 | 검증 |
|---|---|---|
| T1 | unknown → asked → filled (+ amenities union) | PASS |
| T2 | unknown → filled 직접 전이 | PASS |
| T3 | asked → declined → terminal lock | PASS |
| T4 | filled terminal — 재변경 차단 | PASS |
| T5 | 신규 enum 슬롯 (top_priority 등) overwrite | PASS |
| transition_matrix | (cur, requested) 17 케이스 전수 | PASS |
| regression | 기존 area/type union 보존 | PASS |
| T6 | 다중 lifestyle 단서 카테고리 union | PASS |
| T7 | required_amenities dedup | PASS |
| T8 | declined 후 비-lifestyle 필드는 계속 누적 | PASS |
| T9 | overwrite 필드 null 값 보존 | PASS |
| T10 | lifestyle_signal_detected 는 session 저장 X | PASS |
| T11 | hard 의 빈 값 보존 | PASS |
| T12 | 10-turn 멀티턴 시뮬레이션 (모든 슬롯 누적) | PASS |
| T13 | 회상 후 누적 계속 | PASS |
| T14 | emit payload size — 빈 슬롯 제외 | PASS |
| T15 | 인생단계 persona 는 lifestyle 신호 X | PASS |
Live LLM (6/7 PASSED, 23.4s)
실제 Groq gpt-oss-120b 호출. _StubChatService.handle_turn 이 production chat_service.handle_turn 의 extract_filters 결과 형식 mimic.
| # | 시나리오 | 상태 전이 | 결과 |
|---|---|---|---|
| T1 | proactive ask flow (3턴) | unknown→asked→filled | PASS |
| T2 | explicit-first (1턴) | unknown→filled | PASS |
| T3 | declined flow (3턴) | unknown→asked→declined | PASS |
| T4 | 인생단계 first (4턴) | unknown 유지→asked→filled | PASS |
| T5 | explicit full search (1턴) | unknown→filled | PASS |
| T6 | 회상 turn 후 status 유지 (3턴) | filled→filled→filled | FAIL LLM 변동성 |
| T7 | 다중 lifestyle 단서 누적 (4턴) | 모두 filled | PASS |
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_SCHEMA 에 lifestyle_decline_detected: bool 추가 → server-side 자동 declined 전이.
Audit — 슬롯 제약 + 이벤트
| 영역 | 상태 | 비고 |
|---|---|---|
| FILTER_SCHEMA strict required (lifestyle_signal_detected) | OK | fewshot 6개에 박혀 LLM 매번 채움 |
| update_user_profile lifestyle_status enum | OK | strict X, optional |
| _merge_filters 단방향 전이 (terminal lock) | OK | unit 7/7 통과 |
| schema 진화 → 한 곳 수정 (매핑 dict) | OK | drift 차단 |
| forced search 가 gate 우회? | OK | gate 가 forced 도 거부 (단일 진입점) |
| chat_service.handle_turn → _merge_filters 흐름 | OK | extract_filters 결과 통째 update_filters 호출 |
| session.reset / is_member / profile | OK | lifestyle_status 는 filters 와 함께 reset |
| FE updates_meta wire (lifestyle_meta) | FIXED | agent.py +15줄 |
| gate 거부 SSE event 별도 | N/A | ok=False 면 listing event 안 emit (회귀 X) |
| LLM 응답에 [상태] brief leak | OK | SYSTEM_PROMPT 보안 룰이 차단 |
다음 단계
- PR #1014 머지 (squash
467af021) - Critical hot-fix 코드 작성 (
_merge_filters+updates_metawire) — 로컬 - unit 17/17 + live 6/7 검증
- Hot-fix PR 생성 + main 머지 — 다음 작업
lifestyle_decline_detectedschema 추가 (T3 결정성 강화)- production 모니터링 —
lifestyle_status전이율, gate 발동 비율, P50/P95 latency - PM 후속 sync — gate 의 1턴 추가가 UX 에 맞는지 검토
변경 파일 요약
머지된 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)