[랭체인 코리아 밋업 2024 Q2] NaiveRAG부터 Advanced RAG 톺아보기(with code)

2024. 7. 2. 12:29Lecture

Naive RAG부터 Advanced RAG 톺아보기

👉 영상(27:32초부터 ): 랭체인 코리아 밋업 2024 Q2

👉 강의 자료: https://speakerdeck.com/hyerimbaek/langchainkr-2024q2-native-rag-to-advanced-rag-topabogi

 

🪄강의 자료는 연사자분께 개인 블로그 목적으로 사용 가능함을 허락받고 공유하게 되었습니다🪄


연사자

백혜림님

강의 내용

  1. Basic LLM
  2. Native RAG
  3. Advanced RAG
  4. ETC(MultiQuery Retriever, Decomposition, RAG-Fusion, Self-Query)
  5. Colab 예제

코랩 노트북 환경 설정

# 필요 패키지 설치
! pip install langchain_community tiktoken langchain-openai langchainhub chromadb langchain

 

# 필요 패키지 import
import os
import bs4
from langchain import hub
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain_community.document_loaders import WebBaseLoader
from langchain_community.vectorstores import Chroma
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables import RunnablePassthrough
from langchain_openai import ChatOpenAI, OpenAIEmbeddings

# OpenAPI Key 설정
os.environ['OPENAI_API_KEY'] = 'OPENAI_API_KEY'

Basic LLM

RAG가 없는 LLM의 생성

✅ LLM에 Question 넣기 ✅ 잘 출력할 수 있도록 프롬프트 추가 ✅ 잘못된 출력. 주최자 이름만 출력

✅ 왜 이런 결과가 나올까? 

  • LLM은 최신 정보 반영하지 못하는 한계 가짐
  • 이런 현상을 'Hallucination(환각 현상)'. 원하는 출력 나오지 않는 현상

Hallucination

✅ 부정확한 정보 생성하는 현상

✅ Hallucination 현상 줄일 수 있는 대표적인 2가지 방법

1️⃣ LLM을 Fine-tuning하기

💡 문제점 LLM은 거대 모델로 가고 있음. 파인튜닝 💸 많이 듬(A100 등등)

2️⃣ RAG 기법 활용

💡 외부 지식 추가하는 방법


RAG(Retrieval-Augmented Generation)

✅ 검색, 증강, 생성


Native RAG

✅ RAG 발전시키고 싶으면 huggingface LLM 모델 사용, 임베딩 직접 훈련해서 교체, search 기법 변경하는 방법 등이 있음

✅ 외부지식, 검색 과정을 추가해 RAG 기법 활용

  • 외부지식은 PDF, 위키피디아, sql 등 다양함

✅ 외부 지식 안에서 사용자 question과 관련된 검색한 후, 프롬프트로 생성해서 LLM에게 전달


Splitter

✅ LLM에는 제한된 토큰 수(모델별 상이)가 있어, 외부지식이 그대로 LLM에 들어갈 수 없음
외부 지식 로드하고 분할하는 과정 필요
많이 사용하는 method? textsplitter
✅ chunk_size 지정(RecursiveCharacterTextSplitter 활용)
✅ Recursive? 단어 기준 청크 사이즈에 기반해 재귀적 분할
ex) One의 character=3, chunk_size보다 character가 적은 4, 5일 때에는 One을 1개의 chunk로

✅ textsplitter? 단어 상관없이 chunk_size 기반해 분할

✅ chunk_size 중요한 이유? 단어간 의미 살릴 수 있음. 문장 단위의 의미론적 의미 살릴 수 있음

의미 살리기 위해 chunk_size 지정 중요


코드

# 문서 업로드
loader = WebBaseLoader("https://aifactory.space/task/4239/overview", encoding="utf-8")
docs = loader.load()
# 분할하기
text_splitter = RecursiveCharacterTextSplitter(chunk_size=100, chunk_overlap=0)
splits = text_splitter.split_documents(docs)
# splits 개수 조회: 9
len(splits)

