LangChain Expression Language (LCEL)과 최신 체인 구성 방법 (LangChain v0.3.x)
1. LCEL 개념 및 체인 구성의 변화 설명
LangChain Expression Language(LCEL)는 LangChain에 새롭게 도입된 체인 구성 방식입니다. LCEL은 체인을 **선언적(Declarative)**으로 구성할 수 있는 문법/DSL을 제공하며, 여러 단계의 LLM 연쇄 호출을 간결하게 표현해 줍니다. 2023년 8월 LCEL이 공개되면서, LangChain은 프로토타입(POC) 수준의 간단한 체인에서 복잡한 애플리케이션의 프로덕션 수준 체인까지 아우를 수 있게 되었습니다. 기존에 체인을 구성하던 방식과 비교하여 LCEL 도입으로 다음과 같은 변화와 이점이 있습니다:
- 간결한 체인 표현: LCEL에서는 | 연산자를 활용하여 프롬프트, LLM, 출력 파서 등을 파이프라인 형태로 연결합니다. 이로써 코드가 직관적으로 변하고, 체인의 단계가 한 줄 흐름으로 표현됩니다.
- 기존 클래스 의존도 감소: 예전에는 LLMChain, SequentialChain 등 클래스 기반 체인을 사용하여야 했고, 중간 결과를 수동으로 다음 체인 입력에 넣는 등의 작업이 필요했습니다. LCEL에서는 이러한 보일러플레이트를 줄이고, Runnable이라는 공통 인터페이스를 통해 체인을 구성합니다.
- 병렬 처리 및 스트리밍 용이: LCEL로 구성된 체인은 자동으로 병렬 실행 최적화와 스트리밍 출력 등을 지원합니다. 예를 들어, 여러 입력에 대한 처리를 병렬로 수행하거나, LLM 응답을 토큰이 생성되는 대로 스트리밍 출력하는 기능을 기본으로 제공합니다.
- 유연한 확장성과 통합: 모든 LCEL 체인은 내부적으로 Runnable 인터페이스를 구현하므로, **단일한 실행 방식 (.invoke, .stream 등)**으로 호출할 수 있습니다. 또한 LCEL 체인은 LangChain에서 새로 도입되는 LangGraph 등의 상위 개념과도 쉽게 연계되며, 복잡한 분기나 루프, 다중 에이전트 구성 시 개별 노드에서 여전히 LCEL을 활용할 수 있습니다.
LCEL의 핵심 개념은 **Runnable과 체인 조합(Composition)**입니다. Runnable은 한 단계의 작업 단위를 표현하는 인터페이스로, 프롬프트 템플릿, LLM, 파서 등 모든 요소가 Runnable로 취급됩니다. 여러 Runnable을 조합하여 하나의 체인 Runnable을 만들 수 있으며, 주요 조합 방식으로 순차 실행을 위한 RunnableSequence(혹은 | 연산자)와 동시 실행을 위한 RunnableParallel 등이 있습니다. LCEL에서는 이러한 조합을 파이프라인 스타일 문법으로 제공하여, 마치 Unix 파이프처럼 프롬프트 | LLM | 파서 형태로 체인을 표현합니다.
※ 참고: LCEL 도입 이후 LangChain에서 LLMChain이나 SimpleSequentialChain 같은 기존 체인 클래스도 여전히 사용 가능하지만, Deprecated 예정이거나 권장되지 않습니다. 본 수업 자료에서는 모든 프롬프트를 한국어로 작성하고, LCEL 스타일로 최신 체인을 구성하며 더 이상 권장되지 않는 클래스는 사용하지 않습니다.
환경 설정 (OpenAI API 및 LangChain)
실습에 앞서, OpenAI API 키 등의 환경변수를 설정합니다. .env 파일에 OpenAI API 키 등을 저장하고 python-dotenv로 불러오겠습니다. 또한 langchain과 OpenAI API 연동을 위한 패키지를 설치/불러옵니다. (Jupyter Notebook에서 실행한다면 최초 1회만 설정하면 됩니다.)
!pip install langchain openai python-dotenv # 필요한 패키지 설치 (인터넷 연결 필요)
# .env 파일에 OPENAI_API_KEY=<YOUR_KEY> 형식으로 API 키를 넣어 두었다고 가정합니다.
from dotenv import load_dotenv
load_dotenv() # .env 파일의 환경변수를 로드합니다.
import os
openai_api_key = os.getenv('OPENAI_API_KEY')
# OpenAI API 키가 정상적으로 불러와졌는지 확인 (키 문자열 일부 출력)
print(openai_api_key[:8] + "****") if openai_api_key else print("API Key not found!")
이제 LangChain에서 OpenAI GPT-4 (또는 GPT-3.5-turbo 등 사용 가능)를 불러오겠습니다. LCEL 체인은 LLM 모델을 Runnable로 사용하기 때문에, LangChain의 ChatOpenAI 객체를 생성해 두겠습니다:
from langchain_openai import ChatOpenAI
# OpenAI GPT-4 모델 초기화 (temperature 등의 파라미터 조정 가능)
llm = ChatOpenAI(model="gpt-4", temperature=0.7)
위에서 생성한 llm 객체는 이후 실습에서 공통으로 사용됩니다.
2. 단일 체인 실습: 프롬프트 → LLM → 출력 파서 (상품 설명 생성)
먼저 가장 단순한 형태의 체인을 구성해보겠습니다. 단일 체인은 하나의 프롬프트를 LLM에 전달하고, 결과를 받아 **출력 파서(Output Parser)**로 후처리하는 구조입니다. 이번 실습에서는 상품 이름을 입력하면 해당 상품의 매력적인 마케팅 설명문을 생성하는 체인을 만들어 봅니다.
개념 설명: LCEL 기반 체인은 프롬프트 → LLM → 출력 파서 순으로 연결합니다. 프롬프트 템플릿은 입력 변수(예: 상품명)를 받아 LLM에 전달할 프롬프트 문자열을 생성합니다. LLM은 이 프롬프트에 따라 응답을 생성하고, 출력 파서는 LLM의 응답(String)을 필요에 따라 가공하거나 원하는 타입으로 변환합니다. 여기서는 단순히 문자열 출력을 받을 것이므로 기본적인 StrOutputParser를 사용합니다 (LLM 출력 그대로 문자열로 반환).
- 프롬프트 템플릿 정의: LangChain의 PromptTemplate을 사용하여 한국어 프롬프트를 정의합니다. {product} 자리에는 상품명이 들어갈 예정입니다.
- LLM 모델: 앞서 생성한 llm (ChatOpenAI GPT-4)을 사용합니다.
- 출력 파서: StrOutputParser는 LLM의 출력(string)을 가공하지 않고 그대로 반환하는 기본 파서입니다. (추후 구조화된 출력이 필요할 경우 별도 파서를 사용할 수 있습니다.)
이제 코드 셀을 통해 체인을 구성하고 실행 결과를 확인해보겠습니다.
from langchain.prompts import PromptTemplate
from langchain_core.output_parsers import StrOutputParser
# 1. 프롬프트 템플릿 정의 (한국어 프롬프트)
product_prompt = PromptTemplate.from_template(
"제품 이름: {product}\n" +
"이 제품의 특징과 장점을 매력적인 한 단락으로 설명해주세요."
)
# 2. 출력 파서 정의 (문자열 출력 파서)
output_parser = StrOutputParser()
# 3. LCEL 체인 구성: 프롬프트 → LLM → 출력파서
product_chain = product_prompt | llm | output_parser
# 4. 체인 실행 (.invoke) - 예시 입력으로 실행
example_input = {"product": "무선 블루투스 이어폰"} # 예시로 "무선 블루투스 이어폰" 설명 생성
result = product_chain.invoke(example_input)
print(result)
위 코드에서 product_prompt | llm | output_parser 부분이 LCEL의 파이프라인 체인 표현입니다. product_chain.invoke(...)를 호출하면, 내부적으로 다음과 같은 순서로 실행됩니다:
- product_prompt가 example_input에서 {product} 변수를 채워 프롬프트 문장을 생성
- 생성된 프롬프트를 llm(GPT-4 모델)에 전달하여 응답 생성
- 모델 응답 텍스트를 output_parser를 통해 후처리 (여기서는 그대로 문자열 반환)
실행 결과 예시:
제품 이름: 무선 블루투스 이어폰
이 제품의 특징과 장점을 매력적인 한 단락으로 설명해주세요.
결과:
완전 무선 블루투스 이어폰은 선에서 해방된 자유로움을 제공합니다. 선명한 고음질 사운드와 편안한 인체공학적 디자인으로 오랜 시간 착용해도 귀에 무리가 가지 않습니다. 최신 블루투스 기술로 안정적인 연결을 보장하며, 충전 케이스로 언제 어디서나 손쉽게 충전이 가능합니다. 출퇴근이나 운동 중에도 음악에 몰입할 수 있게 해주는 이상적인 제품입니다.
위와 같이, 입력한 상품명 **"무선 블루투스 이어폰"**에 대해 GPT-4 모델이 한 단락의 제품 설명을 생성했습니다. StrOutputParser를 사용했으므로 결과가 문자열로 반환되어 print로 출력되었습니다.
노트: .invoke() 메서드는 체인을 동기적으로 실행하여 결과를 반환합니다. LangChain LCEL에서는 이외에도 .batch() (여러 입력 동시 처리), .stream() (스트리밍 응답) 등을 지원하지만, 본 수업에서는 주로 .invoke()를 사용합니다. 스트리밍 관련 내용은 필요할 경우 추가로 다룰 수 있습니다.
3. 다중 체인 연결 실습: 체인 합성 및 Runnable 병합 (이메일 생성)
이번에는 둘 이상의 LLM 호출을 연쇄하여 복합 작업을 수행하는 체인을 만들어 보겠습니다. 시나리오는 "주어진 상황에 대한 이메일 작성"으로, 먼저 이메일 제목을 생성한 뒤, 이어서 그 제목을 활용해 이메일 본문을 작성하는 예제를 실습합니다. 이를 통해 **체인 합성(chain composition)**과 Runnable 병합 개념을 이해합니다.
체인 구성 전략: 하나의 체인 내에 두 단계의 LLM 호출을 순차로 결합합니다. 첫 번째 단계에서는 입력된 내용을 바탕으로 이메일 제목을 만들고, 두 번째 단계에서는 앞에서 만든 제목을 사용하여 이메일 본문을 생성합니다. LCEL을 이용하면 이러한 연쇄 작업을 한 번의 .invoke() 호출로 처리되는 하나의 체인으로 합성할 수 있습니다.
이를 위해 중간 결과(이메일 제목)를 다음 단계로 전달하는 방법이 필요합니다. LCEL에서는 Runnable의 출력 값을 딕셔너리로 변환하거나 그대로 통과시키는 RunnablePassthrough 등을 사용해 중간 데이터를 구조화할 수 있습니다. 구체적으로, 이전 단계 출력 값을 특정 키로 묶어 딕셔너리로 넘기면, 다음 프롬프트 템플릿에서 해당 키를 변수로 사용할 수 있습니다.
구현 계획:
- 프롬프트1: 사용자로부터 받은 이메일 요청 내용을 입력 받아, "이메일 제목"을 한 문장 생성하는 명령.
- LLM 호출 -> 이메일 제목 출력 (예: "프로젝트 진행 상황 회의 일정 안내")
- 중간 출력 변환: 생성된 제목 문자열을 {subject} 키로 갖는 딕셔너리로 변환 (예: {"subject": "<<생성된 제목>>"})
- 프롬프트2: {subject} 변수를 받아, 해당 제목을 가진 이메일 본문 내용을 정중한 어투로 작성하는 명령. (필요하면 원문 내용을 함께 참고하도록 프롬프트에 포함 가능)
- LLM 호출 -> 최종 이메일 본문 출력.
이 두 단계를 LCEL 체인으로 연결해보겠습니다.
from langchain_core.runnables import RunnablePassthrough
# 1. 이메일 제목 생성용 프롬프트 정의
subject_prompt = PromptTemplate.from_template(
"다음 요청 내용을 바탕으로 이메일 제목을 지어주세요:\n{content}"
)
# 2. 이메일 본문 생성용 프롬프트 정의
body_prompt = PromptTemplate.from_template(
"위에서 생성된 제목을 활용하여, 팀에게 보내는 정중한 이메일 본문을 작성해주세요.\n" +
"제목: {subject}\n본문:"
)
# 3. 두 프롬프트를 LLM과 결합한 체인 구성
email_chain = (
subject_prompt # 1단계: 제목 프롬프트
| llm # LLM 호출 -> 제목 생성
| {"subject": RunnablePassthrough()} # 출력된 제목을 subject 키로 매핑
| body_prompt # 2단계: 본문 프롬프트 (subject를 입력으로 받음)
| llm # LLM 호출 -> 본문 생성
)
# 4. 체인 실행 예시
email_request = {
"content": "다음주에 프로젝트 진행 상황을 논의하기 위해 팀 회의를 요청드리는 이메일"
}
final_email = email_chain.invoke(email_request)
print(final_email)
위 체인은 subject_prompt -> llm을 통해 이메일 제목을 얻은 후, {"subject": RunnablePassthrough()}를 이용해 해당 출력 값을 딕셔너리의 "subject" 값으로 변환합니다. 그러면 이어지는 body_prompt는 자신이 필요로 하는 {subject} 변수를 이 딕셔너리에서 받아 사용할 수 있습니다. 최종적으로 두 번째 llm 호출에서 이메일 본문을 생성하고 체인은 끝납니다.
실행 결과 예시: (모델이 생성한 이메일 제목과 본문 중 본문 부분이 출력됩니다)
안녕하세요 팀원 여러분,
다가오는 다음 주에 프로젝트 진행 상황을 공유하고 논의하기 위해 회의를 개최하고자 합니다.
모두의 일정에 따라 **7월 15일(월) 오후 2시**에 Zoom을 통해 회의를 열 예정입니다. 이 회의에서는 현재까지의 진행 상황을 점검하고, 향후 계획과 과제를 함께 논의하려 합니다.
회의에 참석 가능한지 확인 부탁드리며, 참석이 어렵다면 미리 알려주세요. 원활한 회의 진행을 위해 회의 전에 각자 맡은 부분의 진행 상태를 간략히 정리해 주시면 감사하겠습니다.
그럼 회의에서 뵙겠습니다. 좋은 하루 보내세요!
감사합니다.
위 결과는 예시로, **"프로젝트 진행 상황 회의 일정 안내"**라는 제목에 대한 이메일 본문이 생성된 모습입니다. (실제 실행 시 모델 출력에 따라 내용은 달라질 수 있습니다.)
체인 합성의 장점: 이렇게 LCEL를 사용하면, 단일 .invoke() 호출로 여러 단계의 LLM 처리를 순차 실행할 수 있습니다. 중간에 딕셔너리로 출력 값을 병합함으로써, 이전 단계의 결과를 이후 단계 프롬프트에 **자연스럽게 주입(변수로 전달)**할 수 있습니다. RunnablePassthrough는 입력 값을 그대로 통과시키는 Runnable로, 위에서는 첫 번째 LLM의 문자열 출력을 받아 { "subject": <제목> } 형태로 래핑하는 역할을 했습니다.
만약 여러 개의 값을 병렬로 생성하거나 결합해야 한다면 RunnableParallel 등을 활용해 병렬 실행 후 결과를 딕셔너리로 모을 수도 있지만, 이 예제에서는 순차 흐름이므로 RunnableSequence (파이프 연산)만으로 충분합니다.
추가 팁: LCEL 체인은 .invoke() 외에도 .invoke_batch()를 통해 동일 체인에 여러 입력을 한꺼번에 실행할 수 있고, | 연산 외에 + 연산(여러 체인의 병합) 등도 지원합니다. 또한 chain.invoke 대신 chain.stream을 사용하면 LLM 응답을 스트림으로 받아서 부분 부분 출력할 수 있습니다. 이러한 고급 사용은 필요에 따라 활용하면 됩니다.
4. 조건 분기 체인 흐름 실습: 입력 조건에 따라 요약 또는 이메일 작성
체인이 고정된 흐름이 아닌, 입력에 따라 다른 경로로 분기하게 만들 수도 있습니다. 이번 실습에서는 사용자 입력 내용에 따라 요약 작업을 할지 이메일 작성 작업을 할지 분기 처리를 구현해 봅니다. 예를 들어, 사용자가 입력을 시작할 때 "요약:"으로 요청하면 텍스트 요약을 수행하고, "이메일:"으로 요청하면 이메일 작성 체인을 수행하도록 분기하는 시나리오를 다루겠습니다.
LCEL 자체는 간단한 조건 분기를 표현하기 위한 RunnableBranch 기능을 제공합니다. RunnableBranch는 (조건체인, True인 경우 체인) 튜플과 False인 경우 체인을 인자로 받아, 조건체인 결과가 True/False에 따라 다른 체인을 실행하는 구조를 갖습니다. 조건을 판단하는 부분은 LLM을 사용할 수도 있고, 간단히 Python 함수를 Runnable로 래핑하여 사용할 수도 있습니다.
이번 예제에서는 간단한 문자열 조건이므로 Python 함수를 활용해 보겠습니다. 입력 문자열이 "요약:"으로 시작하면 True를, "이메일:"으로 시작하면 False를 반환하는 함수로 분기를 결정합니다. True일 때는 요약 체인을, False일 때는 이메일 작성 체인을 호출하도록 설정합니다.
먼저 요약 작업과 이메일 작업을 수행하는 두 체인을 준비합니다:
- 요약 체인 (summary_chain): 주어진 긴 글({text} 변수)을 한두 문장으로 요약해주는 단일 LLM 체인.
- 이메일 체인 (email_chain): 앞서 3번 단계에서 만든 email_chain을 그대로 사용하거나 유사하게 구성.
그 다음 분기 조건을 위한 함수를 정의하고, 이를 RunnableBranch에 적용하겠습니다.
from langchain_core.runnables import RunnableBranch, RunnableLambda
# 1. 요약 체인 정의 (프롬프트 → LLM)
summary_prompt = PromptTemplate.from_template("다음 글을 한 문단으로 요약해주세요:\n{text}")
summary_chain = summary_prompt | llm # (StrOutputParser를 쓰지 않아도 기본 문자열 출력)
# 2. 이메일 체인은 이전 실습의 email_chain 활용 (이미 정의되었다고 가정; 없다면 위에서 정의한 코드 재사용)
# 3. 분기 조건 함수 정의 및 Runnable로 래핑
def is_summary_request(user_input: str) -> bool:
return user_input.strip().startswith("요약:") # 입력이 "요약:"으로 시작하면 True
condition = RunnableLambda(is_summary_request)
# 4. 분기 체인 구성: (조건, 조건=True일 때 체인)와 False일 때 체인 지정
branch_chain = RunnableBranch(
(condition, summary_chain), # 조건이 True이면 summary_chain 실행
email_chain # 조건 False이면 email_chain 실행
)
# 5. 다양한 입력에 대해 분기 체인 실행
input1 = "요약: 어제 진행된 회의에서는 프로젝트 일정 변경과 예산 조정에 대한 논의가 있었습니다."
result1 = branch_chain.invoke(input1)
print("[요약 요청 결과]\n", result1, "\n")
input2 = "이메일: 다음 주 월요일 프로젝트 회의 일정을 팀에 공지해줘."
result2 = branch_chain.invoke(input2)
print("[이메일 작성 요청 결과]\n", result2)
위 코드에서는 RunnableBranch((condition, summary_chain), email_chain) 형태로 분기를 구현했습니다. condition은 RunnableLambda로 래핑된 파이썬 함수로, input1/input2를 받아 True/False를 반환합니다.
- input1은 "요약:"으로 시작하므로 condition이 True를 반환하여 summary_chain이 실행됩니다.
- input2는 "이메일:"으로 시작하여 False를 반환, email_chain이 대신 실행됩니다.
실행 결과 예시:
[요약 요청 결과]
프로젝트 일정 변경과 예산 조정에 대해 논의했다는 내용이 주요 요점입니다.
[이메일 작성 요청 결과]
안녕하세요 팀원 여러분,
다음 주 월요일 프로젝트 회의를 개최하고자 합니다. 이번 회의에서는 프로젝트 진행 상황을 공유하고 향후 일정을 논의할 예정입니다...
(이하 생략)
첫 번째 출력은 회의 내용을 한 문장으로 요약한 결과이고, 두 번째 출력은 이메일 작성 체인이 선택되어 생성된 이메일 초안입니다. 이처럼 RunnableBranch를 활용하면 체인 실행 흐름에 조건 분기를 넣을 수 있습니다.
참고: LCEL로 간단한 분기는 구현할 수 있지만, 매우 복잡한 분기 논리나 루프 등이 필요할 경우 LangGraph를 사용하여 흐름 제어를 하는 것을 권장합니다. LangGraph에서는 노드 그래프로 복잡한 흐름을 구성하고, 각 노드 내부에서는 LCEL 체인을 사용할 수도 있습니다. 그러나 일반적인 조건 분기 정도는 LCEL만으로도 충분히 다룰 수 있습니다.
5. 메모리 연동 체인 실습: 대화형 메모리 (ChatMessageHistory) 사용
LangChain에서 메모리는 이전 대화 맥락(메시지 히스토리)을 기억하여 LLM 프롬프트에 포함시키는 기능입니다. 여기서는 ChatMessageHistory 클래스를 활용하여 간단한 대화 메모리를 구성하고, LCEL 체인에 대화 기록을 주입하여 모델이 이전 대화를 참고하면서 답변하도록 해보겠습니다.
시나리오: 사용자가 이전에 자기 이름을 알려주었고, 이후에 본인의 이름이 무엇인지 묻는 질문을 던졌을 때, 챗봇이 앞서 대화에서 알려준 이름을 기억하여 답하는 예제를 만들겠습니다.
이를 위해 두 가지 요소가 필요합니다:
- 메시지 히스토리 저장: ChatMessageHistory 객체에 대화 메시지를 순서대로 추가합니다 (Human 메시지와 AI 메시지 쌍으로).
- 프롬프트에 히스토리 통합: ChatPromptTemplate을 사용하면 시스템 메시지, 히스토리 메시지, 사용자 메시지를 조합한 프롬프트를 쉽게 생성할 수 있습니다. 특히 MessagesPlaceholder("history")를 활용하여 히스토리 부분을 자리채움 할 수 있습니다. LCEL 체인에서 이 프롬프트 템플릿과 LLM을 연결하면, .invoke() 호출 시 입력으로 히스토리와 새 사용자 질문을 함께 전달할 수 있습니다.
먼저 이전 대화 기록을 ChatMessageHistory에 저장하고, 그 후 새로운 질문과 함께 체인을 실행해보겠습니다.
from langchain.memory import ChatMessageHistory
from langchain.schema import HumanMessage, AIMessage, SystemMessage
from langchain.prompts import ChatPromptTemplate, MessagesPlaceholder, SystemMessagePromptTemplate, HumanMessagePromptTemplate
# 1. 과거 대화 기록 생성
history = ChatMessageHistory()
history.add_user_message("내 이름은 홍길동이야.")
history.add_ai_message("홍길동님, 만나서 반가워요.") # AI가 사용자 이름을 기억하여 응답
# 2. 채팅 프롬프트 템플릿 설정 (시스템 메시지 + 히스토리 + 사용자 메시지)
chat_prompt = ChatPromptTemplate.from_messages([
SystemMessagePromptTemplate.from_template("너는 친절하고 도움이 되는 어시스턴트야."),
MessagesPlaceholder(variable_name="history"), # 이전 대화 메모리 자리채움
HumanMessagePromptTemplate.from_template("{input}") # 새로운 사용자 입력
])
# 3. LCEL 체인 구성: 채팅 프롬프트 → LLM
chat_chain = chat_prompt | llm
# 4. 새로운 사용자 질문과 함께 체인 실행 (history와 input을 전달)
new_question = "내 이름이 뭐였지?" # 사용자: 자기 이름을 물어봄
response = chat_chain.invoke({
"history": history.messages, # 이전 메시지 목록 전달
"input": new_question
})
print(response.content)
설명: ChatPromptTemplate.from_messages를 사용하여 메시지 목록을 정의했습니다. 시스템 메시지로 역할 부여(assistant의 성격)를 하고, MessagesPlaceholder("history")로 이전 대화기록을 넣을 위치를 지정했습니다. 마지막으로 사용자 입력 자리인 {input}이 있습니다. 이런 ChatPromptTemplate와 llm을 파이프라인으로 연결하면, .invoke() 시에 딕셔너리 형태로 히스토리와 새로운 입력을 함께 전달해야 합니다.
history.messages는 ChatMessageHistory에 저장된 메시지들을 HumanMessage, AIMessage 등의 객체 목록으로 반환합니다. LangChain LCEL는 이 목록을 받아 프롬프트 내 MessagesPlaceholder("history") 위치에 각 메시지를 적절한 포맷으로 채워줍니다. 최종적으로 LLM은 시스템 메시지 + 히스토리 + 사용자 질문이 모두 포함된 컨텍스트에서 응답을 생성하게 됩니다.
실행 결과 예시:
홍길동님이라고 말씀하셨어요.
모델이 이전 대화 기록을 참고하여 사용자에게 이름을 다시 알려주는 답변을 했습니다. (홍길동님이라고 말씀하셨어요.) 이처럼 ChatMessageHistory와 MessagesPlaceholder를 활용하면, 대화형 메모리를 체인에 통합할 수 있습니다. 대화가 이어질 때마다 history에 메시지를 추가하고 체인을 호출하면, AI는 이전 맥락을 잊지 않고 답변하게 됩니다.
Note: 상기 방법은 매 호출 시 외부에서 history를 관리하여 전달하는 방식입니다. LangChain v0.3에서는 권장되는 방법이며, LangGraph를 사용하면 MemorySaver 등을 통해 자동으로 대화 기록을 관리하는 방식도 있습니다. 기본적인 챗봇 메모리는 위와 같이 ChatMessageHistory를 수동 관리하거나, ConversationBufferMemory (내부적으로 ChatMessageHistory 사용) 등을 사용할 수도 있습니다. 다만 ConversationBufferMemory 등은 곧 deprecated 될 가능성이 있으므로, 새 코드에서는 ChatMessageHistory와 PlaceHolder 조합 방식을 추천합니다.
6. 추가 내용: 출력 파서, Partial 사용, 변수 주입 등
마지막으로, LCEL 체인을 사용할 때 유용한 부가 기능들을 정리합니다.
- 출력 파서(Output Parser): LLM의 문자열 출력을 필요한 형식으로 변환해주는 도구입니다. 앞서 사용한 StrOutputParser는 가장 단순하게 문자열 그대로를 반환했지만, LangChain에는 JSON 출력을 처리하는 파서, 불리언/숫자 변환 파서 등 다양한 내장 파서가 있습니다. 예를 들어 BooleanOutputParser(true_val="True", false_val="False")는 LLM 출력이 "True"/"False" 문자열일 때 이를 파이썬 bool 값으로 변환해줍니다. 출력 파서는 체인에 |로 연결하여 사용할 수 있으며, 파서를 거친 출력은 비문자열 자료형으로 바뀔 수 있다는 점만 유의하면 됩니다.
- Partial 사용 (기본 인자 바인딩): LCEL에서는 프롬프트나 LLM에 일부 인자 값을 미리 고정시킬 수 있습니다. PromptTemplate.partial 메서드를 이용하면 프롬프트의 일부 {변수}에 값을 채워 새로운 프롬프트를 얻을 수 있고, Runnable.bind(**kwargs) 메서드는 LLM 등의 Runnable에 기본 인자를 바인딩하여 체인에서 반복 설정을 줄여줍니다. 예를 들어, 항상 동일한 stop 토큰을 적용하고 싶다면 model.bind(stop="특정토큰")으로 체인 내 해당 LLM 호출에 stop 시퀀스를 고정할 수 있습니다. 프롬프트 템플릿의 partial 예시는 다음과 같습니다:출력: "오늘 날짜는 2025-05-23입니다. 해야 할 일을 알려줘." 이처럼 partial을 활용하면, 변하지 않는 값이나 자주 쓰는 설정을 미리 넣어두고 남은 부분만 채워 체인을 실행할 수 있습니다.
- base_prompt = PromptTemplate.from_template("오늘 날짜는 {date}입니다. {task}") partial_prompt = base_prompt.partial(date="2025-05-23") # date 변수 미리 채움 print(partial_prompt.format(task="해야 할 일을 알려줘."))
- 변수 주입 및 매핑: LCEL 체인에서 변수 전달은 주로 딕셔너리로 이루어집니다. 프롬프트 템플릿에 {변수명}이 있다면 .invoke()나 .invoke_batch() 호출 시 해당 키를 가진 딕셔너리를 넘겨야 합니다. 또한 체인을 합성할 때 한 단계의 출력을 다음 단계에 변수로 주입하려면, 이전 단계 출력값을 키-값 형태로 매핑해야 합니다. 우리 실습의 이메일 체인 예시처럼 {"subject": RunnablePassthrough()}를 사용하면 이전 LLM 출력(제목 문자열)이 subject라는 이름으로 다음 프롬프트에 전달되었습니다. 만약 여러 값을 병렬로 생성하여 하나로 합쳐야 한다면, 예를 들어 RunnableParallel을 사용해 {key1: val1, key2: val2} 형태 딕셔너리를 만든 뒤 다음 단계로 넘길 수도 있습니다. 핵심은 각 단계의 입력과 출력 변수를 명시적으로 관리하는 것으로, LCEL 체인에서는 이를 위해 파이프라인 중간에 딕셔너리 리터럴이나 RunnablePassthrough, 필요한 경우 RunnableLambda 등을 사용해 데이터 형태 변환을 수행합니다.
마지막으로, LCEL 체인의 실행 방법 요약과 권장 패턴을 정리하면 다음과 같습니다:
- 체인 실행: .invoke(input)은 단일 입력에 대한 실행, .invoke_batch([input1, input2, ...])는 복수 입력 병렬 실행, .stream(input)은 스트리밍 응답 (토큰 생성될 때마다 yield) 실행입니다. 대부분의 경우 .invoke로도 충분하며, 스트리밍 UI가 필요할 때 .stream을 고려합니다.
- 디버깅과 추적: LCEL로 작성된 체인은 LangSmith 등의 도구와 연계하여 각 단계의 입력/출력을 로깅할 수 있습니다. 복잡한 체인을 작성할 때는 체인을 작게 나누어 테스트하고, 필요한 경우 중간 출력물을 확인하는 것이 좋습니다.
- Deprecated 객체 대비: v0.3.x 버전 이후로 LLMChain, SequentialChain, ConversationChain 같은 클래스형 체인은 더 이상 주요 예제에서 사용되지 않고 LCEL로 대체되었습니다. 기존 코드 호환을 위해 남아있으나, 새로운 프로젝트에서는 가급적 LCEL 문법과 Runnable 조합을 사용하는 것이 유지보수와 성능 면에서 유리합니다.
以上の内容を 통해, LCEL의 기본 개념과 다양한 체인 구성 방식(단일 체인, 다중 단계 합성, 조건 분기, 메모리 연동)을 살펴보았습니다. 이번 1시간 분량의 실습으로 LangChain Expression Language의 작동 원리와 사용법을 체득하셨기를 바랍니다. LCEL은 초기 학습 곡선이 약간 있지만, 익숙해지면 짧은 코드로 강력한 LLM 파이프라인을 구축할 수 있는 도구입니다. 실제 응용에서 필요에 따라 LCEL 체인을 활용해 더욱 풍부한 AI 애플리케이션을 구현해 보세요!
'HRDI_AI > [인공지능] LangChain과 RAG를 활용한 AI 서비스 개발 Ⅰ' 카테고리의 다른 글
| 7. LangChain 도구 (4) | 2025.05.30 |
|---|---|
| 5. LangChain 메모리 (0) | 2025.05.30 |
| 4. LangChain 출력 파서(OutputParser) (2) | 2025.05.26 |
| 3. LangChain 프롬프트 템플릿 (0) | 2025.05.26 |
| 2. LangChain을 활용한 모델 사용, 비용 모니터링 및 캐싱 전략 (0) | 2025.05.26 |