IT기술/랭체인 (langchain)

LangChain Expression Language(LCEL) 완벽 가이드: 선언적 AI 워크플로우 구성의 혁신

후스파 2025. 7. 9. 11:42
반응형

LangChain Expression Language(LCEL)은 복잡한 AI 워크플로우를 선언적 방식으로 구성할 수 있게 해주는 LangChain의 핵심 기능입니다.
LCEL을 통해 개발자는 간결한 코드로 다양한 컴포넌트를 유연하게 조합할 수 있으며, 프로덕션 환경까지 확장 가능한 시스템을 구축할 수 있습니다.


LCEL의 핵심 특징

선언적 프로그래밍 패러다임

LCEL은 '무엇을 할 것인가'에 집중하는 선언적 접근 방식을 채택합니다. 이는 '어떻게 할 것인가'에 집중하는 명령형 방식과 대조되며, LangChain이 실행 시점에 최적화를 수행할 수 있게 해줍니다.

  • 선언적 구문: 복잡한 로직을 직관적인 파이프(|) 연산자로 표현
  • 모듈성: 프롬프트, 모델, 출력 파서 등 컴포넌트의 재사용성 극대화
  • 비동기/병렬 처리: 단일 코드베이스로 동기/비동기/배치 처리 지원
  • 자동 최적화: 실행 시 내부적으로 성능 최적화 수행

Runnable 인터페이스

모든 LCEL 컴포넌트는 Runnable 인터페이스를 구현합니다. 이는 일관된 API를 제공하여 다양한 컴포넌트를 자유롭게 조합할 수 있게 해줍니다.

# Runnable 인터페이스의 핵심 메서드들
runnable.invoke(input)          # 동기 실행
await runnable.ainvoke(input)   # 비동기 실행
runnable.batch([input1, input2]) # 배치 처리
runnable.stream(input)          # 스트리밍

기본 사용법

간단한 체인 구성

from langchain.prompts import ChatPromptTemplate
from langchain_openai import ChatOpenAI
from langchain_core.output_parsers import StrOutputParser

# 컴포넌트 정의
prompt = ChatPromptTemplate.from_template("{topic}에 대해 설명해주세요")
model = ChatOpenAI(model="gpt-4")
parser = StrOutputParser()

# 파이프 연산자로 컴포넌트 연결
chain = prompt | model | parser

# 실행
response = chain.invoke({"topic": "LCEL"})
print(response)

다양한 실행 방식

# 동기 실행
result = chain.invoke({"topic": "머신러닝"})

# 비동기 실행
import asyncio
async def async_example():
    result = await chain.ainvoke({"topic": "딥러닝"})
    return result

# 배치 처리
topics = [{"topic": "AI"}, {"topic": "ML"}, {"topic": "DL"}]
results = chain.batch(topics)

# 스트리밍
for chunk in chain.stream({"topic": "자연어처리"}):
    print(chunk, end="", flush=True)

복잡한 체인 구성

from langchain_core.runnables import RunnablePassthrough

# 다단계 처리 체인
def format_context(docs):
    return "\n\n".join([doc.page_content for doc in docs])

# RAG 체인 구성
rag_chain = (
    {
        "context": retriever | format_context,
        "question": RunnablePassthrough()
    }
    | prompt
    | model
    | parser
)

# 실행
response = rag_chain.invoke("LCEL의 장점은 무엇인가요?")

고급 기능 구현

병렬 처리: RunnableParallel 사용

RunnableParallel을 사용하면 여러 작업을 동시에 실행할 수 있습니다.

from langchain_core.runnables import RunnableParallel

# 여러 프롬프트 템플릿 정의
joke_prompt = ChatPromptTemplate.from_template("{topic}에 대한 재미있는 농담을 만들어주세요")
poem_prompt = ChatPromptTemplate.from_template("{topic}에 대한 2줄 시를 써주세요")
fact_prompt = ChatPromptTemplate.from_template("{topic}에 대한 흥미로운 사실을 알려주세요")

# 병렬 체인 구성
parallel_chain = RunnableParallel({
    "joke": joke_prompt | model | parser,
    "poem": poem_prompt | model | parser,
    "fact": fact_prompt | model | parser
})

# 실행 - 세 작업이 동시에 처리됨
result = parallel_chain.invoke({"topic": "인공지능"})
print("농담:", result["joke"])
print("시:", result["poem"])
print("사실:", result["fact"])

조건부 분기: RunnableBranch 적용

from langchain_core.runnables import RunnableBranch

# 다양한 주제별 전문 체인
tech_prompt = ChatPromptTemplate.from_template("기술적 관점에서 {topic}을 설명해주세요")
business_prompt = ChatPromptTemplate.from_template("비즈니스 관점에서 {topic}을 설명해주세요")
general_prompt = ChatPromptTemplate.from_template("{topic}에 대해 일반적으로 설명해주세요")

tech_chain = tech_prompt | model | parser
business_chain = business_prompt | model | parser
general_chain = general_prompt | model | parser

