Data science/AI (4)

반응형

 

 

RAG 를 사용하지 않은 기본적인 API Call 방법

 

1. 기본 Openai API

from openai import OpenAI
import os
os.environ["OPENAI_API_KEY"] = "sk..."

client = OpenAI()
completion = client.chat.completions.create(
    model="gpt-4o",
    temperature=0.1,
    messages=[
        {"role":"system", "content":system_prompt},
        {"role":"user", "content":user_query}
    ],
    response_format={"type":"json_object"}
)
response = completion.choices[0].message.content
return(response)

2. Llama_index 활용

from llama_index.llms.openai import OpenAI
from llama_index.core.llms import ChatMessage
import os
os.environ["OPENAI_API_KEY"] = "sk..."

# OpenAI 모델 설정
llm = OpenAI(model="gpt-4o", temperature=0.1)

messages = [
    ChatMessage(role="system", content=system_prompt),
    ChatMessage(role="user", content="Hi"),
]

# Response 를 json 형식으로 받고 싶은 경우 
resp = llm.chat(messages, response_format={"type": "json_object"})
반응형
반응형

 

RAG Framework

RAG (Retrieval Augmented Generation) 은 LLM 이 정보를 검색하고 이를 활용하여 결과 생성할 수 있도록 하는 방법이다. 

 

RAG 의 4가지 컴포넌트

 

유명한 프레임워크로 LangChain 과 LlamaIndex 가 있다. 이 포스트에서는 두 프레임워크에 어떤 차이점이 있는지를 살펴보려고 한다. RAG 에는 4가지 컴포넌트가 있다. Loaders, Splitters, Indexing, Chains 이 그것이다. 각각 LangChain과 LlamaIndex 가 이를 어떻게 구현하고 있는지 살펴보자. 

 

1. Loaders

Loader 는 API, document, DB 와 같은 다양한 소스들을 로드하는 객체이다. LangChain, LlamaIndex 모두 흔하게 사용되는 소스들을 로드할 수 있는 built-in 함수들을 제공한다. 로더를 통해 파일 위치를 입력해주면 Directory 객체가 반환되며, 이를 다시 한 번 더 로드해주면 텍스트를 로드할 수 있다. 

 

LangChain: Text 파일 로드하기

from langchain.document_loaders import TextLoader
# Load a text document
loader = TextLoader("sample.txt")
documents = loader.load()
print(documents[0].page_content)

 

LlamaIndex: 특정 디렉토리안에 있는 텍스트 파일 로드하기 

from llama_index.core import SimpleDirectoryReader
# Load a text document from a directory
loader = SimpleDirectoryReader('path/to/docs')
documents = loader.load_data()
print(documents[0].text)

 

2. Splitters

Splitter 는 도큐먼트를 작은 Chunk 단위로 분해해서 GPT 나 BERT 의 token limit 을 넘기지 않도록 만들어주는 역할을 한다. 

 

LangChain

LangChain 의 `TextSplitter` 는 텍스트를 Character, Word, Sentence 중 어떤 단위로 분할할지를 선택할 수 있다. 아래 코드는 Character 단위로 텍스트를 분할하며, chunk size 는 1000이고, chunk 간에 200개의 중복 두는 방식으로 텍스트를 분할한다. 

 

from langchain.text_splitter import CharacterTextSplitter

# Define a character splitter
splitter = CharacterTextSplitter(chunk_size=1000, chunk_overlap=200)
chunks = splitter.split_documents(documents)
print(chunks[0].page_content)

 

LlamaIndex 

LlamaIndex 에서는 아래와 같이 똑같은 작업을 수행할 수 있다. 

from llama_index import TokenTextSplitter

# Define a token splitter
splitter = TokenTextSplitter(chunk_size=1000)
chunks = splitter.split(documents)
print(chunks[0].text)

 

3. Indexing

Indexing 은 RAG system 에서 가장 핵심 부분이라고 할 수 있다. 유저 쿼리와 가장 관련있는 청크를 빠르고 효율적으로 검색할 수 있어야한다. VectorStoreIndex 라는 말이 자주 등장하는데, 이는 VectorStore 를 빠르게 검색해서 유저 쿼리와 연관된 청크를 반환해주는 색인 시스템을 의미한다고 이해하면 된다. 

 

LangChain

LangChain 의 VectorStoreIndex 는 Splitter 를 통해 만들어진 청크들을 통해 인덱스를 생성한다. 그리고 Similarity search 를 기반으로 동작하게 된다.

