테마
Runnable과 LCEL 파이프라인
LangChain의 핵심 철학 "모든 것은 Runnable"과, 파이프로 연결하는 LCEL을 배웁니다
학습 목표
- Runnable 인터페이스와 상속 구조를 이해한다
- LCEL(LangChain Expression Language)의 파이프 연결 방식을 익힌다
- invoke 중첩과 LCEL의 차이를 비교한다
- 체인도 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}패턴으로 한 체인의 결과를 다른 체인의 입력으로 전달