Làm giao diện với Gradio/Streamlit
Mục tiêu
Phần tiêu đề “Mục tiêu”Sau bài này, bạn sẽ:
- Hiểu hai cách nghĩ khác nhau: Gradio gọi một hàm của bạn mỗi khi có tin mới, còn Streamlit chạy lại cả script mỗi khi người dùng làm gì đó.
- Dựng được một giao diện chat (chat interface) trên trình duyệt, nối với API kiểu OpenAI (OpenAI-compatible), bằng cả hai thư viện.
- Đưa được câu trả lời lên giao diện theo kiểu streaming: chữ hiện dần thay vì chờ cả câu.
- Giữ đúng lịch sử hội thoại (conversation history) ở cả hai tầng: giao diện để vẽ lại, và API để mô hình “nhớ”.
Cần biết trước
Phần tiêu đề “Cần biết trước”- Sử dụng API văn bản, thuộc phần Mô hình ngôn ngữ lớn (Đã học)
Trực giác
Phần tiêu đề “Trực giác”Bạn đã có một đoạn Python gọi mô hình chat, chạy trong cửa sổ dòng lệnh. Giờ bạn muốn đưa cho đồng nghiệp dùng thử. Đồng nghiệp không biết lập trình, và “mở terminal, gõ python chat.py” là quá sức. Thứ họ cần là một trang web có ô nhập, nút gửi, và các bong bóng tin nhắn.
Tự làm trang đó nghĩa là viết HTML, CSS, JavaScript, và một server nhận yêu cầu từ trình duyệt. Đó là cả một nghề khác. Gradio và Streamlit là hai thư viện Python giúp bạn bỏ qua phần đó: vài chục dòng Python thành một ứng dụng web chạy trên máy bạn.
Hai thư viện nghĩ theo hai kiểu rất khác nhau:
- Gradio giống một cái khung ảnh có sẵn. Khung chat đã dựng xong: ô nhập, nút gửi, nút dừng, danh sách tin nhắn. Bạn chỉ đưa vào “bức ảnh”, tức một hàm nhận câu hỏi và trả câu trả lời.
- Streamlit giống một tấm bảng trắng. Bạn viết script vẽ từ trên xuống dưới. Mỗi lần có người chạm vào bảng (gửi tin, bấm nút), Streamlit xóa sạch và chạy lại script từ đầu. Thứ gì cần nhớ qua các lần vẽ lại thì phải ghi vào một cuốn sổ riêng.
Dù dùng thư viện nào, bạn cũng gặp ba câu hỏi: giao diện gọi API ở đâu, làm sao cho chữ hiện dần, và ai nhớ cuộc trò chuyện.
Bài toán: biến đoạn code gọi API thành một ứng dụng chat trên trình duyệt, chữ hiện dần khi mô hình đang trả lời, và mô hình nhớ được những gì đã nói trong cuộc trò chuyện.
Lý thuyết
Phần tiêu đề “Lý thuyết”Bài này không có công thức nào đáng kể. “Lý thuyết” ở đây là cách mỗi thư viện chạy code của bạn. Hiểu đúng điều này thì phần lớn lỗi ở mục Lỗi hay gặp tự biến mất.
Gradio: bạn viết một hàm
Phần tiêu đề “Gradio: bạn viết một hàm”Với Gradio, bạn viết một hàm chat(message, history):
messagelà chuỗi người dùng vừa gửi.historylà các lượt trước, dạng danh sách dict córole("user"hoặc"assistant") vàcontent. Lịch sử này nằm ở giao diện trên trình duyệt; mỗi lần có tin mới, Gradio chuyển nó vào hàm của bạn.
Rồi gr.ChatInterface(chat) dựng cả khung chat quanh hàm đó, và demo.launch() mở một server trên máy, mặc định ở cổng 7860. Hàm của bạn trả lời theo một trong hai cách:
returnmột chuỗi: câu trả lời hiện ra một lần, khi hàm chạy xong.yieldnhiều lần: hàm là một generator (hàm dùngyield). Mỗi lầnyield, Gradio thay toàn bộ tin nhắn đang hiện bằng giá trị mới. Vì vậy bạnyieldcả đoạn đã có, không phải chỉ mảnh mới. Gradio tự tính phần chênh lệch để gửi tới trình duyệt cho nhẹ.
Có hai chi tiết của Gradio 6 mà ví dụ cũ trên mạng hay sai:
- Ở Gradio 6.30.0 (bản bài đã thử),
contentcủa mỗi tin tronghistorylà một danh sách mảnh, ví dụ[{"type": "text", "text": "Xin chào"}], và mỗi tin còn có thêm hai khóametadata,options. Nội dung dạng danh sách mảnh thì API của OpenAI có nhận, nhưng không phải nhà cung cấp nào cũng nhận (nhất là với tinassistant). Cònmetadata,optionslà khóa mà API không định nghĩa. Vì vậy code của bài chuyển mỗi tin về{"role": ..., "content": "chuỗi"}, dạng mà nơi nào cũng nhận. historykhông có tinsystem. Lời dặn cho mô hình là việc của bạn: thêm nó vào đầu danh sách mỗi lần gọi API.
Gradio còn có gr.load_chat(base_url, model=..., token=...), dựng giao diện chat cho một API kiểu OpenAI chỉ bằng một dòng. Bài tự viết hàm chat để bạn thấy và sửa được từng bước.
Streamlit: script chạy lại từ đầu
Phần tiêu đề “Streamlit: script chạy lại từ đầu”Với Streamlit, bạn viết một script bình thường, chạy từ trên xuống dưới. Mỗi lệnh st.* vẽ một thứ lên trang. Điểm mấu chốt: mỗi khi người dùng tương tác, Streamlit chạy lại toàn bộ script (rerun). Với giao diện chat, điều đó dẫn tới ba hệ quả:
- Biến Python thường bị tạo lại từ đầu sau mỗi tin nhắn. Thứ cần nhớ phải nằm trong
st.session_state, một đối tượng giống dict, sống qua các lần chạy lại, mỗi tab trình duyệt một bản riêng. - Màn hình được vẽ lại từ đầu, nên script phải vẽ lại mọi tin nhắn cũ, mỗi tin trong một khối
st.chat_message(role). st.chat_inputtrả vềNoneở mọi lần chạy, trừ lần chạy ngay sau khi người dùng gửi tin. Lần đó nó trả về chuỗi vừa gửi. Vì vậy code chat của Streamlit có dạngif prompt := st.chat_input(...):: chỉ khi có tin mới thì mới gọi API.
Với hệ quả thứ nhất, chỉ khởi tạo lịch sử khi nó chưa có. Nếu gán thẳng st.session_state.messages = [], dòng đó cũng chạy lại sau mỗi tin nhắn và xóa sạch lịch sử:
if "messages" not in st.session_state: st.session_state.messages = []Chạy lại cả script nghe có vẻ phí, và quả thật là vậy nếu đầu script có việc nặng như đọc một file dữ liệu lớn hay tải mô hình. Khi đó, Streamlit có st.cache_resource và st.cache_data để giữ kết quả qua các lần chạy lại. Ứng dụng của bài chỉ tạo client (rất nhẹ), nên không cần.
Lịch sử hội thoại: hai tầng nhớ
Phần tiêu đề “Lịch sử hội thoại: hai tầng nhớ”“Nhớ cuộc trò chuyện” thật ra là hai việc khác nhau:
- Giao diện nhớ để vẽ lại. Gradio tự giữ lịch sử trong khung chat. Ở Streamlit, bạn tự giữ trong
st.session_state.messages. - Mô hình “nhớ” vì bạn gửi lại. API không giữ trạng thái, nên mỗi lượt bạn gửi tin
system, cả lịch sử, và câu hỏi mới. Ở lượt thứ , danh sách gửi đi có tin: một tinsystem, cặp hỏi–đáp cũ, và câu hỏi mới.
Hai tầng phải khớp nhau. Nếu giao diện vẽ câu trả lời nhưng bạn quên lưu nó vào lịch sử, lượt sau mô hình không hề thấy câu trả lời đó (hộp thứ hai ở mục Lỗi hay gặp).
Số tin tăng theo , nên số token gửi lên cũng tăng sau mỗi lượt: cuộc trò chuyện càng dài thì mỗi lần gọi càng tốn tiền và càng chậm, cho tới khi vượt cửa sổ ngữ cảnh (context window), tức số token tối đa mô hình đọc được một lần. Ứng dụng thật phải cắt bớt hoặc tóm tắt lịch sử cũ. Bài này giữ nguyên cả lịch sử cho đơn giản.
Mặc định, cả hai thư viện đều mất lịch sử khi bạn tải lại trang: lịch sử chỉ sống trong một phiên của trình duyệt. Gradio có gr.ChatInterface(..., save_history=True) để lưu các cuộc trò chuyện vào bộ nhớ của trình duyệt (local storage) và hiện chúng ở một khung bên. Muốn giữ trên server thì phải tự lưu vào file hay cơ sở dữ liệu.
Streaming lên giao diện
Phần tiêu đề “Streaming lên giao diện”Gọi API với stream=True, bạn nhận về một luồng mảnh. Mỗi mảnh có một ít chữ mới ở chunk.choices[0].delta.content. Có ba loại mảnh không mang chữ: mảnh mở đầu chỉ báo vai trò assistant (content rỗng), mảnh kết thúc chỉ báo finish_reason (content là None), và mảnh mang số token khi bật include_usage (hoặc khi nhà cung cấp tự gửi), có choices rỗng. Code phải bỏ qua cả ba, nếu không sẽ gặp TypeError khi cộng None vào chuỗi, hoặc IndexError khi đọc choices[0].
Gradio: bạn tự lặp qua luồng, bỏ mảnh rỗng, cộng dồn vào reply rồi yield reply. Đây là vòng lặp trong hàm chat:
for chunk in stream: if not chunk.choices: # có nơi gửi mảnh không có choices continue piece = chunk.choices[0].delta.content if piece: # mảnh có thể là None hoặc chuỗi rỗng reply += piece yield reply # trả cả đoạn đã có, Gradio vẽ lại tin nhắnStreamlit: bạn đưa thẳng luồng của SDK openai vào st.write_stream. Hàm này tự bỏ mảnh rỗng, hiện chữ dần, và khi luồng kết thúc thì trả về cả câu trả lời dạng chuỗi. Bạn lưu giá trị đó vào lịch sử.
Giới hạn
Phần tiêu đề “Giới hạn”- Ứng dụng chỉ chạy trên máy bạn, ở địa chỉ
localhost. Đóng gói và đưa lên server để người khác truy cập là việc của bài Docker và Cloud. - Lịch sử không được cắt, nên cuộc trò chuyện rất dài sẽ tốn token và có thể vượt cửa sổ ngữ cảnh.
- Lỗi giữa luồng chưa được xử lý. SDK không tự thử lại một luồng bị đứt (bài Sử dụng API văn bản). Ứng dụng thật nên bắt lỗi và báo cho người dùng.
- Mô hình chỉ biết những gì nó đã học. Cho mô hình đọc tài liệu của bạn trước khi trả lời là việc của bài RAG chatbot.
Minh họa
Phần tiêu đề “Minh họa”Bảng dưới đặt hai thư viện cạnh nhau. Bạn nên nhìn kỹ ba hàng đầu: ai gọi code của bạn, lịch sử nằm ở đâu, và streaming làm thế nào.
| Tiêu chí | Gradio | Streamlit |
|---|---|---|
| Ai gọi code của bạn | Gradio gọi hàm chat(message, history) mỗi khi có tin mới | Streamlit chạy lại cả script mỗi lần người dùng tương tác |
| Lịch sử nằm ở đâu | Khung chat giữ, truyền vào history | Bạn giữ trong st.session_state |
| Streaming | Cộng dồn rồi yield cả đoạn | st.write_stream(luồng), nhận lại cả câu trả lời |
| Khung chat | Dựng sẵn: gr.ChatInterface(chat) | Tự ghép: st.chat_message, st.chat_input |
Tin system | Bạn thêm vào đầu mỗi lần gọi API | Bạn thêm vào đầu mỗi lần gọi API |
| Hợp với | Demo nhanh một mô hình, một hàm | Ứng dụng nhiều phần: bảng, biểu đồ, thanh bên |
| Lệnh chạy | python gradio_app.py | streamlit run streamlit_app.py |
| Cổng mặc định | 7860 | 8501 |
| Python tối thiểu (PyPI, 10/2026) | 3.10 | 3.11 |
Hàng “Hợp với” là nhận xét chung, không phải luật: Gradio cũng dựng được giao diện nhiều phần bằng gr.Blocks, và Streamlit cũng làm demo nhanh được.
Để thấy “gửi lại cả lịch sử” cụ thể ra sao, đây là danh sách messages mà cả hai ứng dụng của bài gửi lên API ở lượt thứ 3 của một cuộc trò chuyện. Đúng tin. Đây là ví dụ minh họa, chỗ câu trả lời của mô hình chỉ ghi giữ chỗ.
| Vai trò | Nội dung | Lấy từ đâu |
|---|---|---|
system | Bạn là trợ lý thân thiện, trả lời ngắn gọn bằng tiếng Việt. | Hằng SYSTEM trong code |
user | Gradio là gì? | Lịch sử, lượt 1 |
assistant | (câu trả lời của mô hình ở lượt 1) | Lịch sử, lượt 1 |
user | Còn Streamlit? | Lịch sử, lượt 2 |
assistant | (câu trả lời của mô hình ở lượt 2) | Lịch sử, lượt 2 |
user | Vậy để demo nhanh thì nên chọn cái nào? | Ô nhập, vừa gửi |
Ví dụ code
Phần tiêu đề “Ví dụ code”Code trong bài chạy thử lần cuối tháng 10/2026, với Python 3.14, openai 3.37, gradio 6.30.0 và streamlit 1.66.1. Cả hai chương trình chạy trên máy bạn, không chạy trên trang: mỗi chương trình là một ứng dụng web, mở một server nhỏ trên máy và cần mạng để gọi API.
Trước khi chạy, đặt ba biến môi trường như ở bài Sử dụng API văn bản, trong đúng cửa sổ dòng lệnh mà bạn sẽ dùng để chạy ứng dụng. Bảng nhà cung cấp và địa chỉ OPENAI_BASE_URL của từng nơi cũng ở bài đó. Hai chương trình dùng chung lời dặn SYSTEM, để bạn so sánh cho dễ.
Ứng dụng chat bằng Gradio
Phần tiêu đề “Ứng dụng chat bằng Gradio”Lưu code thành gradio_app.py, chạy bằng python gradio_app.py, rồi mở địa chỉ in ra trong cửa sổ dòng lệnh (thường là http://127.0.0.1:7860). Bài dùng Gradio 6 trở lên; với Gradio 5 trở về trước, history có dạng khác.
Chạy trên máy bạn
Chat bằng Gradio, có streaming
- Cần cài
pip install openai 'gradio>=6'- Biến môi trường
OPENAI_BASE_URL(tùy chọn),OPENAI_API_KEY,LLM_MODEL- Lệnh chạy
python gradio_app.py
# Giao diện chat bằng Gradio, nối với API kiểu OpenAI, có streaming.import os
import gradio as grfrom openai import OpenAI
client = OpenAI( base_url=os.environ.get("OPENAI_BASE_URL"), # bỏ trống: dùng OpenAI api_key=os.environ["OPENAI_API_KEY"],)MODEL = os.environ["LLM_MODEL"]SYSTEM = "Bạn là trợ lý thân thiện, trả lời ngắn gọn bằng tiếng Việt."
def to_text(content): # Gradio 6 lưu nội dung mỗi tin thành danh sách mảnh # {"type": "text", "text": ...}. Ta ghép lại thành một chuỗi. if isinstance(content, str): return content return "".join(p["text"] for p in content if p.get("type") == "text")
def chat(message, history): # history: các lượt trước, do Gradio giữ. Tin system ta tự thêm. messages = [{"role": "system", "content": SYSTEM}] for turn in history: messages.append( {"role": turn["role"], "content": to_text(turn["content"])} ) messages.append({"role": "user", "content": message})
stream = client.chat.completions.create( model=MODEL, messages=messages, stream=True ) reply = "" for chunk in stream: if not chunk.choices: # có nơi gửi mảnh không có choices continue piece = chunk.choices[0].delta.content if piece: # mảnh có thể là None hoặc chuỗi rỗng reply += piece yield reply # trả cả đoạn đã có, Gradio vẽ lại tin nhắn
demo = gr.ChatInterface( chat, title="Trợ lý AI Atlas", examples=["Gradio khác Streamlit ở đâu?", "Kể một chuyện cười ngắn"],)
if __name__ == "__main__": demo.launch() # mở http://127.0.0.1:7860Đọc code theo thứ tự Gradio gọi nó:
- Khi khởi động,
client,MODELvàdemođược tạo một lần. ThiếuOPENAI_API_KEYhayLLM_MODELthì chương trình dừng ngay vớiKeyErrornêu tên biến bị thiếu, trước khi mở server. - Mỗi khi bạn gửi tin, Gradio gọi
chat(message, history). Hàm dựngmessagesgồm tinsystem, các lượt cũ (quato_text) và câu hỏi mới, rồi gọi API vớistream=True. - Mỗi mảnh về, hàm cộng chữ mới vào
replyvàyield reply. Tin nhắn trên màn hình dài dần. Khi luồng hết, giá trịyieldcuối cùng là câu trả lời đầy đủ, và Gradio lưu nó vào lịch sử cho lượt sau.
Dòng if __name__ == "__main__": giúp file vừa chạy thẳng được, vừa import được mà không mở server. Hai câu hỏi mẫu hiện trong khung chat lúc còn trống là từ tham số examples; bấm vào là gửi luôn.
Ứng dụng chat bằng Streamlit
Phần tiêu đề “Ứng dụng chat bằng Streamlit”Lưu code thành streamlit_app.py và chạy bằng streamlit run streamlit_app.py, không phải python streamlit_app.py. Streamlit mở trình duyệt ở http://localhost:8501. Streamlit 1.66 cần Python 3.11 trở lên. Khác với Gradio, thiếu biến môi trường thì server Streamlit vẫn mở, nhưng trang hiện lỗi KeyError nêu tên biến bị thiếu.
Chạy trên máy bạn
Chat bằng Streamlit, có streaming
- Cần cài
pip install openai streamlit- Biến môi trường
OPENAI_BASE_URL(tùy chọn),OPENAI_API_KEY,LLM_MODEL- Lệnh chạy
streamlit run streamlit_app.py
# Giao diện chat bằng Streamlit, nối với API kiểu OpenAI, có streaming.import os
import streamlit as stfrom openai import OpenAI
client = OpenAI( base_url=os.environ.get("OPENAI_BASE_URL"), # bỏ trống: dùng OpenAI api_key=os.environ["OPENAI_API_KEY"],)MODEL = os.environ["LLM_MODEL"]SYSTEM = "Bạn là trợ lý thân thiện, trả lời ngắn gọn bằng tiếng Việt."
st.title("Trợ lý AI Atlas")
# Streamlit chạy lại cả file mỗi lần bạn gửi tin. Biến thường bị# tạo lại từ đầu, còn st.session_state thì sống qua các lần chạy lại.if "messages" not in st.session_state: st.session_state.messages = []
# Màn hình được vẽ lại từ đầu, nên vẽ lại mọi tin nhắn cũ.for msg in st.session_state.messages: with st.chat_message(msg["role"]): st.markdown(msg["content"])
# chat_input trả về None cho tới khi người dùng gửi tin.if prompt := st.chat_input("Nhập câu hỏi..."): st.session_state.messages.append({"role": "user", "content": prompt}) with st.chat_message("user"): st.markdown(prompt)
with st.chat_message("assistant"): stream = client.chat.completions.create( model=MODEL, messages=[{"role": "system", "content": SYSTEM}] + st.session_state.messages, stream=True, ) # write_stream hiện chữ dần, rồi trả về cả câu trả lời. reply = st.write_stream(stream) # Lưu câu trả lời, nếu không lượt sau mô hình sẽ "quên" nó. st.session_state.messages.append({"role": "assistant", "content": reply})Đọc code theo một lần chạy lại của Streamlit:
- Lần chạy đầu,
st.session_state.messagesđược tạo rỗng, vòngforkhông vẽ gì,st.chat_inputtrảNone, nên khốiifbị bỏ qua. Trang chỉ có tiêu đề và ô nhập. - Bạn gửi một câu, Streamlit chạy lại script từ dòng đầu. Lần này
st.chat_inputtrả về câu đó. Script thêm câu vào lịch sử, vẽ nó ra, rồi mở khốist.chat_message("assistant")và gọi API.st.write_streamhiện chữ dần và trả về cả câu trả lời; dòng cuối lưu nó vào lịch sử. - Bạn gửi câu thứ hai, script lại chạy từ đầu. Lần này vòng
forvẽ lại hai tin của lượt trước, rồi khốiifxử lý câu mới với cả lịch sử.
Tin system không nằm trong st.session_state.messages, mà được ghép vào lúc gọi API. Nhờ vậy vòng for không vẽ lời dặn đó thành một bong bóng chat.
Gửi hai câu, bạn sẽ thấy câu trả lời hiện dần, và ở lượt thứ hai mô hình trả lời có tính tới lượt đầu.
Lỗi hay gặp
Phần tiêu đề “Lỗi hay gặp”Kiểm tra nhanh
Phần tiêu đề “Kiểm tra nhanh”Tóm tắt và đọc thêm
Phần tiêu đề “Tóm tắt và đọc thêm”Ý chính:
- Gradio và Streamlit biến vài chục dòng Python thành ứng dụng web chạy trên máy, không cần HTML hay JavaScript.
- Gradio: viết hàm
chat(message, history), bọc bằnggr.ChatInterface. Hàm là generator thìyieldcả đoạn đã có, vì mỗi lầnyieldthay toàn bộ tin nhắn. Ở Gradio 6, chuyểnhistoryvề dạngmessagescủa API trước khi gửi. - Streamlit: mỗi lần tương tác, cả script chạy lại. Lịch sử nằm trong
st.session_state(chỉ khởi tạo khi chưa có), vẽ lại bằngst.chat_message, và câu trả lời là giá trịst.write_streamtrả về. - API không giữ trạng thái: lượt thứ gửi tin. Lịch sử càng dài càng tốn token, cho tới khi vượt cửa sổ ngữ cảnh.
Đọc thêm:
- Gradio. Creating a Chatbot Fast. Hướng dẫn chính thức về
gr.ChatInterface: hàm chat, streaming bằngyield,examples,additional_inputs(ví dụ thêm ô system prompt hay thanh trượt). Trang tài liệu không có bản theo phiên bản; bài đã đối chiếu với Gradio 6.30.0. - Gradio. ChatInterface. Trang tham chiếu: đủ tham số của
gr.ChatInterface. - Streamlit. Build a basic LLM chat app. Hướng dẫn chính thức, cùng khuôn
st.session_state–st.chat_message–st.write_streamnhư bài. Lưu ý: ví dụ ở đó đọc key bằngst.secretsvà ghi cứng tên mô hình; bài này đọc cả hai từ biến môi trường. - Streamlit. Session State và st.write_stream. Cơ chế chạy lại và cách
st.write_streamđọc luồng của SDKopenai. Tài liệu ứng với Streamlit 1.66. - Abid, A., Abdalla, A., Abid, A., Khan, D., Alfozan, A., Zou, J. (2019). Gradio: Hassle-Free Sharing and Testing of ML Models in the Wild. Bài báo giới thiệu Gradio, kể vì sao cần một cách nhanh để người không lập trình thử được mô hình.
Trình duyệt đang chặn lưu dữ liệu. Tiến độ sẽ mất khi bạn tải lại trang.
Nội dung theo giấy phép CC BY-NC-SA 4.0. Mã nguồn theo giấy phép MIT.Giới thiệu