포스트

2026-03-14 강의 정리

LangChain LCEL 문법 + Gradio 챗봇 구현 (간단한 QA 애플리케이션)

2026-03-14 강의 정리

목차

  1. 오늘 강의 개요
  2. LCEL 복습 - | 연산자와 Runnable 계열
  3. RunnableParallel - 여러 체인 병렬 실행
  4. RunnableBranch - 조건 분기 처리
  5. Gradio 소개 - 웹 UI를 파이썬으로
  6. gr.Interface - 단순 입출력 UI
  7. gr.ChatInterface - 채팅 UI
  8. gr.Blocks - 커스텀 레이아웃
  9. Gradio history 형식 - 신버전 주의!
  10. LLM + Gradio 연결 - 실제 챗봇 구현
  11. FAQ 챗봇 구현 - Context + LLM 패턴
  12. 프로젝트 1 - 주택청약 FAQ 챗봇 (Weekend 과제)
  13. 핵심 개념 총정리
  14. 자주 나오는 실수 / 주의사항

1. 오늘 강의 개요

오늘은 LangChain LCEL의 Runnable 계열을 심화하고, Gradio로 실제 웹 UI가 있는 챗봇을 만들어본다.

오늘 배우는 것

RunnableParallel → 여러 체인 동시 실행, 딕셔너리로 수집 RunnableBranch → 입력 내용에 따라 다른 체인으로 라우팅 Gradio → 파이썬 코드로 웹 UI 생성 FAQ 챗봇 → 데이터를 context로 활용하는 실전 패턴

환경 설정

1
2
3
4
5
6
7
8
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables import RunnableParallel, RunnableBranch
import gradio as gr

llm    = ChatOpenAI(model="gpt-4o-mini")
parser = StrOutputParser()

2. LCEL 복습 - | 연산자와 Runnable 계열

LCEL (LangChain Expression Language)

  • | 연산자로 체인을 구성하는 방식
  • 기본 패턴: prompt | llm | parser

Runnable 계열 3가지

이름역할
RunnableSequence순차 실행 (| 연산자가 내부적으로 만들어 줌)
RunnableParallel병렬 실행 (여러 체인을 동시에 실행)
RunnablePassthrough입력 그대로 통과 (다른 키와 함께 넘길 때 사용)

3. RunnableParallel - 여러 체인 병렬 실행

개념

  • 여러 체인을 동시에 실행하고 결과를 딕셔너리로 합쳐서 반환
  • 같은 입력을 여러 체인에 동시에 전달

기본 사용법

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
from langchain_core.runnables import RunnableParallel

# 각 체인 정의
summary_chain  = ChatPromptTemplate.from_template('{text}를 한 줄로 요약해주세요') | llm | parser
keywords_chain = ChatPromptTemplate.from_template('{text}에서 키워드 3개만 뽑아주세요') | llm | parser

# 병렬 체인 구성
parallel_chain = RunnableParallel(
    summary  = summary_chain,
    keywords = keywords_chain
)

result = parallel_chain.invoke({
    "text": "LangChain은 LLM 기반 애플리케이션 개발 프레임워크입니다."
})

print(result)
# {
#   'summary': 'LangChain은 LLM 앱 개발을 위한 프레임워크입니다.',
#   'keywords': '1. LangChain  2. LLM  3. 프레임워크'
# }

실행 흐름 시각화

1
2
3
4
5
6
7
8
입력 {"text": "LangChain은 ..."}
               ↓
  ┌────────────┴────────────┐
  │ summary_chain           │ keywords_chain   ← 동시 실행
  │ (요약 생성)              │ (키워드 추출)
  └────────────┬────────────┘
               ↓
       {'summary': ..., 'keywords': ...}   ← 딕셔너리로 합산

주의사항

