chat_v2 회귀 — 라이브 환경 raw data 진단 + 머지 (#919/#920/#921) 종합
실제 docker compose · backend SSE · Qdrant · PostgreSQL · Redis · Playwright 라이브 추적으로 잔여 회귀 4축의 진짜 root cause 식별 + 1차 main 머지 + 후속 PR 우선순위.
- TL;DR
- 머지된 PR — #919 / #920 / #921
- raw data #1 — Qdrant points_count = 0
- raw data #2 — PostgreSQL 매물 100 / ai_messages 0 / ai_threads 부재
- raw data #3 — worker broker 에 actor 미등록
- raw data #4 — asyncio loop 충돌 (actor 실행 실패)
- raw data #5 — SSE 정상 emit (backend OK)
- raw data #6 — Playwright 11/12 fail · selector 노후화
- raw data #7 — 멀티턴/메모리/복잡 시나리오 흐름 진단
- 5 회귀축 vs fix 매트릭스
- 후속 PR 우선순위
1. TL;DR — 회귀 5 축의 raw data 근거 진단
| 회귀 축 | 증상 | raw data 근거 | fix 상태 |
|---|---|---|---|
| SSE token/message commit | 메시지 버블 빈 채 표시 | useAI.ts:347 'messages'(s) typo + useAIChatSender.ts:519~525 store.updateMessage 누락 | ✓ #919 |
| main 잠복 TS 4건 | pre-push hook(tsc) 차단 | SmartFollowups prop · @ts-expect-error · verifyEmail.access_token | ✓ #920 |
| worker actor 미등록 | 임베딩 backfill 절대 안 됨 | tasks/__init__.py 에 chat_v2_indexing import 누락 → broker actor 0 | ✓ #921 |
| UI input testid + spec selector | Playwright 11/12 fail | placeholder dynamic + getByRole(textbox, name) 매칭 실패 | ✓ #921 |
| asyncio loop 충돌 | backfill actor 실행 시 Future cross-loop | worker 로그 got Future attached to a different loop 정확 stack trace | ✗ 후속 PR |
| ai_threads 테이블 부재 | 멀티턴·메모리·복잡 시나리오 영속화 불가 | DB relation "ai_threads" does not exist + ai_messages count=0 | ✗ 후속 PR (migration) |
| null stub 4종 | 비교표·컨설팅·후속액션·결정배지 안 보임 | bc72a127 (#904) 약속한 cleanup PR 미도착 | ✗ 후속 PR |
2. 머지된 PR (3 건)
| PR | 제목 | 변경 | head sha |
|---|---|---|---|
| #919 | chat_v2 token/message event → store.updateMessage (fallback 회귀 fix) | 2 files +29 / 다른 세션 머지 | 17ed1df1 |
| #920 | main 잠복 TS 타입 오류 4건 정리 | 3 files +10/-6 | 529bbe85 |
| #921 | worker actor 등록 + ai-chat-input testid + e2e selector 안정화 | 5 files +14/-9 | b54e15e5 |
3. raw data #1 — Qdrant points_count = 0
$ curl http://localhost:10170/collections/poc_listings_gemini_3072
{
"result": {
"status": "green",
"optimizer_status": "ok",
"indexed_vectors_count": 0,
"points_count": 0,
"segments_count": 8,
"config": {
"params": {
"vectors": {"size": 3072, "distance": "Cosine"},
"shard_number": 1, "replication_factor": 1,
"on_disk_payload": true
},
"hnsw_config": {"m": 16, "ef_construct": 100, "full_scan_threshold": 10000},
...
},
"payload_schema": {"address": {"data_type": "text", ...}, ...}
},
"time": 0.000246796
}
search_listings tool 이 항상 빈 결과.
4. raw data #2 — PostgreSQL 매물 100 / ai_messages 0 / ai_threads 부재
$ docker exec ai-real-estate-service-postgres-1 \
psql -U user -d ai_real_estate -c \
"SELECT count(*) as total, count(*) FILTER (WHERE status='published') as published FROM properties;"
total | published
-------+-----------
100 | 100
$ ... -c "SELECT count(*) FROM ai_messages;"
count
-------
0
$ ... -c "SELECT count(*) FROM ai_threads;"
ERROR: relation "ai_threads" does not exist
properties100건 published — backfill 만 되면 Qdrant 가 즉시 채워질 수 있음.ai_messages0건 — 멀티턴 메시지 영속화가 한 번도 안 됨. 즉 게스트는 localStorage(24h TTL) 만 의존, 회원도 DB 저장 흐름 깨짐.ai_threads테이블 자체가 부재 — 멀티턴 thread 영속화 인프라가 통째로 빠짐. migration 필요.
5. raw data #3 — worker broker 에 actor 미등록 (PR #921 로 fix)
$ grep -rn "chat_v2_indexing\|app.workers.tasks" backend/src/app/workers/
backend/src/app/workers/broker.py:114: from app.workers.tasks.billing_tasks import (...)
backend/src/app/workers/broker.py:117: from app.workers.tasks.contract_sync_tasks import ...
backend/src/app/workers/broker.py:118: from app.workers.tasks.moderation_tasks import (...)
backend/src/app/workers/broker.py:121: from app.workers.tasks.outbox_tasks import ...
backend/src/app/workers/broker.py:122: from app.workers.tasks.tenant_request_tasks import (...)
backend/src/app/workers/broker.py:125: from app.workers.tasks.webhook_tasks import (...)
backend/src/app/workers/tasks/__init__.py:14: from app.workers.tasks.ai_tasks import (...)
backend/src/app/workers/tasks/__init__.py:21: from app.workers.tasks.contract_sync_tasks import ...
backend/src/app/workers/tasks/__init__.py:24: from app.workers.tasks.outbox_tasks import ...
backend/src/app/workers/tasks/__init__.py:25: from app.workers.tasks.webhook_tasks import (...)
# ← chat_v2_indexing 모듈 import 가 어디에도 없음!
$ docker exec redis-1 redis-cli KEYS "*chat_v2*"
dramatiq:chat_v2_indexing.msgs # 메시지가 쌓이는데 처리 actor 가 없음
dramatiq:chat_v2_indexing
backend/src/app/workers/tasks/__init__.py 에 from app.workers.tasks.chat_v2_indexing import (backfill_chat_v2_properties, reindex_chat_v2_property) 추가. 이제 worker 가 actor 를 broker 에 등록.
6. raw data #4 — asyncio loop 충돌 (후속 PR)
PR #921 머지 후 backfill actor 호출 시 worker 로그:
$ docker exec backend-1 python -c "from app.workers.tasks.chat_v2_indexing \
import backfill_chat_v2_properties; \
print(backfill_chat_v2_properties.send())"
enqueued message_id=35da42ee-d101-4c6f-a3e3-1e9e594d9302
$ docker logs ai-real-estate-service-worker-1 --tail 30
File "asyncpg/protocol/protocol.pyx", line 375, in query
RuntimeError: Task <Task pending name='Task-11' coro=<_backfill_async() running
at /app/src/app/workers/tasks/chat_v2_indexing.py:103> cb=[_run_until_complete_cb()
at /usr/local/lib/python3.11/asyncio/base_events.py:181]>
got Future <Future pending cb=[BaseProtocol._on_waiter_completed()]>
attached to a different loop
[dramatiq.middleware.retries.Retries] Retrying message ... in 22697 ms
[dramatiq.middleware.retries.Retries] Retries exceeded for message '35da42ee...'
원인
chat_v2_indexing.py:31 의 _get_session_factory() 는 module-level singleton 으로 create_async_engine 을 한 번만 생성. 그러나 sync actor backfill_chat_v2_properties 가 매 호출마다 asyncio.run(_backfill_async(...)) 로 새 event loop 를 만듦 → 이전 loop 에 bound 된 asyncpg connection pool 이 새 loop 에서 사용 불가.
fix 후보 (후속 PR)
- actor 진입마다 새 engine 생성 —
_get_session_factory의 singleton 패턴을 폐기하고 actor 함수 안에서 매번create_async_engine. dispose 도 함수 끝에서. - nest_asyncio 패치 — 기존 loop 위에 nested loop 허용. 다만 production 권장 X.
- dramatiq async middleware — community plugin
dramatiq-asyncio또는 자체 wrapper 로 actor 를 native async 로 등록.
7. raw data #5 — SSE 정상 emit (backend 자체는 OK)
$ curl -sN -X POST http://localhost:10100/api/v1/assistant/message \
-H "Content-Type: application/json" \
-d '{"thread_id":"smoke-...","message":"전세 5천 이하 강남 원룸 찾아줘","context":{"role":"tenant"}}'
event: metadata
data: {"session_id": "smoke-...", "user_id": "anon-43a0eab98380"}
event: turn_start
data: {"user_message": "전세 5천 이하 강남 원룸 찾아줘"}
event: thinking
data: {"label": "🔎 매물 검색 중...", "text": "🔎 매물 검색 중..."}
event: tool_call
data: {"name": "search_listings", "args": {"top_k": 5, "user_message": "..."}, "call_id": "fc_e3a8..."}
event: updates
data: {"properties": [], "results": [], "can_show_listings": false, "count": 0,
"filters": {"hard": {"deposit_max": 50000000,
"area_keywords": ["강남"],
"room_count": [1],
"property_types": ["oneroom"]},
"confidence": 0.96},
...}
event: properties / suggestions / updates (4 카테고리 quick_buttons) / tool_result
event: token
data: {"text": "전세 예산이 5천만원 이하라서 강남 원룸 매물이 많이 부족한 상황이에요. ...
강남 인근 서초·역삼 쪽도 함께 살펴보실까요? 🥬", ...}
event: done / message / end
data: {"timing_ms": {"total": 12653, "llm": 1023, "tools": 11618,
"prompt_tokens": 12089, "completion_tokens": 155,
"cost_micro_usd": 1906}, "tools_used": ["search_listings"], "turn_count": 2}
properties=[] — Qdrant 0건이라 검색 결과 없음. final message 의 자연체 응답("강남 원룸 매물이 많이 부족한 상황이에요. ...") 도 LLM 이 정상 생성.
8. raw data #6 — Playwright 11/12 fail (PR #921 fix 후 재검증 필요)
$ cd e2e && npx playwright test \
tests/noauth/chat-guest-access.noauth.spec.ts \
tests/noauth/guest-chat-persistence.noauth.spec.ts \
tests/noauth/assistant-leak-fix.noauth.spec.ts \
--project=chromium-noauth --workers=2 --timeout=120000
11 failed
tests/noauth/assistant-leak-fix.noauth.spec.ts:26 tenant 랜딩 진입 → AI 입력창 visible
tests/noauth/assistant-leak-fix.noauth.spec.ts:32 멀티턴 회상 응답에 시스템 프롬프트 프리픽스 leak 없음
tests/noauth/assistant-leak-fix.noauth.spec.ts:72 매물 검색 요청 → tool calling 발화 → 본문에 슬롯 인용
tests/noauth/assistant-leak-fix.noauth.spec.ts:112 에러 메시지 ("열무가 답하다 꼬였어요") 가 화면에 노출되지 않음
tests/noauth/chat-guest-access.noauth.spec.ts:5 /chat — 비회원 진입 허용 + filter 칩 미노출 (#47)
tests/noauth/chat-guest-access.noauth.spec.ts:13 /chat — 비회원 empty CTA "3초 만에 로그인하기"
tests/noauth/chat-guest-access.noauth.spec.ts:22 /chat 헤더 알림 버튼
tests/noauth/chat-guest-access.noauth.spec.ts:34 /chat 검색 버튼 — aria-disabled
tests/noauth/guest-chat-persistence.noauth.spec.ts:44 /home 진입 — 비회원도 AI 입력창 렌더
tests/noauth/guest-chat-persistence.noauth.spec.ts:50 localStorage snapshot reload 복원
tests/noauth/guest-chat-persistence.noauth.spec.ts:82 snapshot 없는 fresh 진입 — 웰컴 메시지만 렌더
1 passed (1.1m)
Error context (대표):
Locator: getByPlaceholder(/원하는 동네나 조건을 입력하세요/)
Expected: visible
Timeout: 8000ms
Error: element(s) not found
getByTestId('ai-chat-input') 으로 갱신했음에도 fail 이 그대로인 것은 docker dev server frontend container 가 Vite HMR 로 main repo 의 testid 변경을 픽업하지 못한 가능성이 큼 (또는 /chat·/home 라우트의 별 회귀). PR #921 머지 + container 재빌드 후 재검증 필요.
9. raw data #7 — 멀티턴·메모리 필터 저장·복잡 시나리오 흐름 진단
9.1 멀티턴 thread_id 흐름
$ grep -n "thread_id\|threadId" frontend/src/store/aiChatStore.ts
169: threadId: string | null;
367: threadId: (raw.threadId as string | null) ?? null,
497: threadId: string | null;
685: /** messages 초기화, 새 threadId 생성, 관련 상태 초기화 */
690: threadId: string,
803: threadId: null as string | null,
981: threadId: state.threadId,
1075: set({ threadId: id });
1276: loadConversation: (threadId, dbMessages, lastMetadata) => {
해석: frontend 는 threadId 를 store 에 보관하고 localStorage 에 persist. 다만 SSE 호출 시 thread_id: '' (빈 string) 으로 보내면 backend 가 매번 새 thread 생성 → 영속화 안 됨. 또한 DB ai_messages 가 0건 이므로 회원 사용자도 reload 시 멀티턴 컨텍스트 복원 불가.
9.2 메모리 필터 저장 — `update_user_profile` tool
chat_v2 agent 의 4 atomic tool 중 하나. backend `tools.py:update_user_profile` 가 호출되어도 영속 layer 가 부재하면 다음 turn 에서 회상 불가.
| 도구 | 읽기/쓰기 | 현재 상태 |
|---|---|---|
search_listings | read (Qdrant) | Qdrant 0건 → 항상 빈 결과 |
get_thread_history | read (ai_messages) | ai_messages 0건 → 회상 불가 |
get_user_profile | read (별 테이블?) | 테이블 식별 필요 |
update_user_profile | write | 영속 layer 확인 필요 |
9.3 복잡 시나리오 — 회귀 시나리오 83 case 미실행
backend/tests/integration/chat_assistant/scenarios.py 는 64 (POC v2) + 19 (regression) = 83 시나리오 SSOT. 다만 라이브 LLM 호출 필요해 일반 CI 에서 실행 안 함. 정량 평가를 원하면 llm_judge.py + eval_split.py 로 직접 실행 (turn 당 ~12초 × 83 = 16분).
10. 5 회귀축 vs fix 매트릭스
| 회귀 축 | 증상 (사용자 관측) | root cause | fix PR | 잔여 |
|---|---|---|---|---|
| ① 메시지 버블 빈 채 | AI 답변이 화면에 안 나옴 | useAI.ts:347 'messages'(s) typo · store.updateMessage 누락 | #919, #920 | — |
| ② 검색 결과 0건 | 매물 못 찾음 | worker actor 미등록 → backfill 0회 → Qdrant points=0 | #921 (actor 등록) | asyncio loop 충돌 fix |
| ③ 멀티턴 깨짐 | "아까 뭐라 했지?" 회상 불가 | ai_threads 테이블 부재 · ai_messages=0 | 없음 | migration + 영속 layer |
| ④ 메모리 필터 저장 안 됨 | 이전 turn 의 조건 다시 입력해야 함 | update_user_profile write layer 부재 또는 미연결 | 없음 | persona/condition store 영속화 |
| ⑤ 부가 UI 빈 깡통 | 비교표·컨설팅·후속액션·결정배지 안 보임 | null stub 4종 #904 cleanup 미완 | 없음 | component 4 + store 4 + mapper 정리 |
| ⑥ Playwright 11/12 fail | 회귀 자동 탐지 불가 | placeholder dynamic + selector 노후화 | #921 (testid + spec) | container HMR 픽업 후 재검증 |
11. 후속 PR 우선순위 (P0 → P3)
- P0 — asyncio loop 충돌 fix (`chat_v2_indexing.py`)
module-level engine singleton 폐기, actor 진입마다 새 engine. 또는 dramatiq async middleware.
→ backfill 성공 → Qdrant points > 0 → search_listings 실제 매물 반환. - P0 — Playwright spec selector container 재검증
PR #921 머지 + frontend container 재빌드 후 Playwright 12/12 확인. 회귀 자동 탐지 가능 상태로 복귀. - P1 — ai_threads 테이블 migration + ai_messages 영속 흐름
멀티턴·메모리·복잡 시나리오의 인프라 기반. alembic revision 신규. - P1 — null stub 4종 완전 폐기
component 4 + store 4 + mapper + import + 사용처.bc72a127 (#904)약속한 cleanup PR. - P2 — 83 회귀 시나리오 정량 평가
scenarios.py + llm_judge.py 실행, train/holdout 점수 측정. Qdrant 채워진 후. - P2 — backend ruff 잠복 68건 정리
ruff --fix 48 + 수동 20. pre-push hook 통과.