로컬 RAG 구성하기 (청크, 임베딩, 검색 품질 확인)
로컬 RAG 구성은 내 문서를 조각(청크)으로 나눠 내 컴퓨터의 임베딩 모델로 벡터를 만들고, 질문과 가까운 조각을 찾아 로컬 모델의 답에 붙이는 일을 모두 내 기기 안에서 하는 방식입니다. 만든 뒤에는 정답 조각이 검색 상위에 나오는지 질문 세트로 확인합니다.
같은 말:로컬 RAG올라마 임베딩ollama embed로컬 벡터 DBRAG 청크 크기
목차
🤔 회사 문서를 밖으로 보내지 않고 AI에게 묻고 싶을 때
사내 규정집, 회의록, 고객 문의 기록을 AI에게 넣고 질문하고 싶은데 외부 서비스에 올리기는 부담스러운 경우가 있습니다. 로컬 모델만으로는 이 문서들을 모르니 엉뚱한 답을 하고, 문서를 통째로 대화창에 붙이기에는 너무 깁니다. 이럴 때 필요한 방식이 RAG이고, 이 과정을 전부 내 컴퓨터 안에서 하는 것이 로컬 RAG입니다.
RAG가 무엇이고 파인튜닝과 어떻게 다른지는 RAG 뜻에서 다뤘습니다. 지금부터는 실제로 올라마와 벡터 저장소를 엮어 로컬 RAG를 만드는 순서, 임베딩 모델과 청크 크기를 고르는 기준, 만든 뒤 검색이 제대로 되는지 측정하는 방법을 정리합니다. 도구 이름과 기본값은 2026년 9월 28일 공식 문서 기준입니다.
🔑 로컬 RAG 구성의 정의
로컬 RAG 구성은 내 문서를 조각(청크)으로 나눠 내 컴퓨터의 임베딩 모델로 벡터를 만들고, 질문과 가까운 조각을 찾아 로컬 모델의 답에 붙이는 일을 모두 내 기기 안에서 하는 방식입니다.
임베딩은 글의 뜻을 숫자 목록(벡터)으로 바꾸는 작업입니다. 뜻이 비슷한 글은 비슷한 숫자 목록이 되므로, 질문을 같은 방식으로 바꿔 가장 가까운 조각을 찾을 수 있습니다. 로컬 RAG에는 세 가지 부품이 필요합니다.
- 임베딩 모델: 글을 벡터로 바꿉니다. 올라마로 내려받아 씁니다
- 벡터 저장소: 벡터와 원래 문장을 함께 저장하고, 가까운 것을 찾아 줍니다. 여기서는 Chroma를 예로 듭니다
- 답을 쓰는 모델: 찾아 온 조각을 근거로 답을 씁니다. 올라마의 일반 대화 모델입니다
🧮 올라마로 임베딩 만들기
올라마는 /api/embed 경로로 임베딩을 만듭니다. 공식 예시는 이렇습니다.
curl http://localhost:11434/api/embed -d '{"model": "embeddinggemma", "input": "Why is the sky blue?"}'input에 문장 목록을 넣으면 한 번에 여러 개를 처리하고, 응답의 embeddings에 벡터 목록이 들어옵니다. 올라마 공식 문서가 짚는 원칙은 두 가지입니다. 의미 검색에는 대부분 코사인 유사도를 쓰고, 색인할 때와 질문할 때 같은 임베딩 모델을 씁니다. 모델을 바꾸면 벡터 공간이 달라져 이미 만든 색인을 전부 다시 만들어야 합니다.
2026년 9월 28일 올라마 라이브러리 기준으로 자주 쓰는 임베딩 모델은 다음과 같습니다.
| 모델 | 크기 | 한 번에 넣을 수 있는 길이 | 특징 |
|---|---|---|---|
embeddinggemma | 622MB | 2K | 구글, 100개 이상 언어로 학습 |
qwen3-embedding | 0.6b 639MB부터 8b 4.7GB | 32K에서 40K | 100개 이상 언어, 출력 차원 조정 가능 |
bge-m3 | 1.2GB | 8K | 100개 이상 언어, 최대 8,192토큰 입력 |
nomic-embed-text | 274MB | 2K | 가볍고 오래 쓰인 모델 |
한국어 문서라면 여러 언어로 학습한 모델에서 고릅니다. 다만 한국어 검색 성능을 모델끼리 비교한 공식 자료는 확인하지 못했으므로, 아래 품질 확인 방법으로 내 문서에서 직접 비교하는 편이 정확합니다.
🧱 저장하고 찾아서 답하는 코드
올라마 파이썬 라이브러리와 Chroma로 짠 최소 구성입니다(pip install ollama chromadb). 올라마 공식 블로그의 RAG 예시를 현재 API 형식에 맞게 고친 것이고, 준이아빠블로그가 실행해 본 코드는 아닙니다.
import ollama
import chromadb
db = chromadb.PersistentClient(path="./rag-db") # 디스크에 저장
col = db.get_or_create_collection(name="docs")
chunks = ["환불은 구매 후 14일 안에 신청합니다.", "배송은 영업일 기준 3일이 걸립니다."]
emb = ollama.embed(model="embeddinggemma", input=chunks) # 여러 청크를 한 번에
col.upsert(ids=[f"c{i}" for i in range(len(chunks))],
embeddings=emb["embeddings"], documents=chunks)
question = "환불은 언제까지 되나요?"
q = ollama.embed(model="embeddinggemma", input=question) # 질문도 같은 모델로
hits = col.query(query_embeddings=q["embeddings"], n_results=2)
context = "\n".join(hits["documents"][0])
answer = ollama.chat(model="llama3.2", messages=[{"role": "user",
"content": f"다음 자료만 근거로 답해 줘. 자료에 없으면 모른다고 해.\n\n{context}\n\n질문: {question}"}])
print(answer["message"]["content"])Chroma는 PersistentClient를 쓰면 지정한 폴더에 저장되고, 여러 번 실행할 때는 get_or_create_collection과 upsert를 권합니다. 레코드 하나의 문서 크기는 16KB로 제한됩니다. 답을 쓰는 모델의 컨텍스트가 짧으면 찾아 온 조각이 잘리므로, 로컬 모델 서빙에서 다룬 컨텍스트 길이 설정도 함께 봅니다.
코드를 짜지 않고 쓰려면 Open WebUI 같은 화면 도구도 있습니다. Workspace의 Knowledge에서 지식 베이스를 만들고 파일을 올린 뒤, 채팅에서 #로 불러 씁니다. 기본값은 청크 1,000자, 겹침 100자, 가져오는 조각 3개입니다.
✂️ 청크를 나누는 기준
청크 크기는 검색 품질을 가장 크게 바꾸는 설정입니다. Chroma 공식 가이드는 청크가 질문과 정확히 맞을 만큼 작아야 하고, 혼자 읽어도 뜻이 통할 만큼 커야 한다고 설명합니다. "기본값은 30초"라는 문장이 "연결 타임아웃"을 설명하는 문장과 다른 청크로 떨어지면, 타임아웃을 물었을 때 30초라는 답을 찾지 못하는 식입니다.
- 시작점: 대부분의 글은 문단과 줄바꿈을 기준으로 자르는 재귀 분할에 약간의 겹침을 두고 시작합니다. Chroma 예시는 500자에 겹침 50자, Open WebUI 기본값은 1,000자에 겹침 100자로 도구마다 출발점이 다릅니다
- 구조가 있는 문서: 마크다운 제목이나 표, 코드처럼 구조가 있으면 그 구조를 기준으로 자릅니다
- 모델 한도: 임베딩 모델이 한 번에 받는 길이를 넘는 청크는 기본 설정(
truncate)에서 오류 없이 뒷부분이 잘립니다. embeddinggemma라면 2K 토큰 아래로 잡습니다
📏 검색 품질을 확인하는 방법
RAG가 엉뚱한 답을 할 때 원인은 답을 쓰는 모델보다 검색 단계에 있는 경우가 많습니다. 그래서 답을 보기 전에 검색이 맞는 조각을 가져오는지부터 따로 측정합니다. Chroma 가이드의 방법을 옮기면 이렇습니다.
- 실제로 받을 질문을 20~30개 적습니다
- 질문마다 나와야 할 청크의 ID를 적어 둡니다
- 질문을 넣고 상위 k개(예: 10개)에 정답 청크가 들어오는 비율을 계산합니다. 이것이 Recall@k입니다
- 첫 정답 청크가 평균 몇 번째에 나오는지도 봅니다. 앞에 나올수록 높은 MRR입니다
결과에 따라 조정하는 방향도 가이드에 정리되어 있습니다.
| 증상 | 조정 |
|---|---|
| 정답 청크가 아예 안 나옴 (재현율 낮음) | 청크를 더 작게, 겹침을 늘림 |
| 정답은 나오는데 순위가 낮음 | 청크에 제목 같은 맥락을 붙이고 메타데이터로 거름 |
| 같은 내용이 여러 번 나옴 | 겹침을 줄임 |
| 엉뚱한 조각이 나옴 | 청크를 키우거나 구조 기준으로 자름 |
제품 번호나 약 이름처럼 글자가 정확히 같아야 하는 질문은 뜻으로 찾는 벡터 검색이 놓치기 쉽다고 가이드가 적습니다. 이런 질문이 많다면 단어 일치 검색을 섞는 하이브리드 검색을 검토합니다.
⚠️ 자주 하는 실수
- 질문과 색인에 다른 임베딩 모델을 씁니다: 벡터 공간이 달라 검색이 되지 않습니다
- 임베딩 모델을 바꾸고 색인을 그대로 둡니다: 전부 다시 만들어야 합니다
- 가져오는 개수만 늘립니다: 토큰이 늘고 관계없는 조각이 섞여 답이 흐려집니다
- 답을 쓰는 모델의 컨텍스트를 기본값으로 둡니다: 올라마는 GPU 메모리 24GiB 미만에서 기본 4K라서, Open WebUI 문서도 8,192 이상으로 올리라고 안내합니다
❓ 자주 묻는 질문
문서가 몇 개부터 RAG가 필요한가요?
공식 기준은 없습니다. 문서 전체가 답을 쓰는 모델의 컨텍스트에 넉넉히 들어간다면 통째로 넣는 편이 간단하고, 그보다 많거나 자주 바뀐다면 RAG가 맞습니다. 판단 순서는 RAG 뜻에 정리했습니다.
파인튜닝으로 문서 내용을 모델에 넣으면 안 되나요?
자주 바뀌는 자료나 출처를 보여 줘야 하는 자료는 RAG가 맞습니다. 파인튜닝이 지식을 넣을 수 있는지는 자료마다 입장이 다른데, 둘의 쓰임새 차이는 파인튜닝 기초에서 이어서 다룹니다.
벡터 저장소는 Chroma만 써야 하나요?
아닙니다. Chroma는 설치가 간단해 예로 들었을 뿐이고, 같은 역할을 하는 도구가 여럿 있습니다. 어떤 도구를 쓰든 임베딩 모델을 고정하고 품질 확인 절차를 두는 원칙은 같습니다.
📋 3줄 요약
-
올라마는 /api/embed로 문서 조각을 벡터로 바꾸고, 공식 문서는 색인할 때와 질문할 때 같은 임베딩 모델을 쓰고 코사인 유사도로 비교하라고 안내합니다.
-
임베딩 모델의 컨텍스트를 넘는 청크는 기본 설정에서 오류 없이 잘리므로, 청크 크기는 embeddinggemma 2K나 bge-m3 8K 같은 모델 한도 아래로 잡습니다.
-
검색 품질은 실제 질문마다 나와야 할 청크를 정해 두고 Recall@k와 MRR로 측정하며, 재현율이 낮으면 청크를 줄이고 겹침을 늘립니다.

제대로 이해했는지 한 문제로 확인해 볼까요?
답을 고르면 바로 풀이가 나와요.
로컬 RAG의 검색 품질을 시험했더니 정답 청크가 상위 10개 안에 들어오는 비율(Recall@10)이 낮게 나왔습니다. Chroma 공식 가이드가 먼저 권하는 조정은 무엇일까요?

