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. 모델

다음으로, PromptValuemodel에 전달합니다. 이 예시에서는 우리의 modelChatModel이기 때문에 BaseMessage를 출력할 것입니다.

모델을 직접 호출해 보세요:

message = model.invoke(prompt_value)
message

결과:

AIMessage(content="파티에서는 왜 아이스크림을 초대하지 않을까?\n\n뜨거워지면 항상 녹기 때문에!")

만약 우리의 modelLLM 유형으로 정의되어 있다면, 문자열을 출력할 것입니다.

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. 전체 프로세스

실행 프로세스는 다음과 같습니다:

  1. chain.invoke({"topic": "ice cream"})를 호출하면, 우리가 정의한 workflow를 시작하고 {"topic": "ice cream"} 매개변수를 전달하여 "아이스크림"에 관한 농담을 생성하는 것과 동등합니다.
  2. 호출 매개변수 {"topic": "ice cream"}를 체인의 첫 번째 구성 요소 prompt에 전달하여, 프롬프트 템플릿을 형식화하여 아이스크림에 대한 재미있는 농담 좀 해줘와 같은 프롬프트를 얻습니다.
  3. 프롬프트 아이스크림에 대한 재미있는 농담 좀 해줘model (gpt4 모델)에 전달합니다.
  4. model에서 반환된 결과를 output_parser 출력 파서에 전달하여 모델 결과를 형식화하고 최종 내용을 반환합니다.

어떤 구성 요소의 출력에 관심이 있다면, 언제든지 promptprompt | 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

간단히 말해서, 위의 프롬프트 템플릿은 contextquestion을 프롬프트 내에서 대체하기 위한 값으로 받아들입니다. 프롬프트 템플릿을 구성하기 전에 관련 문서를 검색하여 컨텍스트의 일부로 사용하기를 원합니다.

테스트로, 메모리 기반의 벡터 데이터베이스를 모방하기 위해 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

프로세스는 다음과 같습니다:

  1. 먼저, 두 개의 항목을 포함하는 RunnableParallel 객체를 만듭니다. 첫 번째 항목 context는 리트리버에 의해 추출된 문서 결과를 포함합니다. 두 번째 항목 question은 사용자의 원래 질문을 포함합니다. 질문을 전달하기 위해 이 항목을 복사하기 위해 RunnablePassthrough를 사용합니다.
  2. 이전 단계에서 얻은 사전을 prompt 구성 요소에 전달합니다. 이 구성 요소는 사용자의 입력(즉, question)과 검색된 문서(즉, context)를 받아 프롬프트를 구성하고 PromptValue를 출력합니다.
  3. model 구성 요소는 생성된 프롬프트를 가져와 OpenAI의 LLM 모델에 전달하여 평가합니다. 모델이 생성한 출력은 ChatMessage 객체입니다.
  4. 마지막으로, output_parser 구성 요소는 ChatMessage를 가져와 Python 문자열로 변환한 후 invoke 메서드에서 반환합니다.