문서 인덱싱(상)과 질의 처리(하)를 두 흐름으로 분리. 색상으로 흐름 구분(노랑=문서, 파랑=질의). "저장된 벡터로 검색" 화살표가 두 흐름을 잇는 핵심 연결.
💡 핵심 개념: 시스템 안의 객체는 시간이 지나며 상태가 바뀜. 같은 Document도 처음엔 raw, 나중엔 indexed. 상태 전이를 모르면 "이 객체 지금 어디까지 진행됐지?" 같은 디버깅이 안 됨.
stateDiagram-v2
direction LR
state "📥 인덱싱 (오프라인)" as DocFlow {
[*] --> D1
D1: 📄 원본 문서
D2: 📦 로드 완료
D3: ✂️ 청크 분할
D4: 🔢 임베딩
D5: 💾 벡터 DB 저장
D1 --> D2: Loader
D2 --> D3: Splitter
D3 --> D4: Embedder
D4 --> D5: Indexer
D5 --> [*]
}
state "🔍 검색·생성 (온라인)" as QueryFlow {
[*] --> Q1
Q1: ❓ 사용자 질문
Q2: 🔢 질의 임베딩
Q3: 🔍 top-k 검색
Q4: 🧩 컨텍스트 구성
Q5: 💬 LLM 응답
Q1 --> Q2: Embedder
Q2 --> Q3: Retriever
Q3 --> Q4: Composer
Q4 --> Q5: LLM
Q5 --> [*]
}
D5 --> Q3: 저장된 벡터로 검색
classDef doc fill:#fef3c7,stroke:#f59e0b,color:#1f2937,stroke-width:2px
classDef query fill:#dbeafe,stroke:#3b82f6,color:#1f2937,stroke-width:2px
class D1,D2,D3,D4,D5 doc
class Q1,Q2,Q3,Q4,Q5 query
🔧 각 상태의 invariant
- D1 원본 — 파일시스템에 저장됨, 메타데이터 없음
- D2 로드 완료 — 텍스트 추출 성공, charset·encoding 확정
- D3 청크 — 각 청크가 토큰 한도 내, 의미 단위로 분리됨
- D4 임베딩 — 벡터 차원 일관 (예: 1536), L2 normalized 여부 결정됨
- D5 저장 — Vector DB에 영속화, source 메타데이터 보존
- Q1-Q5 — 질의 라이프사이클. 응답 후 메모리에서 사라짐 (디버깅 위해 로깅 필수)
📋 디버깅 시나리오: "어느 chunk가 왜 잘못 검색됐지?" 추적 →
- D1(원본) → D2(로드) → D3(청크) → D4(임베딩) → D5(저장) 각 단계 로그 확인
- 어느 단계에서 끊겼는지 보면 "청크가 너무 작아서 의미 손실" 같은 원인 파악
⚠️ 흔한 함정:
- 상태 전이 로깅 없음 — 실패 시 어디서 멈췄는지 모름. 각 전이마다 log + trace_id 필수
- 멱등성 미보장 — 같은 chunk를 두 번 upsert하면 중복.
doc_id + chunk_id unique key
- Q 라이프사이클 응답 후 사라짐 — 재현 불가. 로깅 시스템(W&B, LangSmith) 연결
🛠️ 상태 추적: LangSmith, W&B Weave, OpenTelemetry, 또는 자체 DB에 document_status 컬럼 추가.