LLM 시대, LangChain(랭체인)으로 배우는 AI 소프트웨어 개발
LLM을 사용한 애플리케이션 개발에서 RAG(Retrieval Augmented Generation, 검색증강생성)은 중요한 역할을 합니다. LLM은 학습시에 얻은 지식에 근거하여 회답하지만, 일반용으로 공개되지 않은 정보나 최신정보등 학습시에 얻지 못한 정보에는 대응할 수 없습니다. 이것을 실현하는 것이 RAG(검색증강생성)ㅇ입니다. RAG(검색증강생성)은 질문과 관련된 정보를 색인에서 검색하고 컨텍스트에 정보를 추가하고 LLM에 전달하여 보다 정확한 답변을 생성합니다.
RAG(검색증강생성)의 기본단계는 다음과 같습니다.
색인생성: 문서에서 텍스트 추출, 분할하고, 포함 벡터로 변환한 다음 인덱스에 저장합니다.
정보검색: 사용자 질문과 관련된 정보를 인덱스에서 검색합니다.
답변생성: 검색된 정보를 LLM에 전달하여 답변을 생성합니다.
LangChain(랭체인)은 인덱스 작성부터 정보검색, 답변생성까지 RAG실현에 필요한 모든 기능을 제공합니다. 이런 기능들에 대해 순서대로 설명합니다.
LangChain(랭체인)은 다양한 문서에서 텍스트를 추출하는 기능을 문서 로더로 제공합니다. 아래 표는 LangChain(랭체인)에서 지원하는 문서유형과 로더를 나열합니다. 문서로더에는 일반 텍스트뿐만 아니라 CSV, JSON, HTML, PDF, Word, PowerPoint, 이미지등 다양한 문서유형을 지원하는것이 있습니다.
LangChain(랭체인)에서 지원하는 문서유형 및 로더 목록
이번 예시에서는 PDF파일에서 텍스트를 추출하는 프로그램을 아래 코드에 나와 있습니다. 이 예시로 과학기술정보통신부에서 매년 발생하는 '한국인터넷백서'(2023년) PDF파일에서 텍스트를 추출합니다.
관련 URL: https://www.nia.or.kr/site/nia_kor/ex/bbs/View.do?cbIdx=99871&bcIdx=26937&parentSeq=26937
문서로더를 사용하여 텍스트 추출(src/langchain/rag_loader.py)
우선 필요한 모듈을 가져옵니다.
PyPDFLoader는 PDF파일에서 텍스트를 추출하는 로더입니다. requests는 HTTP요청을 보내는 모듈이고 os는 파일작업을 수행하는 모듈입니다. 그런 다음 PDF파일을 다운로드합니다.
여기에서는 다운로드한 PDF파일을 '2023_한국인터넷_백서.pdf'라는 이름으로 현재 디렉토리에 저장합니다. 또한 파일이 이미 존재하는 경우 다운로드를 건너뜁니다. 그런 다음 PDF파일에서 텍스트를 추출합니다.
여기에서는 다운로드한 PDF파일을 '2023_한국인터넷_백서.pdf'라는 이름으로 현재 디렉토리에 저장합니다. 또한 파일이 이미 존재하는 경우 다운로드를 건너뜁니다. 그런 다음 PDF파일에서 텍스트를 추출합니다.
먼저 PyPDFLoader 클래스의 인스턴스를 만들고 loader변수에 할당합니다. 그런 다음 load메소드를 호출하여 PDF파일에서 텍스트를 추출합니다. 추출된 텍스트는 페이지변수에 저장됩니다. PyPDFLoader는 PDF파일을 페이지별로 분할하여 텍스트를 추출합니다. 마지막으로 추출한 텍스트를 보고 확인합니다.
여기에서는 추출한 텍스트의 페이지수를 표시합니다. 또한, 0부터 카운트하여 100페이지의 텍스트 앞부분 100문자를 표시하고 있습니다. 이 프로그램을 실행하면 PDF파일에서 텍스트 추출되고 페이지수와 40페이지의 텍스트가 표시됩니다.
다음으로 이 텍스트를 분할하는 처리를 합니다. 텍스트 분할에서는 문장이나 단락등 적절한 단위로 텍스트를 분할합니다. LangChain(랭체인)은 다양한 텍스트 분할방법을 제공합니다.
LangChain(랭체인)에서 지원하는 텍스트분할 유형
여기에서는 앞의 예시에 이어 PDF파일에서 추출한 텍스트를 분할해 봅시다. 아래 코드는 텍스트를 분할하는 경우를 보여줍니다.
텍스트 분할기를 이용한 텍스트 분할(src/langchain/rag_splitter.py)
우선 필요한 모듈을 가져옵니다.
여기에서는 RecursiveCharacterTextSplitter클래스를 가져옵니다. RecursiveCharacterTextSplitter는 지정된 크기에 따라 텍스트를 분할하는 기능을 제공합니다. 그런 다음 RecursiveCharacterTextSplitter를 사용하여 텍스트를 분할해 봅시다.
여기서는 RecursiveCharacterTextSplitter클래스의 인스턴스를 만들고 python_splitter변수에 할당합니다.
이 RecursiveCharacterTextSplitter클래스의 생성자는 chunk_size와 chunk_overlap이라는 2가지 파라미터가 있습니다. RecursiveCharacterTextSplitter클래스 인스턴스는 위 그림과 같이 텍스트를 분할할 수 있습니다. 이때 분할된 텍스트를 청크라고 합니다.
chunk_size는 분할할 청크의 크기를 지정합니다. 여기에서는 2000자를 하나의 청크로 분할하도록 지정합니다. chunk_overlap은 청크사이의 중첩크기를 지정합니다. 중첩크기는 청크사이의 중첩(중복)부분의 크기입니다. 중첩은 의미있는 덩어리(예: 문장이나 단락)가 별도의 청크로 분할되는 것을 방지하는데 효과적입니다. 의미있는 덩어리는 단지 청크의 경계에 위치할 수 있습니다. 이때 중첩을 갖게 함으로써, 그 덩어리가 적어도 하나의 청크에 완전하게 포함되게 되어 의미의 일부가 손실되는 위험을 경감할 수 있습니다. 여ㅑ기서는 400자 중첩을 지정합니다. 그런 다음 split_documents메소드를 호출하여 텍스트를 분할합니다.
split_documents메소드는 텍스트를 분할하고 청크목록을 반환합니다. RecursiveCharacterTextSplitter는 청크목록을 받아 각 청크가 지정된 크기 이하가 될 때까지 재귀적으로 분할합니다. 분할된 청크는 splits변수에 저장됩니다. 마지막으로 분할된 청크 수와 0부터 계산한 100번째 청크의 처음 100자를 표시해 봅시다.
다음에 분할한 텍스트를 벡터화하는 처리를 실시해 갑니다. 텍스트 벡터화는 텍스트를 포함 벡터로 변환합니다. LangChain(랭체인)은 다양한 임베디드 모델을 제공합니다. 아래 표는 LangChain(랭체인)에서 지원하는 임베디드 모델을 보여줍니다.
LangChain(랭체인)에서 지원하는 임베디드 모델
아래 코드는 텍스트를 벡터화하는 프로그램을 보여줍니다. 여기서는 OpenAI 임베디드모델을 사용합니다.
내장모델을 이용한 텍스트 벡터화 (src/langchain/rag_embeddings.py)
우선 필요한 모듈을 가져옵니다.
여기서는 OpenAIEmbeddings클래스를 가져옵니다. OpenAIEmbeddings는 OpenAI에서 제공하는 임베디드 모델을 사용하기 위한 클래스입니다.
다음 코드는 OpenAIEmbeddings클래스의 인스턴스를 만듭니다.
OpenAIEmbeddings클래스의 생성자에는 몇가지 파라미터가 있습니다. 여기서는 model파라미터에 'text-embeddings-3-small'을 지정합니다. 그런 다음 embed_query메소드를 호출하여 텍스트를 벡터화합니다.
embed_query메소드는 텍스트를 받고, 포함된 벡터를 반환합니다. 여기에서는 분할된 텍스트의 0으로부터 카운트한 10번째 청크를 벡터화 대상으로 하고 있습니다. 포함된 벡터는 vector변수에 저장됩니다. 마지막으로 내장벡터 차원수와 처음 10개 요소를 표시합니다.
앞서 분할한 텍스트를 포함 벡터로 변환했습니다. 그런 다음 이 포함된 벡터와 원본 텍스트를 벡터 스토어에 저장합니다. 벡터스토어는 임베디드 벡터를 저장하고 그에 대한 빠른 유사검색을 실현하기 위한 인덱스를 구축한 데이터베이스입니다. LangChain(랭체인)은 다양한 벡터 스토어를 제공합니다. 아래 표는 LangChain(랭체인)에서 지원하는 주요 벡터 스토어를 보여줍니다.
LangChain(랭체인)에서 지원되는 주요 벡터스토어
아래 코드는 내장벡터를 Chroma벡터 스토어에 저장하는 프로그램입니다.
벡터스토어에 내장벡터 저장 (src/langchain/rag_vectorstore.py)
Chroma 벡터스토어를 가져옵니다.
그런 다음 Chroma.from_documents메소드를 사용하여 텍스트와 포함된 벡터를 Chroma 벡터스토어에 저장합니다.
이 메소드는 분할된 텍스트를 documents파라미터로 OpenAI의 포함된 모델을 embeddings파라미터로 지정합니다. Chroma.from_documents메소드는 텍스트를 포함된 벡터로 변환하고 이를 Chroma 벡터 저장소에 저장합니다. 이렇게하면 빠른 유사검색이 가능합니다. 저장된 벡터를 사용하여 유사한 텍스트를 검색해 봅시다. 유사검색을 위해 similarity_search메소드를 사용합니다.
similarity_search메소드는 쿼리와 유사한 텍스트를 검색합니다. 여기에서는 '생성형AI의 최신 동향은?'이라는 쿼리를 사용하고 있습니다. k 파라미터는 리턴할 유사텍스트수를 지정합니다.
위와 같이 벡터스토어를 사용하면 많은 양의 텍스트에 관한 정보를 빠르게 검색할 수 있습니다. RAG(검색증강생성)의 일련의 흐름으로 텍스트 추출부터 포함벡터 생성, 벡터스토어 저장(인덱스 생성)까지 설명했습니다. 다음은 LangChain(랭체인)의 Runnable인터페이스에서 벡터스토어를 검색하는 방법을 설명합니다.
벡터 스토어에 저장된 내장 벡터를 사용하여 사용자의 질문과 관련된 정보를 검색합니다. LangChain(랭체인)은 벡터스토어의 래퍼로 리트리버(Retriever)를 제공합니다. 리트리버(Retriever)는 사용자의 질문을 포함한 벡터로 변환하고 벡터스토어에서 유사한 텍스트를 검색합니다. 주목해야 할 점은 벡터스토어를 리트리버(Retriever)로 래핑하여 Runnable인터페이스가 구현된다는 것입니다. 즉, 리트리버(Retriever)는 invoke메소드를 가지며 LCEL을 사용하여 체인의 요소로 취급할 수 있습니다. 이렇게 하면 정보검색을 체인에 통합할 수 있습니다.
아래 코드는 Chroma 벡터스토어를 리트리버(Retriever)로 사용하고 관련 정보를 검색하는 프로그램을 보여줍니다.
리트리버를 이용한 관련 정보 검색
이번에는 chroma벡터스토어를 리트리버로 래핑하는 코드입니다.
as_retriever메소드는 벡터스토어를 리트리버로 래핑합니다. 이렇게 하면 리트리버가 Runnable인터페이스를 구현하고 invoke메소드를 갖게 됩니다.
search_kwargs파라피터는 검색시 옵션을 지정합니다. 여기에서느 k옵션을 3으로 설정하고 유사도가 높은 상위3개의 텍스트를 반환하도록 합니다. 그런 다음 retriever.invoke메소드를 사용하여 사용자의 질문과 관련된 정보를 검색합니다.
invoke메소드는 사용자의 쿼리를 인수로 사용합니다. 여기서는 "생성형 AI의 동향은?"라는 쿼리를 사용합니다. 검색결과는 docs목록에 저장됩니다. 검색한 다음 docs목록의 각 요소를 반복하여 텍스트의 처음 100자를 표시합니다.
이 결과는 앞선 예시의 결과와 동일합니다. 다른점은 리트리버를 사용하는 것으로 Runnable인터페이스로 벡터 스토어로부터 유사한 텍스트를 검색할 수 있다는 것입니다. 이 리트리버를 체인의 일부에 편입하는 것이 용이하게 됩니다.
지금 정보검색을 수행할 준비가 되었습니다. 다음은 정보 검색을 체인에 통합하고 사용자 질문에 답볂을 생성하는 방법을 설명합니다.
이전까지 LangChain(랭체인)을 이용한 질문응답 시스템의 구축에 필요한 컴포넌트에 대해서 설명했습니다. 이번에는 이러한 구성요소를 결합하고 LCEL을 사용하여 체인을 구축하고 사용자 질문에 대한 답변을 생성하는 방법을 설명합니다.
검색결과를 이용한 답변 생성 (src/langchain/rag_generator.py)
우선 이전에 설명한 것 외에 추가된 모듈을 가져옵니다.
ChatOpenAI는 앞서 설명했던 OpenAI대화모델을 사용하는 클래스입니다. StrOutputParser는 앞서 설명한 LLM의 출력을 문자열로 구문 분석하는 파서입니다.
ChatPromptTemplate, MessagePlaceholder, HumanMessagePromptTemplate는 따로 설명했고 프롬프트 템플릿을 만드는 클래스입니다. SystemMessage는 시스템 메시지를 나타내는 클래스입니다. itemgetter는 사전에서 키에 해당하는 값을 얻는 함수입니다. 다음은 프롬프트 템플릿을 만듭니다.
이 프롬프트 템플릿은 앞에서 설명한 방법을 작용하며 시스템 메시지, 채팅기록 및 사용자질문으로 구성됩니다. 시스템 메시지에서 LLM은 '단신은 유능한 조수입니다'라고 역할을 합니다. MessagesPlaceholder는 채팅기록을 나타내는 자리표시자입니다. HumanMessagePromptTemplate은 사용자 질문을 나타내는 프롬프트 템플릿입니다. 이 템플릿은 검색한 컨텍스트와 사용자 질문이 포함된 프롬프트를 생성합니다. 그런 다음 LLM을 설정합니다.
temperature는 생성할 텍스트의 무작위성을 제어하는 파리미터입니다. 낮은 temperature를 설정하면 LLM이 생성하는 텍스트 무작위성이 낮아집니다. 따라서 LLM이 제공한 컨텍스트 정보를 사용할 가능성이 높아집니다. 또한, temperature를 0으로 설정하면 무작위성이 제거됩니다. 따라서 동일한 입력에 대해 매번 동일한 결과를 얻을 수 있스빈다. 그런 다음 검색결과문서를 문자열로 포멧하는 함수를 정의합니다.
이 함수는 검색결과의 문서목록을 받아 각 문서의 내용을 줄바꿈으로 연결한 문자열을 반환합니다. 마지막으로 LCEL을 사용하여 RAG체인를 구축합니다.
이 체인은 다음 흐름으로 처리합니다.
itemgetter("question")에서 입력사전에서 "question"키값(사용자질문)을 가져옵니다.
retriever에서 사용자질문과 관련된 문서를 검색합니다.
format_docs에서 검색결과 문서를 문자열로 포멧합니다.
prompt_template은 시스템 메시지, 채팅기록, 컨텍스트 및 질문을 포함하는 프롬프트를 생성합니다.
llm에서 프롬프트를 기반으로 응답을 생성합니다.
StrOutputParser()에서 LLM출력을 문자열로 구문분석합니다.
이 체인을 사용하여 사용자의 질문에 답변을 생성해 봅시다.
©2024-2025 GAEBAL AI, Hand-crafted & made with Damon Jaewoo Kim.
GAEBAL AI 개발사: https://gaebalai.com
AI 강의 및 개발, 컨설팅 문의: https://talk.naver.com/ct/w5umt5