Indexing, Retrieval, Generation

Embedding
✅ split된 chunk를 컴퓨터가 이해할 수 있는 벡터 형태로 변경

✅ 의미론적으로 유사한 chunk의 경우, 벡터 공간 상에서 인접해 있음
✅ embedding 방법에 따라 성능 천차만별

Vector DB
✅ embedding 된 내용들을 저장

Indexing

VectorDB에 저장된 벡터들의 연관성 파악하는 indexing

Reterieval
코사인 유사도 등에 기반해 Question과 관련 있는 Vector DB 안에서 검색
위 k개 문서 반환

Generation
✅ LLM은 Question과 상위 k개의 문서를 조합해 답변 생성

코드

#임베딩
open_ai_embedding = OpenAIEmbeddings()
#리트리버
vectorstore = Chroma.from_documents(documents=splits,
                                    embedding=open_ai_embedding)

retriever = vectorstore.as_retriever()

 

#생성
prompt = hub.pull("rlm/rag-prompt")
prompt

# ChatPromptTemplate(input_variables=['context', 'question'], metadata={'lc_hub_owner': 'rlm', 'lc_hub_repo': 'rag-prompt', 'lc_hub_commit_hash': '50442af133e61576e74536c6556cefe1fac147cad032f4377b60c436e6cdcb6e'}, messages=[HumanMessagePromptTemplate(prompt=PromptTemplate(input_variables=['context', 'question'], template="You are an assistant for question-answering tasks. Use the following pieces of retrieved context to answer the question. If you don't know the answer, just say that you don't know. Use three sentences maximum and keep the answer concise.\nQuestion: {question} \nContext: {context} \nAnswer:"))])
# Post-processing
def format_docs(docs):
    return "\n\n".join(doc.page_content for doc in docs)
# Chain
rag_chain = (
    {"context": retriever | format_docs, "question": RunnablePassthrough()}
    | prompt
    | llm
    | StrOutputParser()
)
# 체인에 질문하고 답변받기
rag_chain.invoke("랭체인 연사자가 누구야?")

# WARNING:chromadb.segment.impl.vector.local_hnsw:Number of requested results 4 is greater than number of elements in index 2, updating n_results = 2
김태영, 유현아입니다.

Native RAG의 한계

✅ Native RAG 만으로 모든 hallucination 해결할 수 없음

✅ 외부 지식이 거대해지면 RAG는 hallucination 문제에서 벗어날 수 없음

✅ LLM 자체는 이전 단어에 기반해 다음 단어 예측하는 생성 모델이기 때문


Advanced RAG

✅ Native RAG 한계 극복 위한 방안들

Retrieval 기준으로 앞에 Pre-Retrieval, 뒤에 Post-Retrieval

✅ 생성하고 난후, 생성된 결과를 강화하는 방법


Pre-Retrieval

✅ chunking 단계에서, chunk overlap 설정(chunk 자를 때, 앞에 chunk와 겹쳐지는 character의 개수)

✅ 메타 데이터 활용 여부(필터링 등)

✅ query를 어떻게 대폭 늘려서 augmentation할 것인지


Retrieval

✅ 검색 방법 여러 가지 형태

✅ Hybrid Search, elastic search(elastic으로 인덱스 메기는 것), FAISS, FT Embedding(자체를 파인튜닝 임베딩으로)


Post-Retrieval

✅ Re-ranking: 벡터DB에서 상위 연관된 문서 받았지만, 다시 순서를 재설정하는 방식

✅ Filtering: 불필요한 문서 제거


Generation

✅ 이 문장이 hallucination인지 아닌지 검증하는 단계 등


Hybrid Search

✅ Question을 2가지 방법으로 쪼갬

1️⃣ Vector Search(Embedding 방식)

  • Dense vector. 벡터들이 숫자로 꽉 차 있다

