Chroma에 문서를 넣으면 AI가 바로 답할 수 있을까?

LangChain과 Chroma로 문서를 나누고 Embedding해 저장한 뒤, 관련 조각을 검색해 LLM 답변으로 연결하는 RAG 흐름을 쉽게 설명합니다.

LangChain으로 사내 문서 RAG 챗봇 만들기 3/9

문서를 준비하는 단계부터 검색, 대화 화면, 배포와 평가까지 하나씩 연결하는 학습 기록

이전 글 — AI 모델 API는 무엇을 기준으로 골라야 할까?

학교 축제 안내문을 Chroma에 저장했습니다. 이제 “음식 판매는 몇 시까지야?”라고 물으면 AI가 바로 답할 것 같지만, 아직 한 단계가 부족합니다.

결론부터 말하면 Chroma에 문서를 넣는 것만으로 답변은 만들어지지 않습니다. Chroma는 질문과 의미가 가까운 문서 조각을 찾는 Vector Store이고, 검색 결과를 질문과 함께 Chat Model에 전달해야 비로소 자연어 답변이 생성됩니다.

이번 글에서는 문서가 검색 가능한 숫자 표현으로 바뀌는 과정과, 질문할 때 그 문서를 다시 꺼내 Prompt에 넣는 과정을 분리해 살펴봅니다. 실제 모델을 호출하지 않아도 검색 결과부터 확인해야 하는 이유도 함께 설명합니다.

이 글은 시리즈 순서에 맞춰 2026년 5월 21일에 배치했습니다. 패키지와 API는 2026년 8월 16일 LangChain과 Chroma 공식 문서를 기준으로 다시 확인했습니다.

Chroma는 AI 모델이 아니다

Chroma는 Vector Database, 즉 숫자 벡터와 원문·metadata를 보관하고 비슷한 항목을 검색하는 데이터베이스입니다. 질문을 이해해 긴 문장을 쓰는 LLM과 역할이 다릅니다.

도서관을 떠올려봅시다. Chroma는 책의 위치와 주제를 정리한 검색 서랍에 가깝습니다. “축제 운영 시간”과 가까운 안내문을 찾아줄 수는 있지만, 학생에게 친절한 문장으로 답해주는 사서 역할까지 하지는 않습니다.

실제 RAG에서는 역할이 다음처럼 나뉩니다.

역할 하는 일 하지 않는 일
Embedding Model 문서와 질문을 비교할 숫자 배열로 변환 답변 문장 생성
Chroma 벡터·문서·metadata 저장과 유사도 검색 사실 판단과 자연어 답변 생성
Retriever 현재 질문과 관련된 문서 조각 반환 검색 결과가 정답인지 보장
Chat Model 질문과 검색 근거를 읽고 답변 생성 전달받지 않은 문서를 자동으로 검색

부품을 구분하면 오류를 찾기 쉬워집니다. 검색 결과가 틀렸다면 Chat Model의 말투를 고치기 전에 문서 분할과 Retriever를 확인해야 합니다.

문서는 왜 작은 조각으로 나눌까?

학교 안내문 전체를 한 덩어리로 저장할 수도 있습니다. 하지만 축제 시간, 안전 규칙과 분실물 안내가 한 문서에 섞여 있으면 어떤 부분이 질문과 가까운지 찾기 어려워질 수 있습니다.

Chunking, 즉 문서 분할은 긴 글을 검색 가능한 작은 조각으로 나누는 과정입니다. LangChain의 RecursiveCharacterTextSplitter는 문단, 줄바꿈, 공백 같은 구분자를 차례로 사용해 가능한 한 의미가 이어지는 부분을 함께 두려고 합니다.

조각은 작을수록 좋다는 뜻이 아닙니다.

  • 너무 작으면 “판매 시간은”과 “오후 6시까지”가 서로 갈라질 수 있습니다.
  • 너무 크면 여러 주제가 섞이고 모델에 전달할 Token도 늘어날 수 있습니다.
  • 겹치는 부분을 두면 경계의 의미 손실을 줄일 수 있지만 중복 저장과 검색 잡음이 생길 수 있습니다.

모든 문서에 통하는 정답 크기는 없습니다. 한국어 규정집, 표, 코드와 짧은 FAQ는 구조가 다르므로 실제 질문으로 검색 결과를 비교해야 합니다.

Embedding은 문서를 요약하는 과정일까?