from langchain.vectorstores import FAISS
from langchain.embeddings.openai import OpenAIEmbeddings

# Create embeddings
embedding_model = OpenAIEmbeddings()

# Create FAISS index
index = FAISS.from_documents(chunks, embedding_model)

 

LlamaIndex

LlamaIndex 에서는  아래와 같이 Document 로부터 바로 VectorStoreIndex 를 생성할 수 있고, Splitter 를 바로 인자로 주어 생성할 수 있다. 

from llama_index.core import VectorStoreIndex
embed_model = OpenAIEmbedding(model="text-embedding-3-small")

index = VectorStoreIndex.from_documents(
    documents,
    embed_model=embed_model,
    transformations=[SentenceSplitter(chunk_size=16000)]
)

 

4. Chain

RAG 에서 Chain 이란 Retrieval 과 Generation 의 요소들이 결합된 일련의 작업들이라고 보면 된다. LangChain 과 LlamaIndex 모두 이러한 RAG 의 컴포넌트를 엮어서 하나의 체인으로 만드는 기능을 제공한다. 

 

LangChain

LangChain 에서는 아래와 같이 `RetrievalQA` 를 활용해서 LLM 과 Retrieval 가 결합된 체인을 만들 수 있다. 이를 통해 RAG workflow 를 좀 더 간결하게 표현할 수 있다. 

from langchain.chains import RetrievalQA
from langchain.llms import OpenAI

# Combine FAISS index and OpenAI for RAG
llm = OpenAI()
qa_chain = RetrievalQA(llm=llm, retriever=index.as_retriever())
result = qa_chain.run("What is the content of the document?")
print(result)

 

LlamaIndex

LlamaIndex 에서는 VectorStoreIndex 에 as_query_engine 또는 as_chat_engine 을 이용해서 쉽게 Index 와 LLM 을 연결할 수 있다. 해당 Index 를 활용하는 LLM 을 만들라는 의미이다.  query engine 과 chat engine 의 차이점은 chat engine 은 컨텍스트를 기억한다. as_chat_engine 을 보면, memory 인자를 전달함으로써, 과거 기억을 얼마나 전달할지를 정할 수 있다. token_limit 이 작을수록 과거 컨텍스트를 모델에 더 적게 전달한다. 

# Chat Engine 생성하기
memory = ChatMemoryBuffer.from_defaults(token_limit=16000)
chat_engine = index.as_chat_engine(
    chat_mode="context",
    memory=memory,
    system_prompt=system_prompt
)
response = chat_engine.query("What is the content of the document?")
print(response)

 

LangChain 과 LlamaIndex 중 무엇을 선택해야할까?

1. Customization

LangChain 의 장점은 다양한 소스를 기반으로 RAG 를 구현해야할 때 더욱 유연하다는 것이다. 예를 들어, Multi-modal RAG 시스템을 구축한다고 해보자. PDF, API 문서, Web 문서 등을 각각 Load 해서 VectorStoreIndex 를 구축한다고 해보자. LangChain 에서는 `+` operation 을 통해 쉽게 로더를 결합할 수 있다.  

from langchain.chains import SequentialChain
from langchain.llms import OpenAI
from langchain.document_loaders import WebPageLoader, TextLoader
from langchain.vectorstores import FAISS

# Step 1: Load data from multiple sources
pdf_loader = TextLoader("docs/sample.pdf")
web_loader = WebPageLoader(url="https://example.com")
documents = pdf_loader.load() + web_loader.load()

# Step 2: Create embeddings and index
embedding_model = OpenAIEmbeddings()
index = FAISS.from_documents(documents, embedding_model)

# Step 3: Build a chain that retrieves and generates responses
llm = OpenAI()
qa_chain = RetrievalQA(llm=llm, retriever=index.as_retriever())

# Run the chain
response = qa_chain.run("What are the key points in the PDF and website?")
print(response)

 

LlamaIndex 의 장점은 더욱 직관적이며, 심플하게 RAG 를 구현할 수 있다는 것이다. 예를 들어 LlamaIndex 의 경우, 법 문서나 medical report 등을 기반으로 RAG 를 만들 때, 매우 빠르게 최소한의 코드로 구축해볼 수 있다. 

from llama_index.core import VectorStoreIndex, SimpleDirectoryReader

# Load documents from a directory
loader = SimpleDirectoryReader('docs/')
documents = loader.load_data()