2️⃣ Keyword Search(키워드 기반 검색 방식)

단어 수 기반으로 키워드 검색

  • Sparse Vector
  • 대표적인 방법으로는 TF-IDF, BM25

👉 참고: [Pinecone Docs] Understanding hybrid search\

 

Understanding hybrid search - Pinecone Docs

Hybrid search and sparse vectors Understanding hybrid search

docs.pinecone.io

✅ 위의 2가지 방법(Vector Search, Keyword Search)을 합쳐 Vector DB와 연관된 문서 받음

✅ 가중치를 변경하는 smoothing 방법으로 더해서 vector DB에 넣는 방식

 


Long context Reorder

✅ RAG의 한계? 검색된 문서 수 많아질수록 성능이 크게 저하되는 문제점

  • 모델에게 긴 context를 줄때, 중간에 있는 중요한 정보 놓치기 쉬움
  • 이런 현상들을 "lost in the middle" 문제라고 함(우측 상단 '20 total retrieved Documents' 그래프 참고)
  • 이 문제 해결하기 위해 검색된 문서를 다시 정렬

✅ Vector DB에서 가장 연관성이 높은 A, B, C, D를 출력

  • 상위 2개가 문서와 가장 관련성이 높은 것(A, B)
  • Re-order 방법을 사용해 가장 관련성이 높은 문서를 가장 처음과 마지막에 두는 것
  • 덜 관련성이 있는 문서(C,D)를 가운데에 배치

✅ LLM이 중요한 문장을 더 잘 파악하도록 재정렬하는 방식


Re-ranking

✅ Vector DB에서 가장 연관성 높은 4가지를 뽑은 후, Re-ranking 모델로 순위 재정렬하는 방식

  • 연관도 높은 문서 순서인 A, B, C, D를 재정렬했더니 B,C가 오히려 성능 좋음
  • 성능 좋은 B,C만 사용하는 방식

✅ 보통 re-ranking 모델로, hugging face 학습 모델 사용(학습된 cohere 모델 사용하기도 함)


그외 다양한 방법들

 

✅ 다양한 RAG 기법들이 있음

✅ Query 구축하는 방법(Query Construction), 쿼리 증폭하는 방법(Query Translation), Routing, Indexing, Retrieval, Generation 등


Query Translation

Multi Query Retriever

✅ Question의 의미론적으로 다양하게 해석될 수 있음
✅ Question의 의미를 다각도로 확장(패러프레이징)해 Vector DB에 입력

LLM/프롬프트로 Query를 다양한 관점에서 생성하고, 생성된 query를 Vector DB에 넣어서 retriever로 관련 문서 얻음
✅ LLM 입력으로 넣어서 답변 받는 방식

 


코드

# load
loader = WebBaseLoader("https://aifactory.space/task/4239/overview", encoding="utf-8")
docs = loader.load()

# Split
text_splitter = RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=0)
splits = text_splitter.split_documents(docs)

# VectorDB
openai_embedding = OpenAIEmbeddings()
vectorstore = Chroma.from_documents(documents=splits,
                                    embedding=openai_embedding)
#LangChain MultiQueryRetriver 사용
from langchain.retrievers.multi_query import MultiQueryRetriever
from langchain_openai import ChatOpenAI

question = "랭체인 연사자는 누구인가요?"
llm = ChatOpenAI(
    temperature= 0,
    max_tokens = 2048,
    model_name = "gpt-3.5-turbo",
)
retriever_from_llm = MultiQueryRetriever.from_llm(
    retriever=vectorstore.as_retriever(), llm=llm
)
# Set logging for the queries
import logging

logging.basicConfig()
logging.getLogger("langchain.retrievers.multi_query").setLevel(logging.INFO)
unique_docs = retriever_from_llm.invoke(question)
len(unique_docs)

#INFO:langchain.retrievers.multi_query:Generated queries: ['1. 랭체인 연사자의 실명은 무엇인가요?', '2. 랭체인 연사자의 역할은 무엇인가요?', '3. 랭체인 연사자의 경력은 어떻게 되나요?']
4

