
여러분 혹시 한강라면 좋아하시나요? 저는 며칠 전에 한강에 갔다가 라면을 먹었는데, 대기 줄이 정말 길더라구요.
LCEL에 대해 공부하다가 이게 한강라면기계와 같은 것이 아닌가하는 생각이 들었습니다.(저도 왜 이런 생각을 했는지 모르겠네요😂 아마 배고파서 그런것 같아요.) 집에서 라면을 끓일 땐 물 계량, 불 세기, 면 넣는 타이밍까지 전부 직접 챙겨야 합니다. 그런데 한강 라면 기계는 그 과정을 다 자동화해주잖아요?
LCEL도 마치 그 한강 라면 기계와 비슷합니다.
우리가 "무엇을 할지(What)"만 선언하면 "어떻게 할지(How)"는 LangChain이 알아서 처리해줍니다. 덕분에 우리는 복잡한 세부 구현에 매달리지 않고, 큰 흐름만 설계하면 되는거죠.
이번 글에서는 위 특징을 가능하게해주는 핵심개념인 LCEL에 대해 정리해보겠습니다.
공식문서에는 LCEL에 대해 다음과같이 소개하고 있습니다.
The LangChain Expression Language (LCEL) takes a declarative approach to building new Runnables from existing Runnables.
This means that you describe what should happen, rather than how it should happen, allowing LangChain to optimize the run-time execution of the chains.
즉, LCEL은 기존 Runnable들을 조합해 새로운 체인을 선언적으로 표현하는 언어입니다.
중요한 점은 “무엇을 할지”를 기술하는 데 집중할 수 있고, 실행 과정의 최적화는 LangChain이 자동으로 처리한다는 겁니다.
이 중에서 가장 핵심이 되는 키워드가 바로 "Runnable"입니다. LCEL은 모든 요소를 Runnable단위로 추상화하기 때문에, Runnable을 이해하는 것이 곧 LCEL을 이해하는 출발점이라고 할 수 있습니다.
Runnable은 한마디로 입력을 받아 → 어떤 작업을 하고 → 출력을 내는 실행 단위입니다.
LCEL에서는 모든 구성 요소(프롬프트, LLM, 검색기, 변환기 등)가 Runnable로 표현됩니다.
1. 입력 → 출력 구조
- "무언가를 넣으면 결과가 나온다"라는 점에서 함수와 유사합니다.
- ex) "runnable"를 넣으면 "RUNNABLE" 출력
2. 공통 실행 방식 제공
모든 Runnable은 동일한 메서드를 가집니다.
- .invoke(input): 단일 입력 처리
- .batch([inputs]): 여러 입력 처리
- .stream(input): 토큰 단위 스트리밍
- .ainvoke, .abatch, .astream: 비동기 버전
3. 조합 가능
- Runnable들을 |(파이프)로 연결해서 체인(Chain)을 만듭니다.
- ex) 검색기 | 프롬프트 | LLM → RAG 체인
LCEL의 특징 중 하나인 조합 기능을 보면 이런 의문이 들 수 있습니다.
"LCEL로 조립했을 때 뭐가 좋은거지?" 🤔
LCEL을 사용했을 때의 가장 큰 장점은 "복잡한 기능을 단순하고 일관된 방법으로 제공한다"라는 점입니다. 병렬처리, 비동기, 스트리밍, 디버깅, 배포까지 모두 LCEL의 공통 인터페이스 안에서 쉽게 다룰 수 있다는 것이 장점입니다.
이번에는 LCEL에서 자주 쓰이는 Runnable들을 정리해보겠습니다. 처음에는 "이게 뭐야?"싶었는데, 알고보면 되게 단순한 블록들입니다.
가장 간단한 Runnable입니다. 입력을 그대로 통과시켜줍니다. 아무 일도 안 하지만, 디버깅이나 중간 데이터를 확인할 때 은근 유용합니다.
from langchain_core.runnables import RunnablePassthrough
passthrough = RunnablePassthrough()
print(passthrough.invoke("Hello"))
# 출력: "Hello"
python함수를 그대로 Runnable로 감싸는 기능입니다. 이를 통해 개발자는 자신만의 함수를 정의하고, 해당 함수를 RunnableLambda를 사용하여 실행할 수 있습니다.
from langchain_core.runnables import RunnableLambda
to_upper = RunnableLambda(lambda x: x.upper())
print(to_upper.invoke("hello"))
# 출력: "HELLO"
예를 들어, 데이터 전처리, 계산, 또는 외부 API와의 상호작용과 같은 작업을 수행하는 함수를 정의하고 실행할 수 있습니다.
조건에 따라 실행 경로를 분기하는 Runnable입니다. 예를 들어, 질문에 "finance"라는 단어가 있으면 금융 체인으로, "nlp"가 들어가 있으면 NLP 체인으로 보내는 식입니다.
from langchain_core.runnables import RunnableBranch, RunnableLambda
branch = RunnableBranch(
(lambda x: "finance" in x, RunnableLambda(lambda x: "금융 체인 실행")),
(lambda x: "nlp" in x, RunnableLambda(lambda x: "NLP 체인 실행")),
RunnableLambda(lambda x: "기본 체인 실행") # fallback
)
print(branch.invoke("finance keyword"))
마지막은 대화형 시스템에서 정말 중요한 블록입니다. 이 Runnable을 사용하면 이전 대화 내용을 히스토리(history)로 저장해두고, 그 맥락을 이어받아 다음 대화를 할 수 있습니다.
예를 들어,
사용자: "노트북 추천 해줘."
모델: "게임용이 필요한가요, 아니면 코딩을 하기 위해 필요한가요?"
사용자: "코딩용"
모델: "그럼 가볍고 키보드 타건감이 좋은 모델을 추천해드릴게요"
이렇게 앞에서 했던 대화를 기억하고 이어서 답변할 수 있는 것이 바로 RunnableWithMessageHistory덕분입니다.
Runnable은 기본적으로 입력 → 처리 → 출력 구조인데, 실행하는 방법이 다양합니다. 상황에 따라 적절히 골라 쓰면 됩니다.
1. invoke
가장 기본적인 실행 방식입니다. 입력 하나를 동기적으로 처리합니다.
result = chain.invoke("딥러닝이 뭐야?")
print(result)
2. ainvoke
invoke의 비동기 버전입니다. asyncio같은 환경에서 동시에 여러 요청을 처리할 때 유용합니다.
import asyncio
result = await chain.ainvoke("딥러닝이 뭐야?")
print(result)
3. batch
여러 입력을 한꺼번에 넣고 병렬 처리합니다. 리스트를 넣으면 리스트 결과가 나옵니다.
queries = ["딥러닝이 뭐야?", "NLP가 뭐야?", "금융 리스크란?"]
results = chain.batch(queries)
print(results)
4. abatch
batch의 비동기 버전입니다. 비동기 서버 환경에서 동시에 수십 개 요청을 처리할 때 사용합니다.
queries = ["딥러닝이 뭐야?", "NLP가 뭐야?", "금융 리스크란?"]
results = await chain.abatch(queries)
print(results)
5. stream
체인 실행 결과를 토큰 단위로 스트리밍 출력합니다. 즉, 답변이 다 생성될 때까지 기다리지 않고, 하나씩 실시간으로 볼 수 있어요.
for chunk in chain.stream("딥러닝이 뭐야?"):
print(chunk, end="")
6. astream
stream의 비동기 버전입니다. 채팅 애플리케이션에서 토큰 단위로 실시간 출력할 때 자주 씁니다.
async for chunk in chain.astream("딥러닝이 뭐야?"):
print(chunk, end="")
• invoke: 단일 입력 동기 실행
• ainvoke: 단일 입력 비동기 실행
• batch: 여러 입력 동기 실행
• abatch: 여러 입력 비동기 실행
• stream: 스트리밍 동기 실행
• astream: 스트리밍 비동기 실행
👉 결국 상황에 따라 "단일/여러 개" + "동기/비동기/스트리밍" 조합으로 고르면 됩니다.
오늘은 LCEL을 한강 라면 기계 비유로 풀어보고, Runnable의 개념부터 실행 방식, 그리고 주요 블록들까지 정리해봤습니다.
요약하자면..
👉 결국 LCEL을 쓰면 복잡한 구현을 단순한 흐름 설계로 바꿀 수 있다는 것이 핵심입니다.
다음 글에서는 실제로 LCEL을 활용해서 RAG 파이프라인을 직접 구축하는 실습을 다뤄볼 예정입니다. 실제 코드와 실행 결과를 보면서, 오늘 배운 내용이 어떻게 쓰이는지 확인해보는 시간을 가져봅시다.