Skip to content

Runnable과 LCEL 파이프라인

LangChain의 핵심 철학 "모든 것은 Runnable"과, 파이프로 연결하는 LCEL을 배웁니다

학습 목표

  1. Runnable 인터페이스와 상속 구조를 이해한다
  2. LCEL(LangChain Expression Language)의 파이프 연결 방식을 익힌다
  3. invoke 중첩과 LCEL의 차이를 비교한다
  4. 체인도 Runnable이라서 체인끼리 연결 가능함을 이해한다

1. 모든 것은 Runnable

LangChain의 핵심 철학은 단순합니다: 모든 구성 요소가 Runnable을 상속한다.

"Runnable"은 "실행 가능한"이라는 뜻입니다. 프롬프트, LLM, 출력 파서, 리트리버, 체인 등 LangChain의 모든 구성 요소가 동일한 invoke 메서드를 가지고 있습니다.

1.1 클래스를 따라가 보면

실제 코드에서 상속 관계를 확인할 수 있습니다:

python
# PromptTemplate → RunnableSerializable → Runnable
# ChatOllama → BaseChatModel → RunnableSerializable → Runnable
# StrOutputParser → BaseOutputParser → RunnableSerializable → Runnable

어떤 클래스든 최종적으로 Runnable에 도달합니다. 그래서 모든 구성 요소가 invoke를 할 수 있는 것입니다.

1.2 통일된 인터페이스의 장점

python
# 프롬프트도 invoke
prompt_value = prompt.invoke({"country": "France"})

# LLM도 invoke
ai_message = llm.invoke(prompt_value)

# 파서도 invoke
answer = parser.invoke(ai_message)

모든 구성 요소가 같은 패턴(invoke)으로 동작하니까, 하나를 다른 것으로 교체해도 코드 구조가 바뀌지 않습니다.


2. invoke 중첩의 문제

지금까지 우리는 이렇게 코드를 작성했습니다:

python
# 중첩 방식: 안에서 바깥으로 읽어야 함
prompt_value = prompt.invoke({"country": "France"})
ai_message = llm.invoke(prompt_value)
answer = parser.invoke(ai_message)

이 코드는 동작하지만, 각 단계의 결과를 변수에 저장하고 다음 단계에 전달하는 과정이 반복됩니다. 더 복잡한 파이프라인에서는 코드가 길어지고 가독성이 떨어집니다.


3. LCEL 파이프라인

3.1 LCEL이란?

LCEL(LangChain Expression Language)은 Runnable들을 파이프(|) 연산자로 연결하는 문법입니다.

3.2 코드 비교

python
from langchain_ollama import ChatOllama
from langchain_core.prompts import PromptTemplate
from langchain_core.output_parsers import StrOutputParser

llm = ChatOllama(model="llama3.2:1b")
prompt = PromptTemplate(
    template="What is the capital of {country}? Return the name of the city only.",
    input_variables=["country"]
)
parser = StrOutputParser()

invoke 중첩 방식:

python
prompt_value = prompt.invoke({"country": "France"})
ai_message = llm.invoke(prompt_value)
answer = parser.invoke(ai_message)
print(answer)  # "Paris"

LCEL 파이프 방식:

python
# 파이프(|)로 연결하여 체인 생성
chain = prompt | llm | parser

# 체인을 한 번에 실행
answer = chain.invoke({"country": "France"})
print(answer)  # "Paris"

LCEL의 장점:

  • 값이 왼쪽에서 오른쪽으로 흐름 → 가독성 향상
  • 중간 변수 불필요 → 코드가 간결
  • 체인 자체가 Runnable → 재사용 가능

3.3 체인의 타입

python
chain = prompt | llm | parser

print(type(chain))
# <class 'langchain_core.runnables.base.RunnableSequence'>

체인도 RunnableSerializable입니다. 즉, 체인도 Runnable이기 때문에 체인끼리 연결하는 것도 가능합니다.


4. 체인-인-체인 (Chain of Chains)

4.1 체인을 다른 체인의 일부로 사용

체인이 Runnable이라는 것은, 체인을 또 다른 파이프라인의 구성 요소로 쓸 수 있다는 뜻입니다.

4.2 코드 구현

python
from langchain_ollama import ChatOllama
from langchain_core.prompts import PromptTemplate
from langchain_core.output_parsers import StrOutputParser

llm = ChatOllama(model="llama3.2:1b")
parser = StrOutputParser()

# 체인 1: 수도 찾기
capital_prompt = PromptTemplate(
    template="What is the capital of {country}? Return the city name only.",
    input_variables=["country"]
)
capital_chain = capital_prompt | llm | parser

# 체인 2: 나라 찾기
country_prompt = PromptTemplate(
    template="Which country is most famous for {information}? Return the country name only.",
    input_variables=["information"]
)
country_chain = country_prompt | llm | parser

# 체인 연결: 나라 찾기 결과 → 수도 찾기
final_chain = (
    {"country": country_chain}  # country_chain의 결과를 "country" 키로 전달
    | capital_chain
)

# 실행
result = final_chain.invoke({"information": "wine"})
print(result)  # "Paris" (프랑스의 수도)

핵심은 {"country": country_chain} 부분입니다. country_chain의 실행 결과"country" 키의 값으로 들어가서, capital_chain의 {country} Placeholder에 주입됩니다.


5. LCEL이 강력한 이유


핵심 정리

  • LangChain의 모든 구성 요소는 Runnable을 상속하여 invoke로 실행 가능
  • LCEL: 파이프(|)로 Runnable들을 연결하는 표현 언어
  • invoke 중첩보다 LCEL이 가독성, 재사용성 면에서 우월
  • 체인도 Runnable이므로 체인끼리 연결(체인-인-체인)이 가능
  • {"key": chain} 패턴으로 한 체인의 결과를 다른 체인의 입력으로 전달