직접 프롬프트로 MultiQuery 생성하기

retriever = vectorstore.as_retriever()
from langchain.prompts import ChatPromptTemplate

# Multi Query: 다른 시각으로 보는 템플릿을 작성하자!
template = """You are an AI language model assistant. Your task is to generate five
different versions of the given user question to retrieve relevant documents from a vector
database. By generating multiple perspectives on the user question, your goal is to help
the user overcome some of the limitations of the distance-based similarity search.
Provide these alternative questions separated by newlines. Original question: {question}"""
prompt_perspectives = ChatPromptTemplate.from_template(template)

from langchain_core.output_parsers import StrOutputParser
from langchain_openai import ChatOpenAI

generate_queries = (
    prompt_perspectives
    | ChatOpenAI(temperature=0)
    | StrOutputParser()
    | (lambda x: x.split("\n"))
)
generate_queries.invoke("랭체인 연사자는 누구인가요?")

['1. 랭체인 연사자의 신분은 무엇인가요?',
 '2. 랭체인 연사자의 신원은 무엇인가요?',
 '3. 랭체인 연사자의 신분을 알려주세요.',
 '4. 랭체인 연사자에 대해 자세히 알려주세요.',
 '5. 랭체인 연사자의 정보를 찾고 있습니다.']
from langchain.load import dumps, loads

def get_unique_union(documents: list[list]):
    """ retrieved docs들을 하나의 문서로 합치는 과정 """
    # 리스트의 리스트를 쭉 펼치고, 각 문서를 문자열로 변환한다.
    flattened_docs = [dumps(doc) for sublist in documents for doc in sublist]
    # 유니크한 문서를 얻는 과정
    unique_docs = list(set(flattened_docs))
    # Return
    return [loads(doc) for doc in unique_docs]

# Retrieve
question = "랭체인 연사자는 누구인가요?"
retrieval_chain = generate_queries | retriever.map() | get_unique_union
docs = retrieval_chain.invoke({"question":question})
len(docs)

# 3
from operator import itemgetter
from langchain_openai import ChatOpenAI
from langchain_core.runnables import RunnablePassthrough

# RAG
template = """Answer the following question based on this context:

{context}

Question: {question}
"""

prompt = ChatPromptTemplate.from_template(template)

llm = ChatOpenAI(temperature=0)

final_rag_chain = (
    {"context": retrieval_chain,
     "question": itemgetter("question")}
    | prompt
    | llm
    | StrOutputParser()
)

final_rag_chain.invoke({"question":question})

# 랭체인 연사자는 랭체인코리아 (LangChain KR) 입니다.

Decomposition

✅ Question을 2가지 관점으로 분해시키는 것 1️⃣ 랭체인 에이전트 관련 질문
2️⃣ 랭그래프 관련 질문

✅ 분해 방식? LLM에 집어넣어 프롬프트 통해서 '이 질문을 2가지로 분해하고 싶은데 생성해줘' 식의 query 날리게 됨

  • 복잡한 질문의 경우, 1번의 검색 단계로 해결되지 않을 수도 있음
  • 문제를 순차적으로 (첫번째 답변 + 검색을 사용해서 2번째 답변에 답변) 혹은 병렬로(각 답변을 최종답변으로 통합) 해결
  • Least-to-Most Prompting 혹은 IR-CoT 활용할 수 있음

✅ Query 날린 결과? sub-query 3개 출력한 결과를 Vector DB에 넘겨 각각 retriever를 받아서 합차셔 LLM 입력으로 넣게 됨


코드

from langchain.prompts import ChatPromptTemplate

