#
GitHub 이슈를 위한 EEVE와 LangChain을 사용한 간단한 RAG

본문: Maria Khalusova의 Simple RAG for GitHub issues using Hugging Face Zephyr and LangChain을 번역하고, 한국어 모델로 변경했습니다.

번역: 신혁준

이 노트북은 `yanolja/EEVE-Korean-Instruct-10.8B-v1.0` 모델과 LangChain을 사용하여 프로젝트의 GitHub 이슈에 대한 RAG(검색 기반 생성)를 빠르게 구축하는 방법을 보여줍니다.

RAG란 무엇인가?

RAG는 강력한 대규모 언어 모델(LLM)이 특정 콘텐츠를 인식하지 못하거나 해당 콘텐츠를 훈련 데이터에 포함되지 않아서, 혹은 그 콘텐츠를 본 적이 있어도 잘못된 정보를 생성할 때 이러한 문제를 해결하기 위한 인기 있는 접근 방식입니다. 이러한 특정 콘텐츠는 독점적이거나 민감할 수 있으며, 이 예시와 같이 최근의 자주 업데이트되는 데이터일 수 있습니다.

만약 데이터가 정적이고 정기적으로 변경되지 않는 경우, 대규모 모델을 미세 조정(fine-tuning)하는 것을 고려할 수 있습니다. 그러나 많은 경우, 미세 조정은 비용이 많이 들며 데이터 드리프트 문제를 해결하기 위해 반복적으로 수행할 경우 “모델 이동(model shift)“이 발생할 수 있습니다. 이는 모델의 동작이 바람직하지 않은 방식으로 변하는 현상입니다.

RAG (검색 증강 생성)은 모델의 미세 조정이 필요하지 않습니다. 대신, RAG는 LLM에 추가적인 컨텍스트를 제공하기 위해 관련 데이터를 검색하여 더 잘 정보를 제공하는 응답을 생성할 수 있도록 합니다.

다음은 간단한 설명입니다:

- 외부 데이터는 별도의 임베딩 모델을 사용해 임베딩 벡터로 변환되며, 벡터는 데이터베이스에 저장됩니다. 임베딩 모델은 일반적으로 작아서 임베딩 벡터를 정기적으로 업데이트하는 것이 모델을 미세 조정하는 것보다 빠르고 저렴하며 간편합니다.

- 동시에, 미세 조정이 필요하지 않다는 점은 더 강력한 LLM이 출시되면 이를 자유롭게 교체하거나 더 빠른 추론이 필요한 경우 더 작은 증류된 모델로 전환할 수 있는 유연성을 제공합니다.

이제 오픈 소스 LLM, 임베딩 모델, LangChain을 사용하여 RAG를 구축하는 방법을 살펴보겠습니다.

먼저, 필요한 의존성을 설치합니다:

    !pip install -q torch transformers accelerate bitsandbytes transformers sentence-transformers faiss-gpu

    # Google Colab에서 실행하는 경우 LangChain을 설치하기 위해 UTF-8 로케일을 사용하고 있는지 확인하기 위해 이 셀을 실행해야 할 수도 있습니다.
    import locale

    locale.getpreferredencoding = lambda: "UTF-8"

    !pip install -q langchain langchain-community

##
데이터 준비

이 예시에서는 PEFT 라이브러리의 저장소에서 모든 이슈(Open 이슈와 Close 이슈 모두)를 로드할 것입니다.

먼저, GitHub API에 접근하기 위해 GitHub 개인 접근 토큰을 발급받아야 합니다.

    from getpass import getpass

    ACCESS_TOKEN = getpass("YOUR_GITHUB_PERSONAL_TOKEN")

다음으로, huggingface/peft 저장소의 모든 이슈를 로드하겠습니다:

- 기본적으로 풀 리퀘스트도 이슈로 간주되지만, `include_prs=False`로 설정하여 데이터를 가져올 때 이를 제외하도록 설정합니다.
- `state = "all"`로 설정하면 열린 이슈와 닫힌 이슈 모두를 로드하게 됩니다.

    from langchain.document_loaders import GitHubIssuesLoader

    loader = GitHubIssuesLoader(repo="huggingface/peft", access_token=ACCESS_TOKEN, include_prs=False, state="all")

    docs = loader.load()

