LCEL 소개
LCEL (LangChain 표현 언어)은 기본 구성 요소에서 복잡한 작업 체인을 구축할 수 있도록 지원하며 스트리밍 처리, 병렬 처리 및 로깅과 같은 Out-of-the-box 기능을 제공하는 강력한 워크플로 오케스트레이션 도구입니다.
기본 예시: Prompt + Model + 출력 파싱기
이 예시에서는 "농담하기" 작업을 구현하기 위한 완전한 워크플로를 구성하기 위해 LCEL (LangChain 표현 언어)를 사용하여 프롬프트 템플릿, 모델 및 출력 파싱기를 연결하는 방법을 보여줍니다. 이 코드는 체인을 생성하는 방법, 파이프(|
) 기호를 사용하여 다른 구성 요소를 연결하는 방법을 보여주며 각 구성 요소의 역할과 출력 결과를 소개합니다.
먼저, 특정 주제에 대한 농담을 생성하기 위해 프롬프트 템플릿과 모델을 어떻게 연결하는지 살펴봅시다.
의존성 설치
%pip install --upgrade --quiet langchain-core langchain-community langchain-openai
from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import ChatPromptTemplate
from langchain_openai import ChatOpenAI
prompt = ChatPromptTemplate.from_template("{topic}에 관한 농담 좀 해줘")
model = ChatOpenAI(model="gpt-4")
output_parser = StrOutputParser()
chain = prompt | model | output_parser
chain.invoke({"topic": "아이스크림"})
결과
"파티에서는 왜 아이스크림을 초대하지 않을까? 뜨거워지면 녹아버리니까!"
이 코드에서 우리는 LCEL을 사용하여 다른 구성 요소들을 하나의 체인으로 연결합니다:
chain = prompt | model | output_parser
여기서 |
기호는 Unix 파이프 연산자와 유사하며, 다양한 구성 요소를 연결하고 한 구성 요소의 출력을 다음 구성 요소의 입력으로 전달합니다.
이 체인에서 사용자 입력은 프롬프트 템플릿에 전달되고, 프롬프트 템플릿의 출력은 모델에 전달되며, 마지막으로 모델의 출력은 출력 파싱기에 전달됩니다. 각 구성 요소를 개별적으로 살펴보면서 무슨 일이 일어나는지 더 잘 이해해 봅시다.
1. 프롬프트
prompt
는 템플릿 변수 딕셔너리를 수락하고 PromptValue
를 생성하는 BasePromptTemplate
입니다. PromptValue
는 문자열 형식으로 입력으로 LLM
(문자열 형식으로) 또는 ChatModel
(메시지 시퀀스 형식으로)에 전달할 수 있는 프롬프트를 포함하는 래핑된 객체입니다. 이는 BaseMessage
를 생성하고 문자열을 생성하는 논리를 정의하기 때문에 어떤 종류의 언어 모델과도 사용할 수 있습니다.
prompt_value = prompt.invoke({"topic": "아이스크림"})
prompt_value
결과
ChatPromptValue(messages=[HumanMessage(content='아이스크림에 관한 농담 좀 해줘')])
아래에서, 프롬프트 형식의 결과를 채팅 모델에 사용되는 메시지 형식으로 변환합니다:
prompt_value.to_messages()
결과
[HumanMessage(content='아이스크림에 관한 농담 좀 해줘')]
이를 직접적으로 문자열로 변환할 수도 있습니다:
prompt_value.to_string()
결과
'사람: 아이스크림에 관한 농담 좀 해줘.'
2. 모델
다음으로, PromptValue
를 model
에 전달합니다. 이 예시에서는 우리의 model
이 ChatModel
이기 때문에 BaseMessage
를 출력할 것입니다.
모델을 직접 호출해 보세요:
message = model.invoke(prompt_value)
message
결과:
AIMessage(content="파티에서는 왜 아이스크림을 초대하지 않을까?\n\n뜨거워지면 항상 녹기 때문에!")
만약 우리의 model
이 LLM
유형으로 정의되어 있다면, 문자열을 출력할 것입니다.
from langchain_openai.llms import OpenAI
llm = OpenAI(model="gpt-3.5-turbo-instruct")
llm.invoke(prompt_value)
모델 결과:
'\n\n봇: 아이스크림 트럭이 왜 고장 났을까? 녹은 바람이 불어서!'
3. 출력 파싱기
마지막으로, 모델
에서의 출력을 output_parser
에 전달합니다. 이는 문자열이나 BaseMessage
를 입력으로 받는 BaseOutputParser
로, 특히 어떤 종류의 입력이든 간단한 문자열로 변환합니다.
output_parser.invoke(message)
파티에서는 왜 아이스크림을 초대하지 않을까? 뜨거워지면 항상 녹기 때문에!
4. 전체 프로세스
실행 프로세스는 다음과 같습니다:
-
chain.invoke({"topic": "ice cream"})
를 호출하면, 우리가 정의한 workflow를 시작하고{"topic": "ice cream"}
매개변수를 전달하여 "아이스크림"에 관한 농담을 생성하는 것과 동등합니다. - 호출 매개변수
{"topic": "ice cream"}
를 체인의 첫 번째 구성 요소prompt
에 전달하여, 프롬프트 템플릿을 형식화하여아이스크림에 대한 재미있는 농담 좀 해줘
와 같은 프롬프트를 얻습니다. - 프롬프트
아이스크림에 대한 재미있는 농담 좀 해줘
를model
(gpt4 모델)에 전달합니다. -
model
에서 반환된 결과를output_parser
출력 파서에 전달하여 모델 결과를 형식화하고 최종 내용을 반환합니다.
어떤 구성 요소의 출력에 관심이 있다면, 언제든지 prompt
나 prompt | model
과 같은 더 작은 버전의 체인을 테스트하여 중간 결과를 볼 수 있습니다:
input = {"topic": "ice cream"}
prompt.invoke(input)
(prompt | model).invoke(input)
RAG 검색 예제
이제 조금 더 복잡한 LCEL 예제를 설명해보겠습니다. 질문에 대답할 때 배경 정보를 추가하기 위해 강화 검색 예제를 실행하여 체인을 생성합니다.
from langchain_community.vectorstores import DocArrayInMemorySearch
from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.runnables import RunnableParallel, RunnablePassthrough
from langchain_openai.chat_models import ChatOpenAI
from langchain_openai.embeddings import OpenAIEmbeddings
vectorstore = DocArrayInMemorySearch.from_texts(
["harrison worked at kensho", "bears like to eat honey"],
embedding=OpenAIEmbeddings(),
)
retriever = vectorstore.as_retriever()
template = """다음 컨텍스트에 기반하여 질문에 답하세요:
{context}
질문: {question}
"""
prompt = ChatPromptTemplate.from_template(template)
model = ChatOpenAI()
output_parser = StrOutputParser()
setup_and_retrieval = RunnableParallel(
{"context": retriever, "question": RunnablePassthrough()}
)
chain = setup_and_retrieval | prompt | model | output_parser
chain.invoke("해리슨은 어디에서 일했나요?")
이 경우, 구성된 체인은 다음과 같습니다:
chain = setup_and_retrieval | prompt | model | output_parser
간단히 말해서, 위의 프롬프트 템플릿은 context
와 question
을 프롬프트 내에서 대체하기 위한 값으로 받아들입니다. 프롬프트 템플릿을 구성하기 전에 관련 문서를 검색하여 컨텍스트의 일부로 사용하기를 원합니다.
테스트로, 메모리 기반의 벡터 데이터베이스를 모방하기 위해 DocArrayInMemorySearch
를 사용하여 유사한 문서를 검색할 수 있는 리트리버를 정의합니다. 이것도 체인 가능한 runnable 컴포넌트입니다만, 별도로 실행해볼 수도 있습니다:
retriever.invoke("해리슨은 어디에서 일했나요?")
그런 다음, RunnableParallel
을 사용하여 프롬프트에 입력을 준비하고 리트리버를 사용하여 문서를 검색하며, RunnablePassthrough
를 사용하여 사용자의 질문을 전달합니다:
setup_and_retrieval = RunnableParallel(
{"context": retriever, "question": RunnablePassthrough()}
)
요약하면, 완전한 체인은 다음과 같습니다:
setup_and_retrieval = RunnableParallel(
{"context": retriever, "question": RunnablePassthrough()}
)
chain = setup_and_retrieval | prompt | model | output_parser
프로세스는 다음과 같습니다:
- 먼저, 두 개의 항목을 포함하는
RunnableParallel
객체를 만듭니다. 첫 번째 항목context
는 리트리버에 의해 추출된 문서 결과를 포함합니다. 두 번째 항목question
은 사용자의 원래 질문을 포함합니다. 질문을 전달하기 위해 이 항목을 복사하기 위해RunnablePassthrough
를 사용합니다. - 이전 단계에서 얻은 사전을
prompt
구성 요소에 전달합니다. 이 구성 요소는 사용자의 입력(즉,question
)과 검색된 문서(즉,context
)를 받아 프롬프트를 구성하고PromptValue
를 출력합니다. -
model
구성 요소는 생성된 프롬프트를 가져와 OpenAI의 LLM 모델에 전달하여 평가합니다. 모델이 생성한 출력은ChatMessage
객체입니다. - 마지막으로,
output_parser
구성 요소는ChatMessage
를 가져와 Python 문자열로 변환한 후invoke
메서드에서 반환합니다.