# Decomposition (Least to mode prompt사용해도 무방!)
template = """You are a helpful assistant that generates multiple sub-questions related to an input question. \n
The goal is to break down the input into a set of sub-problems / sub-questions that can be answers in isolation. \n
Generate multiple search queries related to: {question} \n
Output (3 queries):"""
prompt_decomposition = ChatPromptTemplate.from_template(template)
from langchain_openai import ChatOpenAI
from langchain_core.output_parsers import StrOutputParser

# LLM
llm = ChatOpenAI(temperature=0)

# Chain
generate_queries_decomposition = ( prompt_decomposition | llm | StrOutputParser() | (lambda x: x.split("\n")))

# Run
question = "랭체인(LangChain)과 랭그래프(LangGraph)는 무엇인가요?"
questions = generate_queries_decomposition.invoke({"question":question})
questions

['1. 랭체인(LangChain)이란 무엇인가요?',
 '2. 랭그래프(LangGraph)는 어떤 기술 또는 시스템인가요?',
 '3. 랭체인(LangChain)과 랭그래프(LangGraph)의 차이점은 무엇인가요?']

RAG-Fusion

✅ Re-ranking(순서 재정렬) 방법 중 1개인 RRF(Reciprocal Rank Fusion)방식 추가

✅ Query를 각각 만들어서 Vector DB에 넣으면 Query별 검색 결과 나옴

✅ 검색된 문서들의 ranking 점수를 RRF 방법으로 재정렬


Reciprocal Rank Fusion(RRF)

✅ Retrieve Rankings 통해 검색된 문서들의 랭킹 정렬하는 방법

✅ 우측 상단 그래프 보면 System 1, System 2, System 3 거치며 랭킹이 변화함

✅ Query별 환경 제각각(SQL, 씨퀄 등)이기에 같은 랭킹 방법으로 나눌 수 없음

✅ 그래서 도입한 것이 RRF Score

  • 각각 리트리버 시스템에 있는 rank로 냐눠서 각 score의 분산 맞춰주는 방식
  • 상대적인 ranking 점수를 매칭해서, 마지막 fuse ranking 부분에서 합치게 됨

코드

from langchain.prompts import ChatPromptTemplate

# RAG-Fusion: Related
template = """You are a helpful assistant that generates multiple search queries based on a single input query. \n
Generate multiple search queries related to: {question} \n
Output (4 queries):"""
prompt_rag_fusion = ChatPromptTemplate.from_template(template)
from langchain_core.output_parsers import StrOutputParser
from langchain_openai import ChatOpenAI

generate_queries = (
    prompt_rag_fusion
    | ChatOpenAI(temperature=0)
    | StrOutputParser()
    | (lambda x: x.split("\n"))
)
from langchain.load import dumps, loads

def reciprocal_rank_fusion(results: list[list], k=60):
    """ RRF공식에서 사용되는 파라미터 k로 여러 순위 문서들을 처리하는 Reciprocal_rank_fusion"""

    # 각 유니크 문서에 대한 fused score를 저장하기 위해 딕셔너리 초기화
    fused_scores = {}

    # 각 ranked 문서들을 반복해라
    for docs in results:
        # 리스트에서 각 문서를 rank로 반복 (리스트의 위치에서)
        for rank, doc in enumerate(docs):
            # 문서를 문자열 형식으로 변환하여 키로 사용
            doc_str = dumps(doc)
            # 문서가 아직 fused_scores 딕셔너리에 없으면 초기 점수 0으로 추가
            if doc_str not in fused_scores:
                fused_scores[doc_str] = 0
            # 문서의 현재 점수가 있다면 검색하기
            previous_score = fused_scores[doc_str]
            # RRF 공식: 1 / (rank + k) 을 사용하여 문서의 점수를 업데이트
            fused_scores[doc_str] += 1 / (rank + k)

    # 최종 재순위 결과를 얻기 위해 문서들을 fused_scores를 기준으로 내림차순으로 정렬.
    reranked_results = [
        (loads(doc), score)
        for doc, score in sorted(fused_scores.items(), key=lambda x: x[1], reverse=True)
    ]

    # 문서와 fused_scores를 각각 포함하는 튜플의 리스트로 재순위된 결과를 반환합니다
    return reranked_results

