- Streamlit có sẵn các thành phần cho chat: ô nhập
st.chat_input, khung tin nhắnst.chat_message, vàst.write_streamđể hiện chữ dần dần. - Streamlit chạy lại cả tệp mỗi lần người dùng tương tác, nên lịch sử hội thoại phải lưu trong
st.session_state. - Tách phần gọi mô hình ra tệp riêng: thử được bằng client giả, đổi mô hình không cần sửa giao diện.
- Cắt bớt lịch sử cũ trước khi gửi, để chi phí không tăng mãi theo độ dài hội thoại; giữ khóa API ngoài code.
Bài này dựa trên hướng dẫn làm ứng dụng chat của Streamlit, trang tham chiếu st.write_stream, trang quản lý bí mật của Streamlit, và trang truyền dần (streaming) trong tài liệu của Anthropic, đọc ngày 11/11/2026. Nếu chưa gọi API mô hình bao giờ, đọc trước bài gọi API AI bằng Python.
Streamlit chạy thế nào
Một ứng dụng Streamlit là một tệp Python bình thường. Mỗi lệnh như st.title hay st.markdown vẽ một thành phần lên trang. Điểm khác với chương trình thường: mỗi khi người dùng gõ một câu hay bấm một nút, Streamlit chạy lại tệp từ đầu tới cuối. Biến thường vì vậy bị đặt lại mỗi lần. Thứ cần nhớ qua các lần chạy, như lịch sử hội thoại, phải để trong st.session_state, một chỗ lưu riêng cho mỗi phiên trình duyệt.
Hướng dẫn chính thức của Streamlit dùng đúng mẫu này: lịch sử là một danh sách các tin nhắn, mỗi tin có vai trò và nội dung; mỗi lần chạy, vẽ lại toàn bộ lịch sử rồi xử lý câu hỏi mới nếu có. Định dạng tin nhắn này trùng với định dạng Messages API cần, nên có thể gửi thẳng lịch sử cho mô hình.
Hai tệp của chatbot
Tệp tro_ly.py không biết gì về giao diện. Hàm cat_lich_su giữ tối đa mười tin gần nhất và bảo đảm tin đầu tiên gửi đi là của người dùng, vì API yêu cầu hội thoại bắt đầu bằng người dùng. Hàm tra_loi_tung_phan gọi Claude ở chế độ truyền dần và trả về từng đoạn chữ ngay khi mô hình sinh ra; lỗi từ thư viện anthropic được đổi thành một lỗi riêng có lời nhắn dễ hiểu. Khi luồng kết thúc, hàm xem lý do dừng: câu trả lời bị cắt vì chạm max_tokens, bị từ chối hay không có chữ cũng thành lỗi riêng đó, để giao diện báo rõ và không lưu câu dở dang vào lịch sử; max_tokens để rộng vì phần suy nghĩ của mô hình cũng tính vào đó. Hàm tao_client đọc khóa từ biến môi trường và báo rõ nếu chưa có.
Tệp app.py là giao diện. Nó vẽ tiêu đề và một dòng nhắc rằng câu trả lời do AI tạo có thể sai, vẽ lại lịch sử, và khi có câu hỏi mới thì thêm vào lịch sử, gọi tra_loi_tung_phan, đưa kết quả cho st.write_stream để hiện dần, rồi lưu câu trả lời đầy đủ vào lịch sử.
"""Giao diện chatbot bằng Streamlit. Chạy: streamlit run app.py
Cài: python -m pip install streamlit anthropic ; đặt biến môi trường ANTHROPIC_API_KEY."""
import streamlit as st
import tro_ly
st.title("Trợ giảng Python")
st.caption("Câu trả lời do AI tạo, có thể sai. Đừng nhập thông tin cá nhân.")
if "lich_su" not in st.session_state:
st.session_state.lich_su = []
for tin in st.session_state.lich_su:
with st.chat_message(tin["role"]):
st.markdown(tin["content"])
if cau_hoi := st.chat_input("Hỏi về Python..."):
st.session_state.lich_su.append({"role": "user", "content": cau_hoi})
with st.chat_message("user"):
st.markdown(cau_hoi)
try:
client = tro_ly.tao_client()
except RuntimeError as e:
st.error(str(e))
st.stop()
with st.chat_message("assistant"):
try:
tra_loi = st.write_stream(tro_ly.tra_loi_tung_phan(client, tro_ly.cat_lich_su(st.session_state.lich_su)))
except tro_ly.LoiAPI as e:
st.session_state.lich_su.pop() # bỏ câu hỏi chưa được trả lời để lịch sử luôn xen kẽ người dùng / trợ lý
st.error(str(e))
st.stop()
st.session_state.lich_su.append({"role": "assistant", "content": tra_loi})
"""Phần xử lý của chatbot (không phụ thuộc giao diện): cắt lịch sử, gọi Claude và trả chữ dần dần (streaming)."""
import os
HE_THONG = "Bạn là trợ giảng Python cho học sinh. Trả lời ngắn gọn bằng tiếng Việt, có ví dụ code khi cần."
class LoiAPI(Exception):
pass
def tao_client():
if not os.environ.get("ANTHROPIC_API_KEY"):
raise RuntimeError("Chưa có ANTHROPIC_API_KEY. Đặt biến môi trường rồi chạy lại.")
import anthropic
return anthropic.Anthropic()
def cat_lich_su(lich_su, toi_da=10):
"""Giữ tối đa `toi_da` tin gần nhất để prompt không dài mãi; tin đầu tiên gửi đi phải là của người dùng."""
gan_day = lich_su[-toi_da:]
while gan_day and gan_day[0]["role"] != "user":
gan_day = gan_day[1:]
return gan_day
def tra_loi_tung_phan(client, lich_su, mo_hinh="claude-haiku-5-5", toi_da_token=16000):
"""Generator: trả từng đoạn chữ ngay khi mô hình sinh ra, để giao diện hiện dần."""
try:
# Haiku 5.5 suy nghĩ trước khi trả lời, và token suy nghĩ tính vào max_tokens dù không hiện ra, nên để rộng chỗ.
# max_tokens chỉ là mức trần: bạn trả tiền cho số token thật sự sinh ra; truyền dần nên không lo hết giờ chờ.
with client.messages.stream(model=mo_hinh, max_tokens=toi_da_token, system=HE_THONG,
messages=lich_su) as luong:
for doan in luong.text_stream:
yield doan
cuoi = luong.get_final_message() # lý do dừng chỉ có khi luồng đã kết thúc
except Exception as e: # noqa: BLE001
if type(e).__module__.startswith("anthropic"):
raise LoiAPI(f"Không gọi được mô hình ({type(e).__name__}). Thử lại sau.") from e
raise
# Chữ dở dang hay ô trống không được coi là đã trả lời xong: báo lỗi để giao diện hiện lời nhắn và không lưu vào
# lịch sử.
if cuoi.stop_reason in ("max_tokens", "model_context_window_exceeded"):
raise LoiAPI("Câu trả lời bị cắt giữa chừng vì quá dài. Hãy hỏi gọn hơn.")
if cuoi.stop_reason == "refusal":
raise LoiAPI("Mô hình từ chối trả lời câu này.")
if not "".join(k.text for k in cuoi.content if k.type == "text").strip():
raise LoiAPI("Mô hình không trả về chữ nào. Thử lại sau.")
Chạy bằng lệnh streamlit run app.py trong thư mục chứa hai tệp, sau khi cài streamlit và anthropic và đặt biến môi trường khóa API. Trình duyệt sẽ mở trang chat ở địa chỉ máy của bạn.
Dựng từng bước
Nếu tự gõ lại từ đầu, nên đi từng bước nhỏ và chạy thử sau mỗi bước, thay vì viết cả hai tệp một lần rồi mới chạy.
Bước một, làm một trang chỉ có tiêu đề và ô nhập, mỗi lần gõ thì in lại đúng câu vừa gõ. Bước này giúp bạn quen với việc Streamlit chạy lại cả tệp: thêm một dòng in ra thời gian hiện tại ở đầu tệp, gõ vài câu, bạn sẽ thấy thời gian đổi sau mỗi lần gửi.
Bước hai, thêm lịch sử. Lần đầu thử lưu vào một danh sách thường, gõ hai câu, thấy câu đầu biến mất. Chuyển sang lưu trong bộ nhớ phiên, gõ lại, thấy cả hai câu còn. Đây là bài học quan trọng nhất của Streamlit.
Bước ba, thay câu trả lời giả bằng lời gọi mô hình thật nhưng chưa truyền dần: chờ xong rồi hiện. Đo xem mất bao lâu với một câu hỏi dài.
Bước bốn, chuyển sang truyền dần và so cảm giác khi dùng. Bước năm, tách phần gọi mô hình ra tệp riêng và viết bộ thử với client giả. Sau năm bước, bạn có đúng hai tệp như trong bài và hiểu vì sao từng dòng có mặt ở đó.
Đưa lên mạng cho người khác dùng
Chạy trên máy là đủ để học và demo. Khi muốn gửi đường dẫn cho bạn bè hay nhà tuyển dụng xem, có vài lựa chọn, trong đó có dịch vụ của chính Streamlit lấy mã trực tiếp từ kho GitHub. Trước khi đưa lên, rà ba điều. Khóa nằm trong phần cấu hình bí mật của dịch vụ, không nằm trong kho. Có hạn mức chi tiêu ở phía tài khoản API, vì ai có đường dẫn cũng gọi được mô hình bằng tiền của bạn. Và trang có dòng nhắc rõ đây là ứng dụng thử, câu trả lời do AI tạo. Với portfolio, kèm theo README mô tả kiến trúc hai tệp và cách chạy thử, như hướng dẫn trong bài portfolio AI trên GitHub.
Vì sao hiện chữ dần
Một câu trả lời vài trăm chữ có thể mất vài giây. Nếu chờ xong mới hiện, người dùng nhìn màn hình trống và tưởng ứng dụng treo. Truyền dần cho chữ hiện ngay khi mô hình sinh ra. Phía Claude, thư viện có hàm messages.stream với thuộc tính text_stream trả từng đoạn chữ; phía Streamlit, st.write_stream nhận một nguồn sinh như vậy, hiện từng đoạn và trả về toàn bộ chuỗi khi xong. Hai thứ nối với nhau bằng một hàm generator chỉ vài dòng.
Tài liệu của Anthropic còn lưu ý lỗi có thể xảy ra giữa chừng khi đang truyền dần. Trong code, lỗi như vậy làm hàm generator ném lỗi riêng; giao diện bắt lỗi, bỏ câu hỏi chưa được trả lời ra khỏi lịch sử để lịch sử luôn xen kẽ người dùng và trợ lý, rồi hiện thông báo.
Giữ chi phí và khóa an toàn
Mỗi câu hỏi gửi kèm cả lịch sử, nên hội thoại càng dài càng tốn. Hàm cắt lịch sử giữ mười tin gần nhất là cách đơn giản nhất. Cách tốt hơn cho hội thoại dài là tóm tắt phần cũ, hoặc dùng prompt caching cho phần đầu cố định.
Khóa API không bao giờ viết trong code. Chạy trên máy, đặt biến môi trường. Khi đưa ứng dụng lên dịch vụ của Streamlit, tài liệu hướng dẫn dùng tệp bí mật trong thư mục .streamlit và đọc bằng st.secrets; tệp đó phải nằm trong danh sách bỏ qua của Git. Xem thêm bài giữ khóa API an toàn. Nếu cho người khác dùng ứng dụng, nên đặt hạn mức chi tiêu trong trang quản lý tài khoản API và giới hạn số câu hỏi mỗi phiên.
Thử mà không mở trình duyệt
Vì phần gọi mô hình nằm riêng, bạn thử nó bằng một client giả trả về vài đoạn chữ cố định, không tốn tiền, không cần mạng. Streamlit còn có công cụ thử giao diện trong mô-đun kiểm thử của nó: chạy app.py, điền câu hỏi vào ô nhập, rồi kiểm tra lịch sử và các tin nhắn hiện ra. Bộ thử đi kèm bài dùng cả hai cách: giả lập hai lượt hỏi đáp, giả lập lỗi API, và trường hợp chưa có khóa.
Mở rộng
Từ khung này có thể làm tiếp nhiều dự án portfolio. Thêm ô tải tệp để chatbot trả lời dựa trên giáo trình, theo cách trong bài RAG là gì. Thêm thanh bên để chọn mô hình hoặc độ dài câu trả lời. Thêm nút xóa hội thoại bằng cách đặt lại st.session_state. Mỗi thay đổi chỉ chạm một trong hai tệp.
- Viết khóa API thẳng trong
app.pyrồi đưa lên GitHub. - Lưu lịch sử trong biến thường: mỗi lần chạy lại là mất.
- Gửi toàn bộ lịch sử không giới hạn, chi phí tăng theo từng câu.
- Không xử lý lỗi giữa chừng, để lại câu hỏi treo trong lịch sử.
- Cho người lạ dùng mà không có hạn mức chi tiêu.
Câu hỏi thường gặp
Streamlit có miễn phí không?
Thư viện là mã nguồn mở. Dịch vụ đưa ứng dụng lên mạng của Streamlit có gói miễn phí với giới hạn; xem trang của họ để biết điều khoản hiện tại.
Dùng mô hình chạy trên máy thay Claude được không?
Được. Chỉ cần viết lại hàm trong tro_ly.py để gọi mô hình trên máy, giao diện giữ nguyên.
Nhiều người dùng cùng lúc thì sao?
Mỗi phiên trình duyệt có st.session_state riêng, nên hội thoại không lẫn nhau. Với nhiều người dùng thật, cần tính tới giới hạn tốc độ của API.
Có cần học HTML, CSS không?
Không, với ứng dụng như trong bài. Khi cần giao diện riêng hơn, Streamlit cho tùy biến giới hạn; ứng dụng phức tạp nên dùng khung web đầy đủ.
Học tiếp
- Gọi API lần đầu: gọi API AI bằng Python.
- Cho chatbot trả lời từ tài liệu: RAG là gì.
- Không để lộ khóa: giữ khóa API an toàn.
Nếu bạn muốn học Python và AI bài bản cùng giảng viên, tham khảo khóa học AI của Big-O Coding.
Khóa AI – Introduction to Machine Learning: nền tảng Máy học từ thuật toán cơ bản đến Neural Networks và LLM, học bằng Python. Học thử buổi 1 miễn phí.


