LangChain 챗봇7장 · 웹 UI 연동과 배포9 / 22← 이전목차다음 →MetaContents

7장. 웹 UI 연동과 배포

스트리밍 답: write_stream 과 stream

라면집에서 주문하고 기다리는데, 주방 쪽 창이 막혀 있으면 다 될 때까지 무엇을 하는지 모릅니다. 창이 열려 있으면 면을 넣고, 파를 썰고, 그릇에 담는 것이 보여서 같은 시간을 기다려도 덜 답답합니다. 챗봇도 같습니다. 모델이 답을 다 쓸 때까지 몇 초 동안 빈 화면을 보여 주는 것보다, 쓰는 대로 한 조각씩 말풍선에 흘려 보여 주는 것이 덜 답답합니다. 3장에서 본 stream 이 조각을 내놓고, Streamlit 의 st.write_stream 이 그 조각을 말풍선에 이어 씁니다.

invoke 로 다 쓴 답을 한 번에 받는 것과 stream 과 write_stream 으로 조각을 받는 대로 이어 쓰는 것을 나란히 놓은 비교 그림

왼쪽 파랑 상자는 invoke 입니다. 답이 끝날 때까지 기다렸다가 한 번에 받아 그립니다. 오른쪽 보라 상자는 stream 과 st.write_stream 입니다. 조각이 생기는 대로 받아 말풍선에 이어 쓰고, 다 쓰면 이어 붙인 전체 글을 돌려줍니다. 여기서 막힌 창이 invoke, 열린 창이 스트리밍, 라면 한 그릇이 답 전체입니다. 비유와 다른 점은, 스트리밍을 해도 답을 다 쓰는 데 걸리는 시간은 같다는 것입니다. 첫 글자가 빨리 보일 뿐입니다.

invoke 와 stream 견주기

견줄 것chain.invoke(…) + st.markdownchain.stream(…) + st.write_stream
첫 글자가 보이는 때답이 다 끝난 뒤첫 조각이 오자마자
돌려받는 것답 전체(글)write_stream 이 이어 붙인 답 전체(글)
기록에 넣기받은 글을 그대로write_stream 이 돌려준 글을 그대로
어울리는 때답이 짧거나 표·사전으로 받을 때답이 길고 손님이 기다리는 채팅

짧은 예시

가짜 모델의 체인에서 stream 이 내놓는 조각을 보고, 같은 체인을 st.write_stream 에 넘겨 봅니다.

Python
from langchain_core.language_models.fake_chat_models import FakeListChatModel
from langchain_core.output_parsers import StrOutputParser
from streamlit.testing.v1 import AppTest

chain = FakeListChatModel(responses=["2박 3일이면 동쪽 해안을 추천합니다."]) | StrOutputParser()
# stream: 답을 한 번에 주지 않고 조각을 하나씩 내놓는다
pieces = list(chain.stream("제주도 일정"))
print(len(pieces), "조각:", pieces[:6], "...")
print("이어 붙이면:", "".join(pieces))

APP = """
import streamlit as st
from langchain_core.language_models.fake_chat_models import FakeListChatModel
from langchain_core.output_parsers import StrOutputParser

chain = FakeListChatModel(responses=["2박 3일이면 동쪽 해안을 추천합니다."]) | StrOutputParser()
with st.chat_message("assistant"):
    # write_stream: 조각이 올 때마다 말풍선에 이어 쓰고, 다 쓰면 전체 글을 돌려준다
    full = st.write_stream(chain.stream("제주도 일정"))
st.session_state.saved = full
"""
at = AppTest.from_string(APP).run()
print("화면의 말풍선:", at.chat_message[0].markdown[0].value)
print("돌려받은 값:", type(at.session_state["saved"]).__name__, "/", at.session_state["saved"])
출력
21 조각: ['2', '박', ' ', '3', '일', '이'] ...
이어 붙이면: 2박 3일이면 동쪽 해안을 추천합니다.
화면의 말풍선: 2박 3일이면 동쪽 해안을 추천합니다.
돌려받은 값: str / 2박 3일이면 동쪽 해안을 추천합니다.

가짜 모델은 답을 한 글자씩 21 조각으로 내놓았습니다. 진짜 모델은 토큰 단위로 조각을 내놓으므로 조각 수가 이보다 적습니다. AppTest 는 흘려 쓰는 모습까지는 보여 주지 않고, 다 쓴 뒤의 말풍선 글만 보여 줍니다. st.write_stream 이 돌려준 값은 이어 붙인 글(str)이라 그대로 기록에 넣으면 됩니다.

코드가 의미하는 것

이 코드는 주방 창을 열어 요리하는 조각을 하나씩 보는 일입니다. 앞부분은 stream 이 내놓는 조각을 직접 받아 보고, 뒷부분은 같은 체인을 st.write_stream 에 넘겨 말풍선에 이어 쓰게 합니다.

코드하는 일챗봇에서 뜻하는 것
chain.stream("제주도 일정")답을 조각 하나씩 내놓는다모델이 쓰는 대로 받는 길입니다. 3장에서 본 것과 같습니다
"".join(pieces)조각을 이어 붙인다조각을 모두 붙이면 한 번에 받은 답과 같은 글이 됩니다
full = st.write_stream(chain.stream(…))(앱 안)조각을 말풍선에 이어 쓰고 전체 글을 돌려준다화면에 흘려 보이는 일과 다 쓴 답을 받는 일을 한 줄로 합니다
st.session_state.saved = full(앱 안)돌려받은 글을 메모장에 적는다대화 기록에 답을 넣는 자리입니다

결과에서 읽을 것은 두 가지입니다.

  • 가짜 모델은 21 조각을 내놓았습니다. 앞의 여섯 조각이 '2', '박', ' ' 처럼 한 글자씩이고, 이어 붙이면 원래의 답 한 문장입니다.
  • `write_stream` 은 보통 글(str)을 돌려줍니다. 말풍선 글과 돌려받은 값이 같으므로 따로 이어 붙이지 않고 그대로 기록에 넣습니다.

주의할 점

  • chain.stream(...) 은 끝에 StrOutputParser() 가 있어야 글 조각을 내놓습니다. 파서가 없으면 AIMessageChunk 라는 메시지 조각이 오는데, st.write_stream 은 이것도 글로 바꿔 보여 줍니다. 기록에 넣기 쉬운 글 조각으로 받는 편이 깔끔합니다.
  • 6장의 RetrievalQA 는 답과 근거 문서를 사전 하나로 돌려주는 체인이라, 이 장의 문서 챗봇은 스트리밍 대신 st.spinner(돌아가는 기다림 표시)로 기다리는 동안을 알립니다.
← 이전 절7장 · 캐시: cache_resource 와 cache_data다음 절 →7장 · 비밀 값: st.secrets 와 .env