1
2
3
4
5
6
7
8
9
10
11
12
# 아래처럼 llm 없이 프롬프트만 연결하면 ChatPromptValue 객체 반환됨
parallel_chain = RunnableParallel(
    summary  = ChatPromptTemplate.from_template('{text}를 한 줄로 요약해주세요'),
    keywords = ChatPromptTemplate.from_template('{text}에서 키워드 3개')
)
# → 텍스트가 아니라 프롬프트 객체가 반환됨 (실수 주의!)

# 완성된 형태:
parallel_chain = RunnableParallel(
    summary  = prompt_summary  | llm | parser,  # ← llm | parser 필수!
    keywords = prompt_keywords | llm | parser
)

딕셔너리 방식으로도 동일하게 사용 가능:

1
2
3
4
5
6
7
# RunnableParallel 클래스 방식
chain = RunnableParallel(answer=rag_chain, source=retriever)

# 딕셔너리 방식 (LCEL에서 자동 변환)
chain = {"answer": rag_chain, "source": retriever}

# 두 방법은 동일하게 동작

4. RunnableBranch - 조건 분기 처리

개념

  • 입력 내용에 따라 다른 체인으로 라우팅(분기)
  • 구조: RunnableBranch( (조건1, 체인1), (조건2, 체인2), 기본체인 )

분류 기준 함수

1
2
3
4
5
6
7
def route_logic(x):
    text = x['question']
    if '오류' in text or '에러' in text:
        return 'technical'
    elif '가격' in text or '요금' in text:
        return 'billing'
    return 'general'

각 팀별 체인 정의

1
2
3
tech_chain    = ChatPromptTemplate.from_template('기술지원팀입니다: {question}') | llm | parser
billing_chain = ChatPromptTemplate.from_template('요금관리팀입니다: {question}') | llm | parser
general_chain = ChatPromptTemplate.from_template('일반상담팀입니다: {question}') | llm | parser

RunnableBranch 구성

1
2
3
4
5
6
7
from langchain_core.runnables import RunnableBranch

branch = RunnableBranch(
    (lambda x: route_logic(x) == "technical", tech_chain),    # 조건1 → 체인1
    (lambda x: route_logic(x) == "billing",   billing_chain), # 조건2 → 체인2
    general_chain   # ← 마지막 인자 = 기본(else) 체인
)

테스트

1
2
3
4
5
6
7
8
9
10
11
12
13
questions = [
    "프린터 오류가 났습니다",
    "월 요금이 얼마인가요?",
    "영업시간 알려주세요"
]
for q in questions:
    print(f"Q: {q}")
    print(f"A: {branch.invoke({'question': q})}")
    print()

# Q: 프린터 오류가 났습니다   → 기술지원팀입니다: ...
# Q: 월 요금이 얼마인가요?    → 요금관리팀입니다: ...
# Q: 영업시간 알려주세요       → 일반상담팀입니다: ...

분기 기준 정리

질문 내용라우팅 결과
“오류” / “에러” 포함기술지원팀 체인 (tech_chain)
“가격” / “요금” 포함요금관리팀 체인 (billing_chain)
그 외일반상담팀 체인 (general_chain)

5. Gradio 소개 - 웹 UI를 파이썬으로

Gradio란?

  • 파이썬 코드 몇 줄로 웹 인터페이스 생성
  • 별도 프론트엔드(HTML/CSS/JS) 지식 불필요
  • 머신러닝/AI 데모에 최적화
  • share=True 로 외부 공유 URL 생성 (72시간 유효)

설치

1
2
!pip install gradio
import gradio as gr

Gradio 컴포넌트 3종류

컴포넌트특징사용 상황
gr.Interface입력→함수→출력 단순 구조빠른 데모, 단일 기능
gr.ChatInterface채팅 UI 자동 제공, historyLLM 챗봇
gr.Blocks완전 자유 레이아웃, 이벤트복잡한 UI, 다중 컴포넌트

6. gr.Interface - 단순 입출력 UI

구조

1
2
3
4
5
6
7
8
9
10
def greet(name):
    return f"안녕하세요, {name}"

