소소한 컴퓨터 이야기

Exploring Query Rewriting (이론)

by Cori

해당 포스트는 Medium 'Florian June'이 작성한 Advanced RAG 포스트 시리즈 그 여섯번째 내용을 정리하며, 쿼리 재작성과 관련된 여러 기법에 대해 다루고 있다.


검색 증강 생성(RAG)에서는 부정확한 표현이나 의미 정보의 부족과 같은 종종 사용자의 원래 쿼리와 관련된 문제에 직면한다. 예를 들어, "2020년 NBA 챔피언은 로스앤젤레스 레이커스입니다! langchain 프레임워크가 무엇인지 알려주세요"라는 쿼리는 직접 검색하면 LLM에서 부정확하거나 답변할 수 없는 응답을 생성할 수 있다. 사용자 쿼리의 의미 공간을 문서의 의미 공간과 일치시키는 것이 중요하며, 쿼리 재작성 기술은 이 문제를 효과적으로 해결할 수 있다.

Query rewriting in RAG, https://miro.medium.com/v2/resize:fit:786/format:webp/1*bwC_8_SlzuKF158CjC3nBg.png

쿼리 재작성은 쿼리와 문서의 의미를 일치시키기 위한 핵심 기술로, 하나씩 살펴보자

 

Hypothetical Document Embeddings (HyDE)

가상 문서를 통해 쿼리와 문서의 의미 공간을 일치시킨다.

An illustration of the HyDE model, https://miro.medium.com/v2/resize:fit:1400/format:webp/1*xDZE2hed5xIZrdOsY3C2JQ.png

Step 1. 쿼리를 기반으로 LLM을 사용하여 k개의 가상 문서를 생성한다. 생성된 문서들은 사실과 다를 수 있으며, 오류를 포함할 수 있지만 관련 문서를 닮아야 한다. 

Step 2. 생성된 가상 문서를 인코더에 입력하여 이를 밀집 벡터 f(dk)로 매핑한다. 인코더는 가상 문서 내의 잡음을 걸러내는 필터링 기능을 수행한다고 가정되며, 여기서 dk는 k번째 생성된 문서, f는 인코더 동작을 의미한다.

Step 3. 다음 공식을 사용하여 k개 문서의 벡터 평균을 계산한다.

Step 4. 벡터 v를 사용하여 문서 라이브러리에서 답변을 검색한다. 3단계에서 설정한 것처럼, 이 벡터는 사용자의 쿼리와 원하는 답변 패턴의 정보를 모두 포함하고 있어 검색 정확도를 향상시킬 수 있다.

이러한 과정을 통해, HyDE는 가상 문서를 생성하여 최종 쿼리 벡터 v가 벡터 공간에서 실제 문서와 최대한 가깝게 일치하도록 한다. 해당 포스트의 작성자가 이해한 HyDE는 다음과 같다.

the objective of HyDE is to generate hypothetical documents, https://miro.medium.com/v2/resize:fit:1100/format:webp/1*yDbcrSUVDyhrJmrAa1j3fg.png

HyDE에서 사용하는 기본 프롬프트 템플릿은 다음과 같다.

