06. RAG와 ChromaDB
문서를 넣고 질문하는 것과 많은 문서에서 필요한 부분을 찾아 질문하는 것은 다르다. RAG는 검색 가능한 자료를 미리 준비하고 질문에 관련된 부분만 생성 모델에 전달하는 방식이다.
6.1 의미 검색
키워드 검색은 나타난 단어나 형태를 중요한 신호로 사용한다. 의미 검색은 질문과 자료를 벡터로 표현해 유사한 내용을 찾는다. “대중교통으로 이동하는 방법”과 “기차와 버스 이용 안내”처럼 단어가 달라도 관련 자료를 찾으려는 목적이다.
의미가 비슷한 문서가 곧 정확한 답은 아니다. 다른 지역의 교통 정보나 예전 요금표도 질문과 비슷하게 보일 수 있다. 대상과 날짜 같은 조건을 별도로 확인해야 한다.
6.1.1 문서 하나에 대한 질의응답
문서가 짧으면 질문과 문서 전체를 한 번의 프롬프트에 넣을 수 있다. “제공한 문서 안에서 답하고 근거가 없으면 모른다고 답한다”는 규칙을 두는 기본 구조다.
이 경우에도 답이 문서에 실제로 있는지 검사해야 한다. 모델이 지침을 받았다고 환각이 완전히 사라지지는 않는다. 후속 질문에 답하려면 이전 대화도 필요한 범위만큼 제공해야 한다.
6.1.2 지식베이스에 대한 질의응답
문서 수와 길이가 늘어나면 모든 내용을 매 요청에 넣기 어렵다. 새 문서가 추가되거나 접근 범위가 달라지는 문제도 생긴다.
사용자는 질문만 보내더라도 프로그램은 어떤 저장소에서 어떤 문서를 가져올지 알아야 한다. 모델 API를 호출했다는 사실만으로 사내 파일과 데이터베이스가 자동 연결되는 것은 아니다.
6.1.3 RAG 패턴
Retrieval은 근거를 찾는 단계, Augmentation은 그 근거를 질문과 함께 입력에 넣는 단계, Generation은 그 입력으로 답을 만드는 단계다.
자료 준비는 질문마다 반복할 필요가 없다. 파일을 읽고 청크로 나눈 뒤 임베딩과 원문을 저장하는 과정을 인덱싱 또는 ingestion이라고 부른다.

