Langchain을 이용하여 LLaMA와 Ollama를 사용한 Retrieval-Augmented Generation (RAG) 시스템을 구현하려면, Langchain의 Document Loaders, Vector Stores, 그리고 LLM 연결 기능을 결합해야 합니다.
LLaMA를 쉽게 사용하기 위해서 Ollama 를 선택했습니다. 여러가지 사유가 있겠지만 GPU설정도 따로 안해줘도 되고 proxy 설정이 다른 것보다 쉬웠는 측면도 있었습니다.
다른 예제들을 보면 OpenAI api를 이용하는 경우가 있었는데 여기에서는 local Embeddings을 사용해보도록 하겠습니다.
1. 일단 Ollama를 설치해줍니다.
아래 다운로드 주소에서 본인 운영체제에 맞는 적당한 버전을 다운로드 해줍니다. (설치 위치 변경이 안되는 부분이 조금 아쉽습니다.)
2. python 설치 + venv 사용
이 부분은 인터넷 검색해보시기 바랍니다. windows와 ubuntu venv 명령이 조금 다릅니다.
여기에서는 python은 3.12사용 하였습니다.
3. llama 설치
hugging face에서 llama를 직접 다운 받아서 하는 방법도 있겠지만 확장자도 변환해야 하고 복잡하므로 ollama 에서 직접 설치하도록 합니다. 3.2 3b 모델로 설치하겠습니다. 지원 모델은 아래 링크에서 확인이 가능합니다. 대략 2GB 정도 하네요
https://ollama.com/library/llama3.2
ollama run llama3.2
llama가 제대로 실행되었다면 prompt가 나옵니다. 몇가지 질문을 해보면 잘 동작되는 모습을 볼 수 있습니다.
(langchain) D:\dev\venv\langchain>ollama lsNAME ID SIZE MODIFIEDllama3.2:latest a80c4f17acd5 2.0 GB 11 minutes ago
4. python 필요 패키지 설치
pip install langchain faiss-cpu langchain-community langchain_ollama
주피터 노트북 설치 편하게 작업하기 위해서 추가로 설치합니다.
pip install notebook
python -m notebook 으로 주피터노트북 실행합니다.
기타 패키지들이 있는데 에러가 발생하면 추가해서 pip로 설치해 줍니다. 이것 저것 시도하다보니 정확하게 어떤 패키지였는지 기억하지 못했습니다.
5. embedding 모델 설치
local Embeddings 을 하기 위해서 ollama에 기본적으로 nomic-embed-text 모델을 제공해 줍니다. embedding 모델에 대한 정보는 다음 링크 참고하시기 바랍니다.
https://ollama.com/blog/embedding-models
ollama pull nomic-embed-text
막상해보니 한글이라서 그런지 생각보다 잘안됩니다. 그래서 추가로 large 모델을 사용하였습니다.
ollama pull mxbai-embed-large
2024.11.24 추가 수정
확인해보니 LLM으로 사용했던 모델을 그대로 사용해도 embedding 하는데 전혀 문제가 없었습니다. 따라서 아래와 같이 수정하고 나서는 결과가 더 좋게 나왔습니다.
local_embeddings = OllamaEmbeddings(model="llama3.2:latest")
6. llama3.2 를 Ollama에서 실행
정상적으로 ollama 가 동작하고 모델도 다운로드 되었다면 "부산"이라고 나오게 됩니다.
from langchain.prompts import PromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain_ollama import OllamaLLM
# 기본 예제
llm = OllamaLLM(model="llama3.2:latest")
template = "{country}의 수도는?"
prompt = PromptTemplate.from_template(template=template)
chain = prompt | llm | StrOutputParser()
result = chain.invoke({"country", "한국"})
print(result)
부산
7. 복잡한 동작 텍스트 분할하기
여기에서는 긴텍스트가 있다고 파일로 존재하고 있다고 생각하고 구현하였습니다.
해당 파일에서 파일을 읽어서 chunk size로 나누게 합니다. 여기에서는 좀 작게 여러개로 나누어 봤습니다.
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain.docstore.document import Document
# 텍스트 파일 경로 설정
file_path = "data/data.txt"
# 텍스트 파일을 읽어서 로딩
try:
with open(file_path, "r", encoding="utf-8") as file:
text = file.read()
except FileNotFoundError:
raise Exception(f"Failed to load text file at path: {file_path}")
# 텍스트를 문서 형태로 변환 (단순히 리스트로 사용 가능)
document = Document(page_content=text)
# 텍스트를 분할하기 위해 텍스트 분할기 사용
text_splitter = RecursiveCharacterTextSplitter(chunk_size=500, chunk_overlap=100)
all_splits = text_splitter.split_documents([document])
# 결과 출력
i = 1
for split in all_splits:
print(f"Chunk {i}:{split.page_content}\n")
i += 1
8. Chroma 를 이용해 벡터스토어에 저장을 합니다.
Chroma는 오픈 소스 벡터 데이터베이스입니다. 파일에 저장도 하는데 여기에서는 따로 저장하지는 않았습니다.
https://python.langchain.com/docs/integrations/vectorstores/chroma/
# https://ollama.com/blog/embedding-models
from langchain_chroma import Chroma
from langchain_ollama import OllamaEmbeddings
#local_embeddings = OllamaEmbeddings(model="nomic-embed-text") # 이것 사용했는데 잘안됨
## ollama pull mxbai-embed-large 다른것 설치함
local_embeddings = OllamaEmbeddings(model="mxbai-embed-large")
vectorstore = Chroma.from_documents(documents=all_splits, embedding=local_embeddings)
embedded_query = local_embeddings.embed_query("LangChain 에 대해서 상세히 알려주세요.")
# 임베딩 차원 출력
len(embedded_query)
9. 유사도 검색
similarity_search
메서드는 Chroma 데이터베이스에서 유사도 검색을 수행합니다.
실제 해보면 한글이라서 그런지 검색이 제대로 되지는 않았습니다. FAISS, Chroma 모두 테스트 해봤는데 결과는 비슷합니다. Embeddings이 문제인것 같았으나 추가 테스트는 해보지 못했습니다.
question = "대한체육회장 선거 날짜는?"
#question = "부상을 당한 사람은 누구인가요?"
docs = vectorstore.similarity_search(question)
len(docs)
10. 종합
from langchain_core.runnables import RunnablePassthrough
from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import ChatPromptTemplate
def format_docs(docs):
return "\n\n".join(doc.page_content for doc in docs)
RAG_TEMPLATE = """
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}
Answer the following question:
{question}"""
rag_prompt = ChatPromptTemplate.from_template(RAG_TEMPLATE)
chain = (
RunnablePassthrough.assign(context=lambda input: format_docs(input["context"]))
| rag_prompt
| model
| StrOutputParser()
)
#question = "토트넘은 몇위가 되었나요?"
docs = vectorstore.similarity_search(question)
# Run
chain.invoke({"context": docs, "question": question})