demo = gr.Interface(
    fn      = greet,                          # 실행할 함수
    inputs  = gr.Textbox(label='이름 입력'),   # 입력 컴포넌트
    outputs = gr.Textbox(label='인사말'),      # 출력 컴포넌트
    title   = '인사봇'
)
demo.launch(share=True)   # share=True: 외부 공유 URL 생성

fn : 입력을 받아 출력을 반환하는 함수 inputs / outputs에 컴포넌트 타입 지정 share=True : ngrok 터널로 외부 접속 가능한 임시 URL 생성


7. gr.ChatInterface - 채팅 UI

기본 에코봇

1
2
3
4
5
6
7
8
9
def echo_bot(message, history):
    return f"Echo: {message}"

demo = gr.ChatInterface(
    fn       = echo_bot,
    title    = '에코챗봇',
    examples = ["안녕하세요", "오늘 날씨 어때요", "FAQ 챗봇 테스트"]
)
demo.launch(share=True)

fn 시그니처 반드시: (message: str, history: list) → str message : 현재 사용자 입력 history : 이전 대화 기록 (자동으로 넘어옴) examples : 클릭 가능한 예시 버튼


8. gr.Blocks - 커스텀 레이아웃

자유로운 레이아웃 구성

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
with gr.Blocks(title='custom layout') as demo:
    gr.Markdown('## Custom Layout Demo')

    with gr.Row():                        # 가로로 배치
        with gr.Column(scale=2):          # 비율 2 (넓은 쪽)
            input_text  = gr.Textbox(label='질문')
            submit_btn  = gr.Button("전송")
        with gr.Column(scale=1):          # 비율 1 (좁은 쪽)
            category_output = gr.Textbox(label='카테고리')

    output_text = gr.Textbox(label='답변')

    def process(text):
        cat = "기술" if any(kw in text for kw in ["오류", "설치", "연결"]) else "일반"
        return cat, f"[{cat}] {text}에 대한 답변입니다"

    submit_btn.click(
        fn      = process,
        inputs  = input_text,
        outputs = [category_output, output_text]  # 반환값과 1:1 매핑
    )

demo.launch(share=True)

gr.Row() : 가로 배치 gr.Column(scale=n) : 비율 설정 (2:1이면 왼쪽이 2배 넓음) 컴포넌트.click(fn, inputs, outputs) : 버튼 클릭 시 함수 실행 outputs 여러 개 → 함수가 튜플(cat, text)로 반환하면 순서대로 매핑


9. Gradio history 형식 - 신버전 주의!

★★★ 가장 중요한 주의사항 ★★★

ChatInterface의 history 형식이 Gradio 버전에 따라 다르다!

구버전 - 튜플 형식

1
2
3
4
5
# 구버전 history 형식
history = [('사용자 메시지', 'AI 응답'), ('...', '...')]

for human, ai in history:   # 튜플 언패킹
    print(human, ai)

신버전 - 딕셔너리 형식 (현재 기본값)

1
2
3
4
5
6
7
8
9
10
11
# 신버전 history 형식
history = [
    {'role': 'user',      'content': '안녕하세요'},
    {'role': 'assistant', 'content': '안녕하세요!'},
    {'role': 'user',      'content': '파이썬 알려줘'},
    {'role': 'assistant', 'content': '파이썬은...'},
]

# 올바른 접근 방법
for msg in history:
    print(msg['role'], msg['content'])

신버전에서 구버전 방식으로 접근하면 에러!

1
2
3
# 신버전에서 이렇게 하면 에러!
for human, ai in history:   # ValueError: too many values to unpack
    print(human, ai)

실무 코드 패턴

1
2
3
4
5
6
def chat_function(message, history):
    # 신버전 딕셔너리 형식으로 처리
    for msg in history:
        role    = msg['role']
        content = msg['content']
        # ... 처리

10. LLM + Gradio 연결 - 실제 챗봇 구현

전체 코드

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage, SystemMessage, AIMessage
import gradio as gr

llm = ChatOpenAI(model="gpt-4o-mini")