# 조건부 분기 체인
branch_chain = RunnableBranch(
    (lambda x: "기술" in x["topic"] or "프로그래밍" in x["topic"], tech_chain),
    (lambda x: "비즈니스" in x["topic"] or "경영" in x["topic"], business_chain),
    general_chain  # 기본 체인
)

# 테스트
print(branch_chain.invoke({"topic": "프로그래밍 언어"}))  # tech_chain 실행
print(branch_chain.invoke({"topic": "비즈니스 모델"}))   # business_chain 실행
print(branch_chain.invoke({"topic": "일반 상식"}))      # general_chain 실행

사용자 정의 함수 통합

from langchain_core.runnables import RunnableLambda

def custom_processor(text: str) -> str:
    """텍스트를 대문자로 변환하고 감탄표 추가"""
    return text.upper() + "!!!"

def extract_keywords(text: str) -> dict:
    """텍스트에서 키워드 추출 (간단한 예시)"""
    words = text.split()
    keywords = [word for word in words if len(word) > 5]
    return {"original": text, "keywords": keywords}

# 커스텀 함수를 체인에 통합
custom_chain = (
    prompt 
    | model 
    | parser 
    | RunnableLambda(custom_processor)
)

# 더 복잡한 처리 체인
analysis_chain = (
    prompt
    | model
    | parser
    | RunnableLambda(extract_keywords)
)

result = analysis_chain.invoke({"topic": "인공지능의 미래"})
print("원본:", result["original"])
print("키워드:", result["keywords"])

실제 적용 사례

RAG 시스템 구현

from langchain_community.vectorstores import FAISS
from langchain_openai import OpenAIEmbeddings
from langchain_community.document_loaders import TextLoader
from langchain.text_splitter import RecursiveCharacterTextSplitter

# 벡터 스토어 설정
embeddings = OpenAIEmbeddings()
text_splitter = RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=200)

# 문서 로드 및 벡터화
loader = TextLoader("documents.txt")
documents = loader.load()
texts = text_splitter.split_documents(documents)
vectorstore = FAISS.from_documents(texts, embeddings)
retriever = vectorstore.as_retriever(search_kwargs={"k": 3})

# RAG 프롬프트 템플릿
rag_prompt = ChatPromptTemplate.from_template("""
다음 컨텍스트를 바탕으로 질문에 답변해주세요:

컨텍스트:
{context}

질문: {question}

답변:
""")

def format_docs(docs):
    return "\n\n".join([doc.page_content for doc in docs])

# RAG 체인 구성
rag_chain = (
    {
        "context": retriever | format_docs,
        "question": RunnablePassthrough()
    }
    | rag_prompt
    | model
    | parser
)

# 실행
response = rag_chain.invoke("LCEL의 주요 장점은 무엇인가요?")
print(response)

다단계 워크플로우 구현

from langchain.chains.summarize import load_summarize_chain

# 문서 처리 파이프라인: 로드 => 요약 => 감정 분석 => 보고서 생성
def load_document(file_path: str):
    loader = TextLoader(file_path)
    return loader.load()

def summarize_text(docs):
    summarize_chain = load_summarize_chain(model, chain_type="stuff")
    return summarize_chain.run(docs)

def analyze_sentiment(text: str):
    sentiment_prompt = ChatPromptTemplate.from_template(
        "다음 텍스트의 감정을 분석해주세요 (긍정/부정/중립): {text}"
    )
    sentiment_chain = sentiment_prompt | model | parser
    return sentiment_chain.invoke({"text": text})

def generate_report(data: dict):
    report_prompt = ChatPromptTemplate.from_template("""
    다음 정보를 바탕으로 종합 보고서를 작성해주세요:

    요약: {summary}
    감정 분석: {sentiment}

    보고서:
    """)
    report_chain = report_prompt | model | parser
    return report_chain.invoke(data)

# 전체 워크플로우 체인
workflow_chain = (
    RunnableLambda(load_document)
    | RunnableLambda(summarize_text)
    | RunnableLambda(lambda summary: {
        "summary": summary,
        "sentiment": analyze_sentiment(summary)
    })
    | RunnableLambda(generate_report)
)

# 실행
report = workflow_chain.invoke("document.txt")
print(report)

에러 복구 메커니즘 구현

from langchain_core.runnables import RunnableRetry
from tenacity import retry, stop_after_attempt, wait_exponential

# 재시도 설정
retry_chain = (
    prompt 
    | model.with_retry(
        stop=stop_after_attempt(3),
        wait=wait_exponential(multiplier=1, min=4, max=10)
    ) 
    | parser
)

# 폴백 체인
def fallback_response(error):
    return "죄송합니다. 현재 서비스에 문제가 있습니다. 나중에 다시 시도해주세요."

fallback_chain = (
    retry_chain
    .with_fallbacks([RunnableLambda(fallback_response)])
)

# 실행
try:
    response = fallback_chain.invoke({"topic": "복잡한 주제"})
    print(response)
