본문 바로가기

STUDY/실전 RAG 기반 생성형 AI 개발

3. RAG 검색 성능 개선

반응형

 

백터 검색의 한계점

 

문서를 임베딩하여 벡터로 저장하고, 이를 사용자 요청과 비교하여 가장 유사한 문서를 가져오는 것이 일반적인 RAG 검색기의 구조이다. 그러나 여기에는 몇가지 한계가 있다. 먼저 문서를 벡터로 변환하는 과정에서 정보의 손실이 발생한다. 벡터 변환이란 문서를 n개의 숫자로 재 표현하는 것을 의미하는데, 어떤 길이와 내용의 문서라도 동일한 n개의 숫자로 나타내기 때문에 정보의 손실이 발생하는 것은 어쩔 수 없다. 또 벡터화된 문서를 검색하는 과정에서도 손실이 발생할 수 있다. 검색해야할 문서가 많은 경우 검색 시간 단축을 위해 최적화 알고리즘을 적용하는 경우가 있는데, 이 때 검색 되어야될 문서가 검색되지 않을 수 있다.

 

vetorstore에는 각종 사례가 들어가 있지만 검색 결과에는 보이지 않는다

 

 


단순한 해결방법: 더 많은 문서를 가져오자!

 

분명 사용자의 요청에 참고가될 문서가 저장소에 있음에도 검색기의 한계로 해당 문서를 찾아오지 못한다면, 가장 단순한 해결방법이 있다. 바로 검색 후 반환되는 문서수를 늘리는 것이다.

 

30개의 문서를 반환하도록 하자, 필요한 문서가 검색되었다.

 

그러나 이렇게 단순히 더 많은 문서 가져오는 방법은 결과적으로 최종 응답의 품질을 개선하지 못할 확률이 높다. 최근 발표된 논문에 따르면 RAG의 정확도는 단순히 참조할 문서가 존재 하는지가 아니라, 참조 문서의 순서에 크게 영향을 받는다고 한다. 답변에 도움이 될 문서가 LLM에 참고할 문서로 전달 될 때, 해당 문서가 수많은 문서들 사이에서도 앞에 위치해야 실제 응답 결과에 도움이 된다는 것이다.

참조할 문서의 위치가 RAG 정확도에 큰 영향을 미친다

 

 


리랭커

 

 

리랭커란 이름 그대로 문서의 순서를 재조정하는 역할을 수행한다. 일반적으로 검색기는 문서와 사용자의 요청을 각각 임베딩하여 그 벡터값을 비교하는 Bi-encoder 형태이다. 그에 반해 리랭커는 질문과 문서를 동시에 분석 하는 Cross-encoder 방식으로, Bi-encoder 방식에 비해 더욱 정확한 유사도 측정이 가능하다. 그렇다면 처음부터 Cross-encoder로 문서를 검색하면 되지 않냐고 할 수 있다. Bi-encoder 방식은 모든 문서를 미리 임베딩하고 사용자의 요청과 비교할 수 있지만, Cross-encoder 방식은 사용자의 요청이 올 때 마다 매번 모든 문서와 비교를 해야하기때문에 매우 비효율적이다. 따라서 Bi-encoder 방식으로 유사한 문서를 먼저 추려내고, 이 문서들의 유사도를 Cross-encoder 방식으로 비교하는게 효율적이다.

 

# BGE Reranker 모델
model_name = "BAAI/bge-reranker-large"
tokenizer = AutoTokenizer.from_pretrained(model_name)
model = AutoModelForSequenceClassification.from_pretrained(model_name)

def rerank_documents(query, docs, top_k=10):
    pairs = [[query, doc.page_content] for doc in docs]
    with torch.no_grad():
        inputs = tokenizer(pairs, padding=True, truncation=True, return_tensors="pt", max_length=512)
        scores = model(**inputs).logits.squeeze(-1)
    ranked_indices = scores.argsort(descending=True)
    reranked_docs = [(docs[i], scores[i].item()) for i in ranked_indices[:top_k]]
    return reranked_docs

print("\n## 리랭킹 결과")
reranked_docs = rerank_documents("고령화 시대에 어떤 제품들이 개발되고 있는지 사례를 알려줘", reference_document_list)
for i, (doc, score) in enumerate(reranked_docs):
    print(f"문서 {i+1} (점수: {score:.4f}):")
    print(doc.page_content + "\n")

리랭킹을 결과 적절한 문서가 상위로 올라왔다.

 

 


쿼리 확장

 

사용자의 요청 자체가 너무 짧거나 모호하면 이 또한 검색기 성능에 악영향을 끼친다. 이럴 때 사용할 수 있는  방법이 쿼리 확장이다. 쿼리 확장은 사용자의 요청을 그대로 사용하는 것이 아니라 키워드를 추가하거나, 맥락을 보강하거나, 답변의 예시를 추가하여 검색의 성능을 끌어올린다.

 

llm = ChatOpenAI(model="gpt-4o-mini")

# Query expansion 함수
def expand_query(original_query):
    expansion_prompt = f"다음 질문을 확장하여 관련된 다양한 키워드와 문구를 간결하게 한국어로 생성해주세요. 설명이나 문장 형태의 답변은 하지 마세요. 원래 질문: '{original_query}'"
    expanded_query = llm.invoke(expansion_prompt).content
    return expanded_query

# 원래 쿼리
original_query = "고령화 시대에 어떤 제품들이 개발되고 있는지 사례를알려줘"

# 쿼리 확장
expanded_query = expand_query(original_query)
print(f"확장된 쿼리: {expanded_query}")

# 확장된 쿼리로 검색 수행
search_results = vectorstore.similarity_search(expanded_query, k=5)

# 결과 출력
for i, doc in enumerate(search_results):
    print(f"관련 문서 {i+1}:")
    print(doc.page_content[:100] + "...\n")

쿼리 확장을 통해 적절한 문서를 검색하였다.

 

 


HyDE(Hypothetical Document Embedding) - 가상문서 임베딩

 

HyDE는 가상문서임베딩의 약자로, 말 그대로 사용자의 질문에 대한 가상의 문서를 생성해서 검색 성능을 향상시키는 방법이다. 쿼리 확장기법의 일부라고 볼 수 있다. 사용자의 질문과 유사한 문서를 검색하는 것 보다, 사용자의 질문에 대한 가상의 문서를 만들고 이와 유사한 문서를 검색하는 것이다.

 

embedding = OpenAIEmbeddings(model="text-embedding-3-small")
llm = ChatOpenAI(model="gpt-4o-mini")

# 사용자 정의 프롬프트 템플릿 생성
custom_prompt = PromptTemplate(
    input_variables=["question"],
    template="다음 질문에 대한 가상의 문서를 생성해주세요: {question}\n\n문서:"
)

# HyDE 설정
hyde = HypotheticalDocumentEmbedder.from_llm(
    llm=llm,
    base_embeddings=embedding,
    custom_prompt=custom_prompt
)

# 쿼리 실행
query = "고령화 시대에 어떤 제품들이 개발되고 있는지 사례를 알려줘"
hyde_embedding = hyde.embed_query(query)
# vectorstore를 사용하여 검색
results = vectorstore.similarity_search_by_vector(hyde_embedding)

# hyde.embed_query() 호출 전후에 중간 결과 출력
print("생성된 가상 문서:")
print(llm.invoke(custom_prompt.format(question=query)).content)

# 결과 출력
for i, doc in enumerate(results[:10]):  # 상위 5개 결과만 출력
    print(f"관련 문서 {i+1}:")
    print(doc.page_content[:100] + "...\n")

 

반응형