본문 바로가기

내일배움캠프

LLM개인과제 - LangChain 기본적인 사용

장고 심화 주차의 두번째 과제, LLM을 활용하는 과제이다. LLM활용 이외의 주제는 주어지지 않았기 때문에 이전부터 생각만 하던 것을 간단하게 구현해보기로 했다.

 

처음 구상은 단순하게 api를 통해서 최근 주가에 대한 정보를 가져오고 그것을 통해서 앞으로의 주가를 gpt가 예측해주는 간단한 웹페이지가 목적이었다. 그렇게 구상만하고 drf과제를 하면서 LLM특강을 듣던 중 LangChain에 대해 알게되었고 RAG가 무엇인지도 알게되었다. 특강에서는 특정 웹페이지를 예시로 들어주셨지만 아마도 분명히 로컬의 서버의 데이터를 참고해서 챗봇이 답변을 하는 기능도 있을 것이라고 생각했고 그것을 이번 과제에 적용해보기로 했다.

 

예상대로 LangChain에는 내부의 디렉토리를 참고하는 DirectoryLoader라는 클래스가 존재했고 프로젝트 구상에 그것을 추가해서 사용자가 기사의 링크를 제공하면 그것을 분석하고 요약해서 txt 파일로 서버에 저장을 하고 gpt가 주가예측에 대한 답변을 할때, 그것을 참고하도록 하는 웹페이지를 만들어보기로 했다.

 

문제가 발생한것 이 시점이었다. 기사의 링크를 제공하면 gpt가 그것의 메인 기사를 읽어내서 요약해야했는데 기사에 붙어있는 수많은 사이드기사들때문에 메인기사를 잘 읽어내지 못했기 때문이다. 해결을 위해서 프롬프트를 여러번 고쳐보고 gpt 모델을 바꿔보고 했지만 잘 해결이 되지 않았다. 결국 해결을 위해서는 다른 방법을 찾아야했다.

 

다음으로 생각해낸 방법은 더 단순한 방법으로 TextLoader를 사용하는 방법이었다. 원래 계획에서는 기사의 url 또는 사용자가 직접 입력한 텍스트를 서버에 저장하는 방식을 생각했지만 기사의 url로 제대로된 데이터를 입력받을 수 없었기때문에 텍스트만을 입력받도록 계획을 수정했을 뿐이었다.

 

from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
from langchain_chroma import Chroma
from langchain_community.document_loaders import DirectoryLoader, TextLoader
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables import RunnablePassthrough
from langchain_openai import OpenAIEmbeddings
from langchain_text_splitters import RecursiveCharacterTextSplitter
from apirequest import stock
import os

# USER_AGENT 환경 변수 설정
os.environ['USER_AGENT'] = 'MyLangChainApp/1.0'

llm = ChatOpenAI(model="gpt-4o-mini")

path = "articles"
loader = DirectoryLoader(path, glob="**/*.txt", loader_cls=lambda p: TextLoader(p, encoding="utf-8"), silent_errors=False)
docs = loader.load()

if not docs:
    raise ValueError("No documents were loaded. Please check the file path and file contents.")

text_splitter = RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=200)
splits = text_splitter.split_documents(docs)

if not splits:
    raise ValueError("Document splitting resulted in an empty list. Please check document contents.")

vectorstore = Chroma.from_documents(documents=splits, embedding=OpenAIEmbeddings())

retriever = vectorstore.as_retriever()
prompt = ChatPromptTemplate.from_messages([("user", """
당신은 금융컨설턴트입니다. 사용자는 당신에게 특정 종목에 대해서 질문합니다. 검색된 문맥과 주어진 주가를 바탕으로 주가의 변동을 예측해서 예상되는 금일의 종가를 사용자에게 제공하세요.

Question: {question}

Context: {context}

Stock Data: {stock}

Answer:
""")])

def format_docs(docs):
    return "\n\n".join(doc.page_content for doc in docs)

rag_chain = (
    {
        "context": retriever | format_docs,
        "question": RunnablePassthrough(),
        "stock": lambda _: stock,
        }
    | prompt
    | llm
    | StrOutputParser()
)

result = rag_chain.invoke("sk하이닉스의 주가변화를 예측해줘")

print(result)

 

대략적으로 만들어진 코드

다듬을 부분은 아직 많지만 일단은 기본적인 기능은 수행할 수 있는 정도이다.