# Build a tree index and query
index = VectorStoreIndex(documents)
query_engine = index.as_query_engine()

# Query the system
response = query_engine.query("Summarize the legal document.")
print(response)

 

2. Ecosystem 통합

LangChain 은 여러 AI Ecosystem 들을 통합할 때 더욱 유용하다. LangChain 은 Multi-LLM, Multi-Retrieval 를 지원한다고 볼 수 있다. 

- Vector Database : FAISS, Pinecone, Chroma

- Language Model : OpenAI, GPT-4, Anthropic's Claude

- API : Hugging Face, Cohere

 

위와 같은 요소들을 자유롭게 결합할 수 있다는 것이 LangChain 의 장점이라고 볼 수 있다. 예를 들어, OpenAI 의 GPT-4 와 HuggingFace 의 BERT 를 태스크에 따라 자유롭게 스위치 할 수 있다.  

from langchain.chains import LLMChain
from langchain.chains import SimpleSequentialChain
from langchain.llms import OpenAI, HuggingFaceHub
from langchain_core.prompts import PromptTemplate

prompt_template = "Tell me a {adjective} joke"
prompt = PromptTemplate(
    input_variables=["adjective"], template=prompt_template
)

gpt_chain = LLMChain(llm=OpenAI(), prompt=prompt)
gpt_chain.invoke("scary")

bert_chain = LLMChain(llm=HuggingFaceHub(model="bert-large-uncased"), prompt=prompt)
bert_chain.invoke("scary")

 

반면 LlamaIndex 의 경우 LangChain 과 같은 유연성은 떨어지지만, 앞서 언급한 것처럼 빠르게 document 기반의 RAG 를 만들 때 유용하다고 할 수 있다. 

 

3. Multi-retrieval

LangChain 의 경우 Multi-retrieval 를 구현할 수 있다. 예를 들어, 유저 쿼리를 받아, 법과 관련된 쿼리라면, 법 문서에 적용된 key-based retrieval 를 호출하고, 과학 관련 쿼리라면, 과학 논문에 적용된 embedding-based retrieval 를 호출하는 multi-retrieval 를 구현해보자. 

from langchain.retrievers.multi_retriever import MultiRetriever
from langchain.retrievers import FAISSRetriever, KeywordRetriever

# Define keyword and embedding-based retrieval systems
keyword_retriever = KeywordRetriever(documents=legal_documents)
embedding_retriever = FAISSRetriever(index=scientific_index)

# Combine them in a MultiRetriever
retriever = MultiRetriever(retrievers={
    'legal': keyword_retriever,
    'science': embedding_retriever
})

# Query the retriever with a legal question
response = retriever.retrieve("What are the recent changes in contract law?")
print(response)

 

4. Community Support 측면

LangChain 은 위와 같은 advanced 된 기능들 때문에 최근 빠르게 관련 커뮤니티가 성장하고 있으며, Llama Index 와 비교하여 좀 더 성장된 ecosystem 을 갖고 있다. LlamaIndex 의 경우 비교적 최근에 등장했지만 마찬가지로 빠르게 관심을 얻고 있으며, 앞서 언급한 것처럼 좀 더 간단한 작업에 적합하다. 

 

결로: 결론적으로 어떤 상황에서 어떤 프레임워크를 선택해야할까?

LangChain 을 선택하면 좋은 상황
- 다양한 구성 요소들을 자유롭게 커스텀하고 싶을 때 (Retrieval, LLM, OpenAI, HuggingFace, FAISS, Chroma...)
- VectorStore 를 다양한 타입의 소스로 구성하고 싶을 때 (Text, PDF, API 등)
- 다양한 Retrieval 전략을 사용하고 싶을 때 (Keyworkd search, post-retrieval ranking 등)

LlamaIndex 를 선택하면 좋은 상황 
- 간단한 RAG System 을 구축하고 싶을 때 (예를 들어, document 를 요약해주는 AI 를 구현)
- 특별한 커스텀이 필요없을 때 
- 구조적인 문서 (법 관련 문서 등) 을 기반으로 RAG 를 구축할 때 잘 작동함 

 

 

 

 

 

출처

- https://medium.com/@tam.tamanna18/langchain-vs-llamaindex-a-comprehensive-comparison-for-retrieval-augmented-generation-rag-0adc119363fe

반응형
반응형

 

 

기본적으로 가상환경을 새롭게 생성하고 거기에 설치하는 것이 좋다.

 