def chat_with_llm(message, history):
    # 1. 시스템 프롬프트로 LangChain 메시지 리스트 시작
    messages = [SystemMessage(content="당신은 친절한 한국어 어시스턴트입니다.")]

    # 2. Gradio history를 LangChain 메시지 형식으로 변환 (신버전 딕셔너리 형식)
    for msg in history:
        if msg['role'] == 'user':
            messages.append(HumanMessage(content=msg['content']))
        else:
            messages.append(AIMessage(content=msg['content']))

    # 3. 현재 입력 메시지 추가
    messages.append(HumanMessage(content=message))

    # 4. LLM 호출 후 텍스트만 반환
    return llm.invoke(messages).content

demo = gr.ChatInterface(
    fn       = chat_with_llm,
    title    = 'AI 챗봇',
    examples = ["안녕하세요", "Python에 대해 알려주세요", "오늘 기분이 좋아요"]
)
demo.launch(share=True)

핵심 흐름 시각화

1
2
3
4
5
6
7
8
9
10
11
12
사용자 입력 (message) + 이전 대화 (history, 딕셔너리 리스트)
                    ↓
Gradio history → LangChain 메시지 리스트 변환
(딕셔너리 role/content → HumanMessage / AIMessage)
                    ↓
[SystemMessage, HumanMessage, AIMessage, ..., HumanMessage(현재)]
                    ↓
llm.invoke(messages)
                    ↓
.content 로 텍스트 추출
                    ↓
Gradio UI에 표시

핵심: Gradio는 딕셔너리로 history를 전달 → LangChain은 메시지 객체를 원함 → 변환 과정 필수 (msg[‘role’] 로 분기)


11. FAQ 챗봇 구현 - Context + LLM 패턴

아이디어: FAQ 데이터를 system 프롬프트에 넣기

1
2
FAQ 데이터 목록 → 하나의 문자열로 변환 → system 프롬프트에 삽입
→ LLM이 FAQ를 참고해서 답변

전체 코드

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser

# FAQ 데이터
faq_data = [
    {"category": "계정", "question": "비밀번호를 잊어버렸습니다. 어떻게 초기화하나요?",
     "answer": "IT 포털(intranet.company.com)에서 '비밀번호 재설정'을 클릭하세요."},
    {"category": "계정", "question": "계정이 잠겼습니다.",
     "answer": "IT 헬프데스크 1234번으로 연락해주세요."},
    # ...
]

# FAQ 데이터를 문자열로 변환 (context 구성)
faq_context = '\n'.join([
    f"Q: {item['question']}\nA: {item['answer']}"
    for item in faq_data
])

# 프롬프트에 FAQ context 주입
prompt = ChatPromptTemplate.from_messages([
    ("system",
     "너는 사내 IT 지원팀 챗봇이야. 아래 FAQ 데이터를 바탕으로 친절하게 답해줘. "
     "데이터에 없는 내용은 'IT 헬프데스크(1234번)으로 문의해주세요'라고 안내해줘.\n\n"
     f"[FAQ 데이터]\n{faq_context}"),
    ("human", "{question}")
])

chain = prompt | llm | StrOutputParser()

# Gradio 연결
def chat_response(message, history):
    return chain.invoke({"question": message})

demo = gr.ChatInterface(
    fn          = chat_response,
    title       = '사내 IT 지원 챗봇',
    examples    = ["비밀번호 어떻게 초기화하나요", "Wi-Fi가 너무 느려요"],
    description = "무엇을 도와드릴까요?"
)
demo.launch(share=True)

FAQ 챗봇 구조

1
2
3
4
5
6
7
8
9
사용자 질문
    ↓
prompt (FAQ context + 질문 조합)
    ↓
LLM (context 기반 답변 생성)
    ↓
StrOutputParser()
    ↓
Gradio UI 출력

이 패턴 = 간단한 RAG(검색 증강 생성)의 원형 context를 직접 system 프롬프트에 넣는 방식 → 데이터가 많아지면 한계 다음 강의에서 벡터 검색 기반 RAG로 발전시킴