##############################################
# HYDE
##############################################
HYDE_TMPL = (
    "Please write a passage to answer the question\n"
    "Try to include as many key details as possible.\n"
    "\n"
    "\n"
    "{context_str}\n"
    "\n"
    "\n"
    'Passage:"""\n'
)
DEFAULT_HYDE_PROMPT = PromptTemplate(HYDE_TMPL, prompt_type=PromptType.SUMMARY)
class HyDEQueryTransform(BaseQueryTransform):
    """Hypothetical Document Embeddings (HyDE) query transform.
    It uses an LLM to generate hypothetical answer(s) to a given query,
    and use the resulting documents as embedding strings.
    As described in `[Precise Zero-Shot Dense Retrieval without Relevance Labels]
    (https://arxiv.org/abs/2212.10496)`
    """
    def __init__(
        self,
        llm: Optional[LLMPredictorType] = None,
        hyde_prompt: Optional[BasePromptTemplate] = None,
        include_original: bool = True,
    ) -> None:
        """Initialize HyDEQueryTransform.
        Args:
            llm_predictor (Optional[LLM]): LLM for generating
                hypothetical documents
            hyde_prompt (Optional[BasePromptTemplate]): Custom prompt for HyDE
            include_original (bool): Whether to include original query
                string as one of the embedding strings
        """
        super().__init__()
        self._llm = llm or Settings.llm
        self._hyde_prompt = hyde_prompt or DEFAULT_HYDE_PROMPT
        self._include_original = include_original

    def _get_prompts(self) -> PromptDictType:
        """Get prompts."""
        return {"hyde_prompt": self._hyde_prompt}

    def _update_prompts(self, prompts: PromptDictType) -> None:
        """Update prompts."""
        if "hyde_prompt" in prompts:
            self._hyde_prompt = prompts["hyde_prompt"]

    def _run(self, query_bundle: QueryBundle, metadata: Dict) -> QueryBundle:
        """Run query transform."""
        # TODO: support generating multiple hypothetical docs
        query_str = query_bundle.query_str
        hypothetical_doc = self._llm.predict(self._hyde_prompt, context_str=query_str)
        embedding_strs = [hypothetical_doc]
        if self._include_original:
            embedding_strs.extend(query_bundle.embedding_strs)
        # The following three lines contain the added debug statements.
        print('-' * 100)
        print("Hypothetical doc:")
        print(embedding_strs)

        return QueryBundle(
            query_str=query_str,
            custom_embedding_strs=embedding_strs,
        )
from llama_index.core import VectorStoreIndex, SimpleDirectoryReader
from llama_index.core.indices.query.query_transform import HyDEQueryTransform
from llama_index.core.query_engine import TransformQueryEngine
import os
os.environ["OPENAI_API_KEY"] = "YOUR_OPENAI_API_KEY"

# Load documents, build the VectorStoreIndex
dir_path = "YOUR_DIR_PATH"
documents = SimpleDirectoryReader(dir_path).load_data()
index = VectorStoreIndex.from_documents(documents)
query_str = "what did paul graham do after going to RISD"

# Query without transformation: The same query string is used for embedding lookup and also summarization.
query_engine = index.as_query_engine()
response = query_engine.query(query_str)
print('-' * 100)
print("Base query:")
print(response)

# Query with HyDE transformation
hyde = HyDEQueryTransform(include_original=True)
hyde_query_engine = TransformQueryEngine(query_engine, hyde)
response = hyde_query_engine.query(query_str)

print('-' * 100)
print("After HyDEQueryTransform:")
print(response)

위 쿼리에서 HyDE는 Paul Graham이 RISD 이후에 무엇을 했는지를 정확하게 상상함으로써 출력 품질을 크게 향상시킨다(가상 문서 참조). 이는 임베딩 품질과 최종 출력을 개선하지만, 여전히 HyDE에도 실패 사례는 존재한다. HyDE는 비지도 학습이라 할 수 있으며, 어떠한 모델도 훈련되지 않는다.

 

Rewrite-Retrieve-Read

논문 Query Rewriting for Retrieval-Augmented Large Language Models (ACL, 2023)에 등장한 아이디어로, 해당 논문에서는 실제 시나리오에서 원래 쿼리가 LLM에 의해 검색되기에 항상 최적이지 않을 수 있다고 본다. 이에 먼저 LLM을 사용하여 쿼리를 재작성해야 한다고 제안하며, 검색과 답변 생성을 원래 쿼리에서 직접 콘텐츠를 검색하고 답변을 생성하는 대신, 재작성된 쿼리를 사용하여 진행해야 한다고 합니다. 

쿼리 "2020년 NBA 챔피언은 로스앤젤레스 레이커스입니다! Langchain 프레임워크가 무엇인지 알려주세요?"가 재작성되는 과정을 코드와 함께 살펴보자

 

Step 1. Env Setting