개별 GitHub 이슈의 내용은 임베딩 모델이 입력으로 받을 수 있는 길이보다 길 수 있습니다. 모든 내용을 임베딩하려면 문서를 적절한 크기의 조각으로 나누어야 합니다.

가장 일반적이고 간단한 문서 나누기(청킹) 방법은 고정된 크기의 청크를 정의하고, 이 청크별 GitHub 이슈의 내용은 임베딩 모델이 입력으로 받을 수 있는 길이보다 길 수 있습니다. 모든 내용을 임베딩하려면 문서를 적절한 크기의 조각으로 나누어야 합니다.

가장 일반적이고 간단한 문서 나누기(청킹) 방법은 고정된 크기의 청크를 정의하고, 이 청크들 간에 겹침이 있을지 여부를 결정하는 것입니다. 청크들 사이에 일부 내용의 중복을 유지하면 청크들 간의 의미적 문맥을 보존할 수 있습니다. 일반적인 텍스트에 대한 추천 분할기는 RecursiveCharacterTextSplitter이며, 여기에서도 이를 사용할 것입니다.

    from langchain.text_splitter import RecursiveCharacterTextSplitter

    splitter = RecursiveCharacterTextSplitter(chunk_size=512, chunk_overlap=30)

    chunked_docs = splitter.split_documents(docs)

##
임베딩 및 검색기 생성

이제 문서가 모두 적절한 크기로 나누어졌으니, 해당 문서들의 임베딩을 사용하여 데이터베이스를 생성할 수 있습니다.

문서 조각의 임베딩을 생성하기 위해 `HuggingFaceEmbeddings`와 `BAAI/bge-m3` 임베딩 모델을 사용할 것입니다. Hugging Face Hub에는 이 외에도 다양한 임베딩 모델이 있으며, Massive Text Embedding Benchmark (MTEB) 리더보드를 통해 성능이 좋은 모델들을 확인할 수 있습니다.

벡터 데이터베이스를 생성하기 위해서는 Facebook AI가 개발한 `FAISS` 라이브러리를 사용할 것입니다. 이 라이브러리는 밀집 벡터의 유사성 검색과 클러스터링을 효율적으로 처리하며, 이는 우리가 여기서 필요한 기능입니다. FAISS는 현재 대규모 데이터셋에서 최근접 이웃(NN) 검색을 위해 가장 널리 사용되는 라이브러리 중 하나입니다.

우리는 LangChain API를 통해 임베딩 모델과 FAISS에 접근할 것입니다.

    from langchain.vectorstores import FAISS
    from langchain.embeddings import HuggingFaceEmbeddings

    db = FAISS.from_documents(chunked_docs, HuggingFaceEmbeddings(model_name="BAAI/bge-base-en-v1.5"))

비정형 쿼리를 입력받아 해당 문서를 반환(검색)할 방법이 필요합니다. 이를 위해 `db`를 백엔드로 사용하여 `as_retriever` 메서드를 사용할 것입니다:

- `search_type="similarity"`는 쿼리와 문서 사이의 유사성 검색을 수행하고자 한다는 것을 의미합니다.
- `search_kwargs={'k': 4}`는 검색기가 상위 4개의 결과를 반환하도록 지시하는 설정입니다.

    retriever = db.as_retriever(search_type="similarity", search_kwargs={"k": 4})

벡터 데이터베이스와 검색기가 설정되었으니, 이제 체인의 다음 부분인 모델을 설정해야 합니다.

##
양자화된 모델 로드

이번 예시에서는 한국어 모델로 `yanolja/EEVE-Korean-Instruct-10.8B-v1.0`를 선택했습니다.

