본 내용은 RAG 시스템 구축을 위한 랭체인 실전 가이드 교재 및 강의 자료, 실습 자료를 사용했음을 알립니다.
- 강의 : [모두의AI] Langchain 강의
- 실습 : Kane0002/Langchain-RAG
RecursiveCharacterTextSplitter
, chunk_overlap
파라미터 설정만 해도 충분한 성능이 나옴Parent Document Retriever
or Long Context Reorder
기법 사용해 보완실습 노트북 : Basic RAG.ipyanb
hub
: 프롬프트 공유 및 관리, 랭체인 활용 시 참고할만한 프롬프트가 다수 존재PyPDFLoader
: PDF 문서 로드RecursiveCharacterTextSplitter
: 더 작은 단위 청크로 분할OpenAIEmbeddings
: 임베딩 벡터로 변환하기 위함Chroma
: 벡터 저장ChatOpenAI
: GPT API 활용RunnablePassthrough
: LCEL이 잘 작동하도록 랭체인에서 개발한 고유 객체 Runnable 일종Runnable 객체의 종류
- Runnable Parallel : 병렬 처리
- RunnableBranch : 조건에 따라 실행할 브랜치 선택
- RunnableLambda : 사용자 정의 함수를 Chain에 결합할 수 있도록 함
StrOutputParser
: LLM 답변을 문자열로 출력하도록 만들기 위한 Output parser 사용from langchain import hub
from langchain.document_loaders import PyPDFLoader
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_openai import OpenAIEmbeddings, ChatOpenAI
from langchain.vectorstores import Chroma
from langchain_core.runnables import RunnablePassthrough
from langchain_core.output_parsers import StrOutputParser
import os
os.environ["OPENAI_API_KEY"] = "YOUR_OPENAI_API_KEY"
as_retriever()
: 벡터 데이터베이스를 기반으로 Retriever를 만들기 위해 사용#헌법 PDF 파일 로드
loader = PyPDFLoader(r"/content/drive/MyDrive/NLP톺아보기/file/대한민국헌법(헌법)(제00010호)(19880225).pdf")
pages = loader.load_and_split()
#PDF 파일을 1000자 청크로 분할
text_splitter = RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=100)
docs = text_splitter.split_documents(pages)
#ChromaDB에 청크들을 벡터 임베딩으로 저장(OpenAI 임베딩 모델 활용)
vectorstore = Chroma.from_documents(docs, OpenAIEmbeddings(model = 'text-embedding-3-small'))
retriever = vectorstore.as_retriever()
-rlm/rag-prompt
: Langchain Hub에서 RAG 전용 프롬프트로 활용됨
#GPT 3.5 모델 선언
from langchain import hub
llm = ChatOpenAI(model="gpt-4o-mini")
#Langchain Hub에서 RAG 프롬프트 호출
prompt = hub.pull("rlm/rag-prompt")
#Retriever로 검색한 유사 문서의 내용을 하나의 string으로 결합
def format_docs(docs):
return "\n\n".join(doc.page_content for doc in docs)
prompt
prompt.messages
Dictionary 객체
Dictionary 객체를 그대로 사전 정의한 프롬프트 템플릿에 전달
완성된 프롬프트 ➡️ LLM에 전달
사용자 질문과 검색 결과물 종합 작성한 LLM 답변 ➡️ StrOutputParser()를 거쳐 문자열 형태로 출력
rag_chain = (
{"context": retriever, "question": RunnablePassthrough()}
| prompt
| llm
| StrOutputParser()
)
rag_chain
의 작동 결과 보기answer = rag_chain.invoke("국회의원의 의무는 뭐야?")
print(answer)
국회의원의 의무는 청렴성을 유지하고 국가 이익을 우선하여 양심에 따라 직무를 수행하는 것입니다. 또한, 국회의원은 자신의 지위를 남용하여 재산상의 이익을 취득하거나 타인을 위해 알선할 수 없습니다. 이외에도 국회의원은 직무상 행한 발언과 표결에 대해 국회 외부에서 책임을 지지 않습니다.
rag_chain
시각화rag_chain.get_graph().print_ascii()
Chain 구동 순서
Parallel input으로 context
, question
구성 확인할 수 있음
Runnable Parallel : rag_chain의 가장 첫 요소로 선언한 Dictionary가 Parallel input으로 활용됨
rag_chain
Dictionary로 context와 question 값을 ChatPromptTemplate에 전달
전달된 템플릿 ➡️ ChatOpenAI로 전달
받은 답변 ➡️ StrOutPutParser로 문자열 처리됨
from langchain import hub
from langchain.document_loaders import PyPDFLoader
from langchain.vectorstores import Chroma
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_openai import OpenAIEmbeddings, ChatOpenAI
from langchain_core.runnables import RunnablePassthrough, RunnableParallel
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables.history import BaseChatMessageHistory, RunnableWithMessageHistory
# PDF 파일 로드 및 처리
loader = PyPDFLoader(r"/content/drive/MyDrive/NLP톺아보기/file/대한민국헌법(헌법)(제00010호)(19880225).pdf")
# 1,000자씩 분할하여 Document 객체 형태로 docs에 저장
pages = loader.load_and_split()
text_splitter = RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=0)
docs = text_splitter.split_documents(pages)
# Chroma 벡터 저장소 설정 및 retriever 생성
vectorstore = Chroma.from_documents(docs, OpenAIEmbeddings(model='text-embedding-3-small'))
retriever = vectorstore.as_retriever()
from langchain.chains.combine_documents import create_stuff_documents_chain
from langchain.chains import create_history_aware_retriever, create_retrieval_chain
from langchain.prompts import ChatPromptTemplate, MessagesPlaceholder
# Define the contextualize question prompt
contextualize_q_system_prompt = """Given a chat history and the latest user question \
which might reference context in the chat history, formulate a standalone question \
which can be understood without the chat history. Do NOT answer the question, \
just reformulate it if needed and otherwise return it as is."""
contextualize_q_prompt = ChatPromptTemplate.from_messages(
[
("system", contextualize_q_system_prompt),
MessagesPlaceholder("chat_history"),
("human", "{input}"),
]
)
history_aware_retriever = create_history_aware_retriever(llm, retriever, contextualize_q_prompt)
from langchain_core.messages import AIMessage, HumanMessage
contextualize_q_prompt = ChatPromptTemplate.from_messages(
[
("system", contextualize_q_system_prompt),
MessagesPlaceholder("chat_history"),
("human", "{input}"),
]
)
chat_history = [
HumanMessage(content='대통령의 임기는 몇년이야?'),
AIMessage(content='대통령의 임기는 5년입니다.')
]
contextualize_q_prompt.invoke({"input":"국회의원은?", "chat_history" : chat_history})
history_aware_retriever = create_history_aware_retriever(llm, retriever, contextualize_q_prompt)
result = history_aware_retriever.invoke({"input":"국회의원은?", "chat_history" : chat_history})
for i in range(len(result)):
print(f"{i+1}번째 유사 청크")
print(result[i].page_content[:250])
print("-"*100)
from langchain.chains import create_retrieval_chain
from langchain.chains.combine_documents import create_stuff_documents_chain
qa_system_prompt = """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.\
{context}"""
qa_prompt = ChatPromptTemplate.from_messages(
[
("system", qa_system_prompt),
MessagesPlaceholder("chat_history"),
("human", "{input}"),
]
)
question_answer_chain = create_stuff_documents_chain(llm, qa_prompt)
rag_chain = create_retrieval_chain(history_aware_retriever, question_answer_chain)
from langchain_core.messages import HumanMessage
#채팅 히스토리를 적재하기 위한 리스트
chat_history = []
question = "대통령의 임기는 몇년이야?"
#첫 질문에 답변하기 위한 rag_chain 실행
ai_msg_1 = rag_chain.invoke({"input": question, "chat_history": chat_history})
#첫 질문과 답변을 채팅 히스토리로 저장
chat_history.extend([HumanMessage(content=question), ai_msg_1["answer"]])
second_question = "국회의원은?"
#두번째 질문 입력 시에는 첫번째 질문-답변이 저장된 chat_history가 삽입됨
ai_msg_2 = rag_chain.invoke({"input": second_question, "chat_history": chat_history})
print(ai_msg_2["answer"])
from langchain_community.chat_message_histories import ChatMessageHistory
from langchain_core.chat_history import BaseChatMessageHistory
from langchain_core.runnables.history import RunnableWithMessageHistory
#채팅 세션별 기록 저장 위한 Dictionary 선언
store = {}
#주어진 session_id 값에 매칭되는 채팅 히스토리 가져오는 함수 선언
def get_session_history(session_id: str) -> BaseChatMessageHistory:
if session_id not in store:
store[session_id] = ChatMessageHistory()
return store[session_id]
#RunnableWithMessageHistory 모듈로 rag_chain에 채팅 기록 세션별로 자동 저장 기능 추가
conversational_rag_chain = RunnableWithMessageHistory(
rag_chain,
get_session_history,
input_messages_key="input",
history_messages_key="chat_history",
output_messages_key="answer",
)
conversational_rag_chain.invoke(
{"input": "대통령의 임기는 몇년이야?"},
config={
"configurable": {"session_id": "240510101"}
}, # constructs a key "abc123" in `store`.
)["answer"]
conversational_rag_chain.invoke(
{"input": "국회의원은?"},
config={"configurable": {"session_id": "240510101"}},
)["answer"]
FROM EEVE-Korean-Instruct-10.8B-v1.0-Q5_K_M.gguf
TEMPLATE """{{- if .System }}
<s>{{ .System }}</s>
{{- end }}
<s>Human:
{{ .Prompt }}</s>
<s>Assistant:
"""
SYSTEM """A chat between a curious user and an artificial intelligence assistant. The assistant gives helpful, detailed, and polite answers to the user's Questions."""
PARAMETER temperature 0
PARAMETER num_predict 3000
PARAMETER num_ctx 4096
PARAMETER stop <s>
PARAMETER stop </s>
ollama create EEVE-Korean-10.8B -f Modelfile.txt
ollama list
주의 : 코랩이 아닌, 로컬에서 실행해야 함
from langchain_ollama import ChatOllama
from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import ChatPromptTemplate
llm = ChatOllama(model="EEVE-Korean-10.8B:latest")
prompt = ChatPromptTemplate.from_template("{topic}에 대한 짧은 농담을 들려주세요. ")
chain = prompt | llm | StrOutputParser()
print(chain.invoke({"topic": "우주여행"}))
Chroma().delete_collection()
from langchain.document_loaders import PyPDFLoader
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain.embeddings import HuggingFaceEmbeddings
from langchain.vectorstores import Chroma
from langchain_ollama import ChatOllama
from langchain import hub
from langchain_core.runnables import RunnablePassthrough
from langchain_core.output_parsers import StrOutputParser
loader = PyPDFLoader(r"../대한민국헌법(헌법)(제00010호)(19880225).pdf")
pages = loader.load_and_split()
text_splitter = RecursiveCharacterTextSplitter(chunk_size=500, chunk_overlap=0)
docs = text_splitter.split_documents(pages)
model_name = "jhgan/ko-sbert-nli"
model_kwargs = {'device': 'CUDA'}
encode_kwargs = {'normalize_embeddings': True}
embedding = HuggingFaceEmbeddings(
model_name=model_name,
model_kwargs=model_kwargs,
encode_kwargs=encode_kwargs
)
vectorstore = Chroma.from_documents(docs, embedding)
retriever = vectorstore.as_retriever()
prompt = hub.pull("rlm/rag-prompt")
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()
)
for chunk in rag_chain.stream("헌법 제 1조 1항이 뭐야"):
print(chunk, end="", flush=True)