pip install langchain
pip install openai
pip install langchainhub
pip install duckduckgo-search
pip install langchain_openai
import os
os.environ["OPENAI_API_KEY"] = "YOUR_OPEN_AI_KEY"

from langchain_community.utilities import DuckDuckGoSearchAPIWrapper
from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.runnables import RunnablePassthrough
from langchain_openai import ChatOpenAI

Step 2. Construct a chain and run example

def june_print(msg, res):
    print('-' * 100)
    print(msg)
    print(res)

def retriever(query):
    return search.run(query)

기본 프롬프트 템플릿을 정의해주자

base_template = """Answer the users question based only on the following context:
<context>
{context}
</context>
Question: {question}
"""
base_prompt = ChatPromptTemplate.from_template(base_template)

모델은 OpenAI 모델을 사용하고, 검색 엔진은 DuckDuckGoSearchAPI (duckduckgo.com 검색 엔진)를 사용한다.

model = ChatOpenAI(temperature=0)
search = DuckDuckGoSearchAPIWrapper()
chain = (
    {"context": retriever, "question": RunnablePassthrough()}
    | base_prompt
    | model
    | StrOutputParser()
)

구축한 chain에 쿼리를 날려보자

query = "The NBA champion of 2020 is the Los Angeles Lakers! Tell me what is langchain framework?"
june_print(
    'The result of query:', 
    chain.invoke(query)
)
june_print(
    'The result of the searched contexts:', 
    retriever(query)
)

결과를 출력해보면 다음과 같이 나온다.

---------------------------------------------------------------------------------------
The result of query:
I'm sorry, but the context provided does not mention anything about the langchain framework.
---------------------------------------------------------------------------------------
The result of the searched contexts:
The Los Angeles Lakers are the 2020 NBA Champions!Watch their championship celebration here!Subscribe to the NBA: https://on.nba.com/2JX5gSN Full Game Highli... Aug 4, 2023. The 2020 Los Angeles Lakers were truly one of the most complete teams over the decade. LeBron James' fourth championship was one of the biggest moments of his career. Only two players from the 2020 team remain on the Lakers. In the storied history of the NBA, few teams have captured the imagination of fans and left a lasting ... James had 28 points, 14 rebounds and 10 assists, and the Lakers beat the Miami Heat 106-93 on Sunday night to win the NBA finals in six games. James was also named Most Valuable Player of the NBA ... Portland Trail Blazers star Damian Lillard recently spoke about the 2020 NBA "bubble" playoffs and had an interesting perspective on the criticism the eventual winners, the Los Angeles Lakers, faced. But perhaps none were more surprising than Adebayo's opinion on the 2020 NBA Finals. The Heat were defeated by LeBron James and the Los Angeles Lakers in six games. Miller asked, "Tell me about ...

검색 결과 얻은 데이터에는 LangChain 관련 내용이 거의 없다는 뜻으로, 쿼리 재작성을 진행해야 한다.

rewrite_template = """Provide a better search query for \
web search engine to answer the given question, end \
the queries with ’**’. Question: \
{x} Answer:"""
rewrite_prompt = ChatPromptTemplate.from_template(rewrite_template)
def _parse(text):
    return text.strip("**")

