이 글은 RAG - Say what? 를 읽고 정리한 글이빈다.


이 글에서는 RAG 의 여러 요소 중에서 사용자의 질문을 변형해서 좀 더 관려성 높은 문서를 찾기 위한 방법인 Query Translation 에 대해서 설명한다.

 

 

매커니즘은 쿼리를 단순하게 LLM 에게 전달하지 말고, 이것들을 잘 reconstruct 해서 연관된 문서를 찾는 방법임.

 

Rewrite-Retrieve-Read:

  • 이 기법은 사용자로부터 쿼리가 주어졌을 때 해당 쿼리를 재작성하고, 검색 엔진을 통해서 검색해보고, 문맥을 가져와서 답변을 작성하는 방법임.
  • Vector Store 에 검색을 하는 방법은 아니긴 하나, 아이디어는 차용할 수 있다.

 

Follow-up question to condensed/standalone one

  • 사용자의 질문과, 사용자의 대화 내역을 포함시켜서 질문을 다시 만드는 방법임.
  • LangChain 에서는 이를 아래와 같은 프롬포트로 사용하고 있다.
Given the following conversation and a follow up question, rephrase the follow up \

question to be a standalone question.

Chat History:

{chat_history}

Follow Up Input: {question}

Standalone Question:

 

 

 

 

RAG Fusion:

  • 사용자의 질문을 다양한 관점에서 다시 작성해서 여러 쿼리를 만들고 이를 이용해서 문서를 검색한다. 그리고 총 조회된 문서들을 reciprocal rank fusion (RRF) 에 따라서 스코어링을 해서 랭킹을 매긴다.
  • 그러니까 1등 문서는 1/1 점, 2등 문서는 1/2 점, 3등 문서는 1/3 점 이런식으로 점수를 매겨서 총 랭킹을 매기는거임.

 

 

LangChain Cook Book 의 RAG Fusion 의 예시는 다음과 같다:

from langchain_core.output_parsers import StrOutputParser
from langchain_openai import ChatOpenAI

prompt = ChatPromptTemplate.from_messages([
     ("system", "You are a helpful assistant that generates multiple search queries based on a single input     query."),
     ("user", "Generate multiple search queries related to: {original_query}"),
     ("user", "OUTPUT (4 queries):")
])


generate_queries = (
    prompt | ChatOpenAI(temperature=0) | StrOutputParser() | (lambda x: x.split("\n"))
)

original_query = "impact of climate change"

vectorstore = PineconeVectorStore.from_existing_index("rag-fusion", OpenAIEmbeddings())
retriever = vectorstore.as_retriever()

from langchain.load import dumps, loads

def reciprocal_rank_fusion(results: list[list], k=60):
    fused_scores = {}
    for docs in results:
        # Assumes the docs are returned in sorted order of relevance
        for rank, doc in enumerate(docs):
            doc_str = dumps(doc)
            if doc_str not in fused_scores:
                fused_scores[doc_str] = 0
            previous_score = fused_scores[doc_str]
            fused_scores[doc_str] += 1 / (rank + k)

    reranked_results = [
        (loads(doc), score)
        for doc, score in sorted(fused_scores.items(), key=lambda x: x[1], reverse=True)
    ]
    return reranked_results

chain = generate_queries | retriever.map() | reciprocal_rank_fusion

chain.invoke({"original_query": original_query})

 

 

Step-Back Prompting:

  • 주어진 질문을 한단계 추상화해서 그 질문으로 답변을 작성하는 방법임.
  • 이것도 Vector Store 를 이용하는 건 아니지만 질문을 reconstruct 하는 매커니즘은 알아두면 좋을듯.

 

 

Query Expansion:

  • 질문이 주어졌을 때 새로운 단어들을 생성해서 이를 기반으로 질문을 확장시켜서 검색하는 방법 이거나, 질문이 주어졌을 때 그 질문에 대한 답변을 적어서, 이걸 오리지날 질문과 합쳐서 문서를 검색하는 방법이다.

 

 

Hypothetical Document Embeddings (HyDE):

  • 질문을 할 경우 가상 답변을 만들어서 이를 기반으로 문서를 검색하는 방법

 

 

Conclusion

  • 공통적인 Query Translation 매커니즘은 사용자의 질문을 그대로 쓰는 것보다 쿼리를 다양한 관점에서 여러개 생성하고, 그걸 통해서 문서를 검색하고, 랭킹을 매겨서 가장 좋은 문서를 찾는 방법임. 
  • 중요한 건 LLM 이 확실하게 어떤 식으로 동작하는지 잘 알지 못하기 때문에 실험적인 접근 방법을 취할 수 밖에 없다. 아렇게도 해보고, 저렇게도 해보고 하면서 단순히 가장 성능이 좋게 나오는 방법을 선택하거나, 여러 방법을 섞거나, 유연하게 동적으로 대응하거나 이런 식으로 해야함. 
  • 실험을 적극적으로 할 수 밖에 없으니 자동화된 테스트와 Evaluation 프로세스를 잘 구축하는 게 중요할 것.
  • 질문을 재생성하는 과정에서 이상하게 재생성 될 수 있으니, 이상한 문서가 검색될 수 있으니 할루시네이션에 대한 주의가 필요하다.

 

Additional Refererences:

+ Recent posts