RAG + AI 에이전트 개발 by LangChain, LangGraph
LangChain은 LLM 애플리케이션 개발 프레임워크이며, LLM이 내장된 다양한 종류의 애플리케이션에서 사용할 수 있으며, LangChain을 사용한 애플리케이션으로는 다음과 같은 예시를 들 수 있습니다.
ChatGPT와 같은 대화형 챗봇
문장 요약 도구
사내 문서나 PDF 파일에 대한 Q&A 앱
나중에 설명할 AI 에이전트
LangChain의 공식적인 구현으로는 Python과 JavaScript/TypeScript 두 가지가 제공되고 있으며, 머신러닝 주변 분야에서는 흔히 볼 수 있듯이 Python 구현이 더 활발하게 개발되고 있습니다. 이번 강좌에서는 Python 구현을 사용합니다.
LLM을 이용한 애플리케이션 개발에 사용할 수 있는 프레임워크 라이브러리는 LangChain 외에도 여러 가지가 있습니다. 예를 들어 LlamaIndex와 Semantic Kernel이 유명합니다.
이러한 LLM 애플리케이션 개발 프레임워크 라이브러리 중에서도 LangChain은 특히 다양한 분야를 다루고 있으며, 활용 사례도 많습니다. 따라서 LLM 애플리케이션 개발을 배우기 위한 첫 단계로 LangChain을 배우는 것은 추천할 만한 선택이며, LangChain을 따라잡음으로써 LLM 애플리케이션 개발에 대한 폭넓은 지식을 얻을 수 있습니다, LangChain의 공식 문서와 쿡북에는 논문 등에서 제안된 방법론의 구현 예시가 다수 수록되어 있으며, 논문 등에서 제안된 방법론의 구현 사례도 다수 포함되어 있습니다.
따라서 LangChain에 대한 지식을 바탕으로 LLM 애플리케이션의 보다 발전적인 기법도 배울 수 있으며, LangChain의 공식 문서와 쿡북에 소개된 발전적인 기법 중 일부는 나중에 소개할 것입니다.
LangChain X 계정: https://x.com/LangChainAI
2024년 10월을 기준으로 LangChain의 전체구성을 보면 아래와 같습니다.
LangChain의 구성 요소로는 우선 LangChain의 각종 구성요소를 제공하는 패키지군(langchain-core, langchain-openai 등의 Python 패키지)이 있습니다. 또한, 공식이 제공하는 생태계로 LangSmith, LangServe, LangGraph가 있습니다. 또한, LangChain을 이용한 구현 템플릿이 LangChain Templates로 제공되고 있습니다.
이번에는 LangChain의 각종 컴포넌트를 제공하는 패키지군을 주로 설명합니다. LangSmith과 LangServe, LangGraph는 나중에 다룹니다.
여기서부터 LangChain의 각종 구성요소를 제공하는 패키지군에 대해 설명드리겠습니다. LangChain 개발 초기에는 'langchain'이라는 하나의 패키지에 모든 기능이 포함되어 있었습니다. 하지만 각종 LLM, 데이터베이스 등의 통합이 증가함에 따라 langchain이라는 하나의 패키지에 의존성이 너무 많다는 문제가 발생했습니다. 그래서 LangChain v0.1 업데이트 전후로 코어 기능을 langchain-core라는 패키지가 제공하고, 주변 기능은 다른 패키지가 제공하도록 분할이 진행되었습니다. (필자도 이 부분 문제때문에 0.1버전까지는 쓰지 않고 v0.2버전부터 다시 사용하고 있습니다.)
langchain-core
langchain-core는 LangChain의 기반이 되는 추상화와 LangChain Expression Language(LCEL)를 제공하는 패키지입니다. 자세한 내용은 나중에 설명하는 'LLM/Chat model'과 'LangChain의 RAG 관련 컴포넌트'에서 설명하지만, LangChain에서는 다양한 언어 모델과 벡터 데이터베이스 등을 통일된 인터페이스로 이용할 수 있도록 하고 있습니다. 이를 위한 추상 기반 클래스는 langchain-core에서 정의되어 있습니다. 또한 'Chain-LangChain Expression Language(LCEL) 개요'와 LangChain Expression Language도 LangChain의 핵심 기능으로서 langchain-core에서 제공됩니다.
partners(langchain-openai 등)와 langchain-community
LangChain에는 OpenAI, Anthropic 등의 언어 모델을 비롯해 다양한 서비스 및 오픈소스와의 통합이 구현되어 있습니다. 예를 들어 OpenAI의 언어 모델 통합은 langchain-openai 패키지에 포함되어 있으며, Anthropic의 언어 모델 통합은 langchain-anthropic 패키지에 포함되어 있습니다. 이처럼 langchain-core가 제공하는 추상 기반 클래스에 대한 구현 클래스로는 langchain-openai, langchain-anthropic 등의 partners라는 패키지를 설치하여 사용하게 됩니다.
partners 패키지로는 langchain-google-genai, langchain-aws, langchainpinecone 등 매우 많은 패키지가 제공되고 있습니다. 참고로 partners 패키지로 독립되어 있지 않은 각종 통합에 대해서는 langchain-community라는 패키지로 묶어서 제공하고 있습니다.
langchain / langchain-text-splitters / langchain-experimental
LangChain에는 langchain-core가 제공하는 각종 추상화와 LCEL, partners 패키지 및 langchain-community가 제공하는 통합 외에도 사용 사례에 특화된 기능 제공이라는 측면이 있습니다. 패키지는 이러한 LLM 애플리케이션의 특정 사용 사례에 특화된 기능을 제공합니다.
또한 LangChain의 기능 중 텍스트를 '청크'라는 단위로 분할하는 Text splitter라는 기능에 대해서는 langchain-text-splitters라는 또 다른 패키지로 제공되고 있습니다. 또한, 연구 및 실험 목적의 코드나 알려진 취약점(CVE)이 포함된 코드에 대해서는 langchainexperimental이라는 패키지로 분리되어 있습니다. 예를 들어, LLM의 출력에 따라 임의의 Python 프로그램이나 임의의 SQL을 실행할 수 있는 기능 중 일부는 langchain-experimental에 포함됩니다.
LangChain 설치
앞에서 LangChain의 컴포넌트가 다양한 패키지로 나뉘어져 있다고 설명했기 때문에, 결국 어떤 패키지를 설치해서 사용해야 하는지 헷갈렸을 수도 있습니다. 기본적인 개념으로는 langchain-core 외에 필요한 최소한의 패키지를 설치하여 사용하게 됩니다. 예를 들어, LangChain에서 OpenAI의 Chat Completions API(GPT-4o 또는 GPT-4o mini)를 사용하려면 langchain-core와 langchain-openai를 설치하면 됩니다. 이번에는 LangChain 컴포넌트 설명은 Google Colab에서 코드를 실행하면서 읽을 수 있도록 되어 있으며, Google Colab에서 다음 명령을 실행하면 langchain-core와 langchain-openai를 설치할 수 있습니다.
langchain-core와 langchain-openai 이외의 패키지에 대해서는 어떤 기능을 제공하는지 알기 쉽도록 필요한 시점에 설치하도록 하겠습니다.
LangSmith는 LangChain 공식이 제공하는 프로덕션급 LLM 애플리케이션을 위한 플랫폼(웹 서비스)인 LangSmith는 LangChain을 사용하여 애플리케이션을 개발할 때 매우 유용합니다.
관련 URL: https://www.langchain.com/langsmith
LangSmith를 사용하면 LangChain의 동작 추적을 쉽게 수집할 수 있어 개발 중 디버깅에 도움이 됩니다. 자세한 내용은 나중에 설명하겠지만, 여기서는 설정만 해봅시다. 참고로 LangSmith에 가입하고, API 키를 발급받아 Google Colab의 보안비밀에 LANGCHAIN_API_KEY에 저장합니다. 그 다음 다음 코드를 실행하면 Google Colab에서 LangChain의 트레이스를 LangSmith에 연동할 준비가 완료됩니다.
LangChain을 사용하는 경우, LangSmith의 설정은 이것만으로 완료됩니다.
아래 LangChain의 주요 구성 요소에 대해 설명하겠습니다. 그 전에 LangChain의 구성 요소에 대한 개요를 설명해드리며, LangChain을 이해하기 위해서는 먼저 어떤 구성 요소가 존재하는지 파악하는 것부터 시작하는 것이 좋습니다. 이 글을 쓰는 시점에서 LangChain에는 다음과 같은 구성요소가 있습니다.
LLM/Chat model: 다양한 언어 모델과의 통합
Prompt template: 프롬프트 템플릿
Example selector: Few-shot 프롬프트의 예시를 동적으로 선택
Output parser: 언어모델의 출력을 지정한 형식으로 변환
Chain : 각종 컴포넌트를 이용한 처리 체인
Document loader: 데이터 소스로부터 문서 불러오기
Document transformer: 문서에 어떤 변환을 가함
Embedding model: 문서를 벡터화한다.
Vector store: 벡터화된 문서를 저장하는 곳
Retriever: 입력된 텍스트와 관련된 문서를 검색
Tool : Function calling 등에서 모델이 사용하는 함수를 추상화
Toolkit : 동시에 사용하는 것을 전제로 한 Tool의 컬렉션
Chat history : 대화 이력 저장처로서 각종 데이터베이스와의 통합
이처럼 LangChain은 많은 컴포넌트를 제공하고 있습니다다. 이번에 이러한 컴포넌트 중에서 LangChain의 기본을 익히는데 특히 중요한 요소들을 다음과 같은 순서로 설명합니다.
LLM/Chat model
프롬프트 템플릿
Output parser
Chain
RAG 관련 컴포넌트
LangChain 설명의 첫 번째 단계로 LangChain의 'LLM'과 'Chat model'에 대해 설명합니다. 'LLM'과 'Chat model'은 LangChain에서 언어 모델을 사용하는 방법을 제공하는 모듈입니다. 이를 통해 다양한 언어 모델을 공통된 인터페이스에서 사용할 수 있습니다. 쉽게 말해, 언어 모델을 LangChain 스타일로 사용할 수 있도록 하는 래퍼라고 할 수 있습니다.
LangChain의 'LLM'은 하나의 텍스트 입력에 대해 하나의 텍스트 출력을 반환하는, 채팅 형식이 아닌 언어 모델을 다루는 컴포넌트입니다. 예를 들어 OpenAI의 Completions API(gpt-3.5-turbo-instruct)를 LangChain에서 사용하려면 'OpenAI'라는 클래스를 사용합니다. 샘플 코드는 다음과 같습니다.
이 코드에서는 gpt-3.5-turbo-instruct를 모델로 설정하고 temperature를 0으로 설정했는데, temperature는 클수록 출력이 랜덤하게, 작을수록 결정적인 출력이 나오는 파라미터입니다. 이번에는 가능한 한 동일한 출력을 얻기 위해 temperature를 최소값인 0으로 설정했습니다. 앞서 설명한 코드의 실행 결과는 예를 들어 다음과 같습니다.
저는 김민지입니다. 만나서 반가워요. 저는 대학교에서 경영학을 전공하고 있어요. 취미는 음악 감상이고, 특히 팝 음악을 좋아해요. 또한 여행을 좋아해서 여러 나라를 다녀보는 것이 꿈이에요. 앞으로 잘 부탁드립니다!
gpt-3.5-turbo-instruct가 생성한 텍스트를 표시할 수 있습니다. 참고로 이전에 설명했듯이 OpenAI의 Completions API는 이미 Legacy로 분류되어 있습니다. 여기서는 하나의 텍스트 입력에 대해 하나의 텍스트 출력을 반환하는, 채팅 형식이 아닌 언어 모델의 예시로만 사용하고 있습니다.
OpenAI의 Chat Completions API(gpt-4o, gpt-4o-mini)는 단순히 하나의 텍스트를 입력하는 것이 아니라, 채팅 형식의 대화를 입력하여 응답을 얻도록 되어 있습니다. 이러한 채팅 형식의 언어 모델을 LangChain에서 다루기 위한 컴포넌트가 'Chat model'이며, LangChain에서 OpenAI의 Chat Completions API를 사용할 때는 'ChatOpenAI' 클래스를 사용합니다. 샘플 코드는 다음과 같습니다.
네, 홍길동님이라고 말씀하셨습니다! 어떻게 도와드릴까요?
LangChain의 'SystemMessage', 'HumanMessage', 'AIMessage'는 각각 ChatCompletions API의 '“role”: “system”', '“role”: “user”', '“role”: “assistant”'에 대응합니다. 따라서 위 코드에서는 내부적으로 다음과 같은 요청을 보내고 있습니다.
앞으로 강좌에서는 OpenAI의 Chat Completions API를 언어모델로 사용하기 때문에 앞에서 설명한 ChatOpenAI클래스를 자주 사용하게 됩니다.
Chat Completions API는 스트리밍으로 응답을 받을 수 있습니다. LLM을 이용한 애플리케이션을 구현할 때, UX 향상을 위해 스트리밍으로 응답을 받고자 하는 경우가 많은데, LangChain에서는 기본적으로 스트리밍을 지원합니다. ChatOpenAI를 스트리밍으로 호출하는 샘플 코드는 다음과 같습니다.
안녕하세요! 어떻게 도와드릴까요?
또한, LangChain에서는 Callback 기능을 사용하여 스트리밍을 구현할 수 있는데, Callback 기능을 사용하면 LLM 처리 시작(on_llm_start), 새로운 토큰 생성(on_llm_new_token), LLM 처리 종료(on_llm_end) 등의 타이밍에 임의의 처리를 실행할 수 있습니다. (on_llm_end) 등의 타이밍에 임의의 처리를 실행할 수 있습니다.
LangChain의 LLM과 Chat model을 잘 사용하기 위해서는 이러한 상속 관계를 이해하는 것이 도움이 되는데, LLM과 Chat model의 상속 관계는 아래와 같습니다.
나중에 설명할 Runnable을 상속받은 BaseLanguageModel이라는 클래스가 있는데, BaseLanguageModel은 LangChain에서 언어 모델을 다루기 위한 최상위 클래스입니다. 그리고 BaseLanguageModel을 상속한 BaseLLM과 BaseChatModel이 존재하는데, OpenAI 클래스 및 기타 LLM 클래스는 BaseLLM을 상속하고, ChatOpenAI 클래스 및 기타 Chat model의 클래스는 Runnable, BaseLanguageModel, BaseLLM, BaseChatModel과 같은 기초가 되는 추상 기반 클래스는 langchain-core에서 제공됩니다. 이에 반해 구체적 구현인 OpenAI 클래스와 ChatOpenAI 클래스는 langchain-openai 패키지로 제공되며, LangChain은 이러한 관계로 LLM과 Chat model을 제공하기 때문에 필요에 따라 모델을 교체할 수도 있습니다. 예를 들어, Anthropic의 Claude를 사용할 경우 langchain-openai 패키지의 ChatOpenAI 클래스 대신 langchain-anthropic 패키지의 ChatAnthropic이라는 클래스를 사용할 수 있습니다. 또한 BaseLLM이나 BaseChatModel을 테스트 더블로 대체하기 위해 Fake를 사용할 수도 있는데, Fake를 이용한 모델 대체에 대해 궁금하신 분은따로 검색해 보시기 바랍니다.
지금까지 LangChain의 LLM과 Chat model에 대해 알아보았습니다. LangChain의 LLM과 Chat model을 사용하면 다양한 언어 모델을 통일된 인터페이스로 다룰 수 있습니다. 이번 강좌에서는 OpenAI의 GPT-4o와 GPT-4o mini를 주로 사용하지만, LangChain 자체는 그 외에도 다양한 언어 모델을 지원합니다. 예를 들어 Anthropic의 Claude, Google의 Gemini, 오픈 모델의 Llama 등을 사용할 수도 있습니다. 또한 LangChain 공식이 지원하지 않는 모델이라도 Custom LLM으로 사용할 수 있습니다.
LLM 애플리케이션 개발에서 매우 중요한 요소는 언어 모델에 입력하는 프롬프트입니다. 여기서는 LangChain에서 프롬프트 처리를 추상화한 컴포넌트에 대해 설명합니다.
가장 먼저 소개할 컴포넌트는 'PromptTemplate'입니다. 이름에서 알 수 있듯이 PromptTemplate을 사용하면 프롬프트를 템플릿화할 수 있습니다.
PromptTemplate을 사용하는 간단한 예는 아래와 같습니다.
다음 요리 레시피를 생각해 봅시다.
요리명: 카레
PromptTemplate의 invoke 메서드를 통해 템플릿의 '{dish}' 부분이 '카레'로 대체되었습니다. 참고로 PromptTemplate은 프로그램에서 문자열의 일부를 대체하는 것일 뿐, 내부적으로 LLM을 호출하는 일은 하지 않습니다.
PromptTemplate을 Chat Completions API와 같은 채팅 형식 모델에 대응시킨 것이 ChatPromptTemplate이며, SystemMessage, HumanMessage, AIMessage를 각각 템플릿화하여 ChatPromptTemplate이라는 클래스로 묶어 처리할 수 있습니다, ChatPromptTemplate이라는 클래스에서 일괄적으로 처리할 수 있으며, ChatPromptTemplate을 사용하는 샘플 코드는 다음과 같습니다.
messages=[SystemMessage(content='사용자가 입력한 요리 레시피를 생각해 주세요.',
additional_kwargs={}, response_metadata={}), HumanMessage(content='카레',
additional_kwargs={}, response_metadata={})]
채팅 형식의 프롬프트에는 대화 기록처럼 여러 개의 메시지를 담을 수 있는 플레이스홀더가 필요한 경우가 많습니다. 이때 사용할 수 있는 것이 MessagesPlaceholder이며, MessagesPlaceholder를 사용하는 예는 다음과 같습니다.
messages=[SystemMessage(content='You are a helpful assistant.', additional_kwargs={}, response_metadata={}), HumanMessage(content='안녕하세요. 저는 홍길동이라고 합니다.', additional_kwargs={}, response_metadata={}), AIMessage(content='안녕하세요, 홍길동님! 무엇을 도와드릴까요?', additional_kwargs={}, response_metadata={}), HumanMessage(content='제 이름을 아시나요?', additional_kwargs={}, response_metadata={})]
이처럼 채팅 형식의 모델 프롬프트에 대화 기록을 포함시키려면 MessagesPlaceholder를 사용하게 됩니다.
본격적으로 LLM 애플리케이션을 개발하다 보면 프롬프트를 소스 코드와 별도로 관리하고 싶은 경우가 많은데, LangSmith의 'Prompts'를 사용하면 프롬프트를 공유하고 버전 관리를 할 수 있습니다.
LangSmith의 Prompts는 LangSmith 화면에서 프롬프트를 편집하고 공유할 수 있습니다. 또한, 프롬프트를 편집하면 Git처럼 버전 관리가 됩니다.
LangSmith의 Prompts에는 LangSmith에서 프롬프트를 시험해볼 수 있는 Playground와 같은 기능도 있습니다. 이렇게 프롬프트를 관리할 수 있는 웹 서비스로는 'PromptLayer'도 유명합니다.
지금까지 LangChain에서 프롬프트를 추상화한 모듈인 PromptTemplate과 ChatPromptTemplate을 통해 프롬프트를 템플릿화하여 처리할 수 있는 모듈에 대해 알아보았습니다.
Prompt template에 대해 배우다 보면, 단순히 문자열을 대체하는 것이므로 파이썬이 표준으로 제공하는 f 문자열이나 format 메서드로 충분하지 않을까 생각할 수 있습니다. 물론 간단한 프롬프트 채우기는 파이썬이 표준으로 제공하는 기능으로 충분할 수도 있습니다.
반면, LangChain의 Prompt template은 채팅 형식의 모델 프롬프트도 지원한다는 점과 나중에 설명할 LCEL의 구성 요소로 사용할 수 있다는 점이 큰 특징입니다. 또한 LLM 애플리케이션에서는 Few-shot 프롬프트의 예시를 동적으로 선택하여 프롬프트에 포함시키고 싶은 경우도 있는데, LangChain의 Prompt template은 이러한 경우에도 대응하고 기능은 LangChain에서 'Example selector'로 제공되고 있습니다. 관심이 있으시다면 꼭 한번 살펴보시기 바랍니다.
이전에 LLM의 입력 관련 컴포넌트인 Prompt template에 대해 알아보았습니다. 이제 LLM의 출력에 대해 알아볼 차례인데, LLM이 특정 형식으로 출력하고 그 출력을 프로그래밍 방식으로 처리하고 싶을 때가 있습니다. 이때 사용할 수 있는 것이 'Output parser'입니다.
Output parser는 JSON과 같은 출력 형식을 지정하는 프롬프트 생성 및 응답 텍스트를 Python 객체로 변환하는 기능을 제공하며, LLM 응답에서 해당 부분을 추출하여 Python 객체(사전형 또는 직접 만든 클래스)에 매핑하는 기본적인 처리를 쉽게 구현할 수 있습니다.
LangChain의 Output parser의 일종인 'PydanticOutputParser'를 사용하면 LLM의 출력을 Python 객체로 변환할 수 있습니다. 여기서는 PydanticOutputParser를 사용하여 LLM이 출력한 레시피를 Recipe 클래스의 인스턴스로 자동 변환하는 예제를 살펴봅시다.
참고로 Output parser는 중요한 컴포넌트 개념을 이해하기 위해 PydanticOutputParser에 대해 설명하는 것이고 실제로 LangChain에서 LLM에 구조화된 데이터를 출력할 때, PydandicOutputParser를 직접 사용하는 것보다 나주엥 소개하는 with_structured_output을 사용하는 것을 추천합니다.
우선, LLM에 출력할 '재료 목록(ingredients)과 '단계(steps)'를 필드로 하는 Recipe 클래스를 Pydantic의 모델로 정의합니다.
이 Recipe 클래스를 주어 PydanticOutputParser를 생성합니다.
그리고 PydanticOutputParser에서 프롬프트에 포함할 출력 형식의 설명문을 생성합니다.
여기서 생성한 format_instructions는 Recipe 클래스에 대응하는 출력 형식을 지정하는 문자열로, format_instructions를 print로 표시하면 다음과 같습니다.
The output should be formatted as a JSON instance that conforms to the JSON schema below.
As an example, for the schema {"properties": {"foo": {"title": "Foo", "description": "a list of strings", "type": "array", "items": {"type": "string"}}}, "required": ["foo"]} the object {"foo": ["bar", "baz"]} is a well-formatted instance of the schema. The object {"properties": {"foo": ["bar", "baz"]}} is not well-formatted.
Here is the output schema:
```
{"properties": {"ingredients": {"description": "ingredients of the dish", "items": {"type": "string"}, "title": "Ingredients", "type": "array"}, "steps": {"description": "steps to make the dish", "items": {"type": "string"}, "title": "Steps", "type": "array"}}, "required": ["ingredients", "steps"]}
```
'출력은 이런 JSON 형식으로 출력해 주세요'와 같은 내용입니다. 이 format_instructions를 프롬프트에 삽입하여 LLM이 이 형식에 따른 응답을 반환하도록 합니다. 이어서 format_instructions를 사용한 ChatPromptTemplate을 생성합니다.
prompt.partial이라는 부분에서는 프롬프트의 일부를 채우고 있습니다. 이 ChatPromptTemplate에 대한 예시로 입력을 입력해 봅시다.
=== role: system ===
사용자가 입력한 요리 레시피를 생각해보세요.
The output should be formatted as a JSON instance that conforms to the JSON schema below.
As an example, for the schema {"properties": {"foo": {"title": "Foo", "description": "a list of strings", "type": "array", "items": {"type": "string"}}}, "required": ["foo"]} the object {"foo": ["bar", "baz"]} is a well-formatted instance of the schema. The object {"properties": {"foo": ["bar", "baz"]}} is not well-formatted.
Here is the output schema:
```
{"properties": {"ingredients": {"description": "ingredients of the dish", "items": {"type": "string"}, "title": "Ingredients", "type": "array"}, "steps": {"description": "steps to make the dish", "items": {"type": "string"}, "title": "Steps", "type": "array"}}, "required": ["ingredients", "steps"]}
```
=== role: user ===
카레
Recipe 클래스의 정의에 따라 출력 형식을 지정하는 프롬프트가 자동으로 내장되어 있습니다. 이 텍스트를 입력으로 하여 LLM을 실행해 봅시다.
이 응답을 Pydantic의 모델 인스턴스로 변환하여 사용하고 싶을 때가 많습니다. PydanticOutputParser를 사용하면 그 변환 과정도 간단합니다.
이렇게 구현하면 Pydantic의 모델 인스턴스를 얻을 수 있습니다. 이 코드를 실행하면 다음과 같이 표시됩니다.
<class '__main__.Recipe'>
ingredients=['닭고기 500g', '양파 1개', '감자 2개', '당근 1개', '카레 가루 3큰술', '코코넛
밀크 400ml', '식용유 2큰술', '소금 약간', '후추 약간'] steps=['닭고기를 한입 크기로 썰고, 소금과
후추로 간을 한다.', '양파는 다지고, 감자와 당근은 깍둑썰기로 준비한다.', '팬에 식용유를 두르고 양파를
볶아 투명해질 때까지 익힌다.', '닭고기를 넣고 겉면이 노릇해질 때까지 볶는다.', '감자와 당근을 추가하고
잘 섞은 후, 카레 가루를 넣고 볶는다.', '코코넛 밀크를 붓고 끓인다.', '중불로 줄이고 20분간 끓여서
재료가 부드러워질 때까지 조리한다.', '소금으로 간을 맞추고, 그릇에 담아낸다.']
지금까지 Output parser를 사용하는 예제를 살펴보았습니다. 핵심은 다음 두 가지입니다.
Recipe 클래스의 정의를 바탕으로 출력 형식을 지정하는 문자열이 자동으로 생성되었다.
LLM의 출력을 쉽게 Recipe 클래스의 인스턴스로 변환할 수 있었다.
이렇게 매우 편리한 Output parser이지만, LLM이 불완전한 JSON을 반환하는 경우 에러가 발생하기 때문에 JSON과 같은 구조화된 데이터를 LLM이 안정적으로 출력하게 하려면 ChatCompletions API의 JSON 모드와 같은 기능을 사용하거나, 함수 호출을 적용하는 것이 유용합니다, LangChain에서 Function calling을 응용하여 구조화된 데이터를 출력하는 방법은 추후 with_structured_output 칼럼에서 소개할 예정입니다.
Output parser의 개념을 이해했다면, 이제 자주 사용하게 될 'StrOutputParser'를 소개할 차례인데, StrOutputParser는 LLM의 출력을 텍스트로 변환하는 데 사용합니다. 예를 들어, ChatOpenAI를 호출하면 AIMessage를 얻을 수 있는데, AIMessage에 대해 StrOutputParser를 호출하면 텍스트를 추출할 수 있습니다. 샘플 코드는 다음과 같습니다.
<class 'str'>
안녕하세요. 저는 AI 어시스턴트입니다.
이 예제 코드만 보면 '굳이 StrOutputParser를 사용하지 않고 ai_message.content라고 써서 텍스트를 출력하면 되지 않을까'라고 생각할 수 있습니다. 하지만 StrOutputParser는 나중에 설명할 LangChain Expression Language(LCEL)의 구성 요소로서 중요한 역할을 합니다, StrOutputParser가 왜 존재하는지 이해할 수 있을 것입니다.
이번에 LLM의 출력을 변환하는 Output parser에 대해 설명했는데, LangChain에서는 이번에 소개한 PydanticOutputParser와 StrOutputParser 외에도 XML, CSV 등의 형식에 대응하는 Output Parser도 제공하고 있습니다. 지금까지 소개한 LLM과 Chat model, Prompt template, Output parser는 LangChain 동작의 근간이 되는 컴포넌트입니다. 이를 이용해 'Chain'을 구축하는 것이 LangChain의 장점입니다.
LLM 애플리케이션에서는 단순히 LLM에 입력해서 출력을 얻고 끝나는 것이 아니라, 처리를 연쇄적으로 연결하고 싶은 경우가 많습니다. 예를 들어 다음과 같은 것들을 생각할 수 있습니다.
Prompt template을 채우고, 그 결과를 Chat model에 주고, 그 결과를 Python의 객체로 변환하고 싶다.
앞에서 소개한 Zero-shot CoT 프롬프트로 단계별로 생각하게 하고 그 결과를 요약하게 하고 싶다.
LLM 출력을 얻은 후, 그 내용이 서비스 정책에 위배되지 않는지(예: 차별적 표현이 아닌지) 확인하고 싶다.
LLM의 출력 결과를 바탕으로 SQL을 실행하여 데이터를 분석해보고 싶다.
이러한 처리의 연쇄를 실현하는 것이 LangChain의 'Chain'입니다.
LCEL(LangChain Expression Language)은 LangChain에서 Chain을 작성하는 방법으로, 프롬프트와 LLM을 '|'로 연결하여 처리의 체인(Chain)를 구현합니다.2023년 10월경부터 LangChain에서는 LCEL을 사용하는 구현이 표준이 되었습니다. 여기서는 Chain의 기본 개념을 이해할 수 있도록 LCEL의 개요를 소개하며, LCEL을 잘 활용하기 위한 자세한 내용은 따로 설명합니다. 이제 LCEL의 기본 예제를 몇 가지 살펴보겠습니다.
먼저 LCEL을 사용하는 가장 간단한 예로 prompt와 model을 연결해 봅시다.
그리고 이것들을 연결한 체인을 만듭니다.
이제 chain을 실행합니다.
위의 'chain = prompt | model'과 같이 프롬프트(PromptTeamplte) 채우기와 모델(ChatOpenAI) 호출이 연쇄적으로 실행되었다는 뜻입니다. LCEL에서는 위의 'chain = prompt | model'과 같이 프롬프트와 LLM을 '|'으로 연결하여 처리의 체인 (Chain)을 구현합니다.
앞의 예제에서는 chain을 invoke한 결과가 AIMessage이므로 ai_message.content와 같이 작성하여 텍스트를 추출하였고, StrOutputParser를 chain에 추가하면 ChatOpenAI와 같은 Chat model의 출력인 AIMessage를 문자열로 변환할 수 있습니다. 샘플 코드는 다음과 같습니다.
prompt와 model, StrOutputParser를 연결하는 이 코드는 LCEL의 가장 기본적인 형태라고 할 수 있습니다.
LCEL의 두 번째 예시로 prompt와 model에 PydanticOutputParser를 연결하여 LLM에 요리 레시피를 생성하게 하고, 그 결과를 Recipe 클래스의 인스턴스로 변환하는 처리 연쇄를 구현해 보겠습니다. 먼저 Recipe 클래스를 정의하고 output_parser(PydanticOutputParser)를 준비합니다.
다음으로 프롬프트(PromptTemplate)와 모델(ChatOpenAI)을 준비합니다.
prompt의 {format_instructions} 부분에는 Recipe 클래스의 정의에 따라 '이런 형식의 JSON을 반환하라'는 텍스트가 포함되어 있습니다. 또한, model의 설정으로 Chat Completions API의 JSON mode를 사용하고 있으며, LCEL의 기법으로 prompt와 model, output_parser를 연결한 chains를 생성합니다.
chain을 실행해 봅시다.
<class '__main__.Recipe'>
ingredients=['닭고기 500g', '양파 1개', '감자 2개', '당근 1개', '카레 가루 3큰술',
'식용유 2큰술', '소금 약간', '후추 약간', '물 4컵'] steps=['양파를 잘게 썰고, 감자와 당근은
큐브 모양으로 자릅니다.', '냄비에 식용유를 두르고 양파를 볶아 투명해질 때까지 볶습니다.',
'닭고기를 넣고 겉면이 노릇해질 때까지 볶습니다.', '감자와 당근을 넣고 함께 볶습니다.',
'카레 가루를 넣고 잘 섞은 후 물을 부어 끓입니다.', '끓기 시작하면 중약불로 줄이고 20분간 끓입니다.',
'소금과 후추로 간을 맞춘 후 불을 끄고 5분 정도 둡니다.', '밥과 함께 서빙합니다.']
최종 출력은 Recipe 클래스의 인스턴스이며, chain.invoke라는 호출을 통해 프롬프트 채우기, LLM 호출, 출력 변환이 연쇄적으로 실행되었다는 것을 알 수 있습니다.
LangChain의 중요한 개념인 'Chain'과 Chain을 자유롭게 구현하기 위한 'LangChainExpression Language(LCEL)'에 대한 개요를 설명했습니다. model, Output parser를 연결하여 Chain으로 일련의 처리를 실행하는 것이 기본이며, LCEL은 보다 복잡한 처리의 연쇄를 구현할 수도 있습니다. 예를 들어, 자체 함수를 Chain에 끼워 넣거나 여러 개의 Chain을 병렬로 연결하여 실행하는 것도 가능합니다.
먼저 RAG(Retrieval-Augmented Generation)에 대해 설명하자면, GPT-4o와 GPT-4o mini는 이 글을 쓰는 시점에서 2023년 10월까지의 데이터로 학습하고 있기 때문에 그 시점까지의 공개된 정보만 알 수 있습니다. 하지만 더 새로운 정보나 사적인 정보를 바탕으로 LLM이 대답하게 하고 싶은 것이 많습니다. 그래서 프롬프트에 컨텍스트(context)를 넣는 방법을 생각해 볼 수 있습니다.
예를 들어, LangChain의 생태계 중 하나인 'LangGraph'는 2024년에 등장했기 때문에 GPT-4o는 LangGraph에 대해 정확하게 답변할 수 없습니다. 그래서 LangGraph의 개요가 적혀 있는 LangChain의 README의 내용을 문맥(context)으로 프롬프트에 포함시켜 질문해 보겠습니다.
관련 URL: https://github.com/langchain-ai/langchain/blob/master/README.md
그러자, LangGraph에 대해 context을 바탕으로 답변이 나옵니다.
이렇게 질문과 관련된 문서를 컨텍스트에 포함시킴으로써 LLM이 본래 알지 못하는 것을 답변하게 할 수 있다. 하지만 LLM은 토큰 수에 제한이 있기 때문에 모든 데이터를 컨텍스트에 담을 수는 없습니다.
그래서 입력을 바탕으로 문서를 검색하고, 검색 결과를 컨텍스트에 포함시켜 LLM이 답변하도록 하는 방법이 있습니다. 이러한 기법을 RAG(Retrieval-Augmented Generation)라고 하는데, RAG의 전형적인 구성은 벡터 데이터베이스를 사용하여 문서를 벡터화하여 저장해두고, 입력된 텍스트와 벡터에 가까운 문서를 검색하여 컨텍스트에 포함시키는 것입니다. 포함시킵니다. 문서 벡터화에는 OpenAI의 Embeddings API 등을 사용합니다.
참고로 텍스트 벡터화란 텍스트를 숫자 배열로 변환하는 것을 말합니다. 텍스트의 벡터화에는 여러 가지 방법이 있지만, 일반적으로 등장하는 키워드나 의미가 가까운 텍스트가 벡터로도 거리가 가까워지도록 변환합니다. 텍스트 벡터화 자체는 최근에 등장한 기술이 아니라 자연어 처리 분야에서 오래전부터 자주 사용되어 왔습니다. 나중에 구체적으로 어떤 벡터가 되는지 예를 들어보겠습니다.
LangChain은 RAG에 사용할 수 있는 다양한 구성 요소를 제공합니다. 먼저 짚고 넘어가야 할 주요 컴포넌트에는 다음 5가지가 있습니다.
Document loader: 데이터 소스에서 문서를 불러오는 역할을 함
Document transformer: 문서에 어떤 변환을 가함
Embedding model: 문서를 벡터화
Vector store: 벡터화된 문서를 저장하는 곳
Retriever: 입력된 텍스트와 관련된 문서를 검색
이들은 아래 그림와 같이 정보원(Source)이 되는 데이터부터 Retriever에 의한 검색까지 연결됩니다.
LangChain의 공식 문서를 불러와 gpt-4o-mini에 질문하는 예시를 통해 이 흐름을 실제로 실행봅시다.
다음으로 문서 로더의 일종인 GitLoader를 사용하여 LangChain의 리포지토리에서 .mdx라는 확장자를 가진 파일을 불러옵니다.
이 코드를 실행하면 다음과 같이 읽은 데이터 개수가 표시됩니다.
371
LangChain에는 매우 많은 Document loader가 제공되고 있습니다. 그 중 몇 가지를 아래 표로 정리했습니다.
관련 URL: https://python.langchain.com/docs/integrations/document_loaders/
LangChain에는 이 글을 쓰는 시점에도 150개 이상의 Document loader가 있으며, 위 링크를 참고하기 바랍니다.
Document loader에서 불러온 데이터를 '문서'라고 부릅니다. 불러온 문서에 어떤 변환을 가하는 경우가 많습니다. 문서에 어떤 변환을 가하는 것이 'Documenttransformer'입니다. 예를 들어, 문서를 일정 길이의 청크(chunk)로 나누고 싶을 때가 있습니다. LangChain에서는 문서를 적절한 크기의 청크로 분할함으로써 LLM에 입력하는 토큰 수를 줄이거나 더 정확한 답변을 얻기 쉬워지는 경우가 있습니다. text-splitters라는 패키지로 분리되어 있습니다. 따라서 먼저 langchain-textsplitters를 설치합니다.
langchain-text-splitters에서 제공하는 CharacterTextSplitter라는 클래스를 사용하여 문서를 청크로 분할하는 예는 다음과 같습니다.
1270
원래 371개였던 문서가 1270개로 분할되었습니다. LangChain에서는 이 외에도 tiktoken으로 측정한 토큰 수로 분할하거나, Python 등의 소스 코드를 가능한 한 클래스나 함수처럼 묶어서 분할할 수 있는 기능도 제공하고 있습니다.
또한, 문서를 청크로 분할하는 것 외에도 몇 가지 변환 프로세스가 지원됩니다.
⑤ Embedding model
문서 변환이 끝나면 텍스트를 벡터화합니다. 이 문서에서는 OpenAI의 Embeddings API를 사용하여 text-embedding-3-small이라는 모델을 사용하여 텍스트를 벡터화합니다. OpenAIEmbeddings와 같이 텍스트 벡터화에 사용할 수 있는 것이 'Embedding model'입니다. 먼저 OpenAIEmbeddings의 인스턴스를 생성합니다.
문서의 벡터화 처리는 다음에 설명할 Vector store 클래스에 데이터를 저장할 때 내부적으로 수행됩니다. 하지만 그것만으로는 벡터화에 대한 이미지가 잘 잡히지 않으므로, OpenAI Embeddings를 사용하여 텍스트를 벡터화해 봅시다.
1536
[0.01985582523047924, 0.02229463681578636, 0.028337715193629265, -0.01978028565645218, 0.04907841980457306, 0.016888242214918137, -0.01323002204298973, 0.009534033015370369, 0.027841318398714066, -0.024344967678189278, -0.0006370185292325914, -0.012086153961718082, -0.006177966948598623, -0.018323473632335663, -0.01399619784206152, 0.06509257107973099, -0.0022472692653536797, 0.00711140688508749, -0.03774764761328697, 0.004615939687937498, 0.017622044309973717, 0.01573358289897442, -0.03578364849090576, 0.017125649377703667, 0.03515775874257088, -0.030280131846666336, -0.015917032957077026, 0.025812571868300438, -0.008999868296086788, -0.09435833245515823, 0.0013650879263877869, -0.0636681318283081, -0.02576940692961216, 0.04251736402511597, -0.018755121156573296, 0.044028133153915405, 0.04519358277320862, 0.012409890070557594, 5.574755505222129e-06, -0.03066861629486084, -0.003485560417175293, -0.02186298929154873, 0.019089648500084877, 0.004178895615041256, 0.030366461724042892, 0.030280131846666336, 0.009463890455663204, 0.011654505506157875, -0.06202786788344383, 0.016024945303797722, -0.029546329751610756, 0.046963341534137726, -0.01397461537271738, -0.008368582464754581, -0.0038983242120593786, 0.03660378232598305, -0.011578966863453388, 0.02158241719007492, 0.01140630804002285, -0.020416967570781708, 0.012420681305229664, 0.00036251716664992273, -0.029049934819340706, 0.06310699135065079, -0.01179479155689478, 0.03263261541724205, -0.013909867964684963, -0.027064351364970207, -0.05887683480978012, -0.004734642803668976, -0.03751024231314659, 0.017848659306764603, 0.05499200150370598, -0.05935164913535118, -0.005913582630455494, -4.607340451912023e-05, -0.018474549055099487, 0.012593341059982777, -0.009366769343614578, 0.038114551454782486, -0.012237231247127056, 0.046272702515125275, -0.006857813335955143, -0.048215121030807495, -0.003091681282967329, -0.00644235173240304, -0.033042680472135544, 0.018420593813061714, -0.012679670006036758, 0.028790945187211037, 0.025618329644203186, 0.0002347087865928188, -0.008476494811475277, 0.01031639613211155, 0.027150681242346764, 0.030992351472377777, 0.04717916622757912, -0.030862856656312943, -0.02001769281923771, 0.03343116492033005, -0.008832604624330997, -0.033236924558877945, 0.03319375962018967, -0.05430136248469353, 0.04579789191484451, -0.021884571760892868, 0.04838778078556061, -0.006167175713926554, 0.007521472405642271, 0.0008693667477928102, -0.05153881385922432, 0.009604175575077534, -0.0361073836684227, -0.03066861629486084, 0.014201231300830841, -0.023675912991166115, 0.01701773703098297, 0.05624378100037575, 0.009285835549235344, -0.0059891208074986935, -0.03170457109808922, 0.019445758312940598, -0.017827076837420464, -0.045538902282714844, -0.000905787106603384, -0.00511773070320487, 0.0028650660533457994, -0.06409978121519089, -0.03362540528178215, 0.01999611034989357, 0.03634479269385338, 0.03019380196928978, 0.10273230820894241, -0.05615745112299919, 0.05279059335589409, 0.0012679670471698046, -0.003906417638063431, -0.020934944972395897, -0.0002266153896925971, 0.013920659199357033, 0.03282685577869415, -0.03953899070620537, 0.008444121107459068, 0.02851037308573723, -0.04275476932525635, -0.016273142769932747, 0.01945655047893524, -0.006512494757771492, -0.022510461509227753, -0.020438550040125847, -0.05512149631977081, -0.03975481539964676, -0.012571758590638638, -0.0371001772582531, -0.040618110448122025, -0.009005263447761536, -0.04053178057074547, 0.041049759835004807, 0.037423912435770035, 0.03319375962018967, -0.04679068177938461, -0.023028438910841942, 0.02193852700293064, 0.02352483570575714, -0.006944142747670412, 0.02725859358906746, 0.017211977392435074, -0.015571714378893375, -0.02035222016274929, -0.06150989234447479, 0.008632967248558998, -0.023244263604283333, -0.031985145062208176, -0.022424131631851196, 0.013758791610598564, -0.010289417579770088, -0.015053736045956612, -0.005821857135742903, 0.030970769003033638, -0.018539296463131905, 0.0106617147102952, -0.01890619844198227, 0.00457277474924922, 0.04579789191484451, -0.04735182598233223, -0.032265715301036835, -0.012269604951143265, 0.033517494797706604, -0.013650879263877869, 0.03310742974281311, -0.041869889944791794, 0.05231577903032303, 0.002156892791390419, -0.009954890236258507, 0.02991323173046112, -0.016251560300588608, -0.013111318461596966, -0.0025696565862745047, 0.0034370000939816236, -0.01900331862270832, 0.02428022027015686, 0.006194153800606728, 0.008751670829951763, -0.004041307605803013, -0.021409759297966957, 0.02674061618745327, -0.02553199976682663, 0.035438328981399536, -0.0007155920611694455, -0.046747516840696335, 0.020837824791669846, 0.057797715067863464, -0.032071471214294434, 0.0015053736278787255, -0.020622000098228455, -0.020309055224061012, 0.006992703303694725, 0.07540896534919739, 0.0037796208634972572, 0.02965424209833145, -0.015679625794291496, 0.020503297448158264, -0.01139551680535078, -0.002608774695545435, -0.000511233520228
"AWS S3에서 데이터를 불러올 수 있는 Document loader가 있나요?"라는 문자열이 1536차원의 벡터(숫자목록)로 변환됩니다.
다음으로 저장할 Vector store를 준비하여 문서를 벡터화하여 저장합니다. 이번에는 'Chroma'라는 로컬에서 사용 가능한 Vector store를 사용합니다. 먼저 Chroma를 사용하기 위해 필요한 패키지를 설치합니다.
청크로 분할한 문서와 Embedding model을 기반으로 Vector store를 초기화합니다.
이제 준비된 문서를 벡터화하여 Vector store에 저장할 수 있게 되었습니다. LangChain은 Chroma 외에도 Faiss, Elasticsearch, Redis등 Vector store로 사용할 수 있는 다양한 통합을 제공하고 있으며, Vector store에 대해 사용자 입력과 관련된 문서를 가져오는 작업을 수행합니다. LangChain에서 텍스트와 관련된 문서를 가져오는 인터페이스를 'Retriever'라고 하는데, Vector store의 인스턴스에서 Retriever를 생성합니다.
Retriever를 사용하여 “AWS S3에서 데이터를 불러올 수 있는 Document loader가 있나요?”라는 질문과 유사한 문서를 검색해봅니다.
4개의 문서가 발견되었고, 그 중 첫 번째 문서는 'docs/extras/integrations/providers/aws_s3.mdx'로 AWS의 S3를 대상으로 하는 Document loader에 대한 내용이 담겨 있습니다. Retriever 내부에서는 주어진 텍스트(query)를 벡터화하여 Vector store에 저장된 문서 중 벡터 거리가 가까운 문서를 찾고 있습니다.
지금까지 문서를 벡터화하여 저장해두고, 사용자의 입력에 가까운 문서를 검색(Retrieve)하는 과정을 살펴보았습니다. 챗봇과 같은 애플리케이션에서는 입력과 관련된 문서를 검색(Retrieve)하는 것 외에도 검색 결과를 PromptTemplate에 컨텍스트로 삽입하여 LLM에게 질문하고 답변(QA)을 요청하는 경우가 있습니다.
이 일련의 과정을 LCEL에서 Chain으로 구현해 봅시다. 먼저 prompt(ChatPromptTemplate)와 model(ChatOpenAI)을 준비합니다.
이어서 LCEL에서 RAG의 Chain을 구현하여 실행합니다.
이 LCEL의 체인에서는 먼저 {“context”: retriever, “question”: RunnablePassthrough()}라고 적혀 있습니다. 이것은 입력이 retriever에 전달되면서 동시에 prompt에도 전달된다는 이미지입니다. 이 설명에 대한 자세한 내용은 다음 강좌에서 설명합니다. 이 코드를 실행하면 LLM의 응답은 다음과 같습니다.
네, AWS S3에서 데이터를 불러올 수 있는 Document loader가 있습니다. `S3DirectoryLoader`와 `S3FileLoader`라는 두 가지 Document loader가 제공됩니다. 이들 loader를 사용하여 AWS S3에서 데이터를 불러올 수 있습니다. 사용 예제는 [여기](https://docs/integrations/document_loaders/ aws_s3_directory)와 [여기](https://docs/integrations/document_loaders/aws_s3_file)에서 확인할 수 있습니다.
Retriever에서 검색한 텍스트를 기반으로 답변하는 것을 볼 수 있는데, LangChain을 통해 RAG의 기본적인 처리를 구현할 수 있었습니다.
이번에는 LangChain의 RAG 관련 컴포넌트에 대한 기본 사항을 설명했습니다. RAG는 LLM 애플리케이션에서 매우 많이 채택되고 있으며, 다양한 변형이 고안되고 있으며, 이러한 컴포넌트를 사용하여 예를 들어 사내 문서에 대한 Q&A가 가능한 챗봇을 구현할 수 있습니다. 아마도 다다음 강좌에서 보다 진보된 RAG 기법을 다룹니다.
지금까지 LangChain을 이용한 RAG 구현의 기본에 대해 알아보았다. 실제로 RAG의 기능을 운영할 때, 문서를 Vector store에 한 번만 저장하면 되는 것이 아니라, 문서 갱신 시 Vector store와 동기화하는 처리가 필요한 경우가 많습니다. 이러한 동기화 처리를 잘 구현하기 위해 LangChain에서는 Indexing API라는 기능을 제공하고 있습니다. 관심이 있으신 분들은 공식 문서 다음 페이지를 참고하시기 바랍니다.
관련 URL: https://python.langchain.com/docs/how_to/indexing/
© 2024 ZeR0, Hand-crafted & made with Damon JW Kim.
Profile: https://gaebal.site
개발문의: https://naver.me/GalVgGKH
블로그: https://blog.naver.com/beyond-zero