7장. 웹 UI 연동과 배포
스트리밍 답: write_stream 과 stream
라면집에서 주문하고 기다리는데, 주방 쪽 창이 막혀 있으면 다 될 때까지 무엇을 하는지 모릅니다. 창이 열려 있으면 면을 넣고, 파를 썰고, 그릇에 담는 것이 보여서 같은 시간을 기다려도 덜 답답합니다. 챗봇도 같습니다. 모델이 답을 다 쓸 때까지 몇 초 동안 빈 화면을 보여 주는 것보다, 쓰는 대로 한 조각씩 말풍선에 흘려 보여 주는 것이 덜 답답합니다. 3장에서 본 stream 이 조각을 내놓고, Streamlit 의 st.write_stream 이 그 조각을 말풍선에 이어 씁니다.
왼쪽 파랑 상자는 invoke 입니다. 답이 끝날 때까지 기다렸다가 한 번에 받아 그립니다. 오른쪽 보라 상자는 stream 과 st.write_stream 입니다. 조각이 생기는 대로 받아 말풍선에 이어 쓰고, 다 쓰면 이어 붙인 전체 글을 돌려줍니다. 여기서 막힌 창이 invoke, 열린 창이 스트리밍, 라면 한 그릇이 답 전체입니다. 비유와 다른 점은, 스트리밍을 해도 답을 다 쓰는 데 걸리는 시간은 같다는 것입니다. 첫 글자가 빨리 보일 뿐입니다.
invoke 와 stream 견주기
| 견줄 것 | chain.invoke(…) + st.markdown | chain.stream(…) + st.write_stream |
|---|---|---|
| 첫 글자가 보이는 때 | 답이 다 끝난 뒤 | 첫 조각이 오자마자 |
| 돌려받는 것 | 답 전체(글) | write_stream 이 이어 붙인 답 전체(글) |
| 기록에 넣기 | 받은 글을 그대로 | write_stream 이 돌려준 글을 그대로 |
| 어울리는 때 | 답이 짧거나 표·사전으로 받을 때 | 답이 길고 손님이 기다리는 채팅 |
짧은 예시
가짜 모델의 체인에서 stream 이 내놓는 조각을 보고, 같은 체인을 st.write_stream 에 넘겨 봅니다.
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(돌아가는 기다림 표시)로 기다리는 동안을 알립니다.