가상환경 생성

virtualenv openai
python3 -m ipykernel install --user --name=openai

# 가상환경 activate
source ./activate

 

pysqlite3 설치

pip install pysqlite3-binary

# 내장된 pysqlite3 대신 pysqlite3 을 사용하도록 설정한다. 
import sys
import pysqlite3
sys.modules["sqlite3"] = pysqlite3

import sqlite3
print(sqlite3.sqlite_version)
>> 3.46.1

 

3.35 이상 버전에서 chromadb 를 사용할 수 있다.

import chromadb

 

 

반응형
반응형

 

기본적인 RAG 를 좀 더 개선해보기 (w/ langchain)

 

기본 RAG 의 개념 : 기본 RAG (Retrieval Augmented Generation) 는 query 를 입력으로 받아 vector store 로부터 relevant 한 chunk 를 검색해서 가져온 후, 이를 prompt 에 추가해서 최종적인 답변을 출력한다.

 

사용된 개념

1. Relevance Check : vector store 에서 retrive 해온 chunk 가 relevant 한지 LLM 을 활용해서 검증한다.

2. Hallucination Check  : retrieved chunk 를 참고해서 생성한 최종 답변이 hallucination 인지 여부를 LLM 을 활용해서 검증한다. 

 

RAG 를 사용하기 위한 프레임워크로 langchain 을 사용한다. (비슷한 역할을 하는 Llamaindex 라는 프레임워크도 있다.)

 

목적: 3가지 웹문서를 vector store 로 저장하고, 이에 기반해 사용자 질문에 답변하는 RAG 를 만든다.

 

1. 필요한 라이브러리 설치

%pip install langchain langchain-openai langchain-openai langchain_chroma langchain-text-splitters langchain_community

 

2. LLM 객체 생성

import getpass
import os

os.environ["OPENAI_API_KEY"] = "API_KEY 를 입력하세요."
from langchain_openai import ChatOpenAI

# openai의 gpt-4o-mini 모델 사용
llm = ChatOpenAI(model="gpt-4o-mini")

 

3. 문서를 로드한다.

- langchain 의 WebBaseLoader를 활용하며, beautiful soup 라이브러리를 활용해 html 태그 등을 parsing 해 본문 내용만 가져온다고 이해하면된다. 

import bs4
from langchain import hub
from langchain_chroma import Chroma
from langchain_community.document_loaders import WebBaseLoader
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables import RunnablePassthrough
from langchain_openai import OpenAIEmbeddings
from langchain_text_splitters import RecursiveCharacterTextSplitter

urls = [
    "https://lilianweng.github.io/posts/2023-06-23-agent/",
    "https://lilianweng.github.io/posts/2023-03-15-prompt-engineering/",
    "https://lilianweng.github.io/posts/2023-10-25-adv-attack-llm/",
]

# Load, chunk and index the contents of the blog.
loader = WebBaseLoader(
    web_paths=urls,
    bs_kwargs=dict(
        parse_only=bs4.SoupStrainer(
            class_=("post-content", "post-title", "post-header")
        )
    ),
)
docs = loader.load()

 

4. 로드한 문서를 Split 해서 Vector store 로 저장한다.

- chunk size 는 1000  토큰이며, 200의 오버랩을 허용한다. 각 청크간에 약간의 중복을 준다는 의미이다. 

- chunk 를 vector 로 저장할 때, embedding 이 필요한데, openai 의 text-embedding-3-small 모델을 사용했다.

- retrieval 의 세팅으로 similarity 를 기준으로 6개의 chunk 를 가져오는 retrieval 객체를 정의한다. 

text_splitter = RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=200)
splits = text_splitter.split_documents(docs)
vectorstore = Chroma.from_documents(documents=splits, embedding=OpenAIEmbeddings(model="text-embedding-3-small"))
# Retrieve and generate using the relevant snippets of the blog.
retriever = vectorstore.as_retriever(search_type="similarity", search_kwargs={'k':6})

 

5. user query 를 입력으로 받아 관련도가 높은 chunk 를 retrieve 해온다.

- 가져온 chunk 6개를 1번 chunk : ... 2번 chunk : ... 이런 형식으로 텍스트 형식으로 저장한다. 

from langchain_core.output_parsers import JsonOutputParser
from langchain_core.prompts import PromptTemplate
from langchain import hub
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables import RunnablePassthrough
from langchain_core.output_parsers import JsonOutputParser
from langchain_core.prompts import PromptTemplate
from langchain_openai import ChatOpenAI

