2024년 12월 7일 토요일

벡터데이터베이스 기반 간단한 PDF, Web 기반 검색 전문 에이전트 만들기

이 글은 벡터데이터베이스 기반 간단한 PDF, Web 기반 검색 전문 에이전트 만드는 방법을 정리한 것이다. 이 글은 LangChain을 사용한다. lanchain은 급속히 버전과 함수가 개선되고 있어 API가 자주 변경되는 경향이 있다(Deprecated error). 이런 이유로, 현지 시점에서 관련 에이전트를 만드는 주요 코드를 남긴다. 
에이전트 예시(Build Ask Data app - Streamlit)
에이전트 예시(Local_RAG_Agents_Intel_cpu)

LangChain 설명 및 사용법은 앞의 블로그 내용(검색)을 참고한다. 다음 같은 순서로 LLM이 사용된다는 것만 이해하고 있으면 코드 사용에 큰 어려움은 없을 것이다. 

사용자 입력 > 컨텐츠 검색 입력 > 에이전트 도구에서 데이터 입력 > 과거 메모리 내용 입력 > 프롬프트 생성 > LLM 전달 > 추론 내용 출력

설치 
파이썬, 아나콘다가 준비되어 있다는 가정에서, 라이브러리 설치는 다음과 같다. 
pip install --upgrade openai langchain-openai langchain langchain-experimental

이 글은 OpenAI API Key, Tavily API Key를 신청해 가지고 있다고 가정한다. 실행 시 로그나 성능을 확인하고자 한다면, Langsmith에 가입한다. 각 API는 해당 웹사이트에서 가입하여 얻을 수 있다. 각 사이트에서 생성한 API키는 .env 파일을 생성해 다음과 같이 입력해 놓는다.
OPENAI_API_KEY=<OpenAI API Key>
LANGCHAIN_TRACING_V2=false
LANGCHAIN_ENDPOINT=https://api.smith.langchain.com
LANGCHAIN_API_KEY=<Langsmith API Key>
LANGCHAIN_PROJECT=AGENT TUTORIAL

코딩
다음과 같이 코딩한다. 우선 필요한 라이브러리를 임포트한다.
import os, getpass
from openai import OpenAI  
from dotenv import load_dotenv
from langchain_community.tools.tavily_search import TavilySearchResults
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain_community.vectorstores import FAISS
from langchain_openai import OpenAIEmbeddings
from langchain.agents import create_openai_functions_agent
from langchain_community.document_loaders import PyPDFLoader
from langchain.tools.retriever import create_retriever_tool
from langchain.agents import AgentExecutor
from langchain_openai import ChatOpenAI
from langchain import hub

os.environ["TAVILY_API_KEY"] = "input your key"
os.environ["LANGCHAIN_PROJECT"] = "AGENT TUTORIAL"
load_dotenv()
client = OpenAI(api_key="input your key")

# Travily의 웹 검색 객체 획득
web_search = TavilySearchResults(k=5) 

# PDF 데이터를 벡터DB에 청크로 저장하고, 문서 검색 객체를 획득
loader = PyPDFLoader("./202212_LiDAR.pdf")  # 적절한 PDF 입력
text_splitter = RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=100)
split_docs = loader.load_and_split(text_splitter)

embeddings = OpenAIEmbeddings(api_key=os.environ["OPENAI_API_KEY"])
vector = FAISS.from_documents(split_docs, embeddings)
vectordb_retriever = vector.as_retriever()
output = vectordb_retriever.get_relevant_documents(
    "PCL(Point Cloud Library) 라이브러리에 대해 설명해줘"
)[0]
print(output)  # PCL 검색 예시

pdf_retriever_tool = create_retriever_tool(
    vectordb_retriever,
    name="pdf_search",
    description="2023년 12월 PCL(Point Cloud Library) 정보를 PDF 문서에서 검색합니다. '2023년 12월 라이다 포인트 클라우드 처리 기술' 과 관련된 질문은 이 도구를 사용해야 합니다!",
) # 문서 검색 객체

# 에이전트 도구들 설정
tools = [web_search, pdf_retriever_tool]

# LLM 객체 설정
llm = ChatOpenAI(model="gpt-4-turbo-preview", temperature=0)

# 프롬프트 설정
prompt = hub.pull("hwchase17/openai-functions-agent")
print(prompt.messages)

# LLM 함수 호출 에이전트 설정
agent = create_openai_functions_agent(llm, tools, prompt)
agent_executor = AgentExecutor(agent=agent, tools=tools, verbose=True)

