RAG(Retrieval-Augmented Generation)는 "모델이 모르는 것을 알게 만드는" 핵심 기술입니다. 이 모듈을 마치면 여러분 자신의 노트·문서를 업로드하고, LLM이 그 내용만을 근거로 정확하게 답변하는 시스템을 직접 구동할 수 있습니다.
왜 RAG인가
LLM은 학습 시점 이후의 정보를 모릅니다. 회사 내부 문서, 개인 노트, 최신 정책 — 이런 지식을 모델에 주입하는 방법은 두 가지입니다: Fine-tuning(비용 높음, 정적) 또는 RAG(비용 낮음, 동적). 우리는 RAG를 선택합니다. 검색으로 관련 청크를 뽑아 컨텍스트에 끼워 넣고, 모델은 그것만 보고 대답합니다.
텍스트 청킹 — chunkPlainText()
원본 문서를 통째로 넣으면 컨텍스트 창이 터집니다. 600자 단위로 자르되, 앞 청크의 마지막 100자를 다음 청크 앞에 중첩(overlap)합니다. 이 overlap이 없으면 문장이 청크 경계에서 잘려 의미가 사라집니다.
export function chunkPlainText(text: string, size = 600, overlap = 100): string[] {
const chunks: string[] = [];
let start = 0;
while (start < text.length) {
const end = Math.min(start + size, text.length);
chunks.push(text.slice(start, end));
if (end === text.length) break;
start += size - overlap;
}
return chunks;
}
팁: 청크 크기는 임베딩 모델의 토큰 한도(text-embedding-3-small은 8191 토큰)와 무관하게, 검색 정밀도를 위해 작게 유지하는 것이 원칙입니다. 600자(≈약 150토큰)는 실무에서 검증된 시작점입니다.
임베딩 생성과 Vercel Blob 저장
각 청크를 text-embedding-3-small(1536차원)로 임베딩한 뒤, Vercel Blob에 JSON으로 저장합니다. 별도 vector DB는 사용하지 않습니다 — 문서 수백 건 규모에서는 in-memory 코사인 검색이 더 빠르고 운영 비용이 0입니다.
import OpenAI from "openai";
import { put } from "@vercel/blob";
const openai = new OpenAI();
export async function embedAndStore(docId: string, chunks: string[]) {
const res = await openai.embeddings.create({
model: "text-embedding-3-small",
input: chunks,
});
const records = chunks.map((text, i) => ({
text,
vector: res.data[i].embedding, // number[1536]
}));
await put(`embeddings/${docId}.json`, JSON.stringify(records), {
access: "private",
});
return records.length;
}
저장 형식은 { text: string, vector: number[] }[]의 단순 배열입니다. 로드할 때는 Blob URL에서 fetch → JSON.parse하면 됩니다.
코사인 유사도 직접 구현 — 15줄
코사인 유사도는 두 벡터가 "같은 방향을 가리키는 정도"입니다. 값은 -1~1이며, 의미가 유사할수록 1에 가깝습니다.
function cosine(a: number[], b: number[]): number {
let dot = 0, normA = 0, normB = 0;
for (let i = 0; i < a.length; i++) {
dot += a[i] * b[i];
normA += a[i] * a[i];
normB += b[i] * b[i];
}
return dot / (Math.sqrt(normA) * Math.sqrt(normB));
}
export function searchTopK(
queryVec: number[],
records: { text: string; vector: number[] }[],
k = 3,
threshold = 0.65
) {
return records
.map(r => ({ text: r.text, score: cosine(queryVec, r.vector) }))
.filter(r => r.score >= threshold)
.sort((a, b) => b.score - a.score)
.slice(0, k);
}
임계값 0.65: 이 값 미만의 청크는 질문과 무관한 것으로 간주해 제거합니다. 너무 낮으면 관련 없는 청크가 컨텍스트에 끼어들고, 너무 높으면 답변 가능한 질문도 "정보 없음"으로 처리됩니다. 0.65는 실험적으로 검증된 기본값입니다.
환각 차단 — 시스템 프롬프트 설계
검색 결과가 있어도, LLM은 자신의 사전 지식을 섞어 답변하려 합니다. 이를 막으려면 시스템 프롬프트로 근거 강제를 명시해야 합니다.
시스템 프롬프트 (temperature 0.2 사용):
당신은 주어진 [CONTEXT] 안의 정보만을 사용해 질문에 답합니다.
답변의 모든 사실 주장에는 반드시 [1], [2] 형식으로 출처 번호를 인용하세요.
[CONTEXT]에 없는 내용은 "제공된 문서에서 해당 정보를 찾을 수 없습니다."라고만 답하세요.
절대로 추측하거나 외부 지식을 사용하지 마세요.
경고: temperature를 0.2로 낮추는 것만으로는 환각을 막을 수 없습니다. 반드시 시스템 프롬프트에 "컨텍스트 외 답변 금지" 조항을 명시적으로 포함하세요.
Faithfulness 측정
RAG 시스템의 품질은 Faithfulness(생성된 답변이 검색된 청크에 얼마나 충실한가)로 평가합니다. Ground Truth 5건을 준비하고 다음 기준으로 측정합니다:
| 항목 | 측정 방법 |
|---|---|
| 출처 인용 여부 | 답변에 [숫자] 패턴 포함 여부 |
| 근거 청크 일치 | 인용된 청크 텍스트가 실제 검색 결과에 있는지 |
| 헛소리(hallucination) | 컨텍스트에 없는 사실 주장 건수 |
| 거절 정확도 | 답할 수 없는 질문에 "정보 없음" 응답 비율 |
5건 기준으로 Faithfulness ≥ 80%를 목표로 합니다.
실습 (Lab)
목표: 본인 노트/문서를 업로드해 검색되는 RAG Q&A 시스템 구동
Step 1 — 문서 업로드 API 구현
POST /api/ingest엔드포인트 작성formData로.txt또는.md파일 수신chunkPlainText()로 청크 분리 →embedAndStore()로 임베딩 후 Blob 저장
Step 2 — 검색 API 구현
POST /api/rag-query엔드포인트 작성- 쿼리 텍스트를
text-embedding-3-small로 임베딩 - Blob에서 해당 문서의 벡터 JSON 로드 →
searchTopK()실행 - 임계값 미달 시 "관련 정보 없음" 즉시 반환
Step 3 — LLM 연결
callLLM("anthropic/claude-sonnet-4-5", messages, { temperature: 0.2 })- 시스템 프롬프트에 환각 차단 지시 +
[CONTEXT]블록 주입 - 답변에서
[1][2]인용 패턴 파싱해 프론트엔드에 출처 표시
Step 4 — Faithfulness 검증
- 본인 문서에서 답 가능한 질문 3개, 답 불가능한 질문 2개 준비
- 5건 모두 실행 후 인용 여부·거절 정확도 직접 기록
체크포인트:
searchTopK()가 빈 배열을 반환할 때 LLM을 호출하지 않고 즉시 "정보 없음"을 반환하는 분기가 구현되어 있어야 합니다. 이 분기가 없으면 빈 컨텍스트로 LLM이 호출되어 환각이 발생합니다.
핵심 정리
chunkPlainText(600, 100)— 600자 청크, 100자 overlap으로 경계 손실 방지text-embedding-3-small1536차원 벡터를 Vercel Blob JSON에 저장, vector DB 불필요- 코사인 유사도 15줄 직접 구현 +
score ≥ 0.65임계값으로 노이즈 청크 차단 - 시스템 프롬프트에 "컨텍스트 외 금지 +
[1][2]인용 의무" 명시해야 환각 차단 완성 temperature 0.2— 결정적 답변 생성, 추측성 표현 억제- Ground Truth 5건 Faithfulness 측정으로 시스템 신뢰도 수치화
본인 노트/문서 업로드 → 검색되는 RAG Q&A 시스템