[Codepresso 러너톤 도전기] Week 1. 랭체인 기본기 익히기
본 게시물은 테디노트의 <랭체인LangChain 노트> - LangChain 한국어 튜토리얼🇰🇷를 참고하고 있습니다.
LangChain 기본 기능 살펴보기
llm 객체 만들어서 invoke 시키는 것은 너무 간단하므로 생략한다. 여기서는 Langchain 이용 시 기본적으로 많이 사용되는 메서드나 패키지들에 대해 설명한다.
입력값을 만드는 다양한 방법
LLM 모델에 입력할 프롬프트 구성 시, '지시', '예시', '맥락', '질문' 과 같은 다양한 구성요소를 조합할 수 있으며 이를 구조적으로 잘 명세하면 더 좋은 결과를 얻을 수 있다. 또한, 일부 변수만 바뀌고 반복적으로 프롬프트를 사용해야 하는 경우도 존재한다. 이럴 때 필요한 것이 바로 템플릿이다. 물론 사람이 직접 템플릿을 구성하고, 입력값으로부터 템플릿에 들어갈 내용들을 구성하도록 코딩할 수도 있지만, 랭체인에서 프롬프트 템플릿들을 제공하므로 이를 잘 이용하는 것이 좋다.
langchain_core.prompts
패키지에서는 생성형 모델에 효과적으로 입력값을 재구성하여 전달할 수 있는 다양한 프롬프트들을 제공한다.
📋 프롬프트 템플릿 클래스
1.
PromptTemplate
가장 기본적인 템플릿 클래스이다. 문자열 기반으로 프롬프트 템플릿을 생성하고 '+' 연산자로 여러 개의 프롬프트 템플릿 객체를 결합할 수 있다는 특징을 지닌다.
Prompt 객체를 생성하는 방법
- 방법 1.
PromptTemplate()
생성자에 매개변수 전달
PromptTemplate()
클래스 생성자에서 매개변수를 전달하여 생성하는 방법이다. 매개변수는 변수 리스트인 input_variables
와 변수가 포함된 템플릿 문자열인 template
으로 구성된다.
template = PromptTemplate(
input_variables=["name", "age"],
template="Remember my name : {nanm} and my age : {age}")
- 방법 2.
.from_template()
메서드 사용
PromptTemplate
의 .from_template()
메서드를 이용헤, 템플릿 문자열만 전달하는 방법이다.
template = PromptTemplate.from_template(
'Remember my name : {name} and my age : {age}')
실제로는 방법2가 더 자주 쓰인다. 개인적인 생각으로는, 방법1이 코드 가독성 부분에서 더 나은 것 같기도 하다.
2.
ChatPromptTemplate
대화형 모델, 챗봇 개발에 주로 사용되는 템플릿으로, 시스템 메시지를 이용하여 언어모델의 페르소나를 손쉽게 지정할 수 있다는 장점이 있다.
ChatPromptTemplate에 변수 전달하는 방법
- 방법 1.
.from_messages
사용
from_messages
메서드에 (role , content)들이 담긴 리스트를 전달한다. 입력 변수가 존재하는 경우, 앞선 방법과 동일한 방식으로 {변수명}
형태로 입력한다.
chat_prompt = ChatPromptTemplate.from_messages(
("system", "You are a teenager girl who loves k-pop"),
("user", "{user_input}")
)
- 방법 2.
SystemMessage
,HumanMessage
등의 프롬프트 객체로 전달
ChatPromptTemplate()
생성자에 직접 역할별 프롬프트 객체 리스트를 매개변수로 전달하는 방법이다. 각 프롬프트 객체에 전달되는 매개변수는 content
만 있다.
chat_prompt = ChatPromptTemplate(
[
SystemMessage(content="You are a teenager girl who loves k-pop"),
HumanMessage(content="{user_input}")
]
)
ChatMessagePrompt
에 전달할 수 있는 프롬프트 유형은 SystemMessage
,HumanMessge
말고도 AIMessage
,FunctionMessage
.ToolMessage
가 있다. AIMessage
의 경우, 출력 형식이 정해져 있는 경우 이용할 수 있고, FunctionMessage
는 함수 호출 결과를 넣을 때 사용할 수 있다. ToolMessage
는 외부 도구를 사용한 경우, 외부 도구로부터의 결과를 넣을 때 사용힌다.
📥 Message Place Holder
MessagePlaceholder
는 입력 프롬프트의 특정 위치에 여러 개의 입력값을 넣어야 하는 경우 사용하는 클래스이다. 주로 과거 대화 내용을 복기시켜야 하는 경우 사용된다. 아래의 예시는 과거 대화 내용 리스트를 message placeholder를 이용해 위치시키도록 하는 코드이다. 코드출처
# 메시지 프롬프트
message_template = ChatPromptTemplate.from_messages([
SystemMessagePromptTemplate.from_template("You are a customer service chatbot. You name is Raj."),
MessagesPlaceholder(variable_name="status"), # 메시지 리스트가 들어올 위치
HumanMessagePromptTemplate.from_template("Can you summarize the ticket in {word_count} words?"),
])
# 과거 대화 리스트
message_list = [
HumanMessage(content="Hi, What's happening to the ticket I raised?"),
AIMessage(content="Hi, your ticket was opened with the status OPEN."),
HumanMessage(content="What is the new status of my ticket?"),
AIMessage(content="It is currently WIP")
]
# 메시지 객체 정의
message_template.format_prompt(
status=message_list,
word_count="100"
)
Prompt Formatting
이렇게 생성한 프롬프트에 실제 입력값을 넣을 떄는 {입력변수명 : 값}
형식으로 전달한다.
input_values = {"user_input" : "Hello"}
chain.invoke(input_values)
또는 생성된 프롬프트 템플릿 객체에서 .format_prompt
메서드를 사용해 chain 정의 이전, 또는 invoke 실행 이전에 프롬프트 메시지를 완성시킬 수도 있다.
message_template = ChatPromptTemplate.from_messages([...])
message_template.format_prompt(
input_variable=input,
...
)
invoke 와 stream, batch
invoke
는 입력값에 대해 생성된 결과물을 한번에 리턴하는 메서드이다. 즉, 요청을 보낸 뒤 결과를 전송받을 때까지 코드가 멈추는 동기 방식으로 동작한다.stream
도 마찬가지로 동기적 방식으로 응답을 처리하는데, 차이가 있다면 스트리밍 방식으로 처리한다. 따라서, 전체 응답을 완료하기 전에 부분적으로 응답을 받을 수 있다는 장점이 있다. 다만, invoke와 마찬가지로 동기적 처리 방식이므로 실행 과정 중에는 다른 작업을 수행할 수 없다.astream
은 비동기 방식으로 스트리밍을 한다. 나타나는 결과 자체는 stream과 동일하나, astream의 동작 중에 다른 작업을 실행할 수 있다.async
예약어와 함께 사용해야 한다.ainvoke
도 비동기 방식으로 invoke 동작을 수행하는 메서드이다.
stream 메서드를 사용하는 경우, 토큰단위로 출력해서 보여줘야한다.
for token in stream_res:
print(token.content, end='', flush=True)
batch
는 매번 다르게 입력되는 입력변수들에 따라 여러 개의 답변을 생성하도록 하는 메서드이다. 매개변수로 각 답변마다 주어지는 {입력변수 : 값} 리스트가 들어간다.
chain.batch([
{"user_input" : "my name is jane, hi"},
{"user_input" : "i don't want to talk with you"}
])
출력값을 보기 좋게 만들기
langchain_core.output_parsers
에서는 출력 파서들을 제공한다. 출력 파서를 체인 맨 마지막에 연결하면, 출력값에서 생성된 문자열 값만 깔끔하게 파싱해서 주거나, 필요한 정보만 추출할 수 있다. 또한 조건부 로직을 적용할 수도 있다.
출력 파서는 텍스트 형식을 전달하는 StrOutputParser
뿐만 아니라, CSV 형식으로 전달하는 CommaSeperatedListOutputParser
도 있다. output parser는 보통 체인 맨 끝에 온다.
LangChain Expression Language(LECL)
앞에서 계속 '체인' 을 언급하였는데, 대체 체인이 무엇이고 어떻게 쓰는 것일까?
체인은 LLM을 사용할 때, 순차적으로 실행되어야 하는 여러 개의 기능, 데이터를 연결시킨 객체를 의미한다. 랭체인에서 체인의 정의는 LECL을 사용한다. 어려운 것은 아니고, 앞서 정의한 프롬프트 객체나 모델, 출력 파서 등의 객체들을 |
연산자로 묶어주면 된다.
chain = prompt | model | StrOutputParser()
위와 같은 방식으로 순차적으로 객체를 나열하면 된다.
만약, 입력값을 받아 프롬프트 객체를 생성해야 한다면 프롬프트보다 앞에 입력 객체에 입력변수와 값을 정의한 입력값을 넣어주면 된다. 이 경우는 아마 유저 입력값보다는 시스템 메시지 조작, 또는 출력값 형식 변경이 필요한 경우일 것이다.
chain = {"system_persona" : "you are a child."} | prompt | model | StrOutputParser()
또는 invoke, stream 메서드에 매개변수로 {입력변수 : 값 } 쌍을 전달할 수도 있다.
res = chain.invoke({"user_input" : "안녕."})
Runnable
원래 invoke
메서드는 입력 변수명과 입력 값으로 구성된 딕셔너리 타입만 받을 수 있다. (최근 업데이트로 인해 이제 템플릿의 입력변수가 1개면 invoke에 바로 입력값을 넣는 것이 가능하다.) 따라서 입력 변수가 여러 개거나, 입력 변수에 대해 람다함수를 적용하거나, 인라인 함수를 적용하는 경우에는 invoke에 전달되는 딕셔너리가 한없이 복잡해진다.
이때 사용할 수 있는 것이 바로 Runnable 이다. Runnable은 입력 변수에 대한 복잡한 조작이나 단순 전달 등의 기능을 지원하며, chain을 정의하는 과정에서 이를 프롬프트, 모델과 함께 정의가 가능하므로 invoke 의 입력값을 보다 단순화 할 수 있다. 또한, runnable 객체는 invoke
메소드로 별도 실행이 가능하다.
RunnablePathThrough
는 입력값을 그대로 전달하거나, assign
메소드를 통해 인자를 추가할 수 있다.
# RunnablePathThrough 메서드 체이닝
chain = {"name" : RunnablePathThrough()}|prompt|model
chain.invoke("jane")
# RunnablePathThrough 객체로 인자 전달
chain = {"name" : RunnablePathThrough()}|prompt|model
RunnablePathThrough().invoke({"name" : "jane"})
# assign 메소드로 추가 인자 전달
(RunnablePassthrough.assign(new_num=lambda x: x["num"] * 3)).invoke({"num": 1})
이 외에 두 개의 체인을 병렬로 실행시킬 수 있는 RunnableParallel
, 함수를 전달시킬 수 있는 RunnableLambda
등의 기능이 있다. Runnable은 이후 과정에서 다시 나오므로, 다시 코드와 함께 설명하도록 한다.
Multimodal
랭체인으로 multi-modal 모델을 구현하는 것도 가능하다. 이미지 멀티모달을 쓰고자 할 경우에는, 이미지 인코딩 과정이 필요하다. 이미지의 web URL을 넣는 경우, 디코딩 과정 없이 바로 입력으로 넣어줄 수 있지만, 로컬에 있는 이미지 파일의 경우에는 인코딩 과정이 필수적이다.
예시코드 : table image to csv style text
- 이미지 인코딩 함수 정의
import base64 # 이미지를 utf-8로 인코딩 하기 위한 라이브러리
import httpx # 웹사이트에서 이미지 읽어오기 위한 라이브러리
# 로컬 이미지 파일
def encode_image_from_file(file_path):
if not os.path.exists(file_path):
raise FileNotFoundError(f"File not found: {file_path}")
elif file_path.split(".")[-1] not in ["jpg", "jpeg", "png"]:
raise ValueError("Only jpg, jpeg, png files are supported")
else:
with open(file_path, "rb") as image_file:
encoded_string = base64.b64encode(image_file.read()).decode("utf-8")
return encoded_string
# 웹 URL 파일
def encode_image_from_url(url):
response = httpx.get(url)
if response.status_code != 200:
raise ValueError(f"Failed to fetch image from {url}")
elif response.headers["content-type"] not in ["image/jpeg", "image/png"]:
raise ValueError("Only jpg, jpeg, png files are supported")
else:
encoded_string = base64.b64encode(response.content).decode("utf-8")
return encoded_string
- 언어모델 객체 생성
multimodal_llm = ChatOpenAI(
temperature=0.1,
model_name="gpt-4o",
)
- 이미지 경로 입력 및 인코딩
image_data = encode_image_from_file("table.jpg") # 이미지 파일 경로
- 입력 프롬프트 생성 및 전달
# 이미지, 텍스트에 대한 프롬프트 생성
message = HumanMessage(
content=[
{"type" : "text",
"text" : "이미지 속의 표를 csv 형태로 변환해"},
{"type" : "image_url" ,
"image_url" : {"url" : f"data:image/jpeg;base64, {image_data}"} }
]
)
# 모델에 프롬프트 전달
res = multimodal_llm.invoke([message])