아래 순서를 그대로 따라가면 “내가 넘어졌던 날을 찾아줘” 같은 자연어 질의에 날짜만 깔끔히 되돌려주는 RAG 시스템을 실험 단계까지 구축할 수 있습니다.
(예시는 NestJS + TypeScript, Qdrant + pgvector 기준이지만, 다른 스택에도 그대로 매핑 가능합니다.)
┌─(1) 일기 RAW ─────────┐
│ 작성·업로드 │
└───────────────────────┘
│
▼
┌─(2) 전처리/사건 추출 파이프라인─────────────┐
│ ・날짜 파싱 │
│ ・LLM으로 “사건 요약” 생성 │
│ (예: `"미끄러져 넘어짐"`) │
│ ・사건 타입/감정(선택) 태깅 │
└────────────────────────────────────────────┘
│
▼
┌─(3) 임베딩 생성──────────────────────────────┐
│ ・`text = <사건 요약>` │
│ ・`embedding = OpenAI/miniLM 등` │
│ ・`metadata = {date, event_type:"넘어짐"}` │
└────────────────────────────────────────────┘
│
▼
┌─(4) Vector DB (Qdrant/pgvector)─────────────┐
│ Hybrid index ← keyword index │
└────────────────────────────────────────────┘
│
▼
┌─(5) Hybrid Retrieval Layer──────────────────┐
│ ① 키워드 필터링 (넘어짐·미끄러짐 등 동의어) │
│ ② 벡터 유사도 K개 │
│ ③ 날짜·메타데이터만 추출 │
└────────────────────────────────────────────┘
│
▼
┌─(6) LLM 재후처리 (RAG)──────────────────────┐
│ Prompt: “아래 문서에서 날짜만 리스트로 줘” │
└────────────────────────────────────────────┘
| 단계 | 구현 팁 | 예시 코드 |
|---|---|---|
| 날짜 파싱 | 정규식 + date-fns |
const date = parse('yyyy년 M월 d일', raw, new Date()) |
| 사건 요약 | OpenAI /chat/completions ; system 프롬프트 |
“일기에서 가장 특징적인 사건을 한 문장으로 요약해줘.” |
| 사건 타입 태깅 | 간단 규칙 → 미세 조정 필요 | `if (/넘어 |
Tip
개발 초기엔 일기 20~30개만 수동으로 라벨링 → 모델이 못 잡아내는 동의어를 사전으로 추가해두면 precision 급상승합니다.
await qdrant.createCollection({
name: 'diary',
vectors: { size: 384, distance: 'Cosine' },
optimizers_config: { default_segment_number: 2 },
});
await qdrant.upsert('diary', [
{
id: uuidv4(),
vector: embedding, // Float32Array
payload: {
date: '2024-09-13',
event_type: 'fall',
text: '비 오는 날 미끄러져 넘어짐'
}
}
]);
const must = [
{ key: 'event_type', match: 'fall' } // ① 키워드/태그 필터
];
const { result } = await qdrant.search('diary', {
vector: embed('넘어짐 사건'),
filter: { must },
limit: 5 // ② 벡터 유사도
});
const docs = result.map(r => `${r.payload.date}: ${r.payload.text}`);
"미끄러져 중심을 잃었다"처럼 ‘넘어짐’ 단어가 없어도 벡터 유사도로 catch"넘어졌다" 언급만 있고 맥락이 다른 문장은 키워드 필터로 우선 확보