Embedding, 임베딩은 텍스트의 의미를 비교할 수 있도록 숫자 배열로 표현하는 과정입니다. 사람이 읽을 요약문이나 문서의 진실성 점수가 아닙니다.

예를 들어 “음식 판매는 언제 끝나요?”와 “먹거리 운영 마감 시간”은 단어가 다르지만 의미가 비슷합니다. 같은 Embedding Model로 바꾸면 두 질문이 가까운 위치에 놓일 수 있고, Chroma는 그 거리를 이용해 관련 문서 후보를 찾습니다.

하지만 가까운 문서가 반드시 최신이거나 정확한 것은 아닙니다. 2025년 안내문과 2026년 안내문은 표현이 비슷할 수 있습니다. 그래서 source, year, version, status 같은 metadata와 서버 측 권한 검사가 필요합니다.

저장 단계와 질문 단계는 따로 움직인다

RAG에는 두 개의 시간이 있습니다. 문서를 미리 읽고 색인하는 준비 시간과, 사용자가 질문할 때 검색하고 답하는 시간이 다릅니다.

Chroma에 저장한 문서는 어떻게 답변이 될까?

문서를 검색 가능하게 만드는 준비 단계와, 질문할 때 관련 문서를 찾아 모델에 전달하는 단계를 나누어 봅니다.

문서 저장하기

질문 전에 한 번 준비하는 색인 흐름
1안내문 읽기축제 운영 안내.txt
2작은 조각으로 나누기문단의 의미를 가능한 한 유지
3Embedding 만들기의미를 비교할 숫자 배열
4Chroma 저장벡터·본문·metadata 보관
5검색 준비아직 Chat Model은 호출하지 않음

검색 준비 완료문서 조각과 Embedding이 Chroma에 저장됐습니다. 이 단계만으로 답변이 생기지는 않습니다.

문서로 답하기

사용자가 질문할 때마다 실행되는 흐름
1사용자 질문음식 판매는 몇 시까지야?
2질문 Embedding문서와 같은 기준으로 변환
3Chroma 검색의미가 가까운 조각 찾기
4Context 구성질문 옆에 검색 근거 추가
5Chat Model 호출근거와 질문으로 답변 생성
6사용자에게 응답출처와 답변을 함께 표시

근거를 참고한 답변“안내문에 따르면 음식 판매는 오후 6시까지입니다.”

Chroma에 저장됨 · 아직 답변은 생성되지 않음검색한 근거가 Prompt에 들어간 뒤 모델이 답변 생성

현재 단계: 안내문 읽기

학교 축제 안내문은 흐름 이해를 위한 가상 자료입니다. Chroma는 문서 검색을 담당하며, 답변 생성에는 별도의 Chat Model 연결이 필요합니다.

문서 저장 흐름이 끝났다는 것은 “검색할 준비가 됐다”는 뜻입니다. 질문 흐름에서는 질문도 같은 Embedding 기준으로 변환하고, Chroma가 돌려준 문서 조각을 Prompt에 실제로 넣어야 합니다.

이 경계를 놓치면 Chroma 검색은 성공했는데도 Chat Model이 근거 없이 답할 수 있습니다. 반대로 모델 연결이 정상이어도 정답 조각을 검색하지 못하면 정확한 답변을 기대하기 어렵습니다.

가장 작은 문서 저장 코드는 어떻게 생길까?

현재 공식 연동은 Chroma를 별도 langchain-chroma 패키지로 제공합니다. 문서 분할과 OpenAI 모델 연동도 각각의 패키지에 들어 있습니다.

python -m pip install -U langchain-chroma langchain-openai langchain-text-splitters

다음 예제는 실제 회사 문서가 아닌 가상의 학교 축제 안내문을 사용합니다.

from langchain_core.documents import Document
from langchain_chroma import Chroma
from langchain_openai import OpenAIEmbeddings
from langchain_text_splitters import RecursiveCharacterTextSplitter

documents = [
    Document(
        page_content=(
            "학교 축제 음식 판매는 오후 6시까지 운영한다. "
            "정리 시간에는 새로운 주문을 받지 않는다."
        ),
        metadata={
            "source": "festival-guide.txt",
            "year": 2026,
            "section": "운영 시간",
        },
    )
]

splitter = RecursiveCharacterTextSplitter(
    chunk_size=300,
    chunk_overlap=50,
)
chunks = splitter.split_documents(documents)

embeddings = OpenAIEmbeddings(
    model="<사용 가능한 Embedding 모델 이름>"
)