왼쪽의 원본 형식은 달라도 분할 뒤에는 공통 검색 단위가 된다. 중요한 점은 오른쪽 저장소에 임베딩만이 아니라 나중에 생성 모델이 읽을 원문과 출처도 연결되어야 한다는 것이다. 벡터는 검색을 위한 표현이며 최종 답의 근거 자체를 대신하지 않는다.
질문이 들어오면 질문을 임베딩하고 저장된 청크와 비교한다. 찾은 청크의 원문을 프롬프트에 넣고 생성 모델을 호출한다. 인덱스가 갱신되지 않으면 모델에 새 문서를 넣었다고 생각해도 실제 검색에는 반영되지 않을 수 있다.
6.2 벡터 저장소
6.2.1 무엇을 저장하는가
임베딩은 텍스트나 이미지의 특성을 숫자 목록으로 표현한 벡터다. 벡터 저장소는 이런 표현을 보관하고 가까운 벡터를 찾는 기능을 제공한다.
일반적인 기록에는 청크 ID, 임베딩, 원문, 메타데이터가 필요하다. 저장소가 모든 항목을 직접 보관하지 않는다면 원문 저장소와 식별자로 연결해야 한다.
벡터의 각 차원을 사람이 읽을 수 있는 주제 이름처럼 해석하는 것은 적절하지 않다. 모델이 만든 표현 공간 안에서 상대적인 거리를 이용하는 것이다.
6.2.2 유사도와 검색
질문 벡터와 문서 벡터 사이의 거리를 계산해 가까운 후보를 고른다. 유클리드 거리는 위치의 차이, 코사인 유사도는 주로 방향의 유사성을 비교한다. 어떤 척도가 적합한지는 임베딩 모델과 저장소 설정에 따른다.
전체 벡터를 매번 비교하면 자료가 많을 때 비용이 커진다. 근사 최근접 이웃 검색인 ANN은 속도를 위해 정확한 이웃을 일부 놓칠 수 있는 검색 방식이다. 검색 품질에는 임베딩뿐 아니라 인덱스와 검색 설정도 영향을 준다.
같은 차원의 벡터라고 모두 비교 가능한 것은 아니다. 서로 다른 모델이 만든 벡터는 같은 의미 공간이라는 보장이 없다. 문서 임베딩과 질문 임베딩은 호환되는 모델과 설정으로 생성해야 한다.
6.2.3 라이브러리와 데이터베이스
벡터 검색 라이브러리는 검색 인덱스 구현에 집중할 수 있다. 데이터베이스는 여기에 원문과 메타데이터, 갱신과 영속화, 접근과 운영 기능을 결합한다.
다만 특정 라이브러리는 무조건 읽기 전용이고 특정 DB는 모든 운영 기능을 제공한다는 식으로 일반화할 수는 없다. 구현과 버전마다 범위가 다르다. 필요한 기능을 각각 확인해야 한다.
| 확인 항목 | 이유 |
|---|---|
| 원문과 메타데이터 보관 | 검색 결과를 답의 근거로 복원 |
| 필터와 식별자 | 문서 범위와 사용자 접근 조건 적용 |
| 추가와 갱신, 삭제 | 최신 자료와 원천 문서 변경 반영 |
| 재시작 후 보존 | 메모리 실험과 운영 저장의 구분 |
| 동시 접근과 운영 | 처리량과 백업, 권한의 요구 충족 |
6.2.4 저장소 선택
FAISS 같은 검색 라이브러리, Chroma와 전용 벡터 DB, PostgreSQL의 pgvector처럼 기존 DB에 벡터를 넣는 방식이 있다.
선택의 출발점은 데이터 규모뿐 아니라 기존 저장소와의 연결, 필터 요구, 변경 빈도와 운영 역량이다. 검색 속도만 비교하고 삭제와 백업, 문서 권한을 빼면 실제 서비스 요구와 어긋날 수 있다.
6.2.5 Chroma에 저장하고 검색하기
책은 Chroma 컬렉션에 문서와 ID를 넣고 질의한다. 컬렉션은 관련 기록을 묶는 단위다. 문서를 추가할 때 임베딩을 직접 전달하거나 설정한 임베딩 함수가 만들도록 할 수 있다.
메모리 클라이언트와 영속 클라이언트는 수명이 다르다. 메모리에서 정상 검색됐다고 프로세스 재시작 뒤에도 자료가 남는 것은 아니다.
검색 결과는 여러 질의를 지원하기 위해 중첩 목록 형태일 수 있다. 첫 질의의 문서와 ID, 거리 결과를 올바르게 대응시켜야 한다. 거리인지 정규화된 관련도 점수인지도 확인해야 하며 높은 값이 항상 더 좋은 것은 아니다.
k 또는 반환 개수는 근거의 양을 정한다. 너무 작으면 필요한 부분을 놓치고, 너무 크면 잡음과 토큰이 늘어난다. 상위 결과는 관련 자료가 없어도 반환될 수 있으므로 개수만으로 충분함을 판단하면 안 된다.
6.3 RAG 직접 구현
6.3.1 자료 검색
질문을 검색 함수에 넣고 관련 청크의 본문을 받는다. 이때부터 최종 답을 생성할 필요는 없다. 먼저 찾은 자료가 질문에 필요한 사실을 포함하는지 직접 볼 수 있어야 한다.
다른 지역이나 다른 버전의 자료가 검색되었다면 원문 메타데이터와 인덱싱, 질문 표현을 확인한다. 검색이 틀린 상태에서 생성 프롬프트만 바꾸면 원인을 놓치기 쉽다.
6.3.2 생성 모델 호출
사용자 질문과 검색한 문맥, 응답 규칙을 조합한다. 모델이 읽는 것은 일반적으로 검색된 텍스트이지 벡터 숫자가 아니다.
원문이 부족하면 모른다고 답하도록 하고, 사실과 출처를 연결한다. 그러나 문서가 외부에서 왔다면 그 안의 지시를 실행하지 않도록 신뢰 경계를 유지해야 한다. RAG는 외부 텍스트를 모델 입력에 추가하기 때문에 입력의 정확성과 안전성이 모두 중요하다.
6.3.3 챗봇 조합
기본 조합은 다음과 같다.
사용자 질문
→ 질문 임베딩
→ 검색과 원문 조회
→ 질문과 문맥으로 프롬프트 구성
→ 생성 모델 호출
→ 응답과 출처 반환이 구조가 한 질문에 답한다고 대화 메모리가 생기는 것은 아니다. 이전 질문에서 가리킨 장소나 대상은 다음 요청에 별도로 전달해야 한다. 다음 장은 그 연결을 다룬다.
6.3.4 용어 정리
| 용어 | 의미 |
|---|---|
| 문서 | 본문과 메타데이터를 가진 원천 단위 |
| 청크 | 검색이나 처리에 사용할 문서 조각 |
| 임베딩 | 자료와 질문의 유사도 비교를 위한 벡터 표현 |
| 인덱싱 | 검색 가능한 자료를 준비하고 저장하는 과정 |
| 검색기 | 질문에 맞는 문서 후보를 반환하는 구성 요소 |
| 문맥 | 최종 생성에 실제로 전달한 근거와 배경 |
| 생성 | 문맥과 지침으로 답변을 작성하는 과정 |
검색 자료, 실제 문맥, 최종 답을 따로 기록하면 실패를 나눠 볼 수 있다. 자료를 찾았지만 길이 제한으로 문맥에서 빠질 수 있고, 문맥에 있어도 생성 모델이 잘못 해석할 수 있다.
질문
자료를 많이 검색하면 환각이 사라질까?
관련 없는 자료와 모순된 자료가 늘 수 있다. 근거의 관련성과 충분함, 답이 근거를 따르는지를 따로 확인해야 한다.
임베딩 모델을 바꾸고 기존 인덱스를 계속 써도 될까?
차원이 같아도 표현 공간이 다를 수 있다. 기존 문서와 새 질문의 벡터 호환성을 확인하고 필요하면 전체를 다시 임베딩해야 한다.
참고: Chroma 개요.