5.6. 전형적인 RAG (랭체인 버전)

랭체인 버전은 다음 튜토리얼에서 진행하겠습니다.

많은 응원 부탁드립니다. 😉

본 페이지는 1:13:10 지점부터 1:19:51 지점까지 보시면 됩니다.

5.6.1. 지식 Load 및 Split

의존 라이브러리 설치
pip install -U langchain langchain-community langchain-text-splitters
 1from pprint import pprint
 2
 3from langchain_community.document_loaders import TextLoader
 4from langchain_text_splitters import RecursiveCharacterTextSplitter
 5
 6# Load 단계
 7doc_list = TextLoader(file_path="./빽다방.txt").load()
 8print(f"loaded {len(doc_list)} documents")  # 1
 9
10# Split 단계
11text_splitter = RecursiveCharacterTextSplitter(
12    chunk_size=140,  # 문서를 나눌 최소 글자 수 (디폴트: 4000)
13    chunk_overlap=0,  # 문서를 나눌 때 겹치는 글자 수 (디폴트: 200)
14)
15doc_list = text_splitter.split_documents(doc_list)
16print(f"split into {len(doc_list)} documents")  # 9
17
18pprint(doc_list)

RecursiveCharacterTextSplitter 에서는 구분자 (디폴트 ["\n\n", "\n", " ", ""])로 나누고 chunk_size 크기 만큼 문서를 모으고, 이어지는 문서는 chunk_overlap 만큼 겹쳐서 문서를 생성합니다.

“빽다방.txt” 파일에서 각 메뉴들이 구분자 \n\n로 나눠지니까 10개 문서로 나눠지지만, 각 문서의 길이가 [98, 68, 52, 68, 114, 126, 81, 105, 65, 83] 이고, 두번째 문서는 68자, 세번째 문서는 52자로서 chunk_size=140 보다 작은 값이라 한 문서로 묶여 처리되었습니다.

그래서 10개의 문서가 아니라, 9개의 문서로만 나눠진 상황입니다. 2번 메뉴와 3번 메뉴가 묶여있네요. 🤔

loaded 1 documents
split into 9 documents
[Document(metadata={'source': './빽다방.txt'}, page_content='1. 아이스티샷추가(아.샷.추)\n  - SNS에서 더 유명한 꿀팁 조합 음료 :) 상콤달콤한 복숭아맛 아이스티에 진한 에스프레소 샷이 어우러져 환상조합\n  - 가격: 3800원'),
 Document(metadata={'source': './빽다방.txt'}, page_content='2. 바닐라라떼(ICED)\n  - 부드러운 우유와 달콤하고 은은한 바닐라가 조화를 이루는 음료\n  - 가격: 4200원\n\n3. 사라다빵\n  - 빽다방의 대표메뉴 :) 추억의 감자 사라다빵\n  - 가격: 3900원'),
 Document(metadata={'source': './빽다방.txt'}, page_content='4. 빽사이즈 아메리카노(ICED)\n  - 에스프레소 4샷이 들어가 깊고 진한 맛의 아메리카노\n  - 가격: 3500원'),
 Document(metadata={'source': './빽다방.txt'}, page_content='5. 빽사이즈 원조커피(ICED)\n  - 빽다방의 BEST메뉴를 더 크게 즐겨보세요 :) [주의. 564mg 고카페인으로 카페인에 민감한 어린이, 임산부는 섭취에 주의바랍니다]\n  - 가격: 4000원'),
 Document(metadata={'source': './빽다방.txt'}, page_content='6. 빽사이즈 원조커피 제로슈거(ICED)\n  - 빽다방의 BEST메뉴를 더 크게, 제로슈거로 즐겨보세요 :) [주의. 686mg 고카페인으로 카페인에 민감한 어린이, 임산부는 섭취에 주의바랍니다]\n  - 가격: 4000원'),
 Document(metadata={'source': './빽다방.txt'}, page_content='7. 빽사이즈 달콤아이스티(ICED)\n  - 빽다방의 BEST메뉴를 더 크게 즐겨보세요 :) 시원한 복숭아맛 아이스티\n  - 가격: 4300원'),
 Document(metadata={'source': './빽다방.txt'}, page_content='8. 빽사이즈 아이스티샷추가(ICED)\n  - SNS에서 더 유명한 꿀팁 조합 음료 :) 상콤달콤한 복숭아맛 아이스티에 진한 에스프레소 2샷이 어우러져 환상조합\n  - 가격: 4800원'),
 Document(metadata={'source': './빽다방.txt'}, page_content='9. 빽사이즈 아이스티 망고추가+노란빨대\n  - SNS핫메뉴 아이스티에 망고를 한가득:)\n  - 가격: 6300원'),
 Document(metadata={'source': './빽다방.txt'}, page_content='10. 빽사이즈 초코라떼(ICED)\n  - 빽다방의 BEST메뉴를 더 크게 즐겨보세요 :) 진짜~완~전 진한 초코라떼\n  - 가격 : 5500원')]

5.6.2. 전체 코드

VectorStore는 FAISS를 사용하겠습니다. Facebook AI Research에서 개발한 효율적인 유사도 검색 라이브러리입니다. 대규모 데이터셋에서도 빠른 유사도 검색이 가능하며, 메모리에 데이터를 저장하고 디스크에 저장/로드할 수 있습니다.