prompt = hub.pull("rlm/rag-prompt")

user_query = "What is agent memory?"
retrieved_docs = retriever.invoke("what is agent memory?")
retrieved_docs_text = ""
for i in range(0,6) :
    doc_content = retrieved_docs[i].page_content
    retrieved_docs_text += f'{i+1}번 chunk: {doc_content}\n'

 

6. 목적에 맞는 최종 prompt 를 작성한다.

- relevance check 와 hallucination check 를 하여 기본 RAG 를 좀 더 개선하는 것이 목적이므로, 이에 대한 로직을 추가한 프롬프트를 작성한다. 전체적인 답변 생성 로직과 함께 필요한 정보 - 유저 query 와 retrieved chunk 를 제공해주어야한다. 

query = {"question": {f"""
     너는 유저로부터 query 를 입력 받아서 최종적인 답변을 해주는 시스템이야.

     그런데, retrieval 된 결과를 본 후에, relevance 가 있는 retrieved chunk 만을 참고해서 최종적인 답변을 해줄거야.
     아래 step 대로 수행한 후에 최종적인 답변을 출력해.

     Step1) retrieved chunk 를 보고 관련이 있으면 각각의 청크 번호별로 relevance: yes 관련이 없으면 relevance: no 의 형식으로 json 방식으로 출력해.\n
     Step2) 만약 모든 chunk 가 relevance 가 no 인 경우에, relevant chunk 를 찾을 수 없었습니다. 라는 메시지와 함께 프로그램을 종료해.
     Step3) 만약 하나 이상의 chunk 가 relevance 가 yes 인 경우에, 그 chunk들만 참고해서 답변을 생성해.
     Step4) 최종 답변에 hallucination 이 있었는지를 평가하고 만약, 있었다면 hallucination : yes 라고 출력해. 없다면 hallucination : no 라고 출력해.
     Step5) 만약 hallucination : no 인 경우에, Step3로 돌아가서 답변을 다시 생성해. 이 과정은 딱 1번만 실행해 무한루프가 돌지 않도록.
     Step6) 지금까지의 정보를 종합해서 최종적인 답변을 생성해

     답변은 각 스텝별로 상세하게 출력해

     아래는 user query 와 retrieved chunk 에 대한 정보야.

     query : {user_query}\n
     retrieved chunks: {retrieved_docs_text}"""}}

 

7. Prompt Template 을 기반으로 RAG Chain 을 호출해 최종 답변을 json 형식으로 출력한다.

- prompt template 은 프롬프트를 구조화된 형식으로 작성할 수 있게 도와주는 클래스이다. 

- rag chain 은 query-> prompt -> llm -> output 파이프라인을 하나의 함수로 실행할 수 있도록 해준다. 

parser = JsonOutputParser()

prompt = PromptTemplate(
    template="Answer the user query.\n{format_instructions}\n{query}\n",
    input_variables=["query"],
    partial_variables={"format_instructions": parser.get_format_instructions()},
)

rag_chain = (
    prompt
    | llm
    | StrOutputParser()
)

rag_chain.invoke({"query": query})

 

최종적으로 생성된 답변

json\n{\n  "step1": {\n    "1": "relevance: yes",\n    "2": "relevance: yes",\n    "3": "relevance: yes",\n    "4": "relevance: yes",\n    "5": "relevance: no",\n    "6": "relevance: no"\n  },\n  "step2": "All retrieved chunks have relevance.",\n  "step3": "The relevant chunks provide information about agent memory, which consists of both long-term and short-term memory. Agent memory is a long-term memory module that records agents\' experiences in natural language. Observations can trigger new statements, and the retrieval model assists in informing the agent’s behavior based on relevance, recency, and importance. Additionally, the reflection mechanism synthesizes these memories into higher-level inferences over time.",\n  "step4": "hallucination: no",\n  "step5": "Since hallucination is no, I will generate the answer again.",\n  "step6": {\n    "final_answer": "Agent memory refers to a long-term memory module that captures a comprehensive list of an agent\'s experiences in natural language, which can be used to inform the agent\'s behavior. It includes short-term aspects, such as in-context learning, as well as long-term memory that retains information over extended periods. The retrieval model helps prioritize memories based on relevance, recency, and importance, while the reflection mechanism synthesizes past events into higher-level insights that guide future behavior."\n  }\n}\n

반응형