- Published on
LangChain RAG 벡터 차원 불일치 에러 해결법
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서론
LangChain으로 RAG를 구성하다 보면 한 번쯤은 dimension mismatch 류의 에러를 만나게 됩니다. 증상은 대개 비슷합니다. 인덱싱(문서 임베딩 저장)은 잘 됐는데, 검색 시점에 갑자기 실패하거나, 반대로 새로 적재하는 순간부터 저장이 안 됩니다.
이 문제는 단순히 임베딩 모델을 바꿔서 생기는 것처럼 보이지만, 실제로는 벡터 DB의 컬렉션 스키마, 인덱스 파라미터, LangChain 래퍼의 설정, 그리고 배포 환경에서의 캐시/상태 꼬임이 함께 얽혀 발생합니다. 특히 운영 환경에서는 “이미 만들어진 컬렉션의 차원은 바꿀 수 없다”는 제약 때문에 즉시 복구가 까다롭습니다.
이 글에서는 다음을 목표로 합니다.
- 차원 불일치가 발생하는 전형적인 패턴을 분류
- 벡터 DB별(Chroma, FAISS, Pinecone, Qdrant)로 어디에서 차원이 고정되는지 설명
- 재현 가능한 코드 예제로 원인 파악
- 무중단에 가까운 마이그레이션 전략(새 컬렉션 생성, 듀얼 라이트, 스위치오버)
- 운영 체크리스트로 재발 방지
벡터 차원 불일치란 무엇인가
임베딩 모델은 텍스트를 길이 d 인 실수 벡터로 변환합니다. 여기서 d 가 “벡터 차원(dimension)”입니다.
- OpenAI
text-embedding-3-small은 1536 차원 - OpenAI
text-embedding-3-large는 3072 차원 sentence-transformers/all-MiniLM-L6-v2는 384 차원
벡터 DB의 컬렉션(또는 인덱스)은 보통 생성 시점에 차원이 고정됩니다. 이후에는 동일 차원의 벡터만 삽입/검색할 수 있습니다. 즉, 기존 컬렉션이 1536 차원으로 만들어졌는데, 쿼리 임베딩이 3072 차원으로 생성되면 검색 단계에서 실패합니다.
대표적인 에러 메시지 패턴은 다음과 같습니다.
InvalidDimensionException: Vector dimension 3072 does not match index dimension 1536expected dim: 1536, got: 3072shapes (3072,) and (1536,) not aligned
가장 흔한 원인 7가지
1) 인덱싱 임베딩 모델과 검색 임베딩 모델이 다름
RAG는 최소 두 번 임베딩을 만듭니다.
- 문서 적재 시점: 문서 임베딩 생성 후 DB에 저장
- 질의 시점: 질문 임베딩 생성 후 유사도 검색
이 둘이 같은 모델이어야 합니다. 팀에서 문서 적재 파이프라인과 API 서버가 분리돼 있으면, 한쪽만 모델을 바꾸고 다른 쪽은 그대로인 일이 잦습니다.
2) 같은 모델 이름이지만 설정이 달라 차원이 달라짐
일부 공급자는 “같은 모델”이라도 옵션에 따라 차원이 달라질 수 있습니다. 예를 들어 특정 SDK는 dimensions 같은 옵션을 제공하기도 합니다. 이 옵션이 인덱싱과 검색에서 다르면 불일치가 납니다.
3) 기존 컬렉션을 재사용하면서 모델만 교체
Chroma/Qdrant/Pinecone 모두 “컬렉션 차원 변경”은 사실상 불가능하거나 권장되지 않습니다. 모델을 바꾸면 새 컬렉션을 만들어야 합니다.
4) 여러 테넌트/환경이 같은 컬렉션을 공유
개발/스테이징/운영이 같은 벡터 DB를 공유하거나, 네임스페이스를 잘못 구성하면 다른 앱이 다른 차원 벡터를 밀어 넣어 컬렉션이 오염됩니다.
5) LangChain VectorStore 인스턴스 생성 코드가 분기되어 있음
예를 들어 API 서버에서는 OpenAIEmbeddings(model=...) 을 쓰고, 배치 인덱서는 HuggingFaceEmbeddings(model_name=...) 을 쓰는 식입니다. 코드 리뷰에서 놓치기 쉽습니다.
6) 캐시/상태 꼬임으로 오래된 인덱스를 참조
배포 후에도 애플리케이션이 이전 컬렉션 이름을 계속 참조하거나, 인덱스 핸들 객체가 재시작 없이 살아있으면 “코드는 바뀌었는데 실제로는 옛 인덱스를 사용”하는 상황이 생깁니다. 이런 류의 상태 꼬임 문제는 RSC 캐시 이슈와도 결이 비슷합니다. 필요하면 Next.js 14 RSC 캐시 꼬임·stale 데이터 해결법도 함께 참고하세요.
7) 벡터 DB 마이그레이션 중 듀얼 라이트/스위치오버가 꼬임
새 컬렉션으로 옮기는 동안 일부 요청은 새 모델, 일부는 옛 모델로 처리되면 차원 불일치가 간헐적으로 발생합니다.
재현 코드: 인덱싱 1536, 검색 3072로 깨뜨리기
아래는 LangChain에서 인덱싱과 검색 임베딩 모델을 다르게 설정해 의도적으로 차원 불일치를 만드는 예시입니다. (실제 실행 시에는 API 키와 설치가 필요합니다.)
from langchain_openai import OpenAIEmbeddings
from langchain_chroma import Chroma
from langchain_core.documents import Document
# 1) 문서 적재: 1536 차원
emb_index = OpenAIEmbeddings(model="text-embedding-3-small")
docs = [
Document(page_content="RAG는 검색과 생성의 결합이다."),
Document(page_content="벡터 DB는 임베딩 차원을 고정한다."),
]
vs = Chroma(
collection_name="rag_docs",
embedding_function=emb_index,
persist_directory="./chroma_store",
)
vs.add_documents(docs)
# 2) 검색: 3072 차원으로 바꿔서 검색
emb_query = OpenAIEmbeddings(model="text-embedding-3-large")
vs_wrong = Chroma(
collection_name="rag_docs",
embedding_function=emb_query,
persist_directory="./chroma_store",
)
# 여기서 dimension mismatch 류 에러가 발생할 수 있음
results = vs_wrong.similarity_search("벡터 차원은 왜 중요해?", k=2)
print(results)
핵심은 “같은 컬렉션 이름을 쓰면서 임베딩 함수만 바꿨다”는 점입니다. 컬렉션은 이미 1536 차원으로 채워져 있는데, 검색은 3072 차원 벡터로 들어오니 실패합니다.
진단: 지금 내 컬렉션 차원은 몇인가
차원 진단은 두 방향에서 동시에 해야 합니다.
- 애플리케이션이 생성하는 임베딩 차원 확인
- 벡터 DB 컬렉션(인덱스)이 기대하는 차원 확인
애플리케이션 임베딩 차원 확인
가장 확실한 방법은 임베딩을 한 번 생성해 길이를 찍는 것입니다.
from langchain_openai import OpenAIEmbeddings
emb = OpenAIEmbeddings(model="text-embedding-3-small")
v = emb.embed_query("dimension check")
print(len(v))
이 값이 인덱스 차원과 일치해야 합니다.
Qdrant 컬렉션 차원 확인 예시
Qdrant는 컬렉션 생성 시 vectors.size 로 차원을 고정합니다.
from qdrant_client import QdrantClient
client = QdrantClient(url="http://localhost:6333")
info = client.get_collection("rag_docs")
print(info.config.params.vectors)
출력에 size 가 나오고, 그 값이 임베딩 길이와 같아야 합니다.
Pinecone 인덱스 차원 확인 포인트
Pinecone은 인덱스 생성 시 dimension을 지정합니다. 운영에서 흔한 실수는 “인덱스는 1536인데, 앱에서 3072를 올리고 있다”입니다. 인덱스 정보를 조회해 dimension을 확인하세요.
FAISS 차원 확인 포인트
FAISS는 인덱스 타입에 따라 d 가 내부에 고정됩니다. 파일로 저장해둔 인덱스를 재사용하는 경우, 예전 차원이 그대로 남아 있을 수 있습니다.
해결 전략 1: 임베딩 모델을 원복하거나, 전 구간을 동일 모델로 통일
가장 빠른 복구는 “인덱싱과 검색을 같은 모델로 맞추는 것”입니다.
- 검색만 새 모델로 바꿨다면 검색을 원복
- 인덱싱만 새 모델로 바꿨다면 인덱싱 파이프라인을 원복하고 잘못 적재된 데이터는 격리
다만 이미 새 차원으로 일부 데이터가 들어갔다면, 그 컬렉션은 오염됐을 가능성이 큽니다. 이 경우 아래의 마이그레이션 전략을 권장합니다.
해결 전략 2: 새 컬렉션 생성 후 재임베딩(정석)
차원이 바뀌는 순간, “새 컬렉션”이 정답입니다.
절차
- 새 컬렉션 이름을 만든다. 예:
rag_docs_v2 - 새 임베딩 모델로 모든 문서를 재임베딩한다.
- 검색 트래픽을 새 컬렉션으로 스위치한다.
- 안정화 후 구 컬렉션 삭제(또는 보관)
코드 예시: 컬렉션 버저닝
from langchain_openai import OpenAIEmbeddings
from langchain_chroma import Chroma
def get_vectorstore(version: str):
collection = f"rag_docs_{version}"
persist_dir = "./chroma_store"
# 버전별로 모델을 명시적으로 고정
if version == "v1":
emb = OpenAIEmbeddings(model="text-embedding-3-small")
elif version == "v2":
emb = OpenAIEmbeddings(model="text-embedding-3-large")
else:
raise ValueError("unknown version")
return Chroma(
collection_name=collection,
embedding_function=emb,
persist_directory=persist_dir,
)
vs_v2 = get_vectorstore("v2")
포인트는 “코드에서 버전과 모델을 강하게 결합”하는 것입니다. 환경변수로 모델명만 바꿔치기하면, 어느 순간 누군가가 컬렉션은 그대로 두고 모델만 바꿔 장애를 재발시킬 수 있습니다.
해결 전략 3: 운영 마이그레이션(듀얼 라이트 + 스위치오버)
문서 수가 많거나, 재임베딩 시간이 길면 한 번에 갈아타기 어렵습니다. 이때는 듀얼 라이트가 현실적인 선택입니다.
듀얼 라이트 패턴
- 쓰기(인덱싱): 구 컬렉션과 신 컬렉션에 동시에 저장
- 읽기(검색): 초기에는 구 컬렉션만 사용
- 일정 커버리지 이상이 되면 읽기를 신 컬렉션으로 전환
이 과정에서 장애가 나기 쉬운 지점은 “일부 요청이 신 컬렉션, 일부 요청이 구 컬렉션을 참조”하는 라우팅 꼬임입니다. 라우팅/재시도/서킷 브레이커 같은 안정성 패턴이 도움이 됩니다. 스트리밍 기반 파이프라인이라면 gRPC 스트리밍 끊김 대응 - Retry·Circuit Breaker 설계도 참고할 만합니다.
예방: 차원 불일치를 코드로 막는 가드레일
운영에서 가장 좋은 해결은 “배포 전에 실패하게 만들기”입니다.
1) 시작 시점에 컬렉션 차원과 임베딩 차원 검증
애플리케이션 부팅 시 다음을 수행하세요.
- 임베딩을 하나 만들어
len()으로 차원 확인 - 벡터 DB에서 컬렉션 차원 조회
- 다르면 즉시 프로세스 종료
def assert_dimension_match(embedding, expected_dim: int):
v = embedding.embed_query("healthcheck")
got = len(v)
if got != expected_dim:
raise RuntimeError(f"embedding dim mismatch: expected {expected_dim}, got {got}")
expected_dim 은 코드 상수로 두기보다 “컬렉션 메타데이터에서 읽어오는 방식”이 더 안전합니다.
2) 컬렉션 메타데이터에 모델명과 차원을 함께 저장
가능한 벡터 DB에서는 컬렉션 메타데이터에 다음을 기록하세요.
embedding_model: 예text-embedding-3-smallembedding_dim: 예1536created_at,schema_version
그리고 앱 시작 시 이 메타데이터와 현재 설정을 비교합니다.
3) 컬렉션 이름에 차원 또는 모델을 포함
예:
rag_docs_openai_3_small_1536rag_docs_mpnet_768
이렇게 하면 “같은 이름의 컬렉션을 재사용하면서 모델만 바꾸는” 실수를 구조적으로 줄일 수 있습니다.
4) 인덱싱 파이프라인과 API 서버의 설정을 단일 소스로 관리
- 같은 레포/같은 설정 모듈을 공유
- 임베딩 모델명과 차원을 코드 상수로 강제
- CI에서 인덱싱/검색 양쪽이 동일 설정을 쓰는지 검사
5) 장애가 났을 때 빠르게 롤백 가능한 배포 체계
차원 불일치는 보통 “새 배포 직후” 터집니다. 롤백이 느리면 데이터가 더 오염됩니다. 배포 실패/권한 문제로 롤백이 막히는 상황을 줄이려면 인프라 권한 체계도 점검해두는 게 좋습니다. 예를 들어 AWS 연동 배포라면 GitHub Actions OIDC로 AWS 배포 실패 해결 가이드처럼 사전 점검이 중요합니다.
벡터 DB별 체크 포인트 요약
Chroma
- 컬렉션을 디스크에
persist했다면, 폴더를 재사용하는 순간 차원이 사실상 고정 - 같은
collection_name을 쓰면서 임베딩 함수만 바꾸면 검색 시점에 터질 수 있음 - 해결은 컬렉션 이름 버저닝과 재인덱싱
Qdrant
- 컬렉션 생성 시
vectors.size가 차원 - 다른 차원 업서트 시 즉시 에러
- 해결은 새 컬렉션 생성 후 마이그레이션
Pinecone
- 인덱스 생성 시 dimension 고정
- 네임스페이스를 잘 나눠도 dimension은 인덱스 단위로 고정
- 해결은 새 인덱스 생성 또는 새 인덱스에 재적재
FAISS
- 인덱스 파일 자체가 차원을 내장
- “이전 인덱스 파일을 로드”하는 코드가 남아 있으면 계속 mismatch 발생
- 해결은 인덱스 파일 버저닝과 재생성
실전 체크리스트
- 인덱싱/검색이 동일 임베딩 모델을 쓰는가
- 임베딩 차원
len()을 로그로 남기고 있는가 - 벡터 DB 컬렉션(인덱스) 차원을 조회해 검증하는가
- 컬렉션 이름을 버전으로 분리했는가
- 모델 교체 시 “새 컬렉션 + 재임베딩” 계획이 있는가
- 듀얼 라이트/스위치오버 시 라우팅이 단일 플래그로 제어되는가
- 잘못 적재된 데이터가 섞이지 않도록 테넌트/환경 분리가 되어 있는가
결론
LangChain RAG의 벡터 차원 불일치 에러는 대부분 “임베딩 모델 변경”에서 시작하지만, 실제 원인은 컬렉션 재사용, 환경 공유, 설정 분산, 상태 꼬임까지 합쳐져 나타나는 경우가 많습니다.
가장 확실한 해결은 새 컬렉션을 만들고 재임베딩하는 것이고, 운영에서는 듀얼 라이트와 스위치오버로 리스크를 줄이는 접근이 안전합니다. 마지막으로, 앱 시작 시 차원 검증과 컬렉션 메타데이터 기록 같은 가드레일을 넣어두면 같은 장애를 반복하지 않게 됩니다.