# 전문가 에이전트 질문 수행
response = agent_executor.invoke(
    {
        "input": "2010년부터 PCL 라이브러리 기술에 대한 대한 내용을 PDF 문서에서 알려줘"
    }
)
print(f'답변: {response["output"]}')

우선 PDF에서 검색하라고 에이전트에게 명령했으므로, tools에 등록된 pdf vector database를 검색하는 tool을 실행한다. 다음은 이 결과의 예이다. 


> Finished chain.
답변: PCL(Point Cloud Library)은 BSD 라이선스로 개발되어 상업 및 연구 목적에 서 무료로 사용할 수 있습니다. 이 라이브러리는 크로스 플랫폼 개발을 지원하여 리눅스, 맥, 윈도우, 안드로이드 등 다양한 운영 체제에서 사용할 수 있습니다. PCL은 잘 모듈화되어 있어 배포가 용이하며, ROS(Robot Operating System), PDAL 등 유명한 오픈소스 프로젝트에서 직접 사용됩니다....

이제, web 검색 도구를 실행해 관련 내용을 추론하고, 기존 추론된 내용은 메모리를 이용해 같이 이용해 보도록 한다. 다음 코드를 입력해 본다. 
from langchain_community.chat_message_histories import ChatMessageHistory
from langchain_core.runnables.history import RunnableWithMessageHistory
message_history = ChatMessageHistory()  # 메모리 사용

agent_with_chat_history = RunnableWithMessageHistory(
    agent_executor,
    lambda session_id: message_history,
    input_messages_key="input",
    history_messages_key="chat_history",
)

response = agent_with_chat_history.invoke(
    {
        "input": "2024년부터 PCL에 대한 새로운 내용을 인터넷 웹 문서에서 알려줘"
    },
    config={"configurable": {"session_id": "MyTestSessionID"}},
)
print(f"답변: {response['output']}")

이 코드를 실행하면, 다음과 같이 travily web search 도구를 이용해, 인터넷에서 해당 질문에 대한 검색을 수행하여, 결과를 LLM이 추론한다.


추론 결과는 다음과 같다.
> Finished chain.
답변: 2024년에 대한 Point Cloud Library (PCL)의 새로운 업데이트와 관련된 정보는 다음과 같습니다:

