이번 제목은 Claude3가 수고해줬습니다. 🙂
이번에 사용한 툴 크게 3가지 입니다.
Ollama - 로컬 LLM을 손쉽게 구동시켜주는 앱으로 데스크탑의 성능만 보장되면 공개된 모델들 중 유명한 모델들은 로컬에서 사용이 가능하게 해줍니다.
Chroma - 백터DB 중 하나로 오늘 사례에서는 작은 코드 라이브러리를 저장하는 용도로 사용됩니다.
Redis - 키-값으로 이루어진 DB로 오늘 사례에는 질문과 답변을 저장하는 용도로 사용됩니다.
이 내용을 완전히 이해하기 위해서는 LCEL, RAG, 백터DB, 임베딩에 대한 배경지식이 필요합니다.
하지만 따라하고 실습하는 것은 가능할 만큼 모든 코드를 공개했으니, 필요한 질문은 댓글 남겨주시면 언제든 설명해드리겠습니다.
part 1 code_embeding
from langchain_text_splitters import Language
from langchain_community.vectorstores import Chroma
from langchain_community.embeddings import OllamaEmbeddings
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_community.document_loaders.generic import GenericLoader
from langchain_community.document_loaders.parsers import LanguageParser
loader = GenericLoader.from_filesystem(
path="/Users/ekwak/libft/libft",
glob="**/*",
suffixes=[".c", ".h"],
parser=LanguageParser(language=Language.CPP, parser_threshold=500),
)
documents = loader.load()
c_splitter = RecursiveCharacterTextSplitter.from_language(
language=Language.CPP, chunk_size=500, chunk_overlap=50
)
texts = c_splitter.split_documents(documents)
db = Chroma.from_documents(texts, OllamaEmbeddings(model="nomic-embed-text"), persist_directory="./chroma_db");part1에서는 특정 디렉토리에서 C와 C++ 소스 파일들을 로드하고, 이들을 텍스트 조각으로 나눈 다음, 이 텍스트 조각들로부터 벡터 임베딩을 생성하여 데이터베이스에 저장하는 과정을 구현한 것입니다.
1. 필요한 라이브러리 가져오기:
Language, RecursiveCharacterTextSplitter: 텍스트를 언어에 맞게 처리하고, 작은 조각으로 나누는 데 도움을 줍니다.
Chroma, OllamaEmbeddings: 텍스트 조각을 벡터로 바꾸고 저장하는 데 사용됩니다.
GenericLoader, LanguageParser: 파일에서 문서를 읽어오고 분석하는 데 사용됩니다.
2. 문서 읽어오기:
GenericLoader.from_filesystem: /Users/ekwak/libft/libft 경로에 있는 모든 C와 C++ 소스 파일(.c, .h)을 찾아서 읽어옵니다. 이때 C++ 언어로 인식하고, 코드를 500 단위로 나눕니다.
3. 문서 불러오기:
loader.load(): 앞서 설정한 대로 문서를 읽어와 documents 변수에 저장합니다.
4. 텍스트 조각 나누기:
RecursiveCharacterTextSplitter.from_language: C++ 언어에 맞춰 텍스트를 작은 조각으로 나눕니다. 각 조각의 크기는 500자이고, 조각 사이에 50자가 겹치도록 설정합니다.
c_splitter.split_documents(documents): 읽어온 문서를 작은 조각으로 나누고, 이를 texts 변수에 저장합니다.
5. 벡터 데이터베이스 만들기:
Chroma.from_documents: 나누어진 텍스트 조각을 벡터로 바꾸어 Chroma 데이터베이스에 저장합니다. 이때 "nomic-embed-text" 모델을 사용하고, 만들어진 데이터베이스는 ./chroma_db 폴더에 저장됩니다.
이렇게 하면 C와 C++ 코드를 작은 조각 으로 나누고, 이를 벡터로 바꾸어 데이터베이스에 저장하는 과정이 완료됩니다. 이렇게 저장된 데이터베이스는 프로그램을 종료하고 다시 시작해도 유지됩니다.
part 2 QA bot
from dotenv import load_dotenv
import os
load_dotenv()
# LANGCHAIN_TRACING_V2 환경 변수를 "true"로 설정합니다.
os.environ["LANGCHAIN_TRACING_V2"] = "true"
# LANGCHAIN_PROJECT 설정
os.environ["LANGCHAIN_PROJECT"] = "RunnableWithMessageHistory"
from langchain_community.vectorstores import Chroma
from langchain_community.embeddings import OllamaEmbeddings
db = Chroma(persist_directory="./chroma_db", embedding_function=OllamaEmbeddings(model="nomic-embed-text"))
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_core.runnables.history import RunnableWithMessageHistory
from langchain.chains import create_history_aware_retriever, create_retrieval_chain
from langchain.chains.combine_documents import create_stuff_documents_chain
from langchain_community.chat_models import ChatOllama
from langchain_community.chat_message_histories import RedisChatMessageHistory
llm = ChatOllama(model_name="llama2:70b")
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."""
prompt = ChatPromptTemplate.from_messages(
[
("system", contextualize_q_system_prompt),
MessagesPlaceholder("history"),
("human", "{input}"),
]
)
retriever = db.as_retriever(
search_type="mmr", # Also test "similarity"
search_kwargs={"k": 8},
)
history_aware_retriever = create_history_aware_retriever(
llm, retriever, prompt
)
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. \
{context}"""
qa_prompt = ChatPromptTemplate.from_messages(
[
("system", qa_system_prompt),
MessagesPlaceholder("history"),
("human", "{input}"),
]
)
question_answer_chain = create_stuff_documents_chain(llm, qa_prompt)
rag_chain = create_retrieval_chain(history_aware_retriever, question_answer_chain)
REDIS_URL = "redis://localhost:6379/0"
def get_message_history(session_id: str) -> RedisChatMessageHistory:
# 세션 ID를 기반으로 RedisChatMessageHistory 객체를 반환합니다.
return RedisChatMessageHistory(session_id, url=REDIS_URL)
with_message_history = RunnableWithMessageHistory(
rag_chain, # 실행 가능한 객체
get_message_history, # 메시지 기록을 가져오는 함수
input_messages_key="input", # 입력 메시지의 키
history_messages_key="history", # 기록 메시지의 키
output_messages_key="answer",
)
while(True):
text = input("What can i help you?")
if text == "/bye":
break
result = with_message_history.invoke(
{"input": text},
config={
"configurable": {"session_id": "1"}
}, # constructs a key "abc123" in `store`.
)["answer"]
print(result)Redis Docker 세팅
docker run -d -p 6379:6379 -p 8001:8001 redis/redis-stack:latestLangSmith 환경 변수 설정
from dotenv import load_dotenv
import os
load_dotenv()
os.environ["LANGCHAIN_TRACING_V2"] = "true"
os.environ["LANGCHAIN_PROJECT"] = "RunnableWithMessageHistory"LangSmith라는 개발자 플랫폼을 사용하기 위해 몇 가지 환경 변수를 설정해줍니다. LangSmith는 우리가 만든 대화형 시스템을 더 쉽게 개발하고 테스트할 수 있게 도와주는 도구입니다.
dotenv를 사용하여 .env 파일에서 환경 변수를 로드합니다.
LANGCHAIN_TRACING_V2와 LANGCHAIN_PROJECT 환경 변수를 설정합니다. 이는 시스템의 동작 방식을 구성하는 데 사용됩니다.
데이터베이스 로드
from langchain_community.vectorstores import Chroma
from langchain_community.embeddings import OllamaEmbeddings
db = Chroma(persist_directory="./chroma_db", embedding_function=OllamaEmbeddings(model="nomic-embed-text"))code_embeding.py에서 저장해둔 db를 로드합니다.
채팅 기록 전처리 프롬프트
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."""
prompt = ChatPromptTemplate.from_messages(
[
("system", contextualize_q_system_prompt),
MessagesPlaceholder("history"),
("human", "{input}"),
]
)"채팅 기록과 채팅 기록의 맥락을 참조할 수 있는 최신 사용자 질문이 주어지면, 채팅 기록 없이도 이해할 수 있는 독립형 질문을 만들어 보세요. 질문에 답변하지 말고 필요한 경우 재구성하고 그렇지 않으면 그대로 반환합니다."이 프롬프트는 사용자의 질문을 더 명확 하게 만들어주는 역할을 합니다. 사용자의 질문이 이전 대화 내용을 참조하고 있다면, 그 내용을 포함하여 독립적인 질문으로 바꿔줍니다. 이렇게 하면 우리 시스템이 질문을 더 잘 이해하고 정확한 답변을 찾을 수 있게 됩니다.
QA 프롬프트
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. \
{context}"""
qa_prompt = ChatPromptTemplate.from_messages(
[
("system", qa_system_prompt),
MessagesPlaceholder("history"),
("human", "{input}"),
]
)"귀하는 질문 답변 작업의 보조자입니다. 검색된 다음 문맥을 사용하여 질문에 답하세요. 답을 모른다면 모른다고 말하세요. "이 프롬프트는 실제로 질문에 답변하는 역할을 합니다. 데이터베이스에서 찾은 정보를 바탕으로 질문에 답하되, 정확한 답을 모를 경우에는 솔직하게 "모른다"고 말하도록 지시하고 있습니다. 이렇게 하면 사용자에게 잘못된 정보를 제공하는 것을 방지할 수 있습니다.
Redis를 이용해 대화 히스토리 관리
rag_chain = create_retrieval_chain(history_aware_retriever, question_answer_chain)
REDIS_URL = "redis://localhost:6379/0"
def get_message_history(session_id: str) -> RedisChatMessageHistory:
# 세션 ID를 기반으로 RedisChatMessageHistory 객체를 반환합니다.
return RedisChatMessageHistory(session_id, url=REDIS_URL)
with_message_history = RunnableWithMessageHistory(
rag_chain, # 실행 가능한 객체
get_message_history, # 메시지 기록을 가져오는 함수
input_messages_key="input", # 입력 메시지의 키
history_messages_key="history", # 기록 메시지의 키
output_messages_key="answer",
)1. 사용자가 대화형 시스템과 상호작용을 시작하면, 각 사용자 세션에 고유한 세션 ID가 할당됩니다.(예제코드에서는 고정값 1을 사용했습니다.)
2. 사용자의 대화 내용(질문과 답변)은 해당 세션 ID에 연결되어 Redis에 저장됩니다. 이를 통해 시스템은 사용자별로 대화 기록을 분리하여 관리할 수 있습니다.
3. 사용자가 새로운 질문을 할 때, 시스템은 get_message_history 함수를 호출하여 해당 사용자의 대화 기록을 관리하는 RedisChatMessageHistory 객체를 얻습니다. 이 객체를 사용하여 이전 대화 내용을 검색하고, 새로운 질문의 맥락을 이해하는 데 필요한 정보를 얻을 수 있습니다.
사용 모델
Chat model : llama2:70b
embeding : nomic-embed-text
참고 자료
LangSmith [https://docs.smith.langchain.com/tracing]
ChatOllama [https://python.langchain.com/docs/integrations/chat/ollama]
Code Split [https://python.langchain.com/docs/integrations/document_loaders/source_code]
Redis [https://python.langchain.com/docs/integrations/vectorstores/redis#inspecting-the-created-index]
Add messege history [https://python.langchain.com/docs/expression_language/how_to/message_history]
Chroma [https://python.langchain.com/docs/integrations/vectorstores/chroma]
#10기랭체인