AI 모델이 특정 문맥에서 유용하게 작동하려면 배경 지식에 접근할 필요가 있다. 예를 들어, 고객 지원 챗봇은 해당 비즈니스에 대한 지식이 필요하고, 법률 분석 봇은 다양한 과거 사례에 대한 정보를 알아야 한다.
개발자들은 일반적으로 Retrieval-Augmented Generation(RAG)을 사용해 AI 모델의 지식을 확장한다. RAG는 지식 베이스에서 관련 정보를 검색하여 사용자의 프롬프트에 추가함으로써 모델의 응답을 크게 향상시키는 방법이다. 문제는 기존 RAG 솔루션이 정보를 인코딩할 때 문맥을 제거한다는 점이다. 이로 인해 시스템이 지식 베이스에서 관련 정보를 검색하지 못하는 경우가 흔히 발생한다.
이 글에서는 RAG의 검색 단계를 크게 개선하는 방법을 소개한다. 이 방법은 “문맥 기반 검색(Contextual Retrieval)“이라고 하며, 두 가지 하위 기술인 문맥 임베딩(Contextual Embeddings)과 문맥 BM25(Contextual BM25)를 사용한다. 이 방법은 검색 실패 횟수를 49% 줄일 수 있으며, 재순위화와 결합하면 67%까지 감소시킬 수 있다. 이는 검색 정확도의 상당한 개선을 의미하며, 이는 곧 다운스트림 작업의 성능 향상으로 이어진다.
여러분은 저희의 쿡북을 통해 Claude를 사용해 자신만의 문맥 기반 검색 솔루션을 쉽게 배포할 수 있다.
때로는 가장 단순한 해결책이 최선일 수 있다. 여러분의 지식 베이스가 200,000 토큰(약 500페이지 분량)보다 작다면, RAG나 기타 유사한 방법 없이도 전체 지식 베이스를 모델에 제공하는 프롬프트에 포함할 수 있다.
몇 주 전, 우리는 Claude를 위한 프롬프트 캐싱 기능을 출시했다. 이 기능은 이 접근 방식을 훨씬 더 빠르고 비용 효율적으로 만들어준다. 개발자들은 이제 API 호출 간에 자주 사용하는 프롬프트를 캐싱할 수 있으며, 이를 통해 지연 시간을 2배 이상 줄이고 비용을 최대 90%까지 절감할 수 있다. (자세한 작동 방식은 프롬프트 캐싱 쿡북에서 확인할 수 있다.)
하지만 지식 베이스가 커짐에 따라 더 확장 가능한 솔루션이 필요해진다. 바로 이때 컨텍스트 기반 검색(Contextual Retrieval)이 필요하다.
컨텍스트 윈도우에 맞지 않는 더 큰 지식 베이스를 다룰 때, RAG(Retrieval-Augmented Generation)가 일반적인 해결책이다. RAG는 다음과 같은 단계를 통해 지식 베이스를 사전 처리한다:
실행 시, 사용자가 모델에 쿼리를 입력하면 벡터 데이터베이스를 사용해 쿼리와 의미적으로 가장 관련 있는 청크를 찾는다. 그런 다음, 가장 관련성이 높은 청크를 생성 모델에 보내는 프롬프트에 추가한다.
임베딩 모델은 의미적 관계를 잘 포착하지만, 중요한 정확한 일치를 놓칠 수 있다. 다행히 이런 상황에서 도움을 줄 수 있는 오래된 기법이 있다. BM25(Best Matching 25)는 어휘 일치를 사용해 정확한 단어나 구문 일치를 찾는 순위 함수다. 특히 고유 식별자나 기술 용어가 포함된 쿼리에 효과적이다.
BM25는 TF-IDF(Term Frequency-Inverse Document Frequency) 개념을 기반으로 작동한다. TF-IDF는 단어가 문서 컬렉션에서 얼마나 중요한지 측정한다. BM25는 문서 길이를 고려하고, 단어 빈도에 포화 함수를 적용해 일반적인 단어가 결과를 지배하지 않도록 개선한다.
BM25가 의미적 임베딩이 실패하는 상황에서 성공할 수 있는 예를 살펴보자. 사용자가 기술 지원 데이터베이스에서 “Error code TS-999”를 쿼리한다고 가정해 보자. 임베딩 모델은 일반적인 오류 코드에 대한 내용을 찾을 수 있지만, 정확한 “TS-999” 일치를 놓칠 수 있다. BM25는 이 특정 텍스트 문자열을 찾아 관련 문서를 식별한다.
RAG 솔루션은 임베딩과 BM25 기법을 결합해 다음과 같은 단계로 가장 적합한 청크를 더 정확하게 검색할 수 있다:
BM25와 임베딩 모델을 모두 활용함으로써, 전통적인 RAG 시스템은 정확한 용어 일치와 넓은 의미적 이해 사이의 균형을 맞추며 더 포괄적이고 정확한 결과를 제공할 수 있다.
임베딩과 BM25(Best Match 25)를 모두 사용해 정보를 검색하는 표준 RAG(Retrieval-Augmented Generation) 시스템. TF-IDF(term frequency-inverse document frequency)는 단어 중요도를 측정하고 BM25의 기반이 된다.
이 접근 방식은 단일 프롬프트에 맞출 수 있는 범위를 훨씬 넘어서는 거대한 지식 베이스로 비용 효율적으로 확장할 수 있게 한다. 하지만 이러한 전통적인 RAG 시스템에는 중요한 한계가 있다: 컨텍스트를 종종 파괴한다는 점이다.
전통적인 RAG(Retrieval-Augmented Generation) 시스템에서는 효율적인 검색을 위해 문서를 작은 덩어리(chunk)로 나누는 것이 일반적이다. 이 방법은 많은 애플리케이션에서 잘 작동하지만, 개별 덩어리가 충분한 맥락을 제공하지 못할 때 문제가 발생할 수 있다.
예를 들어, 여러분이 금융 정보(예: 미국 SEC 제출 서류)를 지식 기반에 포함시켰고, 다음과 같은 질문을 받았다고 가정해 보자: “ACME Corp의 2023년 2분기 매출 성장률은 얼마인가요?”
관련된 덩어리에는 다음과 같은 텍스트가 포함될 수 있다: “회사의 매출은 전분기 대비 3% 증가했다.” 그러나 이 덩어리만으로는 어떤 회사를 가리키는지, 또는 관련된 기간이 언제인지 명확히 알 수 없다. 이로 인해 올바른 정보를 검색하거나 정보를 효과적으로 활용하기 어려워진다.
컨텍스트 기반 검색은 각 청크를 임베딩(“컨텍스트 임베딩”)하고 BM25 인덱스를 생성(“컨텍스트 BM25”)하기 전에, 청크별 설명 컨텍스트를 추가하여 이 문제를 해결한다.
SEC 파일 컬렉션 예제로 돌아가 보자. 다음은 청크가 어떻게 변환될 수 있는지 보여주는 예제다:
original_chunk = "The company's revenue grew by 3% over the previous quarter."
contextualized_chunk = "This chunk is from an SEC filing on ACME corp's performance in Q2 2023; the previous quarter's revenue was $314 million. The company's revenue grew by 3% over the previous quarter."
컨텍스트를 활용해 검색 성능을 개선하려는 다른 접근 방식도 제안된 바 있다. 예를 들어 청크에 일반적인 문서 요약을 추가하는 방법(실험 결과 매우 제한적인 개선만 확인됨), 가상 문서 임베딩, 그리고 요약 기반 인덱싱(평가 결과 낮은 성능 확인) 등이 있다. 이러한 방법들은 이 글에서 제안하는 방식과 차이가 있다.
물론 수천 개, 심지어 수백만 개의 데이터 청크를 수동으로 주석 처리하는 것은 너무 많은 작업이 될 것이다. 문맥 기반 검색(Contextual Retrieval)을 구현하기 위해 우리는 Claude 모델을 사용한다. 전체 문서의 맥락을 활용해 각 청크를 설명하는 간결하고 청크에 특화된 문맥을 제공하도록 모델에 지시하는 프롬프트를 작성했다. 각 청크의 문맥을 생성하기 위해 Claude 3 Haiku 프롬프트를 다음과 같이 사용했다:
<document>
{{WHOLE_DOCUMENT}}
</document>
여기 전체 문서 내에서 위치를 파악하려는 청크가 있습니다
<chunk>
{{CHUNK_CONTENT}}
</chunk>
이 청크의 검색 성능을 개선하기 위해 전체 문서의 맥락에서 이 청크를 설명하는 간결한 문맥을 제공해 주세요. 간결한 문맥만 답변하고 그 외에는 아무것도 작성하지 마세요.
이렇게 생성된 문맥 텍스트는 보통 50~100 토큰 정도로, 청크를 임베딩하고 BM25 인덱스를 생성하기 전에 청크 앞에 추가된다.
실제 전처리 과정은 다음과 같다:
문맥 기반 검색은 검색 정확도를 향상시키는 전처리 기법이다. 문맥 기반 검색을 사용해 보고 싶다면 우리의 쿡북을 참고해 시작할 수 있다.
Claude는 앞서 언급한 특별한 프롬프트 캐싱 기능 덕분에 저렴한 비용으로 컨텍스트 검색을 수행할 수 있다. 프롬프트 캐싱을 사용하면 각 청크마다 참조 문서를 전달할 필요가 없다. 문서를 한 번 캐시에 로드한 후, 이전에 캐시된 내용을 참조하기만 하면 된다. 800 토큰 단위의 청크, 8,000 토큰의 문서, 50 토큰의 컨텍스트 지시문, 그리고 청크당 100 토큰의 컨텍스트를 가정할 때, 컨텍스트화된 청크를 생성하는 일회성 비용은 100만 문서 토큰당 $1.02이다.
우리는 다양한 지식 영역(코드베이스, 소설, ArXiv 논문, 과학 논문), 임베딩 모델, 검색 전략, 평가 지표를 대상으로 실험을 진행했다. 각 영역에서 사용한 질문과 답변의 예시를 부록 II에 포함했다.
아래 그래프는 모든 지식 영역에서 최고 성능을 보인 임베딩 설정(Gemini Text 004)과 상위 20개 청크를 검색한 경우의 평균 성능을 보여준다. 평가 지표로는 1에서 recall@20을 뺀 값을 사용했다. 이 지표는 상위 20개 청크 내에서 검색되지 않은 관련 문서의 비율을 측정한다. 전체 결과는 부록에서 확인할 수 있다. 우리가 평가한 모든 임베딩-소스 조합에서 컨텍스트화가 성능을 향상시키는 것을 볼 수 있다.
실험 결과는 다음과 같다:
컨텍스트 임베딩과 컨텍스트 BM25를 결합하면 상위 20개 청크 검색 실패율이 49% 감소한다.
컨텍스트 기반 검색(Contextual Retrieval)을 구현할 때 염두에 두어야 할 몇 가지 사항이 있다:
청크 경계: 문서를 청크로 나누는 방법을 고려한다. 청크 크기, 경계, 그리고 중첩 정도는 검색 성능에 영향을 미칠 수 있다.
임베딩 모델: Contextual Retrieval은 테스트한 모든 임베딩 모델에서 성능을 향상시키지만, 일부 모델은 더 큰 효과를 볼 수 있다. Gemini와 Voyage 임베딩이 특히 효과적이라는 것을 발견했다.
커스텀 컨텍스트 프롬프트: 기본 제공되는 일반적인 프롬프트도 잘 작동하지만, 특정 도메인이나 사용 사례에 맞게 프롬프트를 조정하면 더 나은 결과를 얻을 수 있다. 예를 들어, 지식 베이스의 다른 문서에서만 정의된 주요 용어의 용어집을 포함할 수 있다.
청크 수: 컨텍스트 윈도우에 더 많은 청크를 추가하면 관련 정보를 포함할 가능성이 높아진다. 그러나 너무 많은 정보는 모델에게 방해가 될 수 있으므로 이에 대한 한계가 있다. 5개, 10개, 20개의 청크를 테스트해본 결과, 20개를 사용했을 때 가장 성능이 좋았다(비교 결과는 부록 참조). 하지만 실제 사용 사례에 맞게 실험해보는 것이 좋다.
항상 평가를 실행한다: 컨텍스트화된 청크를 전달하고 컨텍스트와 청크를 구분하면 응답 생성이 개선될 수 있다.
마지막 단계에서 컨텍스트 기반 검색(Contextual Retrieval)과 리랭킹(Reranking) 기법을 결합해 성능을 더욱 높일 수 있다. 전통적인 RAG(Retrieval-Augmented Generation) 시스템은 지식 베이스에서 잠재적으로 관련 있는 정보 조각을 검색한다. 대규모 지식 베이스를 사용할 경우, 초기 검색 결과는 수백 개의 다양한 관련성과 중요도를 가진 정보 조각을 반환한다.
리랭킹은 가장 관련성이 높은 정보 조각만 모델에 전달하기 위해 흔히 사용되는 필터링 기법이다. 리랭킹은 더 나은 응답을 제공하고, 모델이 처리하는 정보의 양을 줄여 비용과 지연 시간을 줄인다. 주요 단계는 다음과 같다:
컨텍스트 기반 검색과 리랭킹을 결합해 검색 정확도를 극대화한다.
시장에는 여러 리랭킹 모델이 존재한다. 우리는 Cohere 리랭커를 사용해 테스트를 진행했다. Voyage에서도 리랭커를 제공하지만, 테스트할 시간이 없었다. 다양한 도메인에서 실험한 결과, 리랭킹 단계를 추가하면 검색 성능이 더욱 최적화된다는 사실을 확인했다.
특히, 리랭킹된 문맥 임베딩(Contextual Embedding)과 문맥 BM25(Contextual BM25)를 사용했을 때 상위 20개 청크의 검색 실패율이 67% 감소했다(5.7% → 1.9%).
리랭킹된 문맥 임베딩과 문맥 BM25는 상위 20개 청크의 검색 실패율을 67% 줄인다.
재정렬(re-ranking)을 사용할 때 중요한 고려 사항 중 하나는 지연 시간(latency)과 비용에 미치는 영향이다. 특히 많은 양의 데이터 청크를 재정렬할 때 더욱 두드러진다. 재정렬은 런타임에 추가 단계를 거치기 때문에, 모든 청크를 병렬로 처리하더라도 약간의 지연 시간이 불가피하게 발생한다. 더 나은 성능을 위해 많은 청크를 재정렬하는 것과, 더 낮은 지연 시간과 비용을 위해 적은 수의 청크만 재정렬하는 것 사이에는 본질적인 트레이드오프가 존재한다. 따라서 여러분의 특정 사용 사례에 맞는 최적의 균형을 찾기 위해 다양한 설정을 실험해 보는 것을 권장한다.
위에서 설명한 다양한 기술(임베딩 모델, BM25 사용, 컨텍스트 기반 검색 사용, 리랭커 사용, 상위 K개 결과 추출)을 조합해 다양한 데이터셋 타입에 대해 대량의 테스트를 진행했다. 테스트 결과를 요약하면 다음과 같다:
아래는 다양한 데이터셋, 임베딩 프로바이더, BM25 사용 여부, 컨텍스트 기반 검색 적용 여부, 그리고 리랭킹 사용 여부에 따른 Retrievals @ 20 결과를 상세히 분석한 내용이다.
Retrievals @ 10과 @ 5에 대한 분석과 각 데이터셋의 질문 및 답변 예제는 부록 II에서 확인할 수 있다.
데이터셋과 임베딩 프로바이더에 따른 1에서 recall @ 20 값을 뺀 결과.