localaiforum
로그인
가이드 › 모델 활용

RAG: 모델에게 당신의 데이터 주기

LLM의 지식은 학습 컷오프에 얼어 붙었습니다. 당신의 문서는 매주 바뀝니다. 검색 증강 생성(RAG)이 그것을 녹입니다. 질의 시점에 당신 데이터의 관련 조각을 가져와 프롬프트에 붙여넣고, 모델이 증거로부터 인용과 함께 답하게 하는 것. 모델은 추론을 하고, 색인은 기억을 합니다.

파이프라인, 계산 예제 하나로

12,000 토큰 정비 매뉴얼을 끝까지 데려가 보자:

  1. 청크. 먼저 헤더에서 자릅니다(절이 온전히 살아남습니다). 그다음 ~400 토큰을 넘는 절을 다시 자르되 ~60 토큰(10–15%)의 겹침을 유지합니다. 분별 있는 청크 크기는 200–800 토큰. 결과: 대략 35개 청크.
  2. 임베딩. 각 청크가 벡터 하나가 됩니다 — bge-m3는 청크마다 1,024개 float를 뱉습니다. CPU에서는 매뉴얼 전체가 몇 초입니다.
  3. 색인. 벡터 + 텍스트 + 메타데이터(원본 파일, 헤더 경로)를 벡터 저장소에 넣습니다.
  4. 검색. 질문을 임베딩하고 코사인 유사도 상위 k = 5 청크를 가져옵니다(4–10이 분별 있는 범위).
  5. 생성. 프롬프트를 조립합니다: 지시, ID 태그가 붙은 5개 청크, 질문.

의미 경계에서의 청킹은 고정 창을 이깁니다. 헤더 행에서 잘려 나간 표, 4단계에서 잘린 번호 절차는, 임베더가 아무리 좋아도 회수 불가능한 쓰레기입니다.

즉시 제값을 하는 추가 둘: 필터링과 정밀 인용을 위해 헤더 경로(manual › §4 › Bolts)를 메타데이터로 저장하세요. 그리고 질의를 사용자가 입력한 그대로 임베딩하세요 — "정리" 전처리는 대개 검색을 낫게 만들지 않고 나쁘게 만듭니다.

임베딩: 다국어 요구 사항

임베더가 "유사"의 의미를 결정합니다. 한국어와 한/영 혼합 코퍼스에서는 선택지가 좁습니다 — 영어 전용 임베더는 한국어 질의에서 그럴듯하지만 틀린 청크를 돌려주며 조용히 실패합니다:

모델 차원 비고
bge-m3 1024 다국어, 강한 한국어 — 안전한 로컬 기본값
nomic-embed-text-v1.5 768 탄탄한 영어, 긴 컨텍스트
all-MiniLM-L6-v2 384 빠름, 영어 중심, 한국어에 약함

임베딩 모델은 작습니다(파라미터 0.1–0.6B). CPU 코어 하나가 청크 수천 개를 씹습니다 — CPU 추론. 코퍼스는 한 번 임베딩하고, 질의는 물을 때 임베딩하세요. 색인별로 임베더를 동결하세요: 색인 중간에 모델을 바꾸면 다른 공간의 벡터가 섞이고 유사도 점수가 잡음이 됩니다.

벡터 저장소

저장소 언제
sqlite-vec 1인용, ~100만 벡터까지, 운영 제로 — 그냥 파일입니다
ChromaDB 메타데이터 필터가 있는 로컬 우선 프로토타이핑
Qdrant 운영: 필터링, 하이브리드 검색, 확장

셋 다 로컬에서 돕니다. sqlite-vec으로 시작하세요. 벽에 실제로 부딪힐 때 옮기세요, 그 전이 아니라.

프롬프트 조립

Answer only from the excerpts below. Cite every claim as [n].
If the excerpts do not contain the answer, reply "not in the docs".

[1] (manual §4.2) Torque the M8 bolt to 12 Nm after lubrication...
[2] (manual §7.1) Do not re-use stretch bolts...

Question: what torque spec applies to the M8 bolt?

인용은 사용자가 맹신하는 대신 검증하게 합니다. "문서에 없음" 출구가 중요합니다. 그것이 없으면 모델은 형식에서 실패하는 대신 답을 발명합니다.

하이브리드 검색과 리랭킹