except Exception as e:
    print(f"최종 실패: {e}")

성능 최적화와 모니터링

스트리밍 최적화

from langchain.callbacks.streaming_stdout import StreamingStdOutCallbackHandler

# 스트리밍 콜백 설정
streaming_model = ChatOpenAI(
    model="gpt-4",
    streaming=True,
    callbacks=[StreamingStdOutCallbackHandler()]
)

streaming_chain = prompt | streaming_model | parser

# 토큰별 스트리밍
def stream_response(topic):
    for chunk in streaming_chain.stream({"topic": topic}):
        yield chunk

# 사용
for token in stream_response("스트리밍의 장점"):
    print(token, end="", flush=True)

배치 처리 최적화

import asyncio
from typing import List

async def process_batch_async(topics: List[str]):
    """비동기 배치 처리"""
    tasks = [chain.ainvoke({"topic": topic}) for topic in topics]
    results = await asyncio.gather(*tasks)
    return results

# 대용량 배치 처리
topics = [f"주제 {i}" for i in range(100)]
batch_size = 10

async def process_large_batch(topics: List[str], batch_size: int):
    results = []
    for i in range(0, len(topics), batch_size):
        batch = topics[i:i + batch_size]
        batch_results = await process_batch_async(batch)
        results.extend(batch_results)
        await asyncio.sleep(0.1)  # API 레이트 리밋 고려
    return results

# 실행
results = asyncio.run(process_large_batch(topics, batch_size))

LangSmith 통합 모니터링

from langchain.callbacks.tracers import LangChainTracer
from langsmith import Client

# LangSmith 클라이언트 설정
client = Client()
tracer = LangChainTracer(project_name="LCEL-Production")

# 모니터링이 포함된 체인
monitored_chain = (
    prompt 
    | model.with_config(callbacks=[tracer])
    | parser
)

# 메트릭 수집
def collect_metrics(chain_result, execution_time):
    metrics = {
        "execution_time": execution_time,
        "token_count": len(chain_result.split()),
        "success": True
    }
    # 메트릭을 모니터링 시스템에 전송
    return metrics

# 실행 및 모니터링
import time
start_time = time.time()
result = monitored_chain.invoke({"topic": "모니터링"})
execution_time = time.time() - start_time

metrics = collect_metrics(result, execution_time)
print(f"실행 시간: {metrics['execution_time']:.2f}초")
print(f"토큰 수: {metrics['token_count']}")

장점과 활용 전략

개발 생산성 향상

  • 빠른 프로토타이핑: 5분 내 기본 체인 구성 가능
  • 코드 재사용성: 모듈화된 컴포넌트의 자유로운 조합
  • 직관적 구문: 파이프 연산자로 데이터 흐름을 명확히 표현

운영 효율성

  • 유지보수성: 모듈화로 부분 수정이 전체 시스템에 미치는 영향 최소화
  • 확장성: 커스텀 컴포넌트 쉽게 추가
  • 프로덕션 준비: 로깅, 모니터링, 배포 기능 내장

성능 최적화

# 성능 비교 예제
import time

# 기존 방식 (순차 처리)
def traditional_approach(topics):
    results = []
    for topic in topics:
        result = chain.invoke({"topic": topic})
        results.append(result)
    return results

# LCEL 배치 처리
def lcel_approach(topics):
    inputs = [{"topic": topic} for topic in topics]
    return chain.batch(inputs)

# 성능 측정
topics = ["AI", "ML", "DL", "NLP", "CV"]

start = time.time()
traditional_results = traditional_approach(topics)
traditional_time = time.time() - start

start = time.time()
lcel_results = lcel_approach(topics)
lcel_time = time.time() - start

print(f"기존 방식: {traditional_time:.2f}초")
print(f"LCEL 방식: {lcel_time:.2f}초")
print(f"성능 향상: {traditional_time/lcel_time:.2f}배")

마무리

LCEL은 LangChain 생태계의 게임 체인저로, 기존 명령형 코드 대비 60% 이상 코드량 감소 효과를 보입니다.
복잡한 AI 파이프라인을 선언적 방식으로 관리하며 지속적인 진화를 통해 AI 애플리케이션 개발 표준으로 자리매김하고 있습니다.
핵심 포인트:

  • 선언적 프로그래밍: '무엇을' 할지 명시하여 LangChain이 '어떻게' 최적화
  • 파이프 연산자(|): 직관적인 데이터 흐름 표현
  • Runnable 인터페이스: 일관된 API로 모든 컴포넌트 통합
  • 자동 최적화: 병렬 처리, 배치 처리, 스트리밍 자동 지원
  • 프로덕션 준비: 모니터링, 에러 처리, 확장성 내장

LCEL을 마스터하면 복잡한 AI 워크플로우를 간결하고 효율적으로 구현할 수 있으며, 프로토타입부터 프로덕션까지 일관된 코드베이스로 관리할 수 있습니다.

반응형