매주 여러 모델이 새롭게 출시되므로, 최신 모델로 교체하고 싶을 수 있습니다. 오픈소스 LLM의 최신 동향을 파악하는 가장 좋은 방법은 [한국어 오픈소스 LLM 리더보드](https://huggingface.co/spaces/upstage/open-ko-llm-leaderboard, https://lk.instruct.kr/)를 확인하는 것입니다.

추론 속도를 빠르게 하고, Colab 실행을 위해서 양자화된 버전의 모델을 로드할 것입니다:

    import torch
    from transformers import AutoTokenizer, AutoModelForCausalLM, BitsAndBytesConfig

    model_name = "yanolja/EEVE-Korean-Instruct-10.8B-v1.0"

    bnb_config = BitsAndBytesConfig(
        load_in_4bit=True, bnb_4bit_use_double_quant=True, bnb_4bit_quant_type="nf4", bnb_4bit_compute_dtype=torch.bfloat16
    )

    model = AutoModelForCausalLM.from_pretrained(model_name, quantization_config=bnb_config)
    tokenizer = AutoTokenizer.from_pretrained(model_name)

##
LLM 체인 설정

마침내, LLM 체인을 설정하는 데 필요한 모든 구성 요소가 준비되었습니다.

먼저, 로드한 모델과 해당 토크나이저를 사용하여 `text_generation` 파이프라인을 생성합니다.

다음으로, 프롬프트 템플릿을 만듭니다. 이 템플릿은 모델의 형식을 따라야 하므로, 모델 체크포인트를 교체할 경우 적절한 형식을 사용하도록 해야 합니다.

    from langchain.llms import HuggingFacePipeline
    from langchain.prompts import PromptTemplate
    from transformers import pipeline
    from langchain_core.output_parsers import StrOutputParser

    text_generation_pipeline = pipeline(
        model=model,
        tokenizer=tokenizer,
        task="text-generation",
        temperature=0.2,
        do_sample=True,
        repetition_penalty=1.1,
        return_full_text=True,
        max_new_tokens=400,
    )

    llm = HuggingFacePipeline(pipeline=text_generation_pipeline)

    prompt_template = """
    A chat between a curious user and an artificial intelligence assistant. The assistant gives helpful, detailed, and polite answers to the user's questions.
    Human: Context를 읽고 Question에 한국어로 답하세요.
    Context: {context}
    Question: {question}
    Assistant:\n
    """

    prompt = PromptTemplate(
        input_variables=["context", "question"],
        template=prompt_template,
    )

    llm_chain = prompt | llm | StrOutputParser()

참고: 메시지 리스트(딕셔너리 형태: `{'role': 'user', 'content': '(...)'}`)를 적절한 채팅 형식의 문자열로 변환하려면 `tokenizer.apply_chat_template`를 사용할 수 있습니다.

마지막으로, `llm_chain`과 검색기를 결합하여 RAG 체인을 생성해야 합니다. 원본 질문과 검색된 문서들을 최종 생성 단계로 전달합니다:

    from langchain_core.runnables import RunnablePassthrough

    retriever = db.as_retriever()

    rag_chain = {"context": retriever, "question": RunnablePassthrough()} | llm_chain

##
결과 비교

RAG가 라이브러리 관련 질문에 대한 답변을 생성하는 데 어떤 차이를 만드는지 살펴보겠습니다.

수집한 PEFT 라이브러리의 이슈가 영어로 작성되어서 영어로 질문하여 문서를 검색합니다.

프롬프트로 답변은 한글로 나오도록 합니다.

    question = "How do you combine multiple adapters?"

먼저, 컨텍스트를 추가하지 않고 모델 자체만으로 어떤 답변을 얻을 수 있는지 살펴보겠습니다:

    llm_chain.invoke({"context": "", "question": question})

모델은 질문을 전기를 공급하는 어댑터에 관한 것으로 해석했지만, PEFT의 맥락에서 “adapters”는 LoRA 어댑터를 의미합니다.
GitHub 이슈를 검색해 컨텍스트를 추가하면 모델이 더 관련성 높은 답변을 제공할 수 있는지 확인해 보겠습니다:

    rag_chain.invoke(question)

보시다시피, 컨텍스트를 추가하면 동일한 모델이 라이브러리 관련 질문에 대해 훨씬 더 관련성 있고 정보에 기반한 답변을 제공하는 데 큰 도움이 됩니다.

특히, 여러 어댑터를 결합하여 추론하는 기능이 라이브러리에 추가되었으며, 이 정보는 검색된 문서에서 찾을 수 있습니다. 따라서 RAG로 문서 임베딩을 포함하는 것이 유용할 수 있습니다.

< > Update on GitHub