12. 프로젝트 1 - 주택청약 FAQ 챗봇 (Weekend 과제)

파일

  • p1_weekend1_api_and_chain_0314.ipynb

핵심 기술

  • OpenAI API, LangChain LCEL, Gradio

10사이클 목표

사이클목표
1OpenAI API 직접 호출, system 역할에 “주택청약 전문 상담원” 설정
2FAQ 데이터 탐색, 카테고리별 개수 집계, difficulty 필터링
3키워드 매칭으로 관련 FAQ 검색하는 search_faq() 함수 구현
4검색 결과를 system prompt에 넣어 답변하는 ask_faq() 함수
5ChatPromptTemplate으로 FAQ 답변 + 카테고리 분류 프롬프트 구성
6LCEL 체인 + .stream() 스트리밍 출력 실습
7검색→답변 자동화 rag_chain 구현, 5개 테스트 질의 검증
8빈 입력 / 500자 초과 / 숫자만 입력 등 예외 처리 safe_ask()
9gr.ChatInterface로 RAG 체인을 웹 채팅 UI로 구현
10전체 파이프라인 통합 + 10개 질문 테스트 (응답시간, FAQ 수 출력)

전체 파이프라인

1
2
3
4
5
6
7
8
9
10
11
사용자 질문
    ↓
safe_ask (입력 검증)
    ↓
search_faq (키워드 매칭 검색)
    ↓
context 구성 → LLM 호출 (ask_faq)
    ↓
답변 + 참고 FAQ 반환
    ↓
Gradio UI 출력

13. 핵심 개념 총정리

개념핵심 내용
RunnableParallel여러 체인 동시 실행, 결과를 딕셔너리로 반환
RunnableBranch(조건, 체인) 쌍 + 기본 체인 → 분기 라우팅
gr.Interfacefn/inputs/outputs → 즉시 UI 생성
gr.ChatInterface(message, history) 시그니처 → 채팅 특화
gr.Blockswith문으로 레이아웃, .click()으로 이벤트
history 형식신버전: {‘role’: …, ‘content’: …} 딕셔너리
FAQ 챗봇 패턴FAQ를 system 프롬프트에 삽입 → 간단 RAG
LLM+Gradio 연결history 변환 → LangChain 메시지 → LLM → .content

14. 자주 나오는 실수 / 주의사항

실수 1: history 형식 착각 (★ 가장 흔한 실수)

1
2
3
4
5
6
7
8
9
10
# 신버전 Gradio에서 구버전 튜플 방식 사용
def chat(message, history):
    for human, ai in history:   # ValueError! (신버전은 딕셔너리)
        ...

# 올바른 코드
def chat(message, history):
    for msg in history:
        if msg['role'] == 'user':
            ...

실수 2: RunnableParallel에서 llm 빠뜨리기

1
2
3
4
5
6
7
8
9
# 틀린 코드 → ChatPromptValue 객체 반환 (텍스트 아님)
parallel_chain = RunnableParallel(
    summary = ChatPromptTemplate.from_template('{text} 요약해줘')  # llm 없음!
)

# 올바른 코드
parallel_chain = RunnableParallel(
    summary = ChatPromptTemplate.from_template('{text} 요약해줘') | llm | parser
)

실수 3: RunnableBranch 기본 체인 위치

1
2
3
4
5
6
# 기본 체인은 반드시 마지막에!
branch = RunnableBranch(
    (lambda x: ..., chain1),  # 조건1
    (lambda x: ..., chain2),  # 조건2
    default_chain             # ← 마지막! 앞에 두면 에러
)

실수 4: demo.launch() 전에 이전 demo 닫기

1
2
3
4
5
6
# Jupyter에서 같은 포트를 재사용하려면
demo.close()   # 이전 데모 닫기
# 또는
demo1.close()
demo2 = gr.Interface(...)
demo2.launch()

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.

인기 태그