'RAG 만들었는데, 답변이 뭔가 이상해요.' 1편을 읽고 RAG 프로토타입을 만들어본 분들이라면 한 번쯤 느꼈을 겁니다. 분명 관련 문서가 있는데 엉뚱한 걸 가져오거나, 검색은 잘 됐는데 답변이 횡설수설하거나. 심지어 속도도 생각보다 느리고요.
1편에서 RAG의 큰 그림을 그렸다면, 이제는 '그래서 뭘 써야 하는데?'라는 질문에 답할 차례입니다. Vector DB, 임베딩 모델, 청킹 전략... 선택지가 많아서 막막하죠. 오늘은 각각이 뭔지, 언제 뭘 쓰면 되는지 실무 관점에서 정리해볼게요.
복잡한 수학이나 벤치마크 점수는 최소화하고, '내 상황에서 뭘 선택해야 할까'에 집중했습니다. TypeScript로 2년 정도 개발해본 분이라면 충분히 따라올 수 있을 거예요.
Vector DB: 뭘 써야 하지?
먼저 Vector DB가 뭔지 간단히 짚고 갈게요. 일반 데이터베이스가 '이름이 홍길동인 사람 찾아줘'라면, Vector DB는 '이 문장이랑 비슷한 문장 찾아줘'입니다. 텍스트를 숫자 배열(벡터)로 변환해서 저장해두고, 비슷한 숫자 패턴을 가진 것들을 찾아주는 거예요. 도서관 사서가 '이 책이랑 비슷한 책 추천해주세요'라는 요청을 처리하는 것과 비슷하다고 생각하면 됩니다.
문제는 선택지가 너무 많다는 거예요. Pinecone, Qdrant, Weaviate, Chroma, pgvector... 뭘 써야 할까요? 결론부터 말하면, 정답은 없고 상황에 따라 다릅니다. 하지만 '이럴 때 이거' 하는 가이드라인은 있어요.
실무에서 많이 쓰는 Vector DB들
Pinecone은 '그냥 돈 내고 편하게 쓸래' 할 때 선택합니다. 서버 관리 없이 API만 호출하면 돼요. 다만 비용이 꽤 나가고, 데이터가 외부 클라우드에 저장되는 게 부담스러운 조직도 있어요. 빠르게 프로덕션에 올려야 할 때 좋습니다.
Qdrant는 성능이 좋고 오픈소스예요. '우리가 직접 운영할게, 대신 비용 아끼자' 할 때 선택합니다. 클라우드 버전도 있어서, 처음엔 클라우드로 시작하고 나중에 셀프호스팅으로 넘어가는 것도 가능해요. 필터링 기능이 강력해서 '마케팅팀 문서 중에서만 검색' 같은 조건을 걸기 좋습니다.
Chroma는 프로토타입용입니다. pip install chromadb 하고 바로 쓸 수 있어요. LangChain 튜토리얼 대부분이 Chroma를 예시로 쓰는 이유가 있죠. 다만 문서가 수만 건을 넘어가면 느려지기 시작해서, 프로덕션에는 다른 걸 권장합니다.
pgvector는 이미 PostgreSQL을 쓰고 있는 팀에게 최고의 선택입니다. 새로운 DB를 배울 필요 없이 SQL로 벡터 검색을 할 수 있거든요. SELECT * FROM documents ORDER BY embedding <-> query_embedding LIMIT 5 이런 식으로요. 수백만 건까지는 충분히 버팁니다.
처음이라면 Chroma나 pgvector로 시작하세요. 프로토타입 만들어보고, 규모가 커지면 Pinecone이나 Qdrant로 옮기면 됩니다. 어차피 임베딩만 다시 생성하면 되니까, 처음부터 완벽한 선택을 할 필요 없어요.
한 장으로 보는 선택 가이드
복잡하게 생각할 것 없이, 이렇게 정리하면 됩니다.
- "일단 빨리 만들어보고 싶어" → Chroma. 설치 5분이면 끝나요.
- "우리 이미 PostgreSQL 쓰는데" → pgvector. 새로 배울 게 없어요.
- "돈 좀 써도 되니까 편하게 하고 싶어" → Pinecone. 서버 관리 없이 API만 호출.
- "비용은 아끼고 싶은데 프로덕션 수준은 필요해" → Qdrant. 오픈소스인데 성능 좋아요.
문서 수로 따지면, 수만 건 이하면 Chroma/pgvector로 충분하고, 수십만~수백만 건이면 Pinecone/Qdrant를 고려하세요. 진짜 억 단위로 가면 그때는 Milvus 같은 분산 시스템이 필요한데, 거기까지 가는 프로젝트는 드물어요.
임베딩 모델: 텍스트를 숫자로 바꾸는 번역기
임베딩 모델은 텍스트를 숫자 배열(벡터)로 바꿔주는 '번역기'예요. 한국어를 영어로 번역하듯, '연차 휴가 정책'이라는 문장을 [0.23, -0.87, 0.45, ...]처럼 1536개의 숫자로 변환하는 거죠. 이 숫자들이 문장의 '의미'를 담고 있어서, 비슷한 의미의 문장은 비슷한 숫자 패턴을 갖게 됩니다.
중요한 건, 어떤 번역기를 쓰느냐에 따라 결과가 달라진다는 거예요. 어떤 모델은 '연차'와 '휴가'를 비슷하다고 인식하고, 어떤 모델은 다르다고 인식해요. 한국어를 잘 이해하는 모델도 있고, 영어에 최적화된 모델도 있고요.
실무에서 많이 쓰는 임베딩 모델들
OpenAI text-embedding-3-small이 가장 무난한 시작점입니다. API 호출 한 번으로 끝나고, 한국어도 꽤 잘 처리해요. 대부분의 프로젝트에서 이걸로 충분합니다. 더 정확도가 필요하면 large 버전도 있는데, 비용이 더 나가고 속도도 느려져요.
한국어가 중요하다면 Cohere를 고려해보세요. 100개 이상 언어를 지원하는데, 다국어 성능이 OpenAI보다 좋다는 평가가 많아요. 가격도 비슷한 수준이고요.
비용이 부담되거나 데이터 외부 전송이 꺼려진다면 오픈소스 모델을 로컬에서 돌릴 수 있어요. BGE-M3가 인기 있는데, 직접 서버를 운영해야 하니까 초보자에게는 추천하지 않습니다. 나중에 규모가 커지면 그때 고려해도 늦지 않아요.
벤치마크 점수보다 실제로 써보는 게 중요해요. '연차 신청'이라고 검색했을 때 '휴가 요청 방법'도 찾아지는지, '연봉 협상'은 안 찾아지는지 직접 테스트해보세요. 우리 데이터에서 어떻게 동작하는지는 벤치마크가 알려주지 않거든요.
임베딩 모델 선택 정리
- 처음 시작한다면 → OpenAI text-embedding-3-small. 제일 쉬워요.
- 한국어가 중요하다면 → Cohere 테스트해보세요. 다국어 성능이 좋아요.
- 민감한 데이터라 외부로 보내기 싫다면 → 오픈소스 모델 로컬 운영. 다만 난이도 있어요.
- 비용이 걱정된다면 → 일단 시작은 OpenAI로. 문서 10만 건 임베딩해도 몇 달러 수준이에요.
청킹: 문서를 어떻게 쪼갤까?
RAG가 이상하게 동작할 때, 대부분 LLM 탓을 하는데요. 실제로는 청킹이 문제인 경우가 훨씬 많아요. 청킹은 긴 문서를 검색하기 좋은 크기로 쪼개는 과정입니다.
책을 생각해보세요. 누군가 '해리포터에서 볼드모트가 처음 등장하는 장면이 뭐야?'라고 물으면, 책 전체를 주는 것보다 해당 챕터만 주는 게 낫겠죠. 근데 문장 한 줄만 주면 맥락이 없어서 이해가 안 될 거예요. 적당한 크기로 쪼개는 게 핵심입니다.
쪼개는 방법 세 가지
1. 무조건 500자마다 자르기 (고정 크기): 가장 단순해요. 근데 문장 중간에서 잘릴 수 있어서 '연차는 15'에서 끊기고 '일입니다'가 다음 청크로 가버리는 문제가 생겨요. 비추천.
2. 문단 → 문장 순으로 나누기 (재귀적): 먼저 빈 줄로 나누고, 그래도 크면 문장으로 나누고. 문서 구조를 유지하면서 적당한 크기로 쪼갤 수 있어요. LangChain의 RecursiveCharacterTextSplitter가 이 방식입니다. 대부분의 경우 이걸로 충분해요.
3. 의미가 바뀌는 지점에서 자르기 (시맨틱): 가장 똑똑한 방법이에요. 문장들을 분석해서 '여기서 주제가 바뀌네' 하는 지점에서 나눕니다. 정확도는 좋지만 구현이 복잡하고 느려요. 검색 품질이 정말 중요한 프로덕션에서 고려해볼 만합니다.
처음에는 RecursiveCharacterTextSplitter로 시작하세요. 이것만으로도 80%는 해결됩니다. 검색 결과가 이상할 때 청킹 전략을 바꿔보면 돼요.
얼마나 크게 쪼갤까?
청크 크기는 '이 조각 하나만 봐도 질문에 답할 수 있나?'를 기준으로 정하면 됩니다. 너무 작으면 '15일입니다'만 달랑 있어서 뭐가 15일인지 모르고, 너무 크면 관련 없는 내용까지 섞여서 LLM이 헷갈려해요.
- 짧은 답이 필요한 질문 (연차 며칠이야?): 300~500자 정도면 충분.
- 맥락이 필요한 질문 (우리 휴가 정책 뭐가 문제야?): 800~1000자 이상 필요.
- 모르겠으면: 500자로 시작해서 조정하세요. 대부분 여기서 시작해요.
오버랩도 알아두면 좋아요. 청크 사이에 약간 겹치는 부분을 두는 건데, 문맥이 끊기는 걸 방지해줍니다. 청크 크기의 10~20% 정도, 예를 들어 500자 청크면 50~100자 정도 겹치게 하면 돼요.
실제 코드로 보기
import { RecursiveCharacterTextSplitter } from "langchain/text_splitter";
// RecursiveCharacterTextSplitter 설정
// 이게 제일 많이 쓰이는 방식이에요
const splitter = new RecursiveCharacterTextSplitter({
chunkSize: 500, // 한 조각당 최대 500자
chunkOverlap: 50, // 조각 사이에 50자 겹침 (문맥 유지용)
// 이 순서대로 나누기를 시도해요
// 1. 먼저 빈 줄(문단)로 나눠보고
// 2. 그래도 크면 줄바꿈으로 나눠보고
// 3. 그래도 크면 마침표로 나눠보고...
separators: ["\n\n", "\n", ".", " ", ""],
});
// 예시 문서
const document = `
# 연차 휴가 정책
## 1. 기본 연차
입사 1년 미만 직원은 월 1일의 연차가 발생합니다.
1년 이상 근무 시 15일의 연차가 부여됩니다.
## 2. 연차 사용
연차는 최소 하루 단위로 사용 가능합니다.
반차(오전/오후)도 0.5일로 사용할 수 있습니다.
`;
// 문서를 청크로 나누기
const chunks = await splitter.splitText(document);
// 결과: 각 청크가 섹션 구조를 유지하면서 500자 이하로 나뉨
// chunks[0]: "# 연차 휴가 정책\n\n## 1. 기본 연차\n..."핵심은 separators 배열이에요. 빈 줄 → 줄바꿈 → 마침표 → 공백 순으로 나누기를 시도하니까, 문단이 유지되고 문장 중간에서 잘리는 일이 줄어들어요.
프롬프트: LLM에게 문서 잘 전달하기
문서를 찾았으면 이제 LLM에게 전달해야 해요. 그냥 '이거 보고 답해'라고 하면 될 것 같지만, 어떻게 전달하느냐에 따라 답변 품질이 꽤 달라집니다.
기본 프롬프트 템플릿
// RAG용 프롬프트 템플릿
// context: 검색해서 찾은 문서들
// question: 사용자가 물어본 질문
const ragPrompt = (context: string, question: string) => `
당신은 회사 내부 문서를 기반으로 질문에 답하는 도우미입니다.
## 지침
- 아래 제공된 문서만을 참고하여 답변하세요.
- 문서에 없는 내용은 추측하지 말고, "문서에서 해당 정보를 찾을 수 없습니다"라고 답하세요.
- 답변에 사용한 정보의 출처(문서명 또는 섹션)를 함께 언급하세요.
- 간결하고 명확하게 답변하세요.
## 참고 문서
${context}
## 질문
${question}
## 답변
`;
// 사용 예시
const answer = await llm.invoke(
ragPrompt(searchedDocuments, "연차 신청은 어떻게 해요?")
);포인트가 세 가지예요. '문서만 참고해'라고 해서 LLM이 자기 맘대로 지어내는 걸 막고, '모르면 모른다고 해'라고 해서 그럴듯한 거짓말을 방지하고, '출처를 밝혀'라고 해서 나중에 확인할 수 있게 합니다.
프롬프트 팁 몇 가지
- 문서 3~5개로 시작: 너무 많이 주면 LLM이 헷갈려해요. 적게 시작해서 필요하면 늘리세요.
- 중요한 문서를 앞에: LLM은 프롬프트 앞쪽과 뒤쪽을 더 잘 기억해요. 가장 관련 있는 문서를 앞에 두세요.
- 역할 부여하기: 'HR 정책 전문가로서 답변해주세요' 같은 역할을 주면 답변 품질이 올라가요.
검색 품질 높이는 팁
기본 RAG를 만들었는데 '뭔가 아쉽다'면, 여기서 차이를 만들 수 있어요. 크게 두 가지 방법이 있습니다.
1. 벡터 + 키워드 같이 쓰기 (하이브리드 검색)
벡터 검색은 '비슷한 의미' 찾기에 좋은데, 정확한 단어 매칭에는 약해요. 'PO-2024-0312' 같은 주문번호를 찾을 때는 키워드 검색이 더 정확하죠. 둘 다 쓰면 둘 다 잡을 수 있어요.
'연차 소진 기한'이라고 검색하면, 벡터 검색은 '휴가 만료일' 같은 유사 표현도 찾고, 키워드 검색은 정확히 '연차', '소진', '기한'이 들어간 문서를 찾아요. 결과를 합치면 더 정확해집니다.
2. 메타데이터로 범위 좁히기
문서 저장할 때 부서, 날짜, 문서 유형 같은 정보를 같이 저장해두면, 검색할 때 범위를 좁힐 수 있어요. 'HR 문서에서만', '올해 것만' 같은 조건을 걸 수 있죠.
// 문서 저장할 때 메타데이터도 같이 저장
await vectorStore.addDocuments([
{
pageContent: "2024년 연차 정책이 변경되었습니다...",
metadata: {
department: "HR", // 어느 부서 문서인지
documentType: "policy", // 정책인지, 가이드인지, 회의록인지
updatedAt: "2024-01-15", // 언제 업데이트됐는지
},
},
]);
// 검색할 때 필터 적용
const results = await vectorStore.similaritySearch(query, {
filter: {
department: "HR", // HR 문서에서만 검색
documentType: "policy", // 정책 문서에서만 검색
},
k: 5, // 상위 5개만
});
// 이렇게 하면 마케팅팀 문서나 회의록은 검색 결과에 안 나와요이렇게 범위를 좁히면 검색 속도도 빨라지고, 관련 없는 문서가 섞여 나오는 것도 막을 수 있어요.
정리: 뭘 선택해야 하지? 한눈에 보기
오늘 내용을 한 장으로 정리하면 이렇습니다.
- Vector DB: 프로토타입은 Chroma, PostgreSQL 쓰고 있으면 pgvector, 프로덕션은 Pinecone이나 Qdrant.
- 임베딩 모델: OpenAI text-embedding-3-small로 시작. 한국어가 이상하면 Cohere 시도.
- 청킹: RecursiveCharacterTextSplitter로 500자, 50자 오버랩. 이걸로 시작해서 조정.
- 프롬프트: 문서만 참고해, 모르면 모른다고 해, 출처 밝혀. 이 세 가지 지침 필수.
- 검색 품질: 키워드 검색 같이 쓰면 좋고, 메타데이터 필터링으로 범위 좁히기.
가장 중요한 건 직접 테스트예요. 벤치마크 점수보다 '우리 문서로 검색했을 때 원하는 결과가 나오는가'가 중요합니다. 설정 바꿔보고, 결과 확인하고, 다시 바꾸고. 이 과정이 RAG 튜닝의 핵심이에요.
처음부터 완벽하게 할 필요 없어요. 오늘 배운 내용 중 한두 가지만 적용해도 체감 차이가 큽니다. Vector DB를 Chroma에서 pgvector로 바꾸거나, 청크 크기를 300에서 500으로 늘리는 것만으로도 검색 품질이 확 달라지기도 해요.
3편에서는 실제로 RAG를 처음부터 끝까지 만들어봅니다. 오늘 배운 개념들을 TypeScript 코드로 구현할 거예요. Vector DB 설정부터 문서 업로드, 검색, 답변 생성까지. 코드 따라 치면서 직접 만들어보는 시간, 기대해주세요.
이 글이 도움이 되셨나요?
RAG 시스템 구축이나 검색 품질 개선이 필요하신가요?
AI 도입에 대해 궁금한 점이 있으시다면 언제든 문의해 주세요. 전문가가 맞춤형 솔루션을 제안해 드립니다.