rewriter = rewrite_prompt | ChatOpenAI(temperature=0) | StrOutputParser() | _parse
june_print(
    'Rewritten query:', 
    rewriter.invoke({"x": query})

재작성된 쿼리는 다음과 같다.

Rewritten query:
What is langchain framework and how does it work?

langchain을 활용하여 재작성된 쿼리를 사용하는 코드를 작성해보자

rewrite_retrieve_read_chain = (
    {
        "context": {"x": RunnablePassthrough()} | rewriter | retriever,
        "question": RunnablePassthrough(),
    }
    | base_prompt
    | model
    | StrOutputParser()
)
june_print(
    'The result of the rewrite_retrieve_read_chain:', 
    rewrite_retrieve_read_chain.invoke(query)
)

이를 활용한 결과를 출력해보면 다음과 같이 나온다.

--------------------------------------------------------------------------------------
The result of the rewrite_retrieve_read_chain:
LangChain is a Python framework designed to help build AI applications powered by language models, particularly large language models (LLMs). It provides a generic interface to different foundation models, a framework for managing prompts, and a central interface to long-term memory, external data, other LLMs, and more. It simplifies the process of interacting with LLMs and can be used to build a wide range of applications, including chatbots that interact with users naturally.

 

Step-Back Prompting

LLM이 구체적인 세부 사항을 포함한 사례에서 고차원 개념과 기본 원리를 추출하도록 하는 간단한 프롬프트 기법이다. 이 아이디어는 원래 문제에서 파생된 더 추상적인 문제로, "스텝백 문제"를 정의하는 것이라 할 수 있다. 쿼리에 많은 세부 사항이 포함되어 있으면 LLM이 관련 사실을 검색하여 과제를 해결하기 어렵다. 

e.g) 이상 기체 압력 P의 온도가 2배로 증가하고 부피가 8배로 증가하면 어떻게 되는가 ? or 에스텔라 레오폴드는 1954년 8월부터 1954년 11월까지 어느 학교에 다녔는가 ? 

STEP-BACK PROMPTING with two steps of Abstraction and Reasoning guided by concepts and principles, https://miro.medium.com/v2/resize:fit:828/format:webp/1*DnaYNuy-losXF4azq6ntAg.png

두 사례 모두 더 포괄적인 질문을 제기하면 모델이 특정 쿼리에 효과적으로 답변하는 데 도움이 될 수 있다. 

e.g) "에스텔라 레오폴드가 특정 시기에 다녔던 학교는 어디인가?"

=> "에스텔라 레오폴드의 교육 이력은 무엇인가?"

이처럼 더 포괄적인 질문은 일반적으로 원래의 구체적인 질문에 대한 답변을 추론하는 데 필요한 모든 정보를 제공할 수 있으며, 답변하기도 쉽다. Step-back Prompt를 langchain으로 구현해보자

Step 1. Env Setting

import os
os.environ["OPENAI_API_KEY"] = "YOUR_OPEN_AI_KEY"

from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import ChatPromptTemplate, FewShotChatMessagePromptTemplate
from langchain_core.runnables import RunnableLambda
from langchain_openai import ChatOpenAI
from langchain_community.utilities import DuckDuckGoSearchAPIWrapper

Step 2. Construct a chain and execute original queries:

def june_print(msg, res):
    print('-' * 100)
    print(msg)
    print(res)

def retriever(query):
    return search.run(query)

사용할 질문은 'was chatgpt around while trump was president ?' 이다. 기본 프롬프트 템플릿을 정의하자. 

question = "was chatgpt around while trump was president?"
base_prompt_template = """You are an expert of world knowledge. I am going to ask you a question. Your response should be comprehensive and not contradicted with the following context if they are relevant. Otherwise, ignore them if they are not relevant.
{normal_context}
Original Question: {question}
Answer:"""

프롬프트와 검색 엔진을 생성하고, 체인을 구축해서 질문을 던져보자

base_prompt = ChatPromptTemplate.from_template(base_prompt_template)
search = DuckDuckGoSearchAPIWrapper(max_results=4)
base_chain = (
    {
        # Retrieve context using the normal question (only the first 3 results)
        "normal_context": RunnableLambda(lambda x: x["question"]) | retriever,
        # Pass on the question
        "question": lambda x: x["question"],
    }
    | base_prompt
    | ChatOpenAI(temperature=0)
    | StrOutputParser()
)
june_print('The searched contexts of the original question:', retriever(question))
june_print('The result of base_chain:', base_chain.invoke({"question": question}) )

결과는 다음과 같다.

