LCEL 쉽게 이해하기 (feat. 한강 라면)

Rose·2025년 9월 18일

RAG Note

목록 보기
1/2

여러분 혹시 한강라면 좋아하시나요? 저는 며칠 전에 한강에 갔다가 라면을 먹었는데, 대기 줄이 정말 길더라구요.

LCEL에 대해 공부하다가 이게 한강라면기계와 같은 것이 아닌가하는 생각이 들었습니다.(저도 왜 이런 생각을 했는지 모르겠네요😂 아마 배고파서 그런것 같아요.) 집에서 라면을 끓일 땐 물 계량, 불 세기, 면 넣는 타이밍까지 전부 직접 챙겨야 합니다. 그런데 한강 라면 기계는 그 과정을 다 자동화해주잖아요?

LCEL도 마치 그 한강 라면 기계와 비슷합니다.

우리가 "무엇을 할지(What)"만 선언하면 "어떻게 할지(How)"는 LangChain이 알아서 처리해줍니다. 덕분에 우리는 복잡한 세부 구현에 매달리지 않고, 큰 흐름만 설계하면 되는거죠.

이번 글에서는 위 특징을 가능하게해주는 핵심개념인 LCEL에 대해 정리해보겠습니다.

LCEL(LangChain Expression Language)이란?

공식문서에는 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이 자동으로 처리한다는 겁니다.

LCEL의 주요 특징

  • 모듈화(Modularity): 모든 구성 요소가 Runnable 단위로 통일되어 있어 조합이 쉽습니다.
  • 일관된 실행 모델: 동기/비동기, 단일 입력/배치 입력, 스트리밍까지 모두 동일한 인터페이스로 실행 가능합니다.
  • 확장성(Extensibility): 조건 분기, 사용자 정의 함수, 메모리 등 다양한 기능을 유연하게 추가할 수 있습니다.

이 중에서 가장 핵심이 되는 키워드가 바로 "Runnable"입니다. LCEL은 모든 요소를 Runnable단위로 추상화하기 때문에, Runnable을 이해하는 것이 곧 LCEL을 이해하는 출발점이라고 할 수 있습니다.

Runnable

개념

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의 공통 인터페이스 안에서 쉽게 다룰 수 있다는 것이 장점입니다.

주요 Runnable 정리

이번에는 LCEL에서 자주 쓰이는 Runnable들을 정리해보겠습니다. 처음에는 "이게 뭐야?"싶었는데, 알고보면 되게 단순한 블록들입니다.

RunnablePassthrough

가장 간단한 Runnable입니다. 입력을 그대로 통과시켜줍니다. 아무 일도 안 하지만, 디버깅이나 중간 데이터를 확인할 때 은근 유용합니다.

from langchain_core.runnables import RunnablePassthrough

passthrough = RunnablePassthrough()
print(passthrough.invoke("Hello"))  
# 출력: "Hello"

RunnableLambda

python함수를 그대로 Runnable로 감싸는 기능입니다. 이를 통해 개발자는 자신만의 함수를 정의하고, 해당 함수를 RunnableLambda를 사용하여 실행할 수 있습니다.

from langchain_core.runnables import RunnableLambda

to_upper = RunnableLambda(lambda x: x.upper())
print(to_upper.invoke("hello"))  
# 출력: "HELLO"

예를 들어, 데이터 전처리, 계산, 또는 외부 API와의 상호작용과 같은 작업을 수행하는 함수를 정의하고 실행할 수 있습니다.

RunnableBranch

조건에 따라 실행 경로를 분기하는 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"))  

RunnableWithMessageHistory

마지막은 대화형 시스템에서 정말 중요한 블록입니다. 이 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의 개념부터 실행 방식, 그리고 주요 블록들까지 정리해봤습니다.

요약하자면..

  • Runnable은 LCEL의 가장 작은 실행 단위(입력->처리->출력)
  • LCEL은 이 Runnable들을 레고 블록처럼 조합해서 체인을 만드는 언어
  • 병렬 처리, 비동기, 스트리밍, 메모리 같은 기능을 쉽게 적용할 수 있다는 점이 강점

👉 결국 LCEL을 쓰면 복잡한 구현을 단순한 흐름 설계로 바꿀 수 있다는 것이 핵심입니다.

다음 글에서는 실제로 LCEL을 활용해서 RAG 파이프라인을 직접 구축하는 실습을 다뤄볼 예정입니다. 실제 코드와 실행 결과를 보면서, 오늘 배운 내용이 어떻게 쓰이는지 확인해보는 시간을 가져봅시다.

참고자료

profile
개발자를 꿈꾸며, 하루하루 쌓아가는 로제의 지식 아카이브입니다.

0개의 댓글