왜 범용 임베딩은 도메인에서 무너지는가
OpenAI, BGE, E5 같은 범용 임베딩 모델은 웹 전반에서 학습되어 일반적인 의미 유사도는 잘 잡는다. 하지만 사내 문서 검색이나 제품 QA처럼 도메인 어휘가 지배적인 환경에서는 정확도가 급격히 떨어진다. "장애 등급 P1"과 "긴급 인시던트"가 같은 뜻인데 벡터 공간에서 멀리 떨어져 있거나, 약어·제품코드·내부 용어가 모두 비슷비슷한 지점으로 뭉쳐버리는 식이다. 결과적으로 Top-K 검색에서 정답 문서가 5위 밖으로 밀려나고, RAG 파이프라인의 답변 품질이 통째로 무너진다.
먼저 파인튜닝이 아닌 것부터 배제하기
임베딩 파인튜닝은 비용이 든다. 대부분의 정확도 문제는 청킹 전략, 하이브리드 검색(BM25+벡터), 리랭커 도입만으로 해결된다. 아래 순서로 병목을 먼저 확인하자.
- 청크가 너무 크거나 문맥이 끊기지 않았는가 (256~512 토큰 권장)
- BM25 키워드 검색과 앙상블했는가 (약어·코드는 렉시컬이 강함)
- cross-encoder 리랭커로 Top-50을 재정렬했는가
이걸 다 했는데도 Recall@10이 낮다면, 그때 임베딩 자체가 도메인 의미를 못 담는 것이므로 파인튜닝이 정당화된다.
학습 데이터: (질의, 정답, 오답) 삼중항 만들기
가장 현실적인 접근은 대조학습(contrastive learning)용 삼중항이다. 라벨링 예산이 없다면 검색 로그를 활용한다. 클릭된 문서를 positive, 같은 세션에서 노출됐지만 클릭 안 된 문서를 hard negative로 삼는다. 로그가 없으면 LLM으로 각 문서에서 질의를 역생성한다.
-- 검색 로그에서 학습쌍 추출 (클릭=positive)
SELECT
s.query AS query,
MAX(CASE WHEN e.clicked THEN e.doc_id END) AS positive_id,
ARRAY_AGG(CASE WHEN NOT e.clicked THEN e.doc_id END
ORDER BY e.rank LIMIT 3) AS hard_negatives
FROM search_sessions s
JOIN search_events e ON e.session_id = s.id
GROUP BY s.id, s.query
HAVING COUNT(*) FILTER (WHERE e.clicked) = 1;
hard negative가 핵심이다. 랜덤 오답만 쓰면 모델이 쉬운 구분만 배우고 실제 헷갈리는 케이스를 못 잡는다.
sentence-transformers로 파인튜닝하기
base 모델은 다국어라면 BGE-m3나 multilingual-e5-large가 무난하다. MultipleNegativesRankingLoss는 배치 내 다른 샘플을 자동으로 negative로 써서 데이터 효율이 좋다.
from sentence_transformers import (
SentenceTransformer, losses, InputExample
)
from torch.utils.data import DataLoader
model = SentenceTransformer("intfloat/multilingual-e5-large")
# e5 계열은 프리픽스 규칙을 반드시 지켜야 함
examples = [
InputExample(texts=[f"query: {q}", f"passage: {pos}", f"passage: {neg}"])
for q, pos, neg in triplets
]
loader = DataLoader(examples, shuffle=True, batch_size=32)
loss = losses.MultipleNegativesRankingLoss(model)
model.fit(
train_objectives=[(loader, loss)],
epochs=3,
warmup_steps=int(len(loader) * 0.1),
use_amp=True, # 메모리 절약
output_path="./e5-domain",
)
배치 크기가 클수록 in-batch negative가 많아져 성능이 오른다. GPU 메모리가 부족하면 gradient checkpointing이나 매칭 손실 대신 더 작은 base 모델을 고려한다.
평가: 오프라인 지표로 회귀 방지
"체감상 좋아졌다"로 배포하면 안 된다. 홀드아웃 질의셋으로 Recall@K와 nDCG@10을 파인튜닝 전후로 비교하고, 범용 지표(MTEB류)도 같이 봐서 도메인 밖 성능이 무너지지 않았는지 확인한다.
| 모델 | Recall@10 | nDCG@10 | 비고 |
|---|---|---|---|
| e5-large (base) | 0.61 | 0.48 | 범용 |
| e5-large (finetuned) | 0.87 | 0.74 | 도메인 삼중항 3epoch |
| + cross-encoder 리랭크 | 0.87 | 0.81 | Top-50 재정렬 |
운영 시 주의점
파인튜닝된 모델을 배포하면 기존 벡터 인덱스를 전부 재생성해야 한다. 임베딩 공간이 바뀌었기 때문에 구 벡터와 신 벡터를 섞으면 검색이 완전히 깨진다. 인덱스 버전을 태깅하고 블루-그린으로 전환하자. 또한 e5 계열의 query/passage 프리픽스를 학습 때와 추론 때 동일하게 맞추지 않으면 정확도가 크게 떨어지니 서빙 코드에서 반드시 검증한다.
재학습 주기
도메인 어휘는 계속 변한다. 신제품·신규 약어가 늘면 성능이 서서히 저하되므로, 검색 로그 기반 지표를 대시보드로 모니터링하고 nDCG가 임계치 아래로 떨어지면 최신 로그로 재학습하는 파이프라인을 갖추는 것이 장기적으로 안정적이다. 분기 단위 재학습이 대부분의 조직에 현실적인 균형점이다.