RAG 챗봇 만들기 5 - 임베딩 이해하기
임베딩 처음 들었을 때 “텍스트를 숫자로 바꾼다”는 게 이해가 안 됐어요. 근데 막상 해보니까 그냥 API 호출 한 번이더라고요. 개념이 어려워 보여도 코드는 간단해요.
오늘은 RAG의 핵심인 임베딩(Embedding)을 배울 거예요. 텍스트를 숫자로 바꿔서 비교할 수 있게 만드는 기술이에요.
임베딩이 뭐예요?
간단한 설명
텍스트를 숫자 배열(벡터)로 바꾸는 거예요.
"연차 휴가 규정" → [0.23, -0.15, 0.87, 0.42, ..., 0.11]
(1536개의 숫자)
왜 숫자로 바꿔요?
컴퓨터는 텍스트를 직접 비교하기 어려워요. 숫자로 바꾸면:
- 유사도 계산 가능: 두 벡터가 얼마나 비슷한지 수학적으로 계산
- 빠른 검색: 수백만 개 중에서 비슷한 것 빨리 찾기
- 의미 반영: 비슷한 의미의 텍스트는 비슷한 벡터가 됨
의미가 반영된다는 게 뭐예요?
"강아지"의 벡터와 "개"의 벡터 → 거의 비슷함 (같은 의미니까)
"강아지"의 벡터와 "자동차"의 벡터 → 많이 다름 (다른 의미니까)
이게 가능한 이유는 임베딩 모델이 엄청난 양의 텍스트로 학습해서 의미를 파악하기 때문이에요.
임베딩 해보기
OpenAI 임베딩 사용
from langchain_openai import OpenAIEmbeddings
# 임베딩 모델 초기화
embeddings = OpenAIEmbeddings(model="text-embedding-ada-002")
# 텍스트를 벡터로 변환
text = "연차 휴가 규정에 대해 알려주세요"
vector = embeddings.embed_query(text)
print(f"벡터 차원: {len(vector)}")
print(f"벡터 일부: {vector[:5]}")
실행 결과:
1536개의 숫자로 된 벡터가 나왔어요!
유사도 계산해보기
두 텍스트가 얼마나 비슷한지 계산해볼게요.
코사인 유사도
벡터 간의 각도로 유사도를 측정해요. 1에 가까울수록 비슷해요.
import numpy as np
from langchain_openai import OpenAIEmbeddings
def cosine_similarity(vec1, vec2):
"""코사인 유사도 계산"""
return np.dot(vec1, vec2) / (np.linalg.norm(vec1) * np.linalg.norm(vec2))
# 임베딩 모델
embeddings = OpenAIEmbeddings()
# 세 개의 텍스트 준비
text1 = "연차 휴가를 신청하고 싶어요"
text2 = "휴가 사용 방법 알려주세요"
text3 = "점심 메뉴 추천해주세요"
# 임베딩
vec1 = embeddings.embed_query(text1)
vec2 = embeddings.embed_query(text2)
vec3 = embeddings.embed_query(text3)
# 유사도 계산
sim_12 = cosine_similarity(vec1, vec2)
sim_13 = cosine_similarity(vec1, vec3)
print(f"'{text1}' vs '{text2}'")
print(f" 유사도: {sim_12:.4f}")
print(f"\n'{text1}' vs '{text3}'")
print(f" 유사도: {sim_13:.4f}")
실행 결과:
휴가 관련 텍스트끼리는 유사도가 높고, 점심 메뉴랑은 낮네요!
여러 문서 한 번에 임베딩
청크들을 한 번에 임베딩할 수 있어요:
from langchain_openai import OpenAIEmbeddings
embeddings = OpenAIEmbeddings()
# 여러 텍스트 준비
texts = [
"연차는 매년 15일 부여됩니다.",
"병가는 연간 60일까지 사용 가능합니다.",
"경조사 휴가는 결혼 5일, 출산 10일입니다."
]
# 한 번에 임베딩
vectors = embeddings.embed_documents(texts)
print(f"임베딩된 문서 수: {len(vectors)}")
print(f"각 벡터 차원: {len(vectors[0])}")
실행 결과:
임베딩 모델 선택
여러 임베딩 모델이 있어요:
OpenAI 모델
from langchain_openai import OpenAIEmbeddings
# 기본 모델 (추천)
embeddings = OpenAIEmbeddings(model="text-embedding-ada-002")
# 최신 모델 (더 좋지만 비쌈)
embeddings = OpenAIEmbeddings(model="text-embedding-3-small")
| 모델 | 차원 | 비용 | 특징 |
|---|---|---|---|
| text-embedding-ada-002 | 1536 | 저렴 | 가성비 좋음 |
| text-embedding-3-small | 1536 | 중간 | 성능 향상 |
| text-embedding-3-large | 3072 | 비쌈 | 최고 성능 |
무료 대안: HuggingFace
from langchain_community.embeddings import HuggingFaceEmbeddings
# 로컬에서 실행 (무료!)
embeddings = HuggingFaceEmbeddings(
model_name="sentence-transformers/all-MiniLM-L6-v2"
)
무료지만 처음 실행할 때 모델 다운로드에 시간이 좀 걸려요.
비용 절감 팁
임베딩 API는 호출할 때마다 비용이 들어요.
배치로 처리
한 번에 여러 텍스트를 보내면 효율적이에요:
# 비효율적 (API 호출 100번)
for text in texts:
vector = embeddings.embed_query(text)
# 효율적 (API 호출 1번)
vectors = embeddings.embed_documents(texts)
캐싱
같은 텍스트를 반복해서 임베딩하지 않도록:
from langchain.embeddings import CacheBackedEmbeddings
from langchain.storage import LocalFileStore
# 캐시 저장소
store = LocalFileStore("./embeddings_cache/")
# 캐시 적용 임베딩
cached_embeddings = CacheBackedEmbeddings.from_bytes_store(
embeddings,
store,
namespace=embeddings.model
)
RAG에서 임베딩이 쓰이는 곳
정리하면 임베딩은 두 군데서 쓰여요:
1. 문서 저장할 때
[문서 청크들] → [임베딩] → [벡터 저장소에 저장]
2. 검색할 때
[사용자 질문] → [임베딩] → [벡터 저장소에서 유사한 것 검색]
질문과 문서를 같은 임베딩 모델로 변환해야 제대로 비교할 수 있어요.
운영자 실전 노트
실제 프로젝트 진행하며 겪은 문제
- 청크당 토큰 제한(8191) 초과 에러 → chunk_size를 줄여서 해결. 임베딩 전 토큰 수 검증 필요
- 1000개 문서 임베딩하며 API 비용 폭발 → 배치 처리(
embed_documents)와 캐싱으로 80% 절감 - 같은 문서 재처리로 중복 비용 발생 → CacheBackedEmbeddings로 로컬 캐시 구축
- 무료 HuggingFace 모델 써봤는데 느림 → 초기 프로토타입에는 괜찮지만 프로덕션에선 OpenAI 권장
이 경험을 통해 알게 된 점
- 임베딩 비용 관리가 핵심. 배치 처리와 캐싱 없이는 비용 부담 큼
- text-embedding-ada-002가 가성비 최고. 3-small/large는 성능 향상 미미한 편
- 캐싱 전략은 필수 — 같은 문서 반복 처리 방지로 비용 크게 절감
마무리
임베딩은 개념은 복잡해 보이지만 실제 구현은 API 호출 한 줄이다. 초반엔 OpenAI 임베딩으로 시작하고, 비용이 부담되면 캐싱과 배치 처리로 최적화한다.
다음 편에서는 임베딩한 벡터를 저장하고 검색하는 “벡터 저장소”를 배운다. 파일 기반으로 간단하게 시작해본다.
RAG 챗봇 만들기 시리즈:
← 블로그 목록으로