콘텐츠로 이동
Data Prep
상세

빈집 챗봇 시스템

프로젝트 개요

부동산 및 빈집 관련 데이터를 자연어로 질의할 수 있는 RAG 기반 챗봇이다. Text-to-SQL과 문서 검색을 결합하여 정형/비정형 데이터 모두 처리한다.

항목 내용
목적 빈집 관련 데이터 자연어 질의
사용자 지자체 담당자, 정책 연구자
데이터 Oracle DB (정형) + 정책 문서 (비정형)
모델 vLLM 서빙 (오픈소스 LLM)

최근 업데이트 (2026-03)

날짜 변경 사항
03-25 시스템 아키텍처 문서 갱신
03-20 Oracle DB 쿼리 캐싱 도입 (Redis, 평균 응답 시간 40% 감소)
03-15 Hallucination 탐지 모듈 추가 (NLI 기반 factual consistency check)
03-10 관리자 대시보드 v2 출시 (사용 현황, 질의 분류 통계)
03-05 RAG 파이프라인 청크 전략 개선 (semantic chunking 적용)

최근 업데이트 (2026-02)

날짜 변경 사항
02-25 예측 모델 연동 완료 - 빈집 예측 결과 질의 가능
02-20 Multi-turn 대화 구현 완료
02-18 Re-ranking 적용 (Cohere rerank-multilingual)
02-15 SQL 생성 정확도 개선 (Few-shot 예시 50개 추가)

시스템 아키텍처

┌─────────────────────────────────────────────────────────────┐
│                   System Architecture                       │
│                                                             │
│  ┌──────────┐                                              │
│  │  User    │                                              │
│  │ (지자체) │                                              │
│  └────┬─────┘                                              │
│       │ 자연어 질의                                        │
│       ▼                                                     │
│  ┌──────────────────────────────────────────────────────┐  │
│  │                    FastAPI Gateway                    │  │
│  │  ┌────────────────────────────────────────────────┐  │  │
│  │  │           Intent Classifier + Router            │  │  │
│  │  │  SQL | DOC | PREDICTION | HYBRID                │  │  │
│  │  └───────────┬────────────────────┬───────────────┘  │  │
│  └──────────────┼────────────────────┼──────────────────┘  │
│                 │                    │                      │
│       ┌─────────┴────┬───────────────┼───────┐             │
│       ▼              ▼               ▼       ▼             │
│  ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐      │
│  │Text-to-  │ │   RAG    │ │Prediction│ │ Memory   │      │
│  │  SQL     │ │ Engine   │ │   API    │ │ (Redis)  │      │
│  └────┬─────┘ └────┬─────┘ └────┬─────┘ └──────────┘      │
│       │            │            │                          │
│       ▼            ▼            ▼                          │
│  ┌──────────┐ ┌──────────┐ ┌──────────┐                   │
│  │  Oracle  │ │  Elastic │ │ ML Model │                   │
│  │    DB    │ │ + MinIO  │ │  Cache   │                   │
│  └──────────┘ └──────────┘ └──────────┘                   │
│                                                             │
│              ┌──────────────────────┐                      │
│              │   vLLM + Triton      │                      │
│              │  ┌────────────────┐  │                      │
│              │  │  LLM (생성)    │  │                      │
│              │  └────────────────┘  │                      │
│              │  ┌────────────────┐  │                      │
│              │  │  BGE-M3 (임베딩)│  │                      │
│              │  └────────────────┘  │                      │
│              │  ┌────────────────┐  │                      │
│              │  │  Cohere Rerank │  │                      │
│              │  └────────────────┘  │                      │
│              └──────────────────────┘                      │
└─────────────────────────────────────────────────────────────┘

핵심 컴포넌트

1. Intent Classification (업데이트)

class IntentClassifier:
    """질의 유형 분류 - PREDICTION 추가"""

    INTENT_PROMPTS = """
    다음 질문의 유형을 분류하세요:
    - SQL: 통계, 수치, 목록 조회 (예: "서울시 빈집 수는?")
    - DOC: 정책, 법규, 절차 질문 (예: "빈집 정비 절차는?")
    - PREDICTION: 미래 예측, 위험도 (예: "의성군 빈집 예측은?")
    - HYBRID: 데이터 + 해석 필요 (예: "빈집이 가장 많은 지역과 그 원인은?")

    질문: {query}
    유형:
    """

    def classify(self, query: str) -> str:
        response = llm.generate(self.INTENT_PROMPTS.format(query=query))
        return parse_intent(response)  # SQL, DOC, PREDICTION, HYBRID

