Poly: 의료 복지 정책 통합정보 제공 AI 챗봇 서비스
- 프로젝트 개요
- 기술 스택
- 아키텍처
- AI/ML 스택 심층 분석
- RAG 시스템 상세
- LangGraph 파이프라인
- 핵심 모듈 분석
- 데이터베이스 설계
- 성능 최적화
- 디자인 패턴
- 면접 예상 질문 & 답변
- 핵심 숫자 요약
"Poly는 의료 복지 정책 통합정보 제공 AI 챗봇으로, 사용자 프로필(나이, 지역, 건강보험 등)을 기반으로 맞춤형 정책을 RAG 기반으로 추천하는 서비스입니다."
| 문제 | 해결 방법 |
|---|---|
| 정책 정보가 여러 홈페이지에 분산 | 크롤링으로 통합 DB 구축 |
| 능동적 정보 수집 어려움 | AI 챗봇으로 대화형 검색 |
| 지원자격 수동 비교 필요 | 프로필 기반 자동 필터링 |
- 맞춤형 정책 추천: 사용자 프로필 기반 RAG 검색
- 멀티 프로필 지원: 한 계정에서 여러 가족 구성원 관리
- 실시간 스트리밍: 토큰 단위 응답으로 빠른 체감 속도
- 정보 자동 추출: 대화에서 프로필/병력 정보 자동 파악
Frontend: Streamlit
Backend: FastAPI + Uvicorn
AI/ML: LangGraph + LangChain + OpenAI GPT-4o-mini
임베딩: OpenAI text-embedding-3-small (1536차원)
검색: PGVector (벡터) + BM25 (키워드) 하이브리드
DB: PostgreSQL + SQLite (체크포인트)
인증: JWT (Access + Refresh Token)
| 계층 | 기술 | 버전 | 용도 |
|---|---|---|---|
| 백엔드 | FastAPI | 0.108.0 | REST API 서버 |
| 웹 서버 | Uvicorn | 0.34.0 | ASGI 서버 |
| 프론트엔드 | Streamlit | >=1.28.0 | 웹 UI |
| LLM 프레임워크 | LangChain | 0.3.27 | LLM 오케스트레이션 |
| 워크플로우 | LangGraph | 1.0.1 | 상태 머신 기반 파이프라인 |
| LLM | OpenAI GPT-4o-mini | - | 응답 생성, 정보 추출 |
| 임베딩 | text-embedding-3-small | - | 1536차원 벡터 |
| 벡터 DB | PGVector | 0.3.6 | 임베딩 저장/검색 |
| 키워드 검색 | rank-bm25 | 0.2.2 | BM25 re-ranking |
| DB | PostgreSQL | - | 메인 데이터베이스 |
| 체크포인트 | SQLite | - | LangGraph 상태 저장 |
| 인증 | python-jose | 3.3.0 | JWT 토큰 |
| 비밀번호 | bcrypt | 4.0.1 | 해싱 |
┌─────────────────────────────────────────────────────────────────┐
│ 사용자 (웹 브라우저) │
└────────────────────────────┬──────────────────────────────────────┘
│
HTTP/WebSocket
│
┌────────────────────┴────────────────────┐
│ │
┌────▼──────────────────┐ ┌──────────▼────────────┐
│ 프론트엔드 │ │ 백엔드 │
│ (Streamlit) │ │ (FastAPI) │
│ Port: 8501 │ │ Port: 8000 │
│ │ │ │
│ ├─ login.py │ │ ├─ user API │
│ ├─ chat.py │◄────────► ├─ chat API │
│ ├─ my_page.py │ REST API ├─ LangGraph │
│ └─ settings.py │ │ └─ Auth │
└───────────────────────┘ └──────────┬───────────┘
│
┌──────────┴──────────┐
│ │
┌─────▼─────┐ ┌────▼────┐
│ PostgreSQL│ │ SQLite │
│ + PGVector│ │Checkpoint│
└───────────┘ └─────────┘
사용자 입력 → Streamlit → FastAPI → LangGraph 파이프라인
│
├→ session_orchestrator
├→ query_router
├→ info_extractor
├→ user_context_node
├→ policy_retriever (RAG)
├→ llm_answer_creator
└→ persist_pipeline
"Agent는 LLM을 두뇌로 사용하여 자율적으로 작업을 수행하는 시스템입니다."
Poly의 Agent는 LangGraph 기반 상태 머신으로 구현:
┌─────────────────────────────────────────────────────────────────────────┐
│ LangGraph Agent Pipeline │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ 사용자 입력 │
│ │ │
│ ▼ │
│ ┌─────────────────────┐ │
│ │ session_orchestrator │ ← 세션 생명주기 관리 (타임아웃, 턴 카운트) │
│ └──────────┬──────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────┐ │
│ │ query_router │ ← 의도 분류 (save/reset/normal) │
│ └──────────┬──────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────┐ │
│ │ info_extractor │ ← LLM으로 정보 추출 (NER + 구조화) │
│ │ (GPT-4o-mini) │ │
│ └──────────┬──────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────┐ │
│ │ user_context_node │ ← 프로필/컬렉션 병합 + 요약 텍스트 생성 │
│ └──────────┬──────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────┐ │
│ │ policy_retriever │ ← RAG: 벡터 + BM25 하이브리드 검색 │
│ │ (OpenAI Embedding) │ │
│ └──────────┬──────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────┐ │
│ │ llm_answer_creator │ ← 최종 응답 생성 (GPT-4o-mini, 스트리밍) │
│ └──────────┬──────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────┐ │
│ │ persist_pipeline │ ← (선택) 대화 히스토리 DB 저장 │
│ └─────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────┘
| 기능 | LangChain만 | LangGraph |
|---|---|---|
| 순차 체인 | O | O |
| 조건부 분기 | 제한적 | O (conditional_edges) |
| 상태 공유 | 수동 관리 | State 자동 관리 |
| 사이클/루프 | X | O |
| 체크포인트 | X | O (SqliteSaver) |
| 멀티턴 대화 | 복잡 | 간단 |
class EphemeralContextState(TypedDict, total=False):
# ── 세션/제어 ───────────────────────────────────
session_id: str # 세션 식별자
end_session: bool # 세션 종료 플래그
turn_count: int # 대화 턴 수
# ── 메시지 (append-only reducer!) ───────────────
messages: Annotated[List[Message], operator.add]
rolling_summary: Optional[str] # 대화 요약
# ── 프로필/컬렉션 (계층 구조) ────────────────────
ephemeral_profile: Dict[str, Any] # 이번 세션 임시 프로필
ephemeral_collection: Dict[str, Any] # 이번 세션 임시 컬렉션
merged_profile: Dict[str, Any] # DB + ephemeral 병합
merged_collection: Dict[str, Any] # DB + ephemeral 병합
# ── RAG 결과 ────────────────────────────────────
retrieval: Dict[str, Any] # {used_rag, rag_snippets, keywords, ...}
# ── 스트리밍 ────────────────────────────────────
streaming_mode: bool
streaming_context: Dict[str, Any]# operator.add를 사용한 Annotated 타입
messages: Annotated[List[Message], operator.add]
# 각 노드는 새 메시지만 반환 → 자동으로 기존에 append
def info_extractor_node(state: State) -> Dict[str, Any]:
return {
"messages": [new_message], # 이것만 반환
"ephemeral_profile": merged_profile,
}사용자 질문: "저는 동작구에 사는 당뇨 환자인데 받을 수 있는 혜택이 뭔가요?"
│
▼
┌─────────────────────────────────────────────────────────────────────────┐
│ Step 1: Synthetic Query 생성 │
│ │
│ • 질문이 너무 일반적인지 판단 (키워드 추출) │
│ • 일반적이면 프로필 + 컬렉션 기반 synthetic query 생성 │
│ │
│ 결과: "동작구 거주; 당뇨병 관련 의료·복지 지원 정책" │
└─────────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────┐
│ Step 2: 임베딩 생성 (OpenAI text-embedding-3-small) │
│ │
│ • 모델: text-embedding-3-small (1536차원) │
│ • 캐싱: MD5 해시 키 + FIFO 방식 (최대 100개) │
│ • 빈 텍스트: 제로 벡터 [0.0] * 1536 │
│ │
│ 성능: │
│ - 캐시 히트: ~0.001s │
│ - 캐시 미스: ~0.8s │
└─────────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────┐
│ Step 3: PGVector 벡터 검색 │
│ │
│ SQL: │
│ SELECT d.*, (1 - (e.embedding <=> query_vec)) AS similarity │
│ FROM embeddings e JOIN documents d ON d.id = e.doc_id │
│ WHERE e.field = 'title' AND d.region = '동작구' │
│ ORDER BY e.embedding <=> query_vec │
│ LIMIT 12 (RAW_TOP_K) │
│ │
│ • <=> 연산자: 코사인 거리 (PGVector) │
│ • region 필터: 하드 필터 (인덱스 활용) │
└─────────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────┐
│ Step 4: BM25 키워드 Re-ranking │
│ │
│ 컬렉션 계층별 키워드 가중치: │
│ L0 (이번 턴 추출): weight = 3 │
│ L1 (이번 세션): weight = 2 │
│ L2 (DB 저장): weight = 1 │
│ │
│ BM25 스코어 계산: │
│ score = Σ IDF(qi) × (freq × (k1 + 1)) / (freq + k1 × (1-b+b×dl/avgdl))│
│ k1 = 1.5, b = 0.75 │
└─────────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────┐
│ Step 5: 하이브리드 스코어 계산 │
│ │
│ final_score = 0.8 × vector_similarity + 0.2 × bm25_normalized │
│ │
│ 비율 선택 이유: │
│ - 의료 정책은 의미적 유사성이 더 중요 (80%) │
│ - 하지만 "당뇨", "임산부" 등 정확한 키워드 매칭도 필요 (20%) │
└─────────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────┐
│ Step 6: 프로필 기반 후처리 필터링 │
│ │
│ 필터 종류: │
│ 1) 중위소득 필터: "중위소득 120% 이하" 조건 파싱 → 사용자 150%면 제외 │
│ 2) 기초생활보장 필터: 수급자 필수 정책 → 비수급자 제외 │
│ 3) 장애등급 필터: "장애 1~3급" 조건 → 등급 외 제외 │
└─────────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────┐
│ Step 7: 최종 Top-K 선택 │
│ │
│ • SIMILARITY_FLOOR = 0.3 (유사도 임계값) │
│ • MIN_CANDIDATES_AFTER_FLOOR = 5 (최소 후보 수 보장) │
│ • CONTEXT_TOP_K = 8 (LLM에 전달할 최대 문서 수) │
└─────────────────────────────────────────────────────────────────────────┘
# app/langgraph/nodes/policy_retriever.py
_embedding_cache: Dict[str, List[float]] = {} # 캐시 저장소
_cache_order: List[str] = [] # FIFO 순서 관리
EMBEDDING_CACHE_SIZE = 100
def _embed_text(text: str) -> List[float]:
# 1. 캐시 키 생성 (MD5 해시)
cache_key = hashlib.md5(text.encode('utf-8')).hexdigest()
# 2. 캐시 히트
if cache_key in _embedding_cache:
return _embedding_cache[cache_key]
# 3. 캐시 미스 → OpenAI API 호출
response = client.embeddings.create(
model="text-embedding-3-small",
input=text
)
embedding = response.data[0].embedding
# 4. 캐시 저장 (FIFO)
_embedding_cache[cache_key] = embedding
_cache_order.append(cache_key)
# 5. 캐시 크기 초과 시 가장 오래된 항목 제거
if len(_cache_order) > EMBEDDING_CACHE_SIZE:
oldest_key = _cache_order.pop(0)
del _embedding_cache[oldest_key]
return embedding┌─────────────────────────────────────────────────────────────────────────┐
│ 컬렉션 계층 구조 │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ L0 (이번 턴) ─────► BM25 weight = 3 (가장 높음) │
│ │ │
│ │ "방금 사용자가 말한 '당뇨', '항암치료'" │
│ │ │
│ ▼ │
│ L1 (이번 세션) ─────► BM25 weight = 2 │
│ │ │
│ │ "이번 대화에서 누적된 정보 (ephemeral_collection)" │
│ │ │
│ ▼ │
│ L2 (DB 저장) ─────► BM25 weight = 1 (가장 낮음) │
│ │
│ "DB에 영구 저장된 사용자의 과거 병력/상태" │
│ │
└─────────────────────────────────────────────────────────────────────────┘
왜 계층 구조인가?
─────────────────
1. 현재 질문에 더 관련 있는 정보에 높은 가중치
2. 대화 컨텍스트 일관성 유지
3. 사용자의 즉각적 의도 반영
# app/agents/new_pipeline.py
from langgraph.graph import StateGraph, END
from langgraph.checkpoint.sqlite import SqliteSaver
def build_graph():
graph = StateGraph(State)
# 노드 등록
graph.add_node("session_orchestrator", session_orchestrator_node)
graph.add_node("router", router_node)
graph.add_node("info_extractor", info_extractor_node)
graph.add_node("user_context", user_context_node)
graph.add_node("policy_retriever", policy_retriever_node)
graph.add_node("answer_llm", answer_llm_node)
graph.add_node("persist_pipeline", persist_pipeline_node)
# Entry point
graph.set_entry_point("session_orchestrator")
graph.add_edge("session_orchestrator", "router")
# 조건부 분기 (핵심!)
graph.add_conditional_edges(
"router",
route_edge,
{
"info_extractor": "info_extractor",
"persist_pipeline": "persist_pipeline",
END: END,
},
)
# 기본 흐름
graph.add_edge("info_extractor", "user_context")
graph.add_edge("user_context", "policy_retriever")
graph.add_edge("policy_retriever", "answer_llm")
graph.add_edge("persist_pipeline", END)
# 체크포인터 (SQLite)
checkpointer = SqliteSaver(conn)
return graph.compile(checkpointer=checkpointer)def route_edge(state: State):
nxt = state.get("next") or "info_extractor"
action = state.get("user_action")
if nxt == "end":
if action == "reset_drop":
return END
if action == "reset_save" or state.get("end_session"):
return "persist_pipeline"
if action == "save":
return "persist_pipeline"
return END
return "info_extractor"역할: 사용자 발화에서 프로필/병력 정보 자동 추출
# Pydantic 스키마
class ExtractedProfile(BaseModel):
age: Optional[ProfileField] # 나이
region_gu: Optional[ProfileField] # 거주 구
median_income_ratio: Optional[ProfileField] # 중위소득 비율
disability_grade: Optional[ProfileField] # 장애등급 (0/1/2)
pregnancy_status: Optional[ProfileField] # 임신상태
# ...
class Triple(BaseModel):
subject: str # "self", "child", "spouse" 등
predicate: str # "disease", "treatment" 등
object: str # 자유 텍스트
code: Optional[str] # "E11", "C509" 등
confidence: float # 0~1
class ExtractResult(BaseModel):
profile: ExtractedProfile
collection: ExtractedCollection
residual_query: Optional[str] # 정보 제거 후 남은 질문복합 문장 분리:
입력: "저는 중위소득 50%고, 임신중인데 받을 수 있는 혜택이 뭐가 있나요?"
추출 결과:
- profile.median_income_ratio = {"value": "50", "confidence": 0.95}
- profile.pregnancy_status = {"value": "임신중", "confidence": 0.9}
- residual_query = "받을 수 있는 혜택이 뭐가 있나요?"
핵심 파라미터:
RAW_TOP_K = 12 # 초기 검색 문서 수
CONTEXT_TOP_K = 8 # LLM에 전달할 최대 문서 수
SIMILARITY_FLOOR = 0.3 # 유사도 임계값
BM25_WEIGHT = 0.2 # 하이브리드 검색에서 BM25 비율
EMBEDDING_CACHE_SIZE = 100 # 임베딩 캐시 크기
# 컬렉션 계층별 가중치
LAYER_WEIGHTS = {
"L0": 3, # 이번 턴 추출
"L1": 2, # 이번 세션
"L2": 1, # DB 저장
}시스템 프롬프트:
SYSTEM_PROMPT = """
당신은 의료·복지 지원 정책 안내 상담사입니다.
## 중요: 추가 필터링 금지
- 제공된 정책 문서는 이미 사용자 프로필 기반으로 필터링이 완료된 상태입니다
- 제공된 모든 정책을 사용자에게 안내해야 합니다
## 답변 형식
1. **한 줄 결론** (굵게)
2. 각 정책마다:
- 정책명 + 지역
- 지원 내용 (benefits 기반)
- 지원 자격 (requirements 기반)
- URL
3. 다음 단계 안내
## 제약
- 제공된 컨텍스트만 사용, 추측 금지
- **제공된 모든 정책을 출력** (임의로 개수를 줄이지 않음)
"""스트리밍 구현:
def run_answer_llm_stream(input_text, used, profile_ctx, ...):
stream = client.chat.completions.create(
model="gpt-4o-mini",
messages=messages,
temperature=0.3,
stream=True,
)
for chunk in stream:
if chunk.choices[0].delta.content is not None:
yield chunk.choices[0].delta.content-- users 테이블
users:
├─ id (UUID, PK)
├─ username (VARCHAR, UNIQUE)
├─ password_hash (VARCHAR)
├─ main_profile_id (FK → profiles.id)
└─ created_at, updated_at
-- profiles 테이블
profiles:
├─ id (INT, PK)
├─ user_id (FK → users.id)
├─ name (VARCHAR)
├─ sex (ENUM: M/F)
├─ birth_date (DATE)
├─ residency_sgg_code (VARCHAR)
├─ insurance_type (ENUM)
├─ median_income_ratio (FLOAT)
├─ disability_grade (INT: 0/1/2)
└─ pregnant_or_postpartum12m (BOOLEAN)
-- documents 테이블 (정책 DB)
documents:
├─ id (INT, PK)
├─ title (VARCHAR)
├─ requirements (TEXT)
├─ benefits (TEXT)
├─ region (VARCHAR)
└─ url (VARCHAR)
-- embeddings 테이블 (PGVector)
embeddings:
├─ id (INT, PK)
├─ doc_id (FK → documents.id)
├─ field (VARCHAR) -- 'title'
└─ embedding (vector(1536))| 프론트엔드 | DB |
|---|---|
| name | name |
| gender | sex (M/F) |
| birthDate | birth_date |
| location | residency_sgg_code |
| healthInsurance | insurance_type |
| incomeLevel | median_income_ratio |
| disabilityLevel | disability_grade |
| pregnancyStatus | pregnant_or_postpartum12m |
_embedding_cache: Dict[str, List[float]] = {}
# FIFO 방식, 최대 100개
# 캐시 히트: ~0.001s
# 캐시 미스: ~0.8s| 특성 | MemorySaver | SqliteSaver |
|---|---|---|
| 저장 위치 | RAM | 디스크 |
| 재시작 시 | 손실 | 유지 |
| 메모리 | 계속 증가 | 일정 |
| 동시성 | 단일 스레드 | WAL 모드 |
_checkpointer_conn = sqlite3.connect(
CHECKPOINT_DB_PATH,
check_same_thread=False,
)
_checkpointer_conn.execute("PRAGMA journal_mode=WAL")
_checkpointer_conn.execute("PRAGMA synchronous=NORMAL")_connection_pool = ConnectionPool(
conninfo=DB_URL,
min_size=2,
max_size=10,
timeout=30,
max_lifetime=300, # 5분마다 재활용
)- 비스트리밍: 전체 응답 생성 후 전송 (3~5초 대기)
- 스트리밍: 첫 토큰까지 0.5~1초, 이후 실시간 표시
| 패턴 | 적용 위치 | 이유 |
|---|---|---|
| Singleton | LangGraph 앱, OpenAI 클라이언트 | 초기화 비용 절감 |
| Repository | user_repository.py | DB 접근 추상화 |
| Strategy | 지역별 크롤러 | 크롤링 전략 분리 |
| Factory | crawler_factory.py | 크롤러 생성 추상화 |
| Adapter | UserProfile.to_db_dict() | 필드명 변환 |
| State Machine | LangGraph | 대화 상태 관리 |
A: "복잡한 대화 흐름을 상태 머신 기반으로 관리하기 위해서입니다. 각 노드(세션 관리, 정보 추출, 검색, 응답 생성)가 독립적으로 동작하면서도 상태를 공유할 수 있어, 멀티턴 대화와 세션 타임아웃 같은 기능을 체계적으로 구현할 수 있었습니다."
A: "실험을 통해 결정했습니다. 의료 정책 특성상 의미적 유사성(벡터)이 더 중요하지만, '임산부', '장애인' 같은 키워드 매칭도 필요해서 BM25를 20% 반영했습니다. 이 비율에서 정확도가 가장 높았습니다."
A: "MemorySaver는 서버 재시작 시 세션이 손실되고 메모리가 계속 누적됩니다. SqliteSaver는 디스크 기반이라 영구 저장되고, WAL 모드로 동시성도 우수합니다."
A: "순수 벡터 검색만 사용하면 의미적으로 유사하지만 핵심 키워드가 없는 문서가 상위에 올라올 수 있습니다. 예를 들어 '당뇨 환자 지원'을 검색할 때, '만성질환 관리 사업'이 의미적으로 유사할 수 있지만 '당뇨'라는 키워드가 없을 수 있죠. BM25를 20% 반영하면 키워드 매칭도 고려되어 정확도가 올라갑니다."
A: "대화의 시간적 맥락을 반영하기 위해서입니다.
- L0(이번 턴): 방금 말한 정보가 가장 중요
- L1(이번 세션): 이전 턴에서 말한 정보도 관련
- L2(DB): 오래전 등록한 정보는 가중치 낮게
BM25 re-ranking에서 L0에 3배, L1에 2배, L2에 1배 가중치를 줘서 현재 의도에 더 부합하는 정책을 상위에 노출합니다."
A: "사용자가 정보 입력과 질문을 한 문장에 섞어서 말할 때가 많습니다.
예: '저는 당뇨가 있고 동작구에 사는데 혜택이 뭐가 있나요?'
- 정보: '당뇨', '동작구' → ephemeral_profile/collection에 저장
- 질문: '혜택이 뭐가 있나요?' → residual_query로 분리
이렇게 분리하면 정책 검색 쿼리가 더 깔끔해집니다."
A: "세 가지 이유입니다:
- 비용 효율: ada-002 대비 5배 저렴
- 성능: 1536차원으로 충분한 표현력
- 속도: 평균 0.8초 응답 시간"
A: "체감 응답 시간 단축입니다.
- 비스트리밍: 전체 응답 생성 후 한 번에 전송 (3~5초 대기)
- 스트리밍: 첫 토큰까지 0.5~1초, 이후 실시간 표시
사용자는 AI가 '생각하고 있다'는 느낌을 받아 UX가 개선됩니다."
A: "코사인 거리(Cosine Distance) 연산자입니다.
ORDER BY embedding <=> query_vector코사인 유사도 = 1 - 코사인 거리이고, 거리가 작을수록 유사합니다."
A: "결정성 vs 창의성 트레이드오프입니다.
- info_extractor (temp=0.0): 정보 추출은 결정적이어야 함
- llm_answer_creator (temp=0.3): 자연스러운 응답 필요, 하지만 환각 방지"
A: "질문이 너무 일반적일 때입니다. '혜택 알려주세요' 같은 경우 그대로 임베딩하면 의미 있는 검색이 안 됩니다. 그래서 프로필+컬렉션 정보를 조합해 '동작구 거주; 당뇨병 환자; 관련 의료·복지 지원 정책' 같은 의미 있는 쿼리를 만듭니다."
| 항목 | 값 | 설명 |
|---|---|---|
| 임베딩 차원 | 1536 | text-embedding-3-small |
| 캐시 크기 | 100개 | FIFO 방식 |
| RAW_TOP_K | 12 | 초기 검색 문서 수 |
| CONTEXT_TOP_K | 8 | LLM에 전달할 최대 문서 수 |
| SIMILARITY_FLOOR | 0.3 | 유사도 임계값 |
| BM25_WEIGHT | 0.2 | 하이브리드 검색에서 BM25 비율 |
| L0 가중치 | 3 | 이번 턴 키워드 |
| L1 가중치 | 2 | 이번 세션 키워드 |
| L2 가중치 | 1 | DB 저장 키워드 |
| rolling_summary 주기 | 15턴 | 대화 요약 업데이트 |
| Access Token 만료 | 24시간 | JWT |
| Refresh Token 만료 | 30일 | JWT |
| 세션 타임아웃 | 15분 | 비활성 세션 종료 |
| 기능 | 파일 |
|---|---|
| FastAPI 진입점 | app/main.py |
| JWT 인증 | app/auth.py |
| 사용자 API | app/api/v1/user.py |
| 채팅 API | app/api/v1/chat.py |
| LangGraph 빌더 | app/agents/new_pipeline.py |
| State 정의 | app/langgraph/state/ephemeral_context.py |
| 정보 추출 | app/langgraph/nodes/info_extractor.py |
| 컨텍스트 구성 | app/langgraph/nodes/user_context_node.py |
| 정책 검색 (RAG) | app/langgraph/nodes/policy_retriever.py |
| 응답 생성 | app/langgraph/nodes/llm_answer_creator.py |
| 프로필 필터링 | app/langgraph/utils/retrieval_filters.py |
| 필드 변환 | app/schemas.py |
이 문서는 Poly 프로젝트 면접 준비를 위해 작성되었습니다.