retrieval_chain_rag_fusion = generate_queries | retriever.map() | reciprocal_rank_fusion
docs = retrieval_chain_rag_fusion.invoke({"question": question})
len(docs)
from langchain_core.runnables import RunnablePassthrough

# RAG
template = """Answer the following question based on this context:

{context}

Question: {question}
"""

prompt = ChatPromptTemplate.from_template(template)

final_rag_chain = (
    {"context": retrieval_chain_rag_fusion,
     "question": itemgetter("question")}
    | prompt
    | llm
    | StrOutputParser()
)

final_rag_chain.invoke({"question":question})

랭체인(LangChain)은 랭체인코리아(LangChain KR)가 주관하는 밋업 행사의 이름이며, 랭그래프(LangGraph)는 이경록이 발표하는 주제 중 하나인 고급RAG와 관련된 주제로, 다중 에이전트와 LangGraph 제작에 대한 내용을 다룹니다.

Self-Query

✅ 의미상 조회하려는 내용 + 필터링하려는 메타데이터까지 포함
✅ 의미론적 단어? "랭체인 발표주제"
✅ 메타데이터에서 참조할 대상? "2024"
✅ Query 안에서 필요한 내용을 필터링해서 Vector DB로 넘겨서 찾는 방식
✅ 메타데이터가 있어야지 적용할 수 있는 방식

 

코드

%pip install --upgrade --quiet  lark langchain-chroma
from langchain.schema import Document
embeddings = OpenAIEmbeddings()
docs = [
    Document(
        page_content="시간을 아껴주는 말하는 앵무새 :: 유튭 정리에서 데이터 시각화까지",
        metadata={"name":"전미정", "year": 2023},
    ),
    Document(
        page_content="Whisper보다 6배빠른 ditil-Whisper로 오디오데이터에서 RAG 수행기",
        metadata={"name":"백혜림", "year": 2023},
    ),
    Document(
        page_content="Native RAG부터 Advanced RAG 톺아보기",
        metadata={"name":"백혜림", "year": 2024},
    ),
    Document(
        page_content="초보자도 할 수 있는 고급RAG : 다중 에이전트와 LangGraph 제작",
        metadata={"name":"이경록", "year": 2024},
    ),
    Document(
        page_content="한국어 오픈액세스 LM의 시각과 그 이후",
        metadata={"name":"이준범", "year": 2023},
    ),
    Document(
        page_content="LLM으로 LLM을 해킹했습니다.",
        metadata={"name":"백승윤", "year": 2023},
    ),
    Document(
        page_content="LCEL 치트시트",
        metadata={"name":"김태영", "year": 2024},
    ),
]
vectorstore = Chroma.from_documents(docs, embeddings)
from langchain.llms import OpenAI
from langchain.retrievers.self_query.base import SelfQueryRetriever
from langchain.chains.query_constructor.base import AttributeInfo

metadata_field_info = [
    AttributeInfo(
        name="name",
        description="랭체인 연사자 이름",
        type="string or list[string]",
    ),
    AttributeInfo(
        name="year",
        description="랭체인발표 진행한 년도",
        type="integer",
    )
]
document_content_description = "랭체인코리아 발표자 목록"
llm = ChatOpenAI(temperature=0)

retriever = SelfQueryRetriever.from_llm(
    llm,
    vectorstore,
    document_content_description,
    metadata_field_info,
    verbose=True
)
# This example only specifies a relevant query
retriever.invoke("2024년에 발표한 랭체인 발표주제는 무엇인가요?")

✅ 다양한 RAG 방법들이 있음

✅ RAG 성능이 안 좋을때, 다양한 방법 활용해 성능 개선 필요


 

✅ Advanced RAG를 넘어서 Modular RAG으로 넘어가는 추세

✅ 모듈별로 묶어서 RAG 진행하고 있음