2. Multi-turn 대화 (신규)

class ConversationManager:
    """대화 맥락 관리"""

    def __init__(self, redis_client, ttl=3600):
        self.redis = redis_client
        self.ttl = ttl

    async def get_context(self, session_id: str) -> list:
        """이전 대화 내용 조회"""
        history = await self.redis.get(f"conv:{session_id}")
        return json.loads(history) if history else []

    async def add_turn(self, session_id: str, user_msg: str, assistant_msg: str):
        """대화 턴 추가"""
        history = await self.get_context(session_id)
        history.append({"user": user_msg, "assistant": assistant_msg})

        # 최근 10턴만 유지
        if len(history) > 10:
            history = history[-10:]

        await self.redis.setex(
            f"conv:{session_id}",
            self.ttl,
            json.dumps(history)
        )

    def build_prompt_with_context(self, query: str, history: list) -> str:
        """맥락 포함 프롬프트 구성"""
        context = "\n".join([
            f"사용자: {turn['user']}\n어시스턴트: {turn['assistant']}"
            for turn in history[-3:]  # 최근 3턴
        ])

        return f"""이전 대화:
{context}

현재 질문: {query}
"""

Multi-turn 예시:

[Turn 1]
사용자: 서울시 빈집 현황 알려줘
응답: 서울시 빈집은 총 3,847채입니다. 강북구가 523채로 가장 많습니다.

[Turn 2]
사용자: 그 중 방치 빈집은?
응답: 서울시 방치 빈집은 312채입니다. 강북구 45채, 도봉구 38채 순입니다.
       (맥락: "서울시"를 이전 턴에서 가져옴)

[Turn 3]
사용자: 강북구 상세 현황 보여줘
응답: 강북구 빈집 상세 현황입니다:
      - 총 빈집: 523채
      - 방치 빈집: 45채 (8.6%)
      - 정비 예정: 12채
      ...

3. 예측 모델 연동 (신규)

class PredictionService:
    """빈집 예측 모델 연동"""

    def __init__(self, api_base: str, cache: Redis):
        self.api = api_base
        self.cache = cache

    async def get_prediction(self, oa_code: str = None, region: str = None):
        """예측 결과 조회"""
        if oa_code:
            return await self._get_oa_prediction(oa_code)
        elif region:
            return await self._get_region_summary(region)

    async def _get_oa_prediction(self, oa_code: str) -> dict:
        # 캐시 확인
        cached = await self.cache.get(f"pred:{oa_code}")
        if cached:
            return json.loads(cached)

        # API 호출
        response = await httpx.get(f"{self.api}/prediction/{oa_code}")
        result = response.json()

        # 캐시 저장 (1시간)
        await self.cache.setex(f"pred:{oa_code}", 3600, json.dumps(result))
        return result

    def format_prediction_response(self, data: dict) -> str:
        """예측 결과를 자연어로 포맷팅"""
        return f"""
**{data['region_name']} 빈집 예측 결과**

현황:
- 현재 빈집률: {data['current_ratio']:.1%}
- 등록 빈집: {data['current_count']:,}

1년 후 예측:
- 예측 빈집률: {data['predicted_ratio']:.1%} ({data['change']:+.1%}p)
- 위험 등급: {data['risk_level']}

주요 위험 요인:
{self._format_factors(data['top_factors'])}
"""

4. Re-ranking 적용 (신규)

import cohere

class RerankedRAGEngine:
    def __init__(self, es_client, cohere_client, llm):
        self.es = es_client
        self.co = cohere_client
        self.llm = llm

    async def search_and_generate(self, query: str) -> dict:
        # 1. 초기 검색 (top 20)
        initial_results = await self.es.search(
            index="policy_documents",
            body=self._build_hybrid_query(query),
            size=20
        )

        # 2. Re-ranking (top 5)
        docs = [hit["_source"]["content"] for hit in initial_results["hits"]["hits"]]
        rerank_response = self.co.rerank(
            query=query,
            documents=docs,
            top_n=5,
            model='rerank-multilingual-v3.0'
        )

        # 3. 상위 문서로 컨텍스트 구성
        top_docs = [docs[r.index] for r in rerank_response.results]
        context = "\n\n".join(top_docs)

        # 4. LLM 생성
        response = await self.llm.generate(
            f"""다음 문서를 참고하여 질문에 답하세요.

            문서:
            {context}

            질문: {query}
            """
        )

        return {"answer": response, "sources": top_docs}

