[LLM 개발기초] 4. LangChain 활용 #1
이번 챕터부터는 LangChain 을 다양하게 활용하는 예제들을 실행해 보겠습니다.
LangChain 은 AI로부터 답을 얻기까지 여러 단계로 구분하고, 각 단계별로 도구를 만들어 서로 연결할 수 있도록 해주는 라이브러리입니다. 따라서 단계별로 다양한 도구들을 불러와서 이들을 조합하면 내가 원하는 방향으로 AI 를 튜닝할 수 있습니다.
가장 먼저 실험해 볼 도구는 프롬프트(Prompt)입니다. 앞선 챕터에서 예제로 체험한 것 처럼, AI 에게 던지는 메시지를 변형해서 내가 의도한대로 대답을 얻거나 AI 와의 상호작용을 더욱 효율적으로 만드는데 사용됩니다.
Prompt 실습 1: 프롬프트와 LCEL 을 이용한 기본 예제
LangChain 에는 LCEL(LangChain Expression Language) 기능이 탑재되어 있습니다. 그래서 앞서 얘기한 도구들을 만들면 아래 같은 표현식으로 서로 연결할 수 있습니다.
chain = prompt | llm | output_parser
이렇게 연결된 chain 을 이용해서 LLM 모델을 실행할 수 있습니다. 이 과정에서 도구들을 연결하고 동작을 최적화하며, 디버깅을 위한 준비 등을 알아서 해줍니다. 이번 실습에서는 LCEL chain 을 이용합니다. [04_01_prompt_test_01.py]
from langchain_community.llms import Ollama from langchain.prompts import ChatPromptTemplate from langchain_core.output_parsers import StrOutputParser # 1. 프롬프트 템플릿 생성 prompt = ChatPromptTemplate.from_template( "당신은 유능한 기상학자입니다. 다음 질문에 답해주세요. <질문>: {question}" ) # 2. Ollama 모델 초기화 llm = Ollama(model="llama3.1") # 3. 스트림 출력 파서 생성 class CustomStreamOutputParser(StrOutputParser): def parse(self, text): return text output_parser = CustomStreamOutputParser() # 4. chain 연결 (LCEL) chain = prompt | llm | output_parser # 5. chain 실행 및 결과 출력 for chunk in chain.stream({"question": "크기에 따른 태풍의 분류 방법을 알려주세요."}): print(chunk, end="", flush=True)
코드를 단계별로 살펴보면
- ChatPromptTemplate 을 이용하여 프롬프트를 생성합니다.
- 프롬프트를 생성하는 가장 단순한 방법을 사용하였습니다.
- 프롬프트에 LLM 에 전달할 요구사항을 명시하면 됩니다.
- 내가 할 질문은 5번 chain 실행 과정에서 입력하게 됩니다.
- 로컬 PC에 설치된 ollama 와 llama3.1 모델을 사용합니다.
- 출력 파서(OutputParser)를 이용하면 출력 결과를 원하는대로 수정할 수 있습니다.
- 예제에서는 StrOutputParser 를 이용해서 CustomStreamOutputParser 를 만들었습니다.
- StrOutputParser 는 출력을 문자열로 받는 가장 기본적인 OutputParser 입니다.
- JSON, XML, CSV 등 다양한 출력 포맷을 사용하고 싶은 경우 아래 내용을 참고하세요
- 실습 코드에서는 파서를 만들지만, 내부에서 아무일도 하지 않습니다.
- LCEL 을 이용해서 chain 을 생성합니다.
- LangChain 은 도구들을 연결해서 실행할 준비를 합니다.
- chain 을 실행하고 결과를 stream 형식으로 출력합니다.
- 이때 prompt 에 필요한 질문을 함께 전달합니다.
실행해보면 아래처럼 결과가 출력될 것입니다.
$ python -u "c:\___Workspace\Llama\test\llama_test\LLM_test\04_01_prompt_test_01.py" 기상학적으로, 태풍은 크기에 따라 다섯 가지 분류가 있습니다. 1. **소형태풍**: 이들은 10분간 평균 풍속이 약 33m/s에서 39m/s 사이인 태풍입니다. 2. **중형태풍**: 이들은 10분간 평균 풍속이 약 40m/s에서 49m/s 사이인 태풍입니다. 3. **중대형태풍**: 이들은 10분간 평균 풍속이 약 50m/s에서 59m/s 사이인 태풍입니다. 4. **대형태풍**: 이들은 10분간 평균 풍속이 약 60m/s에서 69m/s 사이인 태풍입니다. 5. **매우대형태풍**: 이들은 10분간 평균 풍속이 70m/s 이상인 태풍입니다. 태풍의 크기는 바람의 속도와 관련되어 있습니다. 태풍은 바람의 속도가 증가할수록 더 큰 크기를 가진다. $
Prompt 실습 2: 대화내용을 기억하는 연속 채팅
Langchain의 ChatPromptTemplate은 대화형 시스템에서 각 메시지의 역할(시스템, 사용자, 봇)을 명확하게 구분하고, 이전 대화 내용을 기억하여 더욱 자연스러운 대화를 이어나갈 수 있도록 돕습니다. 그리고 대화 내용을 기록하는 역할도 해줍니다.
이번 실습에서는 ChatPromptTemplate을 이용해서 대화 내용을 기억하면서 대화를 이어나갈 수 있도록 해보겠습니다. [04_02_advanced_prompt.py]
from langchain_community.llms import Ollama from langchain.prompts import ChatPromptTemplate, MessagesPlaceholder from langchain_core.output_parsers import StrOutputParser # 1. 대화내용 저장을 위한 ChatPromptTemplate 설정 prompt = ChatPromptTemplate.from_messages([ ("system", "당신은 유능한 기상학자입니다. 답변은 200자 이내로 하세요."), MessagesPlaceholder("chat_history"), # 1-1. 프롬프트에 대화 기록용 chat_history 추가 ("user", "{question}") ]) # 2. Ollama 모델 초기화 llm = Ollama(model="llama3.1") # 3. 스트림 출력 파서 생성 class CustomStreamOutputParser(StrOutputParser): def parse(self, text): return text output_parser = CustomStreamOutputParser() # 4. chain 연결 (LCEL) chain = prompt | llm | output_parser # 5. 채팅 기록 초기화 chat_history = [] # 6. chain 실행 및 결과 출력을 반복 while True: # 6-1. 사용자의 입력을 기다림 user_input = input("\n\n당신: ") if user_input == "끝": break # 6-2. 체인을 실행하고 결과를 stream 형태로 출력 result = "" for chunk in chain.stream({"question": user_input, "chat_history": chat_history}): print(chunk, end="", flush=True) result += chunk # 6-3. 채팅 기록 업데이트 chat_history.append(("user", user_input)) chat_history.append(("assistant", result))
코드를 단계별로 살펴보면
- ChatPromptTemplate 을 이용해서 프롬프트를 만듭니다. 이때 대화내용을 저장하기 위한 chat_history 설정을 해줍니다.
- 1-1. MessagesPlaceholder 를 이용해서 대화 기록용 chat_history 를 추가합니다.
- 로컬 PC에 설치된 ollama 와 llama3.1 모델을 사용합니다.
- 출력 파서(OutputParser)를 설정합니다.
- 이번 예제에서는 특별히 하는 일이 없습니다.
- LCEL 을 이용해서 chain 을 생성합니다.
- 대화내용을 저장할 chat_history 배열을 만듭니다.
- chain 을 실행합니다.
- 6-1. 먼저 사용자의 입력을 받습니다.
- 6-2. 사용자의 입력과 저장된 대화내용을 이용해서 chain 을 실행합니다.
- 6-3. 대답이 모두 출력되면 chat_history 에 질문과 대답을 넣어줍니다.
코드를 실행해보면 앞선 예제와는 달리 대화를 계속 이어나갈 수 있습니다. (종료: Ctrl + C)
$ python -u "c:\___Workspace\Llama\test\llama_test\LLM_test\04_02_advanced_prompt.py" 당신: 1년 동안 전 세계에서 밠생하는 태풍의 평균 갯수는? 밠생하는 태풍의 평균 갯수에 대해, 저는 그 정보를 제공하겠습니다. 전 세계에서 1년 동안 밠생하는 태풍의 평균 갯수는 약 80개입니다. 이 숫자는 전 세계의 태풍 발생 지역을 감안해 정의되었으며, 아시아와 대서양의 태풍 활동이 가장 활발한 지역입니다. 당신: 앞선 질문에서 지역을 아시아로 한정하면 대답은 어떻게 바뀌지? 아시아의 경우 1년 동안 밠생하는 태풍의 평균 갯수는 약 54개 정도입니다. 이 숫자도 전 세계의 태풍 발생 지역 중 아시아를 포함한 지역에서 발생하는 태풍의 총 수를 감안해 정의되었습니다. 당신:
대화를 자세히 보면 1번째 질문을 기억하고 있음을 알 수 있습니다. 2번째 질문에서 태풍에 대한 언급없이 지역만 변경해서 대답해 달라고 했는데 태풍의 갯수에 대한 대답을 해줍니다.
Prompt 실습 3: 출력되는 내용을 수정
이번에는 output parser 를 더욱 업그레이드해서 사용해 보겠습니다. 실습 코드는 태풍에 대한 내용을 출력할 때 “태풍” 단어를 이모지로 바꿔서 출력합니다. [04_03_custom_output_parser.py]
- 이전 실습에 있던 CustomStreamOutputParser 의 parse() 함수를 수정해서 사용하면 연산이 느린 PC에서 1글자 단위로 입력이 들어와서 단어 단위의 처리를 못하는 문제가 있습니다.
- 그래서 RunnableGenerator 를 사용하는 방식으로 바꿔서 구현했습니다.
- 이 방법 대신 6-2 코드에서 출력을 수정해도 됩니다.
from typing import Iterable from langchain_community.llms import Ollama from langchain.prompts import ChatPromptTemplate, MessagesPlaceholder from langchain_core.runnables import RunnableGenerator from langchain_core.messages import AIMessageChunk # 1. 대화내용 저장을 위한 ChatPromptTemplate 설정 prompt = ChatPromptTemplate.from_messages([ ("system", "당신은 유능한 기상학자입니다. 답변은 200자 이내로 하세요."), MessagesPlaceholder("chat_history"), # 1-1. 프롬프트에 대화 기록용 chat_history 추가 ("user", "{question}") ]) # 2. Ollama 모델 초기화 llm = Ollama(model="llama3.1") # 3. 스트림 출력 파서 생성 🌪️ def replace_word_with_emoji(text: str) -> str: # 문자열에서 태풍을 이모지로 바꿔주는 함수 return text.replace("태풍", "🌪️") def streaming_parse(chunks: Iterable[AIMessageChunk]) -> Iterable[str]: # AI 출력을 받아서 처리하는 함수 buffer = "" for chunk in chunks: # 속도가 느린 컴퓨터에서 실행하는 경우 단어가 완성될 때까지 모아서 처리 buffer += chunk while " " in buffer: word, buffer = buffer.split(" ", 1) yield replace_word_with_emoji(word) + " " if buffer: yield replace_word_with_emoji(buffer) streaming_parser = RunnableGenerator(streaming_parse) # 4. chain 연결 (LCEL) chain = prompt | llm | streaming_parser # 5. 채팅 기록 초기화 chat_history = [] # 6. chain 실행 및 결과 출력을 반복 while True: # 6-1. 사용자의 입력을 기다림 user_input = input("\n\n당신: ") if user_input == "끝": break # 6-2. 체인을 실행하고 결과를 stream 형태로 출력 result = "" for chunk in chain.stream({"question": user_input, "chat_history": chat_history}): print(chunk, end="", flush=True) result += chunk # 6-3. 채팅 기록 업데이트 chat_history.append(("user", user_input)) chat_history.append(("assistant", result))
이전 실습과 다른 부분은 동일하므로 변경된 3번 부분만 설명합니다.
- 3. LLM stream 이 뱉어내는 단어들을 streaming_parse() 함수에서 처리합니다.
- stream 에서 나오는 단어들을 buffer 에 모아둡니다.
- buffer 에 공백문자가 있으면 공백을 기준으로 단어를 추출합니다.
- 추출된 단어에서 “태풍” 문자가 검색되면 이모지로 바꿔줍니다.
이 코드를 실행해보면 아래와 같이 이모지가 “태풍” 문자를 대체합니다.
$ python -u "c:\___Workspace\Llama\test\llama_test\LLM_test\04_03_custom_output_parser.py" 당신: 전 세계에서 1년 동안 밠생하는 태풍의 평균 갯수는? 전 세계적으로 1년 동안 발생하는 🌪️의 평균 갯수는 약 80개 정도입니다. 이 중 약 60%가 태평양에 발생하고, 나머지 갯수는 인도양과 북서태 평양 지역에서 발생합니다. 당신: 태풍의 종류에 대해서 알려줘 기상학적으로 🌪️은 다음 세 가지 유형으로 분류할 수 있습니다. 1. **트레치**: 가장 강력한 종류입니다. 강풍(약 200km/h 이상)과-heavy rainfall이 특징입니다. 2. **슈퍼🌪️**: 전 세계에서 발생하는 가장 강력한 🌪️입니다. 풍속은 250 km/h 이상이며, 거대한 물리적 피해를 남길 수 있습니다. 3. **서스포트**: 가장 느린 종류입니다. 풍속은 약 100km/h 미만으로, 비교적 가벼운 피해를 낼 수 있습니다. 이러한 유형은 기상학적으로 분류되며, 각 지역에서 발생하는 🌪️의 특징과 영향을 고려하여 분류됩니다. 당신:
참고자료
- The Art of Prompt Engneering — 1. Prompt Engineering이란 무엇인가?
- [LangChain] Prompt Template 사용 방법 정리
- 실습에 사용된 소스코드 (GitHub)