러너블 이어 붙이기
러너블 이어 붙이기 (How to chain runnables)
LangChain에서 러너블(runnable)은 파이프라인을 구성하는 단위예요. 그런데 실제 제품을 만들다 보면 하나의 러너블만 쓰는 경우는 드물죠. 이번 가이드에서는 여러 러너블을 순서대로 이어 붙여 하나의 체인으로 만드는 법을 살펴볼게요.
출처: 공식문서
사전 준비
이 가이드를 따라가려면 다음 개념에 익숙해져 있으면 좋아요.
- LangChain Expression Language (LCEL)
- 프롬프트 템플릿 (Prompt templates)
- 채팅 모델 (Chat models)
- 출력 파서 (Output parser)
파이프 연산자 | 로 이어 붙이기
LangChain Expression Language(LCEL)의 핵심 특징 중 하나가 어떤 두 러너블이든 서로 "이어 붙일(chained)" 수 있다는 점이에요. 앞 러너블의 .invoke() 호출 결과가 그대로 다음 러너블의 입력으로 전달됩니다. 이때 파이프 연산자(|)를 쓰거나, 같은 동작을 하는 더 명시적인 .pipe() 메서드를 쓸 수 있어요.
이렇게 만들어진 결과물인 RunnableSequence는 그 자체로 또 하나의 러너블이에요. 그래서 다른 러너블과 똑같이 invoke, stream, 추가 체이닝이 모두 가능하죠. 이 방식으로 체이닝하면 효율적인 스트리밍(결과가 나오는 대로 바로 흘려보냄)과 LangSmith 같은 도구를 통한 디버깅·추적 같은 장점을 얻을 수 있어요.
LangChain에서 흔한 패턴을 예로 들어볼게요. 프롬프트 템플릿으로 입력을 포맷해서 채팅 모델에 넘기고, 마지막에 출력 파서를 거쳐 채팅 메시지 출력을 문자열로 바꾸는 흐름이에요.
먼저 채팅 모델 하나를 골라요.
pip install -qU "langchain[google-genai]"
import getpass
import os
if not os.environ.get("GOOGLE_API_KEY"):
os.environ["GOOGLE_API_KEY"] = getpass.getpass("Enter API key for Google Gemini: ")
from langchain.chat_models import init_chat_model
model = init_chat_model("gemini-2.5-flash", model_provider="google_genai")
이제 프롬프트 템플릿과 출력 파서를 준비하고, 셋을 |로 이어 붙여요.
from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import ChatPromptTemplate
prompt = ChatPromptTemplate.from_template("tell me a joke about {topic}")
chain = prompt | model | StrOutputParser()
API Reference: StrOutputParser | ChatPromptTemplate
프롬프트와 모델은 둘 다 러너블이고, 프롬프트 호출의 출력 타입이 채팅 모델의 입력 타입과 같아서 서로 이어 붙일 수 있어요. 만들어진 시퀀스는 다른 러너블처럼 그냥 invoke 하면 됩니다.
chain.invoke({"topic": "bears"})
"Why don't bears wear shoes?\n\nBecause they prefer to go bear-foot!"
타입 강제 변환 (Coercion)
이 체인에 다른 러너블을 더 붙여서 또 다른 체인을 만들 수도 있어요. 다만 체인 구성 요소들이 요구하는 입출력에 따라 다른 종류의 러너블로 입출력을 포맷해야 할 수 있어요.
예를 들어, 농담을 만드는 체인에 "이 농담이 재밌는지" 평가하는 체인을 이어 붙인다고 해볼게요. 이때 다음 체인에 넘기는 입력을 어떻게 포맷하는지 신경 써야 해요. 아래 예시에서 체인 안의 dict는 자동으로 파싱되어 RunnableParallel로 변환되는데, 이는 모든 값을 병렬로 실행하고 결과를 dict로 돌려줘요.
이 형태가 마침 다음 프롬프트 템플릿이 기대하는 입력 포맷과 같아요. 직접 확인해 볼게요.
from langchain_core.output_parsers import StrOutputParser
analysis_prompt = ChatPromptTemplate.from_template("is this a funny joke? {joke}")
composed_chain = {"joke": chain} | analysis_prompt | model | StrOutputParser()
composed_chain.invoke({"topic": "bears"})
API Reference: StrOutputParser
'Yes, that\'s a funny joke! It\'s a classic pun that plays on the homophone pair "bare-foot" and "bear-foot." The humor comes from:\n\n1. The wordplay between "barefoot" (not wearing shoes) and "bear-foot" (the foot of a bear)\n2. The logical connection to the setup (bears don\'t wear shoes)\n3. It\'s family-friendly and accessible\n4. It\'s a simple, clean pun that creates an unexpected but satisfying punchline\n\nIt\'s the kind of joke that might make you groan and smile at the same time - what people often call a "dad joke."'
함수도 러너블로 자동 변환되기 때문에 체인에 직접 커스텀 로직을 추가할 수도 있어요. 아래 체인은 앞선 예시와 논리적으로 같은 흐름을 만들어요.
composed_chain_with_lambda = (
chain
| (lambda input: {"joke": input})
| analysis_prompt
| model
| StrOutputParser()
)
composed_chain_with_lambda.invoke({"topic": "beets"})
'Yes, that\'s a cute and funny joke! It works well because:\n\n1. It plays on the double meaning of "roots" - both the literal roots of the beet plant and the metaphorical sense of knowing one\'s origins or foundation\n2. It\'s a simple, clean pun that doesn\'t rely on offensive content\n3. It has a satisfying logical connection (beets are root vegetables)\n\nIt\'s the kind of wholesome food pun that might make people groan a little but also smile. Perfect for sharing in casual conversation or with kids!'
다만 이렇게 함수를 쓰면 스트리밍 같은 동작에 지장이 생길 수 있다는 점을 기억해 두세요. 자세한 내용은 러너블로 함수 변환하기 가이드를 참고하세요.
.pipe() 메서드
같은 시퀀스를 .pipe() 메서드로도 만들 수 있어요. 직접 보면 이렇게 생겼습니다.
from langchain_core.runnables import RunnableParallel
composed_chain_with_pipe = (
RunnableParallel({"joke": chain})
.pipe(analysis_prompt)
.pipe(model)
.pipe(StrOutputParser())
)
composed_chain_with_pipe.invoke({"topic": "battlestar galactica"})
API Reference: RunnableParallel
"This joke is moderately funny! It plays on Battlestar Galactica lore where Cylons are robots with 12 different models trying to infiltrate human society. The humor comes from the idea of a Cylon accidentally revealing their non-human nature through a pickup line that references their artificial origins. It's a decent nerd-culture joke that would land well with fans of the show, though someone unfamiliar with Battlestar Galactica might not get the reference. The punchline effectively highlights the contradiction in a Cylon trying to blend in while simultaneously revealing their true identity."
여러 러너블을 한 번에 넘기는 축약형도 지원해요.
composed_chain_with_pipe = RunnableParallel({"joke": chain}).pipe(
analysis_prompt, model, StrOutputParser()
)
관련 가이드
- 스트리밍 (Streaming): 체인의 스트리밍 동작을 이해하려면 스트리밍 가이드를 확인해 보세요.