LLM 시대, LangChain(랭체인)으로 배우는 AI 소프트웨어 개발
많은 사람들이 LLM을 이용한 애플리케이션을 만들려고 할 때 생각하는 것이 ChatGPT와 같은 챗봇이 아닐까요?
이번에는 LangChain(랭체인)과 Streamlit을 사용하여 웹기반 챗봇을 만들면서 구현방법을 배워봅시다. 작성하는 챗봇은 멀티모달입력(이미지 및 텍스트)을 허용하고 LLM을 사용하여 답변을 생성합니다. 또한 답변 생성에는 앞서 소개했던 RAG(검색증강생성, Retrieval Augmented Generation)을 사용합니다. 즉, 주어진 질문에 대해 관련 정보를 검색하고 해당 정보를 기반으로 답변을 생성하는 메커니즘을 구축합니다.
아래 그림은 생성할 챗봇 이미지를 보여줍니다. 화면에는 텍스트 입력과 이미지 업로드버튼이 표시되며 사용자는 텍스트와 이미지를 입력하여 챗봇과 상호작용할 수 있습니다. 챗봇은 입력된 이미지와 텍스트를 기반으로 관련 정ㅂ조를 인덱스에서 검색하고 그 정보를 기반으로 LLM을 사용하여 응답을 생성합니다.
이런 챗봇의 유스케이스는 사용자가 업로드한 쓰레기 이미지를 바탕으로, 쓰레기의 분별방법을 회답한다고 하는 것이 생각됩니다. 이 경우, 인덱스(데이터베이스)에 미리 지역별 쓰레기분별방법에 관한 정보를 등록해 두고, 사용자가 업로드한 이미지를 기반으로 해당 이미지와 관련 정보를 검색합니다.
아래 그림에서는 버튼 배터리 이미지를 업로드하고 방법을 묻습니다. 챗봇은 업로드된 이미지를 바탕으로 버튼 배터리에 대한 정보를 검색하고 해당 정보를 사용하여 답변을 생성합니다. 이 예시에서는 서울의 쓰레기분리배출를 미리 인덱스에 등록하고 있습니다. RAG(검색증강생성)를 사용하면 지역별 응답을 생성할 수 있습니다. 예를들어, 그림의 응답에는 서울시 버튼 배터리를 회수하지 않는 정보가 포함되어 있습니다. 이는 일반적인 쓰레기 분리수거에 대한 정보뿐만 아니라 지역별 정보를 포함하여 생성할 수 있습니다.
이번에는 위와 같은 챗봇을 6단계로 나누어 구축하는 방법을 소개합니다.
사용자 인터페이스 구현
질문응답 시스템으로 확장
대화이력 구현
컨텍스트 확장
RAG 구현
멀티모달 대응
구현단계별로 예제 실행에 필요한 모듈들을 설치합니다.
이번에는 streamlit과 langchain을 설치합니다.
챗봇 시스템의 기반이 되는 사용자 인터페이스를 완성시킵니다. 사용자 인터페이스 구축은 Streamlit라고 하는 프레임워크를 사용합니다. Streamlit를 선택한 이유는 데이터사이언스 및 머신러닝 프로젝트에 특화되어 있으며, 웹시스템 관련 기술을 익히지 않아도 대화형 웹애플리케이션을 쉽게 구축할 수 있습니다.
사용자 인터페이스만 구현한 프로그램은 아래 코드와 같습니다.
사용자 인터페이스 구현(src/chat/step1.py)
Streamlit을 사용하는 프로그램은 일반적으로 Python스크립트와 약간 다른 동작을 합니다.
Streamlit의 애플리케이션은 실행 중에 여러 번 전체 프로그램이 다시 실행될 수 있는 특징이 있고, 다음과 같은 타이밍에 프로그램이 실행됩니다.
Streamlit 앱이 처음 로드될 떄
사용자가 위젯(텍스트나 슬라이더등) 값을 변경할 경우
사용자가 앱 재실행을 수동으로 트리거 할 떄 (예: 전송 버튼을 클릭시)
이런 이벤트가 발생하면 Streamlit은 프로그램을 처음부터 끝까지 다시 실행합니다.
이 문장은 streamlit 모듈을 st라는 이름으로 가져옵니다. st라는 짧은 이름을 지정하는 것은 관례입니다. 다음 문장은 앱 타이틀을 설정합니다.
여기에서는 웹시스템 타이틀에 "멀터모달 RAG 챗봇"이라는 이름을 설정합니다. 제목은 브라우저 화면에 표시됩니다. 그런 다음 이미지 업로드하는 위젯을 추가합니다.
st.file_uploader함수는 이미지파일을 업로드하는 위젯을 추가합니다. 이 함수는 사용자가 업로드한 이미지 파일을 반환합니다. 여기에서는 "이미지를 선택하십시오"라는 캡션이 보이는 업로더를 추가합니다. 또한, type인수에 ["jpg", "jpeg", "png"]를 지정하는 것으로 업로드할 ㅅ수 있는 이미지 파일 확장자를 제한하고 있습니다. 그런 다음 업로드된 이미지를 표시합니다.
여기서는 uploaded_file이 None이 아닌 경우, 즉, 사용자가 이미지를 업로드하면 이미지를 표시합니다. st.image함수는 이미지를 표시하는 웨젯을 추가합니다. 다음은 텍스트 입력필드를 추가합니다.
st.text_input함수는 텍스트입력필드를 추가합니다. 또한, 사용자가 브라우저에서 입력한 값을 변수 user_input에 저장합니다. 마지막으로 보내기버튼을 추가합니다.
이 부분에서는 if문을 사용하여 사용자가 "보내기"버튼을 클릭했는지 여부를 확인합니다. 사용자가 버튼을 클릭하면 위 코드는 st.write함수를 사용하여 사용자가 입력한 메시지를 웹페이지를 표시합니다.
이 예시 프로그램은 아래 명령으로 실행할 수 있습니다.
명령이 실행되면 브라우저가 자동으로 시작되고 위에서 아래와 같은 화면이 표시됩니다. 이 화면에서 텍스트박스에 메시지를 입력하면 화면에 사용자가 입력한 메시지가 표시됩니다.
이전에는 사용자입력을 그대로 출력했습니다. 여기에서는 사용자 입력을 LLM에 보내고 응답을 출력하도록 확장합니다. LLM에 대한 연결은 LangChain(랭체인)을 사용하여 수행합니다. 위에서 사용한 코드를 사용자 입력에 응답할 수 있도록 확장된 프로그램이 아래 코드에 나와 있습니다.
우선 추가로 사용하는 컴포넌트를 추가합니다.
이 코드는 LangChain(랭체인) 모듈에서 ChatOpenAI클래스를 가져옵니다. ChatOpenAI클래스는 OpenAI 모델에 대한 액세스를 제공합니다. 다음은 ChatOpenAI클래스를 사용하여 AI가 사용자입력에 대한 응답을 생성하도록 합니다.
이 부분에서는 우선 ChatOpenAI클래스를 인스턴스화하고 변수llm에 저장합니다. 그리고 llm객체(여기서는 OpenAI LLM)의 invoke메소드에 user_input을 전달하여 LLM에 대한 응답을 생성합니다. 그리고 OpenAI API키를 사용하는 방법은 '.env'파일을 사용하여 불러오는 방식을 이용했습니다. 프로젝트 루트폴더에 '.env'파일을 만들고 아래와 같이 내용을 추가합니다.
Python코드에 API키를 불러오는 것은 아래와 같은 코드를 사용합니다.
만약 dotenv라이브러리가 없는 경우, 아래와 같이 명령을 실행하여 설치합니다.
생성된 응답은 response변수에 저장되고, st.write함수를 사용하여 웹페이지에서 "ai:"와 같이 표시됩니다.
동작을 확인해 보면 아래 소스코드 프로그램을 실행하고 텍스트박스에 질문을 입력하고 [제출]버튼을 클릭하면 아래 그림과 같이 LLM에서 질문에 대한 응답을 반환하고 사용자질문과 같이 표시됩니다.
질문응답 시스템으로 확장(src/chat/step2.py)
사용자 입력에 대해 챗봇이 응답할 수 있습니다. 그러나, 아직 자연스러운 대화를 할 수 있는 것은 아닙니다. LLM이 지금까지 대화내용을 기억하지 못하기 때문입니다. 그렇다면, 대화내용을 기억하는 방법은 무엇이 있을까요?
이를 위해서는 과거 대화내용을 기록하고 LLM에 문의할 때마다 과거 대화기록을 전달해야 합니다. 대화이력을 프롬프트로 제공함으로써 LLM은 과거 대화이력도 참고하면서 질문에 답변할 수 있게 됩니다.
대화기록 구현(src/chat/step3.py)
다음은 예제에서 사용하는 HumanMessage클래스를 가져옵니다.
HumanMessage클래스는 사용자의 메시지를 나타내는 클래스입니다. 이 프로그램은 대화기록을 메시지 목록으로 저장합니다. 다음으로 대화기록을 세션상태로 저장할 수 있도록 초기화합니다.
여기서 if문은 Streamlit세션에 대화기록을 저장하기 위한 상태를 빈목록으로 초기화합니다. st.session_state는 Streamlit에서 제공하는 객체로 세션 중에 데이터를 보관하기 위한 사전 객체 역할을 합니다. 이 사전객체에 history라는 키로 빈목록을 등록합니다. 이 예시는 이벤트가 발생할 때마다 호출하지만 최초 첫번째에서만 실행할 때 초기화가 실해오딥니다. 이 목록에는 사용자 메시지(HumanMessage객체)와 LLM응답 메시지가 번갈아 추가됩니다.
여기서 st.session_state를 사용하는 것에 의문을 가지는 독자가 있을 수 있습니다. 단순히 history변수에 목록을 저장하는 것이 충분하지 않을까요? 위 소스코드는 버튼을 클릭하는 것과 같은 이벤트가 발생할 때마다 다시 실행됩니다. 변수 이용에서는 매회 초기화되어 버리기 때문에 여러번 실행되어도 계속해서 보관유지되는 st.session_state의 이용이 필요합니다. 또한, history외에도 LLM인스턴스도 st.session_state에 저장됩니다. 이렇게하면 대화기록과 LLM인스턴스가 세션중에 유지됩니다. 다음은 버튼 클릭시 처리를 살펴봅시다.
우선 대화기록에 사용자의 메시지를 추가합니다. 사용자 메시지를 만들려면 HumanMessage 클래스를 사용합니다. 그런 다음 llm객체의 invoke메소드를 호출하여 대화기록을 인수로 전달합니다. 이 메소드는 대화기록을 기반으로 LLM에 쿼리하고 응답을 생성하고 response변수에 저장합니다. response변수는 AIMessage객체로 대화기록에 추가됩니다. 마지막으로 response변수값을 대화기록에 추가합니다.
다음은 대화기록을 보여줍니다.
여기에서는 st.session_state.history에 저장된 대화기록을 표시합니다. reversed함수를 사용하여 대화기록을 역순으로 표시합니다. 이렇게 하면 새 메시지가 위레 표시됩니다. 메시지 객체는 HumanMessage클래스와 AIMessage클래스의 인스턴스중 하나입니다. message.type은 메시지유형을 나타내는 문자열('human' 또는 'ai')을 보유합니다. message.content는 메시지내용을 나타내는 문자열을 보유합니다.
이 프로그램을 실행하면 아래와 같이 사용자와 AI대화가 표시됩니다.
컨텍스트에 대화이력을 이용함으로써 사용자와 AI의 자연스러운 대화를 실현할 수 있게 되었습니다. 그러나, 컨텍스트의 사용은 대화이력에 국한되지 않습니다. 텍스트로 표현할 수 있는 모든 정보를 문맥에 포함할 수 있습니다. 추가 컨텍스트를 활용하면 LLM은 학습시 액세스할 수 없었던 정보를 사용하여 보다 적절한 답변을 생성할 수 있습니다. 이용가능한 정보로는 사용자 고유의 데이터, 예를 들면, 개인 취향, 과거 구입이력, 지리적위치, 캘린더 예정, 건강상태 및 활동 데이터 등 다양한 정보를 생각할 수 있습니다.
우선 컨텍스트에 임의의 정보를 추가하는 방법을 배웁니다. 여기에서는 LangChain(랭체인)의 기능을 사용하여 다음 정책에서 컨텍스트를 추가합니다.
프롬프트 템플릿으로 체인 만들기
프롬프트 템플릿에 변수로 추가 컨텍스트 정보 포함
체인을 사용하여 LLM에 쿼리함
컨텍스트 확장(src/chat/step4.py)
우선 프롬프트템플릿을 만드는 클래스를 가져옵니다.
다음으로 체인을 만드는 함수를 정의합니다.
이 함수는 프롬프트 템플릿을 만들고 템플릿을 사용하여 체인을 만듭니다. 프롬프트 템플릿은 ChatPromptTremplate클래스의 from_messages메소드를 사용하여 만듭니다. 이 메소드는 메시지 목록을 받고 프롬프트 템플릿을 만듭니다. 개별 메시지는 튜플로 표현되며 각 튜플에는 메시지역할(예: 'system', 'human')과 메시지 내용을 나타내는 문자열이 포함됩니다. 메시지 내용에는 변수가 포함될 수 있으며 이러한 변수는 나중에 대체됩니다. 여기서 {input}, {info}는 자리표시자입니다. {input}은 사용자입력을 나타내고 {info}는 컨텍스트에 추가할 정보를 나타냅니다.
또한, ("placeholder", "{history}")는 대화기록을 나타내는 자리표시자입니다. "placeholder"는 프롬프트 템플릿에서 여러 메시지를 함께 변수로 사용하는 자리표시자입니다. history는 대화기록을 나타내는 자리표시자로 대화이력을 나타내는 메시지 목록으로 대체됩니다. input과 info가 문자열이지만, history는 목록입니다.
마지막으로 파이프(|) 연산자를 사용하여 프롬프트 템플릿과 언어모델을 결합하여 체인을 만듭니다. 여기에서는 언어모델로 ChatOpenAI클래스를 사용합니다. 모델에는 'gpt-4o-mini'를 지정하고 temperature에는 0을 지정합니다. temperature에 0을 지정하여 언어모델의 출력을 확정적으로 만들 수 있습니다. 이것은 컨텍스트로 준 대화이력과 참고정보를 중시하도록 하는 것이 목적입니다. 다음으로 create_chain함수를 사용하여 체인을 만듭니다.
생성한 체인을 재사용할 수 있도록 st.session_state.chain에 저장합니다. 마지막으로 체인을 사용하여 LLM에 문의합니다.
여기서는 체인 객체의 invoke메소드를 호출하여 LLM에 쿼리합니다. invoke메소드는 input, history, info의 3가지 키를 가진 사전형을 전달합니다. input은 사용자 입력을 나타내고, history는 대화기록을 나타내고, info는 컨텍스트에 추가할 정보를 나타냅니다. 여기서 사용자고유의 정보를 상정해, info에는 사용자 나이가 10세인 것을 나타내는 문자열을 지정하고 있습니다.
이 프로그램을 실행한 결과를 아래 그림에 나타납니다. 이 예제에서는 사용자가 '추천하는 술은?'이라고 질문합니다. 그러나, LLM은 사용자 나이를 고려하여 '10세라면 술을 마실 수 없어요!'라고 대답하고 있습니다. 이는 사용자 나이가 10세임을 컨텍스트로 제공했기 때문입니다. 이것으로 문맥으로서 준 사용자 나이가 응답에 반영된 것을 알 수 있습니다.
지금까지 컨텍스트에 임의의 텍스트를 추가하는 방법을 배웠습니다. 여기에서는 사용자의 입력과 관련된 정보를 동적으로 검색하고 컨텍스트에 추가하여 방법을 배웁니다. 이 방법으로 RAG(Retrieval Augmented Generation,검색증강생성)입니다.
RAG(검색증강생성)에서는 미리 정보를 등록한 인덱스가 필요합니다. 색인은 프로그램이 실행되기 전에 만들어져 있다고 가정합니다. 인덱스에 저장하는 정보의 후보로서는 주로 LLM훈련데이터에 포함되지 않는 다양한 정보가 나옵니다. 예를 들어, 최신 정보나 기업 내부데이터, 개인 고유정보등을 들 수 있습니다. 이번에는 인덱스를 작성하고 인덱스를 이용하여 챗봇을 구현하는 방법을 설명합니다. 여기에서는 인덱스를 미리 파일 시스템에 저장하여 챗봇이 실행될 때 로드하게 됩니다.
우선 파일시스템에 인덱스를 만듭니다. 여기에서는 터미널에서 색인을 만드는 프로그램을 만듭니다. 이 프로그램 인수는 CSV파일을 지정하고 해당 파일에서 색인을 만듭니다.
CSV파일을 로드하려면 LangChain(랭체인)에서 제공하는 클래스를 사용합니다. LangChain(랭체인)은 CSV이외의 로더도 제공하기 떄문에 이 프로그램을 약간 수정하여 다른 형식파일에서도 인덱스를 만들 수 있습니다.
아래 코드는 색인을 만드는 프로그램을 보여줍니다. LangChain(랭체인)을 사용한 인덱싱 절차의 기본사항은 5장쪽 내용을 살펴보시기 바랍니다. 여기서는 CSV파일에서 텍스트를 추출하기 위해 CSVLoader클래스를 사용합니다.
다른 유형의 파일에서 텍스트를 추출하려면 알맞은 로더를 선택합니다. 이 프로그램은 인덱스를 영속화하기 위해 persist_directory인수를 지정합니다.
persist_directory인수는 인덱스를 저장할 디렉토리를 지정합니다. 이 디렉토리에는 인덱스정보가 저장됩니다. 챗봇에서는 여기에서 만든 인덱스를 읽고 이용합니다.
인덱싱 프로그램은 아래와 같이 실행할 수 있습니다.
다음 코드를 파주시 분류배출 정보(pajusi-ecyclablewaste.csv)를 인덱스에 추가하는 예시입니다.
만약 오류가 발생하면 아래 명령을 실행해서 chardet 라이브러리를 설치합니다.
색인생성(src/chat/make_index.py)
학습에 사용한 CSV파일
작성된 인덱스를 이용해 챗봇을 구현합니다. 아래 코드는 RAG를 이용한 챗봇 프로그램을 보여줍니다. 이 프로그램은 사용자로부터의 질문에 관련하는 정보를 인덱스로부터 얻고 그것을 바탕으로 대답을 생성합니다.
RAG구현(src/chat/step5.py)
주요 코드를 설명합니다.
새로 추가된 Chroma클래스와 OpenAIEmbeddings클래스를 가져옵니다. 또한 itemgetter함수도 가져옵니다.
Chroma클래스 인스턴스를 만들고 as_retriever메소드를 사용하여 retriever객체를 만듭니다. search_kwargs는 검색시 파라미터를 지정합니다. 여기에서는 k를 3으로 설정하여 검색결과의 상위 3건을 취득하도록 하고 있습니다. retriever객체는 LangChain(랭체인)의 Runnable인터페이스를 구현하고 있어 invoke메소드를 가지고 있습니다. retriever는 사용자의 질문과 관련된 정보를 색인에서 검색하기 위해 다음과 같이 사용됩니다.
이 return문에서는 병렬구문을 사용하여 체인을 만듭니다. prompt는 그 앞에 지정된 사전을 전달합니다. 이 사전에는 input, info, history의 3가지 키가 있으며, input과 history는 itemgetter함수를 사용하여 각 키에 해당하는 값을 가져옵니다. info는 input, retriever를 파이프 연산자로 결합하고 format_docs함수를 적용합니다. retriever가 반환하는 정보는 입력과 관련된 3개 문서입니다. format_docs함수는 문서 목록을 받고 이를 정형화하여 하나의 문자열로 묶습니다.
format_docs함수 구현은 다음과 같습니다.
이 함수는 문서목록을 받고 이를 하나의 문자열로 정형화합니다. 여기에서는 리스트내 포장표기로 각 문서의 page_content속성을 취득해 그것들을 개행으로 결합하고 있습니다.
또한, 체인 호출 부분에서는 인수 사전에서 info를 생략하고 있습니다. 이는 retriever를 사용하여 얻은 정보를 info로 사용하도록 변경했기 때문입니다.
실행해서 "TV를 버리는 방법은 무엇입니까?"라고 질문을 해봅시다.
파주시 지역의 폐기내용이 적용되어 응답해주는 것을 알 수 있습니다. 만약 RAG를 하지 않게 되면 아래와 같은 일반정보가 표시됩니다.
지금까지는 텍스트 데이터를 기준으로 사용했습니다. 그러나 멀티모달 LLM은 이미지와 음성과 같은 다른 데이터형식도 대응할 수 있습니다. 여기에서는 이미지를 프롬프트의 일부로 사용할 수 있도록 프로그램을 확장합니다. 또한 이미지데이터도 RAG를 사용하여 답변을 생성하는 것도 생각해 봅시다.
이미지 데이터를 사용하여 인덱스를 검색하는 방법은 이미지 데이터에서 내장 벡터를 생성하고 해당 벡터를 사용하여 인덱스를 검색합니다. 이미지 데이터로부터 직접 임베딩 벡터를 생성하는 구현도 있을 수 있지만, 여기서는 이미지 데이터를 일단 텍스트 데이터로 변환하고 나서 인덱스를 작성합니다. 다만, OpenAIEmbeddings클래스가 텍스트 데이터만을 받아들일 수 있어 이미지 데이터를 텍스트 데이터로 변환하는 부분에 멀티모달 LLM을 사용합니다.
멀티모달 지원(src/chat/step6.py)
추가로 필요한 클래스를 추가합니다.
StrOutputParser 클래스는 LLM의 응답메시지에서 텍스트만 검색하는 클래스입니다. base64모듈은 이미지 데이터를 Base64형으로 인코딩하는데 사용됩니다. [제출]버튼을 클릭할 때 처리를 확인해봅시다.
여기서 이미지를 지정되면 get_image_description함수를 사용하여 이미지 설명을 검색하고 설명을 image_description변수에 저장합니다. 그리고 이미지 설명을 입력과 결합하여 "input"키에 저장합니다. 또한, 이미지 데이터를 "image"키에 저장합니다. 한편 이미지가 지정되지 않은 경우는 "input"키에 사용자의 입력만을 "image"키에는 None을 가지고 있습니다.
다음은 이미지 설명을 얻는 함수를 살펴봅시다.
이 함수는 이미지 데이터를 받고, 이미지에 대한 설명을 반환합니다. 먼저 이미지 데이터를 변수로 사용하는 프롬프트 템플릿을 만듭니다. {image_data}는 이미지 데이터를 나타내는 자리표시자입니다. 그런 다음 프롬프트 템플릿과 LLM을 결합하여 체인을 만듭니다. 여기서는 LLM으로 멀티모달에 해당하는 gpt-4o-mini를 지정합니다. 이 체인은 이미지 데이터를 입력으로 받아 이미지 생성을 생성합니다. 다음은 RAG(검색증강생성)에 대응하는 체인의 작성부분을 확인해 봅시다.
프롬프트 템플릿에 {message}자리표시자를 추가합니다. 그리고 이 메시지값을 create_message함수로 생성합니다. create_message함수는 이미지데이터 유무에 따라 알맞은 메시지가 생성됩니다.
인수로 사전을 받고, 그 내용에 따라 메시지를 생성합니다. 이미지 데이터가 있는 경우, 이미지 데이터와 텍스트가 포함된 이미지를 반환합니다. 이미지 데이터가 없으면 텍스트만 포함된 메시지를 반환합니다.
챗봇은 주어진 텍스트("이 쓰레기는 어떻게 버려야 할까요?")와 이미지를 통해 관련 쓰레기를 버리는 방법을 검색하고 결과를 보여줍니다. 색인에서 분류배출하는 내용을 CSV파일로 정리한 것을 사용하여 지정된 쓰레기 관련 정보가 반영되어 있는지 확인할 수 있습니다. 색인에 다른 데이터를 등록하면 이미지와 텍스트로 구성된 다른 질문에 적절히 답변할 수 있습니다.
©2024-2025 GAEBAL AI, Hand-crafted & made with Damon Jaewoo Kim.
GAEBAL AI 개발사: https://gaebalai.com
AI 강의 및 개발, 컨설팅 문의: https://talk.naver.com/ct/w5umt5