빈집 챗봇 시스템
프로젝트 개요
부동산 및 빈집 관련 데이터를 자연어로 질의할 수 있는 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