빅데이타 & 머신러닝/생성형 AI (ChatGPT etc)

Langchain을 이용한 LLM 애플리케이션 개발 #11 - 벡터DB 검색 내용을 요약하기

Terry Cho 2024. 1. 12. 09:03

ContextualCompression을 이용하여, 벡터 데이터베이스의 검색 내용을 요약하고 중복 제거하기

 

조대협 (http://bcho.tistory.com)

 

벡터 데이터 베이스에서 관련된 문서를 찾아온 후에, 이 문서의 내용을 프롬프트에 컨텍스트로 삽입하여 LLM에 전달해야 한다. 그런데 LLM은 입력 사이즈에 대한 한계가 있기 때문에, 검색해온 문서의 크기가 클 경우에는 입력사이즈 제한에 걸려서 프롬프트에 삽입하지 못할 수 있다. 프롬프트에 넣을 수 있는 사이즈라 하더라도, 원본 문서는 질문에 대한 답변을 줄 수 있는 정보뿐만 아니라 관련없는 텍스트가 많이 포함되어 있을 수 있다. 이런 문제를 해결하는 방법중의 하나는 LLM을 이용하여 검색된 문서를 질의와 상관있는 정보 위주로 요약해서 프롬프트에 삽입하면 된다. 

이런 일련의 작업을 Langchain에서는 LLM을 이용한 Contextual Compression Retriever라는 기능으로 제공한다. 벡터 데이터 베이스에서 검색해온 문서를 LLM을 이용하여 자동 요약하여 리턴해준다. 

 

사용법은 의외로 간단하다.

 

import pinecone

import openai

import logging

import os

from langchain.retrievers import ContextualCompressionRetriever

from langchain.retrievers.document_compressors import LLMChainExtractor

from langchain.embeddings.openai import OpenAIEmbeddings

from langchain.llms import OpenAI

from langchain.vectorstores import Pinecone

 

#create embedding API and llm

os.environ["OPENAI_API_KEY"] = "{YOUR_OPENAI_APIKEY}"

embedding = OpenAIEmbeddings()

llm = OpenAI()

 

#Connect database

pinecone.init(api_key="{YOUR_PINECONE_APIKEY}", environment="gcp-starter")

index = pinecone.Index("terry-wiki")

text_field = "text"

vectordb = Pinecone(

    index, embedding.embed_query, text_field

)

 

compressor = LLMChainExtractor.from_llm(llm)

compression_retriever = ContextualCompressionRetriever(

    base_compressor = compressor,base_retriever = vectordb.as_retriever())

 

query = "Where is the best locaction for vacation?"

docs = compression_retriever.get_relevant_documents(query,k=5)

for doc in docs:

    print(doc)

    print("\n")



앞의 예제와 마찬가지로, pinecone 데이터 베이스 연결을 생성하고, embedding API로 사용할 OpenAIEmbedding 객체를 생성한다. 추가로, 검색된 텍스트에 대한 요약을 LLM을 사용하기 때문에 OpenAI LLM 객체를 생성하여  llm에 저장하였다. 

 

compressor = LLMChainExtractor.from_llm(llm)

compression_retriever = ContextualCompressionRetriever(

    base_compressor = compressor,base_retriever = vectordb.as_retriever())

다음으로, 검색된 텍스트를 요약할 Extractor를 생성한다. LLMChainExtractor라는 이름으로 앞에서 생성한 OpenAI() llm 객체를 사용하도록 하였다. 그리고 ContextualCompressionRetriever를 생성하고, LLMChainExtract를 지정하고, 검색에 사용할 데이터 베이스를 지정한다. 

 

이렇게 Retriever가 생겅되었으면 get_relevant_documents를 이용하여, 유사 문서를 검색한다. 

다음은 실행결과이다. 원본 위키 문서가 수백줄에 해당하는데 반해서 아래 결과는 수줄내로 텍스트의 내용을 요약한것을 확인할 수 있다. 

 

page_content='Austria (, ;  ), officially the\xa0Republic of Austria ( ), is a country in Central Europe. Around Austria there are the countries of Germany, Czech Republic, Slovakia, Hungary, Slovenia, Italy, Switzerland, and Liechtenstein. The people in Austria speak German, a few also speak Hungarian, Slovenian and Croatian. The capital of Austria is Vienna (Wien).' metadata={'chunk': 10.0, 'source': 'https://simple.wikipedia.org/wiki/Austria', 'title': 'Austria', 'wiki-id': '55'}



page_content='The sizes of cities can be very different. This depends on the type of city. Cities built hundreds of years ago and which have not changed much are much smaller than modern cities. There are two main reasons. One reason is that old cities often have a city wall, and most of the city is inside it. Another important reason is that the streets in old cities are often narrow. If the city got too big, it was hard for a cart carrying food to get to the marketplace. People in cities need food, and the food always has to come from outside the city.\n\nUrban history \n\nUrban history is history of civilization. The first cities were made in ancient times, as soon as people began to create civilization . Famous ancient cities which fell to ruins included Babylon, Troy, Mycenae and Mohenjo-daro.\n\nBenares in northern India is one among the ancient cities which has a history of more than 3000 years. Other cities that have existed since ancient times are Athens in Greece, Rome and Volterra in Italy, Alexandria in Egypt and York in England.' metadata={'chunk': 15.0, 'source': 'https://simple.wikipedia.org/wiki/City', 'title': 'City', 'wiki-id': '144'}



page_content='There are no strict rules for what land is considered a continent, but in general it is agreed there are six or seven continents in the world, including Africa, Antarctica, Asia, Europe, North America, Oceania(or Australasia), and South America.' metadata={'chunk': 4.0, 'source': 'https://simple.wikipedia.org/wiki/Continent', 'title': 'Continent', 'wiki-id': '117'}



page_content='August is named for Augustus Caesar who became Roman consul in this month.' metadata={'chunk': 17.0, 'source': 'https://simple.wikipedia.org/wiki/August', 'title': 'August', 'wiki-id': '2'}



LLMFilter

Retriever를 이용해서 검색한 내용은 전체문서를 chunk 단위로 나눈 텍스트에 대한 임베딩 데이터로 검색하였기 때문에, 전체 문서를 대표하는 사실 어렵다. 예를 들어 휴가지에 대한 질의에 대한 검색 결과로 어떤 문서가 리턴되었을때, 그 문서에 휴가지에 대한 내용이 한줄이고 나머지 99줄이 다른 내용이라 하더라도 휴가지에 대한 한줄 문서의 임베딩 값에 의해서 그 문서가 검색될 수 있다. 

그래서 검색된 문서가 실제로 질의와 많은 연관성이 있는지 다시 확인해서 연관성이 낮다면 그 문서를 제거하고 다른 문서를 사용하는 것이 더 좋은 결과를 얻을 수 있는데, 이러한 기능을 지원하는 것이 LLMFilter이다. LLMFilter는 ContextualCompressionRetriever와 같이 사용될 수 있으며, 검색된 결과문서가 질의와 연관성이 얼마나 높은지를 LLM을 이용하여 판단하고, 연관성이 높지 않다면 그 문서를 제거하는 기능을 한다. 

 

사용법은 매우 간단하다. ContextualCompressionRetriever 부분에서 LLMExtract대신 LLMChainFilter를 사용하도록하면된다. 

 

import pinecone

import openai

import logging

import os

from langchain.retrievers import ContextualCompressionRetriever

from langchain.retrievers.document_compressors import LLMChainFilter

from langchain.embeddings.openai import OpenAIEmbeddings

from langchain.llms import OpenAI

from langchain.vectorstores import Pinecone

 

#create embedding API and llm

os.environ["OPENAI_API_KEY"] = "{YOUR_OPENAI_APIKEY}"

embedding = OpenAIEmbeddings()

llm = OpenAI()

 

#Connect database

pinecone.init(api_key="{YOUR_PINECONE_APIKEY}", environment="gcp-starter")

index = pinecone.Index("terry-wiki")

text_field = "text"

vectordb = Pinecone(

    index, embedding.embed_query, text_field

)

 

filter = LLMChainFilter.from_llm(llm)

compression_retriever = ContextualCompressionRetriever(

    base_compressor = filter,base_retriever = vectordb.as_retriever(),k=2)



query = "Where is the best locaction for vacation?"

docs = compression_retriever.get_relevant_documents(query)

for doc in docs:

    print(doc)

    print("\n")

 

LLMChainFilter를 사용하면 LLM을 이용하여 의미상 질문과 관련 없는 정보를 걸러낼 수 있지만, LLM 모델을 호출해야 하기 때문에, 속도가 느리고 추가적인 비용이 든다. 이를 보완하는 방법으로  LLMChainFiltere 대신에 EmbeddingsFilter를 사용하는 방법이 있다. 이 방식은 검색된 문서와 질의의 임베딩 벡터간의 유사도를 측정하여 검색된 문서가 얼마나 연관성이 있는지를 판단한다. 

 

데이터베이스에서 질문을 임베딩 벡터로 이미 검색했기 때문에 같은 내용이라고 착각할 수 도 있지만, 벡터데이터베이스에서의 검색은 질문과 임베딩된 문장간의 검색이고, EmbeddingFilter는 검색된 질문과 검색된 문서간의 유사도 비교이기 때문에 다르다고 볼 수 있다. 

Filter와 Extractor를 함께 사용하기

앞서 Contextual Compressor에서 LLMChainExtractor,LLMChainFilter 등을 살펴봤는데, 이를 같이 사용할 수는 없을까? 예를 들어 관련 없는 문서들을 LLMChainFilter를 통해서 제거하고, 관련된 문서들만 LLM을 통해서 요약하는 유스케이스를 구현할 수 없을까?

ContextualCompressorRetriever는 Extractor 부분에 여러 필터를 파이프라인식으로 연결함으로써 이 기능들을 같이 적용할 수 있다. 

 

아래 코드는 파이프라인을 적용한 코드로 먼저 EmbeddingsRedundantFilter를 적용하였. 앞에서는 소개하지 않은 필터인데, 벡터 데이터베이스를 검색하면 많은 비율로 같은 문서가 검색결과로 나오는 경우가 있다. 이유는 하나의 문서가 여러개의 chunk로 분할되어 벡터 데이터베이스에 저장되기 때문에, 검색이 chunk 단위로 이루어지게 되고 그래서 한문서에 유사한 내용이 있는 chunk 가 많기 때문에 같은 문서가 검색되게 된다. 이는 참조 정보에 대한 다양성을 저해할 수 있기 때문에, top-k값을 늘려서 검색 결과의 수를 늘리, 그중에서 중복된 문서를 제거하면 다양한 검색 결과를 얻을 수 있다. 

 

중복을 제거한 후에는 위에서 살펴보았던 Extractor를 통해서 검색 결과를 요약하고, LLM Filter를 이용하여 관계 없는 문서를 제거하도록 하였다. 

 

from langchain.llms import OpenAI

import pinecone

import os

from langchain.embeddings.openai import OpenAIEmbeddings

from langchain.vectorstores import Pinecone

from langchain.retrievers import ContextualCompressionRetriever

from langchain.retrievers.document_compressors import LLMChainFilter

from langchain.retrievers.document_compressors import LLMChainExtractor

from langchain.retrievers.document_compressors import DocumentCompressorPipeline

from langchain_community.document_transformers import EmbeddingsRedundantFilter

 

#create embedding API

os.environ["OPENAI_API_KEY"] = "{YOUR_OPENAI_KEY}"

llm = OpenAI(temperature=0)

embedding = OpenAIEmbeddings()

 

#Connect database

pinecone.init(api_key="{YOUR_PINECONE_APIKEY}", environment="gcp-starter")

index = pinecone.Index("terry-wiki")

text_field = "text"

vectordb = Pinecone(

    index, embedding.embed_query, text_field

)

 

llm_filter = LLMChainFilter.from_llm(llm)

llm_extractor = LLMChainExtractor.from_llm(llm)

redundant_filter = EmbeddingsRedundantFilter(embeddings=embedding)



pipeline_compressor = DocumentCompressorPipeline( transformers=[redundant_filter,llm_extractor,llm_filter])

 

compression_retriever = ContextualCompressionRetriever(

    base_compressor = pipeline_compressor,base_retriever = vectordb.as_retriever(),k=10)

 

#query = "Where is the best place for summer vacation?"

query ="Where is the cuba? and nearest country by the Cuba?"

docs = compression_retriever.get_relevant_documents(query)

for doc in docs:

    print(doc)

    print("\n")