순수 벡터 검색은 키워드 검색이 정확히 잡아내는 문자열 — 에러 코드, 함수 이름, 부품 번호 — 을 놓칩니다. 하이브리드 = BM25 + 벡터, 상호 순위 융합으로 병합. 거의 공짜이고 둘 중 하나만 쓴 것보다 확실히 낫습니다.

품질을 위해서는 리랭커를 더하세요: 후보 20–50개를 검색하고, 크로스 인코더(bge-reranker 패밀리 — bge-m3와 같은 다국어 스토리)로 상위 5개로 재랭킹합니다. 리랭커는 청크와 질의를 함께 읽는데, 이것은 단일 벡터 임베더가 할 수 없는 것입니다. 대부분의 필요를 덮는 값싼 레시피: 하이브리드 25개 검색, 5개로 리랭킹.

진단: 검색 실패인가 생성 실패인가?

뭔가 조정하기 전에 질문 하나에 답하세요. 옳은 청크가 검색되었는가?

  1. 실패한 질의에 대해 상위 k 청크를 점수와 함께 출력하세요.
  2. 답을 담은 청크가 목록에 없으면 검색 문제입니다 — 청킹, 임베딩을 고치거나 k를 올리세요.
  3. 목록에 있는데 답이 틀리면 생성 문제입니다 — 더 큰 모델, 더 나은 프롬프트, 방해물을 밀어내는 리랭킹.

이 테스트 하나가 파이프라인의 잘못된 절반을 조정하는 시간을 아껴줍니다.

실패 양상

증상 원인 해결
옳은 문서가 검색됐는데 답이 틀림 상위 k에 방해물 리랭킹; k 축소
답이 문서에 있는데 표면화되지 않음 상위 k 기근 또는 나쁜 경계 k 상향; 헤더 기준 청킹
자신만만하지만 낡은 답 색인이 문서보다 오래됨 변경 시 재색인
한국어 질의가 영어 문서를 놓침 단일언어 임베딩 bge-m3
인용이 엉뚱한 청크를 가리킴 청크가 출처 메타데이터를 잃음 청크마다 출처 + 헤더 저장

감이 아니라 측정하세요

답을 아는 실제 질문 20–50개의 골든 세트를 유지하세요. 숫자 둘을 따로 매기세요: 검색 적중률(옳은 청크가 상위 k에 있는가)과 답 정확도. 청크 크기, 임베더, k를 바꿀 때마다 추적하세요. 골든 세트 없는 RAG 작업은 점성술입니다.

파인튜닝을 해야 할 때

파인튜닝은 행동 — 형식, 스타일, 용어 — 을 바꿉니다. 사실을 더하는 형편없는 방법입니다. 모델은 튜닝된 것을 인용할 수 없고, 새 문서는 새 학습 실행을 의미합니다. 법칙: 사실 → RAG, 행동 → LoRA, 둘 다는 프롬프팅을 소진한 뒤에만: 파인튜닝.

로컬 스택, 가장 작은 것부터

Open WebUI는 문서 업로드와 하이브리드 검색을 내장하고 배송합니다 — 거기서 시작하면 파이프라인 코드를 절대 안 쓸 수도 있습니다. RAG의 정직한 코어는 ~50줄입니다. 프레임워크가 무엇을 하는지 알게 되도록 한 번은 쓰세요:

chunks = chunk_by_header(read("manual.md"), max_tokens=400, overlap=60)
index = [(embed(c), c) for c in chunks]          # done once

def answer(question):
    hits = topk_by_cosine(embed(question), index, k=5)
    ctx = "\n\n".join("[{}] {}".format(i, c) for i, (v, c) in enumerate(hits))
    prompt = TEMPLATE.replace("{ctx}", ctx).replace("{q}", question)
    return chat(prompt)                           # small local model is plenty

권한, 예약 갱신, 평가 하니스가 필요할 때 LlamaIndex나 LangChain에 손을 뻗으세요 — 위 루프를 위해서가 아니라. 한국어 문서에는 구체적으로, 다국어 임베딩(bge-m3)은 선택이 아니고, 헤더 기반 청킹은 마크다운 ## 헤더를 존중하는 것과 같은 방식으로 한국어 문서 구조(장/절 제목)를 존중해야 합니다.

관련: 파인튜닝 · 프롬프팅 · 구조화 출력 · CPU 추론