ai? 맨땅에 헤딩 -6(langChain) : 간단 daum news로 RAG 실습

2024. 5. 20. 10:28Architecture/A.I

반응형

2024.04.19 - [Architecture/A.I] - ai? 맨땅에 헤딩 -1(langChain) : ai와 공생하기!

2024.04.24 - [Architecture/A.I] - ai? 맨땅에 헤딩 -2(langChain) : 튜토리얼 따라해보기!

2024.04.26 - [Architecture/A.I] - ai? 맨땅에 헤딩 -3(langChain) : 주요 컴포넌트 체크!

2024.04.29 - [Architecture/A.I] - ai? 맨땅에 헤딩 -4(langChain): vector DB 간단 사용!

2024.05.13 - [Architecture/A.I] - ai? 맨땅에 헤딩 -5(langChain) : langsmith 셋팅 및 tracing해보기!!

실습내용 : langchain을 이용하여 특정 daum news에 대해 질문을 던져 답변을 받는 것을 만들어 봅니다.

  1. import
    필요한 lib import를 해줍니다.
import bs4
from langchain import hub
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain_community.document_loaders import WebBaseLoader
from langchain_community.vectorstores import FAISS
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables import RunnablePassthrough
from langchain_openai import ChatOpenAI, OpenAIEmbeddings

2. Load
    load는 웹문서, pdf, csv, html등 상황에 맞게 load 시키면 됩니다.

# daum 뉴스 기사를 load 합니다.
loader = WebBaseLoader(
    web_paths=("https://v.daum.net/v/20240509160411633",),
    bs_kwargs = dict(
        parse_only=bs4.SoupStrainer(
            "div",
            attrs={"class": ["article_view","head_view"]},
        )
    ),
)
 
# 윗부분만 수정하여 webBaseLoader or PyPDFLoader 등 다양하게 수정 가능.
# csv, html 등등 다 가능.
docs = loader.load()
print(f"문서의 수: {len(docs)}")
docs

3. Split
      - RecursiveCharacterTextSplitter는 문서를 지정된 크기의 청크로 나눕니다
         chunk_size : 1000이면 1000자
         chunk_overlap : 겹치는 부분이 있도록 함. 100자는 앞뒤로 겹치도록 함. (어색하지 않게 앞의 내용 포함)

text_splitter = RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=100)
 
splits = text_splitter.split_documents(docs)
len(splits)

4. FAISS or Chroma 벡터 스토어에 저장(메모리)   
   위에서 쪼개진 청크(splits)를 바탕으로 벡터표현(Embedding)을 생성 합니다. (유사도)

5. 검색(retriever)
    vectorstore안에 저장된 것을 retriever(검색) 합니다.

# 벡터스토어를 생성합니다.
vectorstore = FAISS.from_documents(documents=splits, embedding=OpenAIEmbeddings())
 
# 뉴스에 포함되어 있는 정보를 검색하고 생성합니다.
retriever = vectorstore.as_retriever()

6. PROMPT   
     prompt는 langsmith에서 이미 만들어놓은 prompt를 활용합니다.
     아래는 rlm/rag-prompt의 내용 입니다.

만약 답을 모른다면 그냥 모른다고 대답하라고 되어있습니다.

HUMAN

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.

Question: {question} 

Context: {context} 

Answer:
prompt = hub.pull("rlm/rag-prompt")
prompt

아래에서 {question}과 {context}가 있는데

  • {question}은 사용자의 질문이며
  • {context}는 우리가 위에서 split한 문서 중에 질문에 가장 관련성(유사도) 높은 문서가 context에 들어갑니다.
  • 즉, context에 들어가는 내용은 위에서 vector store에서 검색(retriever)한 단락 입니다.
print(prompt.messages[0].prompt.template)

아래의 소스를 분석해보면 3가지로 나눠지는데

  1. 질문 -> 2) 검색 -> 3) 검색된 단락 합치기
{"context": retriever | format_docs, "question": RunnablePassthrough()}

사용자 질문을 받아주는 친구 RunnablePassthrough

  • 데이터를 전달해주는데 사용됩니다.
  • 또한 키/값의 map형태로 들어가는데
  • 아래소스에서는 context와 question이 key이며 value는 retriever값과 사용자의 질문값 입니다.

retriever

  • 내 질문에 대한 연관성 높은 단락들을 뽑아줌.

def format_docs(docs)

  • 검색 된 단락들을 합쳐주는 역할.

다음으로 검색 된 단락들을 합쳐서 prompt에 넘겨줍니다.

해당 prompt로 llm을 찌릅니다.

StrOutputParser

  • simply converts any input into a string.
from langchain.callbacks.base import BaseCallbackHandler
 
 
class StreamCallback(BaseCallbackHandler):
    def on_llm_new_token(self, token: str, **kwargs):
        print(token, end="", flush=True)
 
 
llm = ChatOpenAI(
    model_name="gpt-4-turbo-preview",
    temperature=0,
    streaming=True,
    callbacks=[StreamCallback()],
)
 
 
def format_docs(docs):
    # 검색한 문서 결과를 하나의 문단으로 합쳐줍니다.
    return "\n\n".join(doc.page_content for doc in docs)
 
 
# 체인을 생성합니다.
rag_chain = (
    {"context": retriever | format_docs, "question": RunnablePassthrough()}
    | prompt
    | llm
    | StrOutputParser()
)

7. 마지막으로 질문 입니다. rag_chain.invoke()

_ = rag_chain.invoke(
    "카카오뱅크가 대포통장에 대한 조치에 대해서 알려줘!"
)

rag_chain.invoke(
    "카뱅의 조치때문에 사용자가 불편한점은 무엇인가요? 불릿형태로 알려줘"
)

rag_chain.invoke("저런 조치로 인해 피싱범들을 몇명이나 잡을수 있을까?")

rag_chain.invoke(
    "카카오뱅크그룹의 임직원 숫자는 몇명인가요?"
)

출처 : 주변 크루가 알려준 유튜브를 통해 알게 된 테디노트님의 영상을 참고 하였습니다 :)

반응형