Re-ranking 효과:

지표 Before After
Recall@5 0.72 0.89
MRR 0.65 0.81
답변 정확도 (Human eval) 78% 91%

인프라 구성

서빙 스택

컴포넌트 기술 역할
API Gateway FastAPI 요청 라우팅, 인증
LLM 서빙 vLLM 빠른 추론
임베딩 Triton + BGE-M3 벡터 임베딩
Re-ranking Cohere API 검색 결과 재순위
벡터 검색 Elasticsearch 문서 검색
파일 저장 MinIO 정책 문서 저장
캐시/세션 Redis 쿼리 캐싱, 대화 맥락
DB Oracle 빈집 데이터

모니터링

┌─────────────────────────────────────────────────────────────┐
│                   Monitoring Stack                          │
│                                                             │
│  ┌─────────────┐    ┌─────────────┐    ┌─────────────┐     │
│  │ Prometheus  │───▶│  Grafana    │    │  Langfuse   │     │
│  │ (메트릭)    │    │ (대시보드)  │    │ (LLM 추적)  │     │
│  └─────────────┘    └─────────────┘    └─────────────┘     │
│                                                             │
│  핵심 메트릭:                                               │
│  ├─ 응답 시간 P95: 2.1s → 1.8s (개선)                     │
│  ├─ SQL 생성 정확도: 85% → 92%                            │
│  ├─ RAG 답변 정확도: 78% → 91%                            │
│  └─ 일일 질의량: ~500건                                    │
└─────────────────────────────────────────────────────────────┘

질의 예시

SQL 질의

사용자: 2023년 서울시 구별 빈집 현황 알려줘

처리: Intent=SQL → Text-to-SQL → Execute → Format

응답:
2023년 서울시 빈집 현황입니다:
- 강북구: 523채 (방치 45채)
- 도봉구: 412채 (방치 38채)
- 노원구: 387채 (방치 29채)
...
총 3,847채의 빈집이 등록되어 있습니다.

예측 질의 (신규)

사용자: 의성군 빈집 앞으로 어떻게 될까?

처리: Intent=PREDICTION → Prediction API → Format

응답:
경북 의성군 빈집 예측 결과입니다:

**현황:**
- 현재 빈집률: 12.3%
- 등록 빈집: 3,050채

**1년 후 예측:**
- 예측 빈집률: 14.8% (+2.5%p)
- 위험 등급: 매우 높음

**주요 위험 요인:**
1. 인구 변화율 -8.2%/년 (고령화 가속)
2. 30년 이상 노후 건물 65%
3. 거래 건수 전년 대비 -23%

**고위험 집계구:**
- 금성면 대리: 예측 빈집률 28%
- 춘산면 신흥리: 예측 빈집률 25%

Multi-turn 질의

[Turn 1]
사용자: 전국에서 빈집이 가장 많은 지역은?
응답: 경북 의성군이 3,050채로 가장 많습니다.

[Turn 2]
사용자: 거기 예측 결과 알려줘
응답: (맥락에서 "의성군" 파악)
경북 의성군 빈집 예측 결과입니다...

[Turn 3]
사용자: 정비 지원금 신청 절차는?
응답: (RAG 검색)
빈집 정비 사업 지원금 신청 절차는...

성능 지표

지표 이전 현재 목표
응답 시간 (P95) 2.1s 1.8s <2.0s
SQL 정확도 85% 92% >90%
RAG 정확도 78% 91% >85%
예측 연동 성공률 - 99% >99%

향후 계획

일정 작업 상태
~~2월 중~~ Multi-turn 대화 완료
~~2월 중~~ 예측 모델 연동 완료
3월 시각화 자동 생성 진행 예정
3월 알림 기능 (조건 모니터링) 진행 예정
Q2 음성 인터페이스 (STT/TTS) 검토

마지막 업데이트: 2026-02-25