곽은철
곽은철
⚔️ 베테랑 파트너

Chroma와 Ollama로 코드 분석, Redis로 대화 기록 저장

이번 제목은 Claude3가 수고해줬습니다. 🙂

이번에 사용한 툴 크게 3가지 입니다.

  1. Ollama - 로컬 LLM을 손쉽게 구동시켜주는 앱으로 데스크탑의 성능만 보장되면 공개된 모델들 중 유명한 모델들은 로컬에서 사용이 가능하게 해줍니다.

  2. Chroma - 백터DB 중 하나로 오늘 사례에서는 작은 코드 라이브러리를 저장하는 용도로 사용됩니다.

  3. 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:latest


LangSmith 환경 변수 설정

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기랭체인

5
2개의 답글

👉 이 게시글도 읽어보세요

모집 중인 AI 스터디