라이프스타일 AI 챗봇 ‘열무’ 🌱 — 아키텍처 · 라이프사이클 분석
어디살지(부동산 임차) · 2026-06-01 하루 동안 구축된 풀스택 분석 · 12개 PR · ~4,400 LOC
TL;DR
‘열무’는 조건보다 “어떤 삶을 원하는지”를 먼저 묻는 라이프스타일 상담 봇(A봇)이다. 대화로 사용자의 거주 취향을 점진 수집(collection score)하다가, 핵심 조건(P0)이 차면 매물검색(B봇)으로 핸드오프한다. 백엔드는 헥사고날(domain·application·adapters·api), 실시간은 SSE 스트리밍, 세션은 Redis(휘발)+PostgreSQL(영속). 오늘 하루에 골격(#1023)부터 영속화·위치인식·이력 로그인 인지·핸드오프 새 방까지 12개 PR로 완성됐다.
1. 한눈에 — 무엇을 하는 봇인가
기존 부동산 챗봇이 “보증금/지역/평수”를 캐묻는 폼이라면, 열무는 “출근길이 편한 동네, 카페 많은 골목, 조용한 주택가…” 같은 생활 결을 먼저 묻고 그 안에서 정량 조건을 자연스럽게 추출한다.
2. 시스템 아키텍처 (헥사고날)
backend/src/app/chat_lifestyle/ ← 포트&어댑터(헥사고날)
├─ domain/
│ ├─ models.py LifestyleProfile · P0/P1 스코어링 모델
│ └─ ports.py LLM · 세션 · 영속 · 스냅샷 포트(인터페이스)
├─ application/ (유스케이스 — 벤더 비의존)
│ ├─ lifestyle_agent.py SSE 스트리밍 에이전트(대화 오케스트레이션)
│ ├─ lifestyle_scorer.py 수집 점수 P0/P1 계산
│ ├─ lifestyle_tools.py tool 신호(핸드오프 등)
│ ├─ llm_factory.py LLM 프로바이더 팩토리(Responses/Chat)
│ └─ suggestion_engine.py 컨텍스트 기반 퀵리플라이 칩 생성
├─ adapters/ (벤더 구현 — 교체 가능)
│ ├─ llm/openai_responses_adapter.py · openai_chat_adapter.py
│ ├─ persistence/pg_profile_adapter.py · pg_thread_adapter.py · ip_region_adapter.py
│ ├─ session/redis_session.py
│ └─ snapshot/profile_snapshot.py
└─ api/lifestyle_router.py /v1/lifestyle/{message,threads,snapshot,profile}
레이어 책임
| 레이어 | 책임 | 핵심 |
|---|---|---|
| domain | 순수 규칙 | LifestyleProfile, P0(필수)·P1(보강) 점수 |
| application | 유스케이스 | agent(SSE 흐름)·scorer·tools·suggestion_engine |
| adapters | 벤더 구현 | OpenAI(Responses/Chat)·Redis·PostgreSQL·IP 지역 |
| api | 전송 | SSE 라우터 + DTO |
프론트는 BE 미러 — useLifestyleSender(SSE 훅) · LifestyleHistoryOverlay ·
LifestyleProgressBar · ProfileSummaryCard 가 AIAssistantBottomSheet의
chatMode='lifestyle' 분기로 매물 모드와 한 컴포넌트에 통합된다.
3. 대화 라이프사이클 (런타임)
| 단계 | 동작 | 신호 |
|---|---|---|
| ① 진입 | 웰컴 메시지 + 추천 칩 4개 | suggestion_engine 초기 칩 |
| ② 수집 | 턴마다 SSE 스트리밍 응답 + 프로필 추출 | updates.collection_score ↑ |
| ③ 보강 | 부족한 P0 필드를 자연스러운 후속 질문으로 유도 | missing_p0_fields |
| ④ 준비완료 | P0 충족 → 핸드오프 버튼 노출 | can_handoff=true |
| ⑤ 핸드오프 | “내 취향으로 매물 찾기” → 스냅샷 번들 생성 | snapshot_bundle |
| ⑥ 전환 | 매물검색(B봇) 새 대화방에서 프로필 기반 검색 | handleNewChat→search |
세션 SSOT = DB. 휘발 상태(스트리밍·진행)는 Redis, 영속 프로필·대화는 PostgreSQL.
재진입 시 /threads·/profile로 복원된다.
4. SSE 스트리밍 프로토콜
POST /v1/lifestyle/message (text/event-stream)
event:metadata {thread_id}
event:token {delta} ← 응답 토큰 점진 출력
event:thinking {text} ← "🌱 조건 분석 중…" tool 진행
event:updates {collection_score, can_handoff, missing_p0_fields, profile_summary}
event:suggestions {items[]} ← 다음 턴 퀵리플라이 칩(best-effort)
event:end {} ← 로딩 해제 지점
event:error {message}
설계 함정(#1028 해소): 칩 생성용 2차 LLM 호출이 메인 스트림과 충돌(GeneratorExit)하면
suggestions/end가 누락돼 프론트가 무한 로딩에 빠진다. → end를 먼저 보장하고 칩은 best-effort로 분리,
tool 진행 메시지로 “멈춘 게 아님”을 시각화.
5. 개발 타임라인 — 2026-06-01 하루 (12 PR)
| 시각 | PR | 내용 | LOC |
|---|---|---|---|
| 10:16 | #1023 | 풀스택 골격 — 헥사고날 BE + FE 통합 + SSE + 스코어링 + 핸드오프 | 2,733 |
| 10:30 | #1024 | 데스크탑 폰프레임 입력바 클릭 불가 + 오버레이 갇힘 | 8 |
| 11:51 | #1025 | 대화 이력 영속화/API/복원 풀스택 | 604 |
| 11:59 | #1026 | 로딩 흰 버블·하단 여백 매물 모드와 통일 | 2 |
| 12:18 | #1027 | 위치 인식 + LLM 칩 (지명 하드코딩/오버피팅 제거) | 256 |
| 12:43 | #1028 | SSE 멈춤 해소 + tool 진행 메시지 | 173 |
| 13:41 | #1029 | 대화 내역 오버레이를 챗봇 프레임 안으로 + 메뉴 바로 열기 | 42 |
| 14:20 | #1031 | 핸드오프 tool 신호 기반 전환 + 지명 하드코딩 제거 | 30 |
| 14:41 | #1032 | 대화 내역 오버레이 폰 프레임 레이아웃 | 126 |
| 14:47 | #1033 | 전송 후 입력창 텍스트 남는 회귀(draft 복원 레이스) | 55 |
| 15:21 | #1034 | 이력 로그인 인지 — 회원 본인 + 비회원 anon 머지 | 294 |
| 15:28 | #1035 | 취향→매물 핸드오프 새 대화방에서 시작 | 108 |
골격 #1023(2,733 LOC, 41 파일) 이후 11개 PR은 대부분 “현장 버그·UX·오버피팅 제거”의 빠른 반복. 총 ~4,400 insertions / 87 파일.
6. 핵심 기술 결정
오버피팅 제거 (#1027 #1031)
지명·핸드오프 트리거를 하드코딩 분기로 짜지 않고 위치 인식 + LLM 칩 생성·tool 신호 기반 전환으로 일반화. 특정 입력 패치가 아닌 근본 해결.
이력 로그인 인지 (#1034)
항상 anon-lifestyle 단일 버킷만 보던 구조를, 회원은 본인 user_id + 로그인 이전 anon을 머지해 노출. 순수 함수 분리 + 단위테스트 7.
핸드오프 새 방 (#1035)
sendDirect 전에 handleNewChat()로 새 threadId 동기 리셋 → 직전 검색 대화에 안 붙고 깨끗한 새 방에서 검색.
입력 draft 레이스 (#1033)
전송 후 setInputValue('')만 하면 draft 복원 effect가 텍스트를 재주입. clearInputDraft()+가드로 해소.
SSE 무한 로딩 (#1028)
2차 LLM(칩)과 메인 스트림 충돌로 end 누락 → 무한 로딩. end 보장 + 칩 best-effort 분리.
폰 프레임 레이아웃 (#1024 #1029 #1032)
데스크탑 폰프레임 안 입력/오버레이가 갇히거나 깨지던 문제를 컨테이너 기준으로 일반화(매직넘버 제거).
7. 현재 상태 · 남은 것
- 완성: 대화→수집→핸드오프 풀 루프, 영속 이력, 위치 인식, 로그인 인지, 폰프레임 UX.
- 주의
anon-lifestyle은 전 비회원이 공유하는 전역 버킷 — 회원 뷰에 다른 게스트 대화가 합쳐 보인다. 사용자별 격리가 필요하면 BE claim/이관이 후속 과제. - 후속 LLM 모드 라우팅 — 별도 벤치(llm-mode-decision) 결과대로 recency 질의는 웹서치 ON, 일반은 groq로 라우팅 도입 여지.
분석 대상: ai-real-estate-service main, 작성자 chlee 의 2026-06-01 커밋 #1023~#1035. 어제(05-31)는 라이프스타일 관련 0건 — 본 챗봇은 하루에 구축됨. 개인용 분석 산출물.