----------------------------------------------------------------------------------------------------
The searched contexts of the original question:
While impressive in many respects, ChatGPT also has some major flaws. ... [President's Name]," refused to write a poem about ex-President Trump, but wrote one about President Biden ... The company said GPT-4 recently passed a simulated law school bar exam with a score around the top 10% of test takers. By contrast, the prior version, GPT-3.5, scored around the bottom 10%. The ... These two moments show how Twitter's choices helped former President Trump. ... With ChatGPT, which launched to the public in late November, users can generate essays, stories and song lyrics ... Donald Trump is asked a question—say, whether he regrets his actions on Jan. 6—and he answers with something like this: " Let me tell you, there's nobody who loves this country more than me ...
----------------------------------------------------------------------------------------------------
The result of base_chain:
Yes, ChatGPT was around while Trump was president. ChatGPT is an AI language model developed by OpenAI and was launched to the public in late November. It has the capability to generate essays, stories, and song lyrics. While it may have been used to write a poem about President Biden, it also has the potential to be used in various other contexts, including generating responses from hypothetical scenarios involving former President Trump.

완전히 잘못된 답변을 출력하고 있는 것을 볼 수 있다. step_back_question_chain과 step_back_chain을 구성해보자. Step-back Prompting을 위해, few-shot 예제들을 정의해준다.

# Few Shot Examples
examples = [
    {
        "input": "Could the members of The Police perform lawful arrests?",
        "output": "what can the members of The Police do?",
    },
    {
        "input": "Jan Sindel’s was born in what country?",
        "output": "what is Jan Sindel’s personal history?",
    },
]
# We now transform these to example messages
example_prompt = ChatPromptTemplate.from_messages(
    [
        ("human", "{input}"),
        ("ai", "{output}"),
    ]
)
few_shot_prompt = FewShotChatMessagePromptTemplate(
    example_prompt=example_prompt,
    examples=examples,
)

정의한 few-shot example을 활용해 step back 프롬프트를 구상하자

step_back_prompt = ChatPromptTemplate.from_messages(
    [
        (
            "system",
            """You are an expert at world knowledge. Your task is to step back and paraphrase a question to a more generic step-back question, which is easier to answer. Here are a few examples:""",
        ),
        # Few shot examples
        few_shot_prompt,
        # New question
        ("user", "{question}"),
    ]
)

step back question 체인을 구축하고, 구축한 체인을 활용하여 정보를 추출해보자

step_back_question_chain = step_back_prompt | ChatOpenAI(temperature=0) | StrOutputParser()
june_print('The step-back question:', step_back_question_chain.invoke({"question": question}))
june_print('The searched contexts of the step-back question:', retriever(step_back_question_chain.invoke({"question": question})) )

 

response_prompt_template = """You are an expert of world knowledge. I am going to ask you a question. Your response should be comprehensive and not contradicted with the following context if they are relevant. Otherwise, ignore them if they are not relevant.
{normal_context}
{step_back_context}
Original Question: {question}
Answer:"""
response_prompt = ChatPromptTemplate.from_template(response_prompt_template)
step_back_chain = (
    {
        # Retrieve context using the normal question
        "normal_context": RunnableLambda(lambda x: x["question"]) | retriever,
        # Retrieve context using the step-back question
        "step_back_context": step_back_question_chain | retriever,
        # Pass on the question
        "question": lambda x: x["question"],
    }
    | response_prompt
    | ChatOpenAI(temperature=0)
    | StrOutputParser()
)
june_print('The result of step_back_chain:', step_back_chain.invoke({"question": question}) )

결과는 다음과 같고, 응답이 많이 개선된 것을 확인할 수 있다.

----------------------------------------------------------------------------------------------------
The step-back question:
When did ChatGPT become available?
----------------------------------------------------------------------------------------------------
The searched contexts of the step-back question:
OpenAI released an early demo of ChatGPT on November 30, 2022, and the chatbot quickly went viral on social media as users shared examples of what it could do. Stories and samples included ... March 14, 2023 - Anthropic launched Claude, its ChatGPT alternative. March 20, 2023 - A major ChatGPT outage affects all users for several hours. March 21, 2023 - Google launched Bard, its ... The same basic models had been available on the API for almost a year before ChatGPT came out. In another sense, we made it more aligned with what humans want to do with it. A paid ChatGPT Plus subscription is available. (Image credit: OpenAI) ChatGPT is based on a language model from the GPT-3.5 series, which OpenAI says finished its training in early 2022.
----------------------------------------------------------------------------------------------------
The result of step_back_chain:
No, ChatGPT was not around while Trump was president. ChatGPT was released to the public in late November, after Trump's presidency had ended. The references to ChatGPT in the context provided are all dated after Trump's presidency, such as the release of an early demo on November 30, 2022, and the launch of ChatGPT Plus subscription. Therefore, it is safe to say that ChatGPT was not around during Trump's presidency.

 

Query2Doc

LLM의 몇 가지 프롬프트를 사용하여 가상 문서를 생성한 다음 이를 원래 쿼리와 병합하여 새로운 쿼리를 구성한다. 대형 언어 모델을 사용한 쿼리 확장이라 할 수 있으며, 이는 LLM의 몇 가지 프롬프트를 사용하여 가상 문서를 생성한 다음, 이를 원래 쿼리와 결합하여 새로운 쿼리를 생성한다.

query2doc few-shot prompting, https://miro.medium.com/v2/resize:fit:720/format:webp/1*jtNdHW5yA3raGEuVAGYbbA.png

밀집 검색(Dense Retrieval)에서 새 쿼리 q+는 원래 쿼리(q)와 가상 문서(d')를 [SEP]로 구분하여 단순히 연결한 것이다: Query2doc는 HyDE가 기본적으로 정답 문서와 가상 문서가 동일한 의미를 다른 단어로 표현한다고 가정한다. Query2doc와 HyDE의 또 다른 차이점은 Query2doc가 논문에 설명된 대로 지도 학습된 밀집 검색기를 훈련시킨다는 것이며, 해당 기능은 아직 LangChain이나 LlamaIndex에 구현되어 있지 않다.

 

ITER-RETGEN

이전 세대의 결과를 이전 쿼리와 결합하고, 이후 관련 문서를 검색하여 새로운 결과를 생성하는 과정을 여러 번 반복하여 최종 결과를 얻는다. ITER-RETGEN 접근법은 생성된 콘텐츠를 사용하여 검색을 안내하는데, "검색 강화 생성"과 "생성 강화 검색"을 Retrieve-Read-Retrieve-Read 흐름 내에서 반복적으로 구현한다.

ITER-RETGEN iterates retrieval and generation. In each iteration, https://miro.medium.com/v2/resize:fit:4800/format:webp/1*baWXFVpZ1Ju7wYhwRcFFNA.png

주어진 질문 q와 검색 말뭉치 D = {d} (d=단락)에 대해, ITER-RETGEN은 연속적으로 T번의 검색 생성을 수행한다. 각 반복 t에서, 먼저 이전 반복에서 생성된 yt-1을 사용하여 q와 결합하고, 상위 k개의 단락을 검색한다. 다음으로, LLM M에게 검색된 단락들(Dyt-1||q로 표현됨)과 q를 프롬프트에 포함시켜 출력을 생성하도록 한다. 최종 응답으로 yt가 생성되며, ITER-RETGEN 또한 Query2doc과 마찬가지로 LlamaIndex와 LangChain에서 지원하지 않는다.

 

여기서 소개한 기술 이외에도, 쿼리 라우팅, 쿼리를 여러 하위 질문으로 분해하는 등의 몇 가지 방법이 있다. 이것들은 쿼리 재작성에 속하지 않지만, 사전 검색 방법에 해당하며 쿼리를 보다 정제하는데 도움을 줄 수 있다.


Ref. 

https://medium.com/@florian_algo/advanced-rag-06-exploring-query-rewriting-23997297f2d1

'AI > Natural Language Processing' 카테고리의 다른 글

Self-RAG (이론)  (0) 2024.06.17
Exploring RAG for Tables (이론)  (0) 2024.06.14
Exploring Semantic Chunking (이론)  (0) 2024.06.12
Re-ranking (이론)  (0) 2024.06.11
Using RAGAs + LlamaIndex for RAG evaluation (이론)  (1) 2024.06.11

블로그의 정보

코딩하는 오리

Cori

활동하기