- **GitHub Pull Requests**: 2024년에는 PCL 개발에 몇 가지 주목할 만한 기여가 있었습니다. 예를 들어, 2024년 2월 10일에 larshg에 의해 열린 [#5958 (https://github.com/PointCloudLibrary/pcl/pulls)는 기능을 두 개의 라이브러리로 분할하 
는 작업에 대한 초안입니다. 또한, 2024년 1월 15일에 larshg에 의해 열린 [#5932 (https://github.com/PointCloudLibrary/pcl/pulls)는 관련된 또 다른 기여입니다.

- **공식 문서 및 웹사이트**: PCL은 대규모 오픈 프로젝트로, 포인트 클라우드 처리를 위한 다양한 최신 알고리즘을 포함하고 있습니다. 이러한 알고리즘에는 노이즈 데이터에서 이상치를 필터링하는 것과 같은 작업을 위한 필터링, 특징 추정, 표면 재구성, 등록, 모델 피팅 및 세분화가 포함됩니다. [공식 문서](http://pointclouds.org/documentation/index.html)와 [공식 웹사이트](https://pointclouds.org/)에서는 PCL에 대한 자세한 정보와 리소스를 제공합니다.

결론
langchain의 tool 에이전트 기능을 이용하면, 파일 벡터 검색, 웹 검색, 계산, 추론, 텍스트 및 차트 생성 등과 같은 기능을 쉽게 개발할 수 있다.

참고로, langchain과 연동되는 langsmith 사이트를 방문하면, 다음과 같이 얼마나 토큰을 사용했는 지 확인할 수 있다. 
LangSmith 로그 결과

레퍼런스
사용자 정의 체인을 위한 LCEL은 기본으로 Runnable 에서 파생되어 처리된다. 이는 다음 표준적인 인터페이스를 지원한다.
  • stream: 응답 청크를 스트리밍
  • invoke: 입력에 대한 체인을 실행 호출
  • batch: 입력 목록에 대해 체인들을 실행 호출
  • astream: 비동기적으로 응답 청크를 스트리밍
  • ainvoke: invoke의 비동기 버전
LCEL는 유닉스 파이프라인처럼 입력 | 처리 | 실행 | 출력 형태로 유연한 LLM 오케스트레이션을 지원한다.
from langchain_openai import ChatOpenAI
from langchain_core.prompts import PromptTemplate
from langchain_core.output_parsers import StrOutputParser

model = ChatOpenAI()
prompt = PromptTemplate.from_template("Explain about {topic} as one paragraph.")
chain = prompt | model | StrOutputParser()

다음 코드를 이용해 체인 그래프의 구조를 확인할 수 있다.
chain.get_graph().nodes
chain.get_graph().edges
chain.get_graph().print_ascii()

각 체인은 이터레이션(iteration)을 이용해 각 단계를 개별 관찰, 실행, 제어할 수 있도록 한다.

Runnable
runnable을 사용하면, 프롬프트 실행 중 동적으로 입력 단계에 참여할 수 있다. 이는 프롬프트에 데이터를 전달할 수 있다.
from langchain_core.prompts import PromptTemplate
from langchain_openai import ChatOpenAI
from langchain_core.runnables import RunnablePassthrough # RunnablePassthrough().invoke({"num": 10})
from langchain_core.runnables import RunnableLambda, RunnablePassthrough

prompt = PromptTemplate.from_template(
    "List top {n} famous people which have birthday {today}."
)
llm = ChatOpenAI(temperature=0, model_name="gpt-4o")

chain = (
    {"today": RunnableLambda(get_today), "n": RunnablePassthrough()}
    | prompt
    | llm
    | StrOutputParser()
)
print(chain.invoke({'n': 3}))

사용자 정의 람다를 지원하는 RunnableLambda, 입력에 따른 동적 라우팅 RunnableBranch, 병렬 처리 RunnableParall, 이전 내용 기억을 하는 RunnableWithMessageHistory 등이 있다. 

@chain을 사용하면, 알아서 RunnableLambda로 주어진 함수를 래핑한다.
@chain
def custom_chain(text):
    chain1 = prompt1 | ChatOpenAI(model="gpt-4o-mini") | StrOutputParser()
    output1 = chain1.invoke({"topic": text})
    chain2 = prompt2 | ChatOpenAI(model="gpt-4o-mini") | StrOutputParser()
    return chain2.invoke({"sentence": output1})

PydanticOutputParser
LLM 출력을 기계처리 가능한 구조화된 형태로 변환하는 파서이다. 다음은 그 예이다. 
class EmailSummary(BaseModel):
    person: str = Field(description="person who send email")
    email: str = Field(description="email address of sender")
    subject: str = Field(description="email title")
    summary: str = Field(description="email's summerized text")
    date: str = Field(description="meeting date and time in email")

parser = PydanticOutputParser(pydantic_object=EmailSummary)
print(parser.get_format_instructions())

이와 유사한, StructuredOutputParser, JsonOutputParser, PandasDataFrameOutputParser 등이 있다.

OutputFixingParser
출력 파싱 시 발생하는 오류를 자동으로 수정한다. 

LLM 모델 종류
lanchain이 지원하는 주요 LLM 모델은 openai, anthropic claude, cohere aya, facebook llama, microsoft phi, google gemini (ChatGoogleGenerativeAI), GPT4All 등이 있다. 이 중 몇몇 모델은 멀티모달을 지원한다.
gemini = ChatGoogleGenerativeAI(model="gemini-1.5-pro-latest")

system_prompt = (
    "You're writer. From given image, write short novel."
)

user_prompt = "Write short novel from the follow image."

multimodal_gemini = MultiModal(
    llm, system_prompt=system_prompt, user_prompt=user_prompt
)
IMAGE_URL = "house.jpg"
answer = multimodal_gemini.stream(IMAGE_URL)

Caching
이미 얻은 답변을 캐쉬처리 하여, LLM API 호출을 줄여주고, 실행 속도를 높여준다. InMemoryCache, SQLiteCache 등이 있다.

체인 직렬화
앞서 정의한 LLM 체인을 직렬화(langchain_core.load)하면, 개별로 파일 저장해, 필요 시 동적으로 로딩하여 사용할 수 있다. 

LLM 모델 로컬 실행
LLM 모델을 로컬 PC에서도 실행할 수 있다. 이 경우, 허깅페이스, Ollama를 이용한다. 
llm = HuggingFacePipeline.from_model_id(
    model_id="beomi/llama-2-ko-7b",  
    task="text-generation",  
    pipeline_kwargs={"max_new_tokens": 512},
)

llm = ChatOllama(model="EEVE-Korean-10.8B:latest")

문서 로더
LangChain은 매우 다양한 문서 종류를 지원한다. PDF, CSV, Excel, Word, Web, JSON, Arxiv 등을 지원한다.

문서 청크 분할 및 요약
문서를 LLM에 입력 가능하도록 청크로 분할하는 CharacterTextSplitter(/n 기준 분할), 재귀적 문서 분할, 토큰 제한 분할, 의미론적 분할, 코드 분할, HTML 분할, JSON 분할(RecursiveJsonSplitter) 등이 있다. 

문서를 요약하는 방법은 전체 문서 요약하는 Stuff, 분할 요약하는 Map reduce, 분할 요약 후 정리하는 Map refine 및 chain of density, clustering map refine 방법 등이 있다. 

임베딩 모델
문서의 청크를 검색 가능한 벡터 형태의 임베딩 벡터로 변환하는 모델을 말한다. 보통, OpenAIEmbedding, CacheBackedEmbeddings, HuggingFace Embeddings, OllamaEmbeddings, GPT4All 등이 사용된다. 

벡터 데이터베이스와 검색기
보통, Chroma, Vectordatabase, FAISS, Pinecone 등이 사용된다. 여기서 원하는 임베딩 벡터를 검색해 문서를 리턴하는 방법은 vectorstore 기반 similarity search, MMR, ContextualCompressionRetriever(압축 검색), ContextualCompressionRetriever(컨테텍스트 압축), LongContextReorder(긴 문맥 기록), ParentDocumentRetriever(계층적 문서 검색), MultiQueryRetriever(다중 질의 후 검색), SelfQueryRetriever 등이 있다.

이와 더불어, 재순위(Reranker)를 이용해, 원하는 답을 검색할 때까지 우선순위를 조정할 수 있다. Cross Encoder Reranker, Cohere reranker, Jina Reranker, FlashRank reranker 등이 있다.

메모리
채팅과 같이 앞의 입출력에 대한 맥락을 기억해야 할 때 사용한다. RunnableWithMessageHistory 등이 있다.

Fallback 오류 방지
만약 체인 실행 중 여러 이유로 에러가 발생할 경우, 다시 재 시도하는 방법이 폴백이다. with_fallbacks 함수를 사용한다.

도구와 에이전트
앞서 언급한 도구나 사전 정의된 에이전트를 이용하면, 멀티 에이전트를 만들 수 있다. 도구들 중에서는 파일관리 도구, 이미지 생성 도구, 보고서 작성 도구 등이 있다.
랭체인 무료 제공 도구 예

예를 들어, agent_toolkits에는 langchain 커뮤니티 생태계에서 업로드한 다양한 에이전트가 있다. 이 중에 엑셀 분석 에이전트, SQL agent인 create_sql_agent 등은 많이 사용된다.
랭체인 커뮤니티 제공 에이전트 예

이런 에이전트는 BaseToolkit, BaseTool, RunnableSerializable를 기본 클래스로 파생되어 개발할 수 있다. 이 클래스의 아키텍처는 컴포짓(Composite) 디자인 패턴으로 다음과 같다.
도구를 정의할 때 앞의 @chain 처럼 @tool 데코레이터를 사용할 수도 있다. description은 어떤 도구를 LLM이 선택할 지를 체인에서 결정하는 힌트로 동작한다. 예는 다음과 같다.
import re
import requests
from bs4 import BeautifulSoup
from langchain.agents import tool

@tool
def get_word_length(word: str) -> int:
    """Returns the length of a word."""
    return len(word)

@tool
def add_function(a: float, b: float) -> float:
    """Adds two numbers together."""
    return a + b

@tool
def naver_news_crawl(news_url: str) -> str:
    """Crawls a naver.com news article and returns the body content."""
    response = requests.get(news_url)
    if response.status_code == 200:
        soup = BeautifulSoup(response.text, "html.parser")
        title = soup.find("h2", id="title_area").get_text()
        content = soup.find("div", id="contents").get_text()
        cleaned_title = re.sub(r"\n{2,}", "\n", title)
        cleaned_content = re.sub(r"\n{2,}", "\n", content)
    else:
        print(f"HTTP fail code: {response.status_code}")
    return f"{cleaned_title}\n{cleaned_content}"

tools = [get_word_length, add_function, naver_news_crawl]

from langchain_openai import ChatOpenAI
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
llm_with_tools = llm.bind_tools(tools)

LLM 평가
생성 결과에 대한 평가 방법은 자동 계산이 가능한 BLEU, ROUGE, METEOR, SemScore, 수동 평가, 작업 기반 평가, LLM 기반 평가 등이 있다. 평가용 라이브러리는 RAGAS, LangSmith, W&B 등이 있다.

평가를 위해, 테스트 데이터셋을 from ragas.testset.generator import TestsetGenerator 를 통해 자동 생성할 수 있다. 다만, 토큰 비용이 과대해 질 수 있는 문제가 발생할 수 있다. 이 경우, 오픈소스 LLM 모델을 사용할 수 있다.