의존 라이브러리 설치
pip install -U langchain langchain-community langchain-openai langchain-text-splitters faiss-cpu tiktoken
  1import os.path
  2from pprint import pprint
  3from typing import List
  4from uuid import uuid4
  5
  6import faiss
  7from langchain.chains.llm import LLMChain
  8from langchain.chains.retrieval_qa.base import RetrievalQA
  9from langchain_community.docstore import InMemoryDocstore
 10from langchain_community.document_loaders import TextLoader
 11from langchain_core.messages import AIMessage
 12from langchain_core.prompts import PromptTemplate
 13from langchain_core.runnables import RunnableLambda
 14from langchain_core.vectorstores import VectorStore
 15from langchain_openai import ChatOpenAI
 16from langchain_openai.embeddings import OpenAIEmbeddings
 17from langchain_community.vectorstores import FAISS
 18from langchain_text_splitters import RecursiveCharacterTextSplitter
 19
 20faiss_folder_path = "faiss_index"
 21
 22embedding = OpenAIEmbeddings(model="text-embedding-3-small")
 23
 24
 25def get_vector_store() -> VectorStore:
 26    if not os.path.exists(faiss_folder_path):
 27        doc_list = TextLoader(file_path="./빽다방.txt").load()
 28        print(f"loaded {len(doc_list)} documents")  # 1
 29
 30        text_splitter = RecursiveCharacterTextSplitter(
 31            chunk_size=140,
 32            chunk_overlap=0,
 33            length_function=len,
 34            is_separator_regex=True,
 35        )
 36        doc_list = text_splitter.split_documents(doc_list)
 37        print(f"split into {len(doc_list)} documents")  # 9
 38
 39        차원수 = len(embedding.embed_query("hello"))  # 1536
 40        # 차원수 = 1536
 41
 42        index = faiss.IndexFlatL2(차원수)
 43
 44        vector_store = FAISS(
 45            embedding_function=embedding,
 46            index=index,
 47            docstore=InMemoryDocstore(),
 48            index_to_docstore_id={},
 49        )
 50
 51        uuids = [str(uuid4()) for _ in range(len(doc_list))]
 52        vector_store.add_documents(documents=doc_list, ids=uuids)
 53
 54        vector_store.save_local("faiss_index")
 55    else:
 56        vector_store = FAISS.load_local(
 57            faiss_folder_path,
 58            embedding,
 59            allow_dangerous_deserialization=True,
 60        )
 61
 62    return vector_store
 63
 64
 65def main():
 66    vector_store = get_vector_store()
 67
 68    question = "빽다방 카페인이 높은 음료와 가격은?"
 69
 70    # 직접 similarity_search 메서드 호출을 통한 유사 문서 검색
 71    # search_doc_list = vector_store.similarity_search(question)
 72    # pprint(search_doc_list)
 73
 74    # retriever 인터페이스를 통한 유사 문서 검색
 75    # retriever = vector_store.as_retriever()
 76    # search_doc_list = retriever.invoke(question)
 77    # pprint(search_doc_list)
 78
 79    # Chain을 통한 retriever 자동 호출
 80    # llm = ChatOpenAI(model_name="gpt-4o-mini")
 81    # retriever = vector_store.as_retriever()
 82    # qa_chain = RetrievalQA.from_chain_type(llm=llm, retriever=retriever)
 83    # ai_message = qa_chain.invoke(question)
 84    # print("[AI]", ai_message["result"])  # keys: "query", "result"
 85
 86    llm = ChatOpenAI(model_name="gpt-4o-mini")
 87    retriever = vector_store.as_retriever()
 88    prompt_template = PromptTemplate(
 89        template="Context: {context}\n\nQuestion: {question}\n\nAnswer:",
 90        input_variables=["context", "question"],
 91    )
 92
 93    rag_pipeline = (
 94        RunnableLambda(
 95            # 아래 invoke를 통해 전달되는 값이 인자로 전달됩니다.
 96            lambda x: {
 97                "context": retriever.invoke(x),
 98                "question": x,
 99            }
100        )
101        | prompt_template
102        | llm
103    )
104    ai_message: AIMessage = rag_pipeline.invoke(question)
105    print("[AI]", ai_message.content)  # AIMessage 타입
106    print(ai_message.usage_metadata)
107
108
109if __name__ == "__main__":
110    main()

5.6.3. 실행 결과

[AI] 빽다방에서 카페인이 높은 음료와 그 가격은 다음과 같습니다:

1. **빽사이즈 원조커피(ICED)**
   - 카페인: 564mg
   - 가격: 4000원

2. **빽사이즈 원조커피 제로슈거(ICED)**
   - 카페인: 686mg
   - 가격: 4000원

이 두 음료는 카페인 함량이 높으므로, 카페인에 민감한 어린이와 임산부는 섭취에 주의해야 합니다.
{'input_tokens': 499, 'output_tokens': 132, 'total_tokens': 631, 'input_token_details': {'audio': 0, 'cache_read': 0}, 'output_token_details': {'audio': 0, 'reasoning': 0}}

랭체인/랭그래프 버전도 기대해주세요. 🥳