vector_store = Chroma(
    collection_name="festival_guide",
    embedding_function=embeddings,
    persist_directory="./chroma_db",
)

vector_store.add_documents(chunks)

30050은 코드 흐름을 설명하기 위한 가정입니다. 모든 문서에 권장하는 최적값이 아닙니다.

persist_directory를 지정하면 로컬 디렉터리에 데이터를 유지할 수 있습니다. 메모리 안에서만 실험하려면 이 값을 빼는 방식도 공식 연동에서 지원합니다. 실제 서비스에서는 백업, 동시 접근, 파일 권한과 배포 환경을 추가로 설계해야 합니다.

이 코드는 공식 문서를 바탕으로 연결 지점을 단순화한 학습 예제입니다. 이 Astro 저장소에는 Python 패키지, API Key나 Chroma 데이터를 추가하지 않았고 실제 Embedding 요청도 실행하지 않았습니다.

LLM보다 검색 결과를 먼저 확인하자

문서를 저장한 다음 바로 Chat Model을 붙이면, 답이 틀렸을 때 검색과 생성 중 어디가 문제인지 알기 어렵습니다. 먼저 Chroma가 어떤 문서를 돌려주는지 직접 확인합니다.

question = "축제 음식 판매는 몇 시까지야?"

retriever = vector_store.as_retriever(
    search_kwargs={"k": 3}
)
contexts = retriever.invoke(question)

for document in contexts:
    print(document.page_content)
    print(document.metadata)

k=3은 상위 세 조각을 가져오는 이해용 예시입니다. 많이 가져온다고 항상 좋아지는 것은 아닙니다. 관련 없는 조각과 오래된 규정이 함께 들어오면 오히려 답변을 흐릴 수 있습니다.

이 단계에서 다음을 확인합니다.

  1. 정답 문장이 들어 있는 조각이 검색됐는가?
  2. source, 연도와 문서 구역이 올바른가?
  3. 질문과 무관한 조각이 너무 많이 섞이지 않았는가?
  4. 사용자가 볼 수 없는 문서가 반환되지 않았는가?

운영 로그에 문서 원문과 개인정보를 그대로 남기라는 뜻은 아닙니다. 필요한 문서 ID와 metadata만 안전하게 기록하고 접근 권한과 보존 기간을 정해야 합니다.

검색 결과를 모델에 전달해야 RAG가 된다

검색 결과가 올바른 것을 확인했다면 Context 문자열을 만들고 Prompt에 넣습니다. 다음 코드는 각 단계를 눈에 보이게 펼친 2-Step RAG 예제입니다.

from langchain_core.prompts import ChatPromptTemplate
from langchain_openai import ChatOpenAI

context_text = "\n\n".join(
    document.page_content for document in contexts
)

prompt = ChatPromptTemplate.from_messages([
    (
        "system",
        "제공된 Context만 사용해 답하세요. "
        "근거가 없으면 확인할 수 없다고 말하세요.\n\n"
        "Context:\n{context}",
    ),
    ("human", "질문: {question}"),
])

model = ChatOpenAI(
    model="<사용 가능한 Chat 모델 이름>"
)

response = (prompt | model).invoke({
    "context": context_text,
    "question": question,
})

print(response.content)

흐름은 단순합니다.

  1. Retriever가 관련 문서를 찾습니다.
  2. 애플리케이션이 문서 내용을 Context로 묶습니다.
  3. Prompt가 질문과 Context의 위치를 정합니다.
  4. Chat Model이 전달받은 내용으로 답변을 생성합니다.

Prompt에 “근거 안에서만 답하라”고 적어도 사실성이 완전히 보장되지는 않습니다. 모델은 여전히 다음 Token을 생성하며, 문서를 잘못 해석하거나 없는 내용을 덧붙일 수 있습니다. 검색 근거와 최종 답변은 따로 평가해야 합니다.

로컬 Chroma면 문서가 외부로 나가지 않을까?

Chroma를 로컬 디렉터리에 저장하면 Vector Store 자체는 내 컴퓨터에 둘 수 있습니다. 하지만 위 예제의 OpenAIEmbeddings는 문서를 숫자로 바꾸기 위해 외부 API를 호출하고, ChatOpenAI에는 검색한 문서 조각이 전달될 수 있습니다.

따라서 “Vector Database가 로컬이다”와 “전체 RAG가 오프라인이다”는 다른 말입니다. 다음 경계를 각각 그려봐야 합니다.

  • 원본 문서를 읽는 위치
  • Embedding Model이 실행되는 위치
  • Chroma가 실행되고 저장되는 위치
  • Chat Model이 실행되는 위치
  • Prompt, 검색 문서와 응답을 기록하는 로그 위치

외부 전송이 어려운 문서라면 조직이 승인한 Embedding·Chat Model과 배포 구조를 사용해야 합니다. 로컬 모델을 쓴다고 파일 권한, 포트 노출과 로그 보안이 자동으로 해결되는 것도 아닙니다.

같은 문서를 여러 번 넣으면 어떻게 될까?

색인 스크립트를 실행할 때마다 같은 문서를 새 ID로 추가하면 중복 조각이 쌓일 수 있습니다. 검색 결과에 같은 내용이 반복되고 저장 공간도 늘어납니다.

실제 적재 과정에서는 문서 ID와 버전을 정하고 다음을 설계해야 합니다.

  • 새 문서 추가와 기존 문서 갱신을 구분하기
  • 삭제되거나 폐기된 원문의 벡터도 제거하기
  • Embedding Model을 바꾸면 필요한 문서를 다시 색인하기
  • 원본 문서 버전과 적재 시간을 metadata로 남기기
  • 실패한 적재를 다시 실행해도 중복되지 않게 만들기

Chroma에 파일을 한 번 넣었다고 원본의 변경이 자동으로 계속 반영되는 것은 아닙니다. 원본과 색인을 맞추는 책임은 애플리케이션의 적재 흐름에 남습니다.

한 단계 더 깊게: 같은 Embedding 기준이 필요한 이유

문서를 자로 재고 질문을 저울로 잰다면 두 값을 제대로 비교할 수 없습니다. Vector 검색도 문서와 질문이 같은 Embedding 공간에 있어야 의미 있는 거리를 계산할 수 있습니다.

색인할 때 사용한 Embedding Model과 질문할 때 사용한 Model이 호환되지 않으면 벡터의 차원이나 의미 기준이 달라질 수 있습니다. 모델을 교체하면 기존 문서를 다시 Embedding하고 검색 품질을 재평가해야 할 수 있습니다.

유사도 점수도 정답 확률이 아닙니다. 가까운 후보라는 뜻일 뿐 최신성, 권한과 사실성을 보장하지 않습니다. 점수의 방향과 범위는 Vector Store와 검색 방식에 따라 다를 수 있으므로 다른 시스템의 숫자를 그대로 비교하면 안 됩니다.

자주 생기는 오해

Chroma에 문서를 넣으면 모델이 그 내용을 학습하나요?

아닙니다. 문서 조각과 Embedding을 검색할 수 있게 저장할 뿐 모델 가중치는 바뀌지 않습니다. 질문할 때 관련 조각을 다시 Prompt에 넣어야 합니다.

Chroma가 답변까지 만들어주나요?

아닙니다. Chroma는 관련 문서 후보를 검색합니다. 자연어 답변은 별도의 Chat Model이 생성합니다.

Chunk는 작게 나눌수록 정확한가요?

항상 그렇지 않습니다. 너무 작으면 의미가 끊기고, 너무 크면 여러 주제가 섞일 수 있습니다. 실제 질문 세트로 검색 결과를 비교해야 합니다.

검색 결과가 맞으면 최종 답변도 항상 맞나요?

아닙니다. 모델이 근거를 잘못 해석하거나 내용을 덧붙일 수 있습니다. 검색 품질과 답변 품질을 분리해 확인해야 합니다.

세 줄 요약

  • Chroma는 문서와 Embedding을 저장하고 검색하는 Vector Store이며 답변을 생성하는 AI 모델이 아닙니다.
  • RAG는 문서를 나누어 저장하는 준비 단계와 질문에 맞는 조각을 찾아 Prompt에 넣는 실행 단계가 모두 필요합니다.
  • 최종 답변을 보기 전에 검색된 문서와 metadata를 확인해야 오류가 검색에 있는지 생성에 있는지 찾을 수 있습니다.

다음 편 예고

LangChain은 Loader, Splitter, Embedding, Vector Store와 Chat Model을 같은 흐름으로 연결했습니다. 그렇다면 이 연결 코드를 직접 작성하면 무엇이 불편해질까요?

다음 편 — LangChain 없이 RAG를 만들면 무엇이 불편할까?에서 추상화가 줄여주는 코드와 그 대가를 살펴봅니다.

공식 문서