Lộ trình học›Claude API›Building with the Claude API Mở bản đồ

6.9 · Streaming response — Hiển thị text khi Claude đang sinh

Bài viếtCơ bản → Trung c25 phút đọc

Hãy so sánh 2 trải nghiệm: Trải nghiệm A — Không stream:

Sau bài này, bạn sẽ
  1. Giải thích lý do streaming cải thiện UX chatbot một cách đột phá
  2. Bật streaming với stream=True và xử lý event stream
  3. Dùng simplified streaming API (client.messages.stream) để code gọn
  4. Lấy cả streaming text và final complete message cho lưu trữ
  5. Nhận diện 6 loại event trong stream

Mở đầu: 10 giây cứng đơ đủ để mất user

User: "Giải thích quantum computing..."
[Spinner quay ... 8 giây ...]
[Toàn bộ 500 từ hiện ra cùng lúc]

Trải nghiệm B — Có stream:

User: "Giải thích quantum computing..."
"Quantum " (0.3s)
"Quantum computing " (0.5s)
"Quantum computing is a form of " (1s)
...

Data nội bộ Anthropic cho thấy: khi bật streaming, user engagement tăng 40%, bounce rate giảm 50%. Lý do đơn giản — não người ghét waiting, yêu immediate feedback.

Streaming không chỉ là "feature đẹp". Với chatbot, nó là bắt buộc.

Vấn đề: Response time có thể 10-30 giây

Với prompt dài, model lớn, tác vụ phức tạp:

  • Time to first token (TTFT): ~500ms
  • Token per second (TPS) của Sonnet: ~80 tok/s
  • 800 token response = 10 giây generation

Nếu chờ hết → 10s màn hình đơ.

Với streaming:

  • User thấy chữ đầu sau 500ms
  • Đọc dần dần, "thời gian chờ" biến mất về cảm nhận

Cách streaming hoạt động

┌──────────────────────────────────────────────────┐
│                                                  │
│   Non-stream:                                    │
│                                                  │
│   ┌─────────┐             ┌──────────────┐       │
│   │ Request │ ────────▶   │              │       │
│   │         │  đợi 10s    │  Full text   │       │
│   │         │ ◀────────   │              │       │
│   └─────────┘             └──────────────┘       │
│                                                  │
│   Stream:                                        │
│                                                  │
│   ┌─────────┐             ┌─────┐                │
│   │ Request │ ────────▶   │     │ 0.5s: "Quan"   │
│   │         │ ◀────────   └─────┘                │
│   │         │             ┌─────┐                │
│   │         │ ◀────────   │     │ 0.6s: "tum"    │
│   │         │             └─────┘                │
│   │         │ ◀────────   ... tiếp tục           │
│   └─────────┘                                    │
│                                                  │
└──────────────────────────────────────────────────┘

Thay vì 1 HTTP response lớn, bạn nhận nhiều event nhỏ qua Server-Sent Events (SSE).

6 loại event trong stream

Anthropic gửi 6 loại event trong suốt stream:

┌─────────────────────────────────────────────────────┐
│                                                     │
│  1. MessageStart                                    │
│     "Tôi bắt đầu message mới"                       │
│                                                     │
│  2. ContentBlockStart                               │
│     "Bắt đầu 1 block (text / tool / thinking)"      │
│                                                     │
│  3. ContentBlockDelta ← event quan trọng nhất       │
│     "Đây là 1 chunk text"                           │
│     (lặp lại nhiều lần)                             │
│                                                     │
│  4. ContentBlockStop                                │
│     "Kết thúc block này"                            │
│                                                     │
│  5. MessageDelta                                    │
│     "Update stop_reason, usage..."                  │
│                                                     │
│  6. MessageStop                                     │
│     "Hết message"                                   │
│                                                     │
└─────────────────────────────────────────────────────┘

Trong đa số use case, bạn chỉ quan tâm ContentBlockDelta — chứa chunk text thật.

Cách 1: Low-level với stream=True

messages = [{"role": "user", "content": "Count from 1 to 5 slowly"}]

stream = client.messages.create(
    model=model,
    max_tokens=200,
    messages=messages,
    stream=True,  # ← Bật streaming
)

for event in stream:
    print(event)

Output (rút gọn):

MessageStartEvent(message=Message(id='msg_...', ...))
ContentBlockStartEvent(index=0, content_block=TextBlock(text=''))
ContentBlockDeltaEvent(index=0, delta=TextDelta(text='1'))
ContentBlockDeltaEvent(index=0, delta=TextDelta(text=', '))
ContentBlockDeltaEvent(index=0, delta=TextDelta(text='2'))
ContentBlockDeltaEvent(index=0, delta=TextDelta(text=', 3'))
...
ContentBlockStopEvent(index=0)
MessageDeltaEvent(delta={'stop_reason': 'end_turn'})
MessageStopEvent()

Filter chỉ lấy text

stream = client.messages.create(
    model=model,
    max_tokens=200,
    messages=messages,
    stream=True,
)

full_text = ""
for event in stream:
    if event.type == "content_block_delta":
        chunk = event.delta.text
        print(chunk, end="", flush=True)  # In ngay, không newline
        full_text += chunk

print("\n\nFinal text:", full_text)

end="" để không xuống dòng. flush=True để in ngay, không đợi buffer.

Cách 2: High-level với client.messages.stream()

SDK có API gọn hơn qua context manager:

messages = [{"role": "user", "content": "Count from 1 to 5"}]

with client.messages.stream(
    model=model,
    max_tokens=200,
    messages=messages,
) as stream:
    for text in stream.text_stream:
        print(text, end="", flush=True)

Khác biệt:

  • client.messages.stream() (không có .create) — dùng context manager
  • stream.text_stream — iterator chỉ yield text chunk (đã filter)
  • Cleaner, ngắn hơn 50%

Khi nào dùng cách nào?

CáchDùng khi
stream=True + manual filterCần handle tool_use, thinking blocks, edge case
client.messages.stream()Chatbot thường — 95% use case

Lấy complete message sau stream

Bạn cần stream cho UX, nhưng cũng cần full message để:

  • Lưu vào database
  • Append vào messages history cho multi-turn
  • Log usage tokens

Với context manager, sau khi stream xong có stream.get_final_message():

messages = [{"role": "user", "content": "Describe Vietnam briefly"}]

with client.messages.stream(
    model=model,
    max_tokens=500,
    messages=messages,
) as stream:
    # Stream đến user
    for text in stream.text_stream:
        print(text, end="", flush=True)
    
    # Sau khi xong, lấy full message
    final = stream.get_final_message()

print("\n\n=== Message metadata ===")
print(f"Stop reason: {final.stop_reason}")
print(f"Input tokens: {final.usage.input_tokens}")
print(f"Output tokens: {final.usage.output_tokens}")

full_text = final.content[0].text
messages.append({"role": "assistant", "content": full_text})

Best of both worlds: realtime UX + complete metadata.

Integration vào chatbot từ bài 6.8

Update function chat() để support streaming:

def chat_stream(messages: list, verbose: bool = False) -> str:
    """
    Chat với streaming output.
    
    Returns: full response text
    """
    with client.messages.stream(
        model=model,
        max_tokens=1000,
        messages=messages,
    ) as stream:
        # Print từng chunk
        for text in stream.text_stream:
            print(text, end="", flush=True)
        
        # Newline after stream
        print()
        
        # Get metadata
        final = stream.get_final_message()
        
        if verbose:
            print(f"  [tokens: {final.usage.input_tokens}+{final.usage.output_tokens}]")
    
    return final.content[0].text

Dùng trong chatbot loop:

messages = []

while True:
    user_input = input("\nYou: ").strip()
    if not user_input or user_input == "quit":
        break
    
    add_user_message(messages, user_input)
    print("Claude: ", end="")
    
    answer = chat_stream(messages, verbose=True)
    add_assistant_message(messages, answer)

UX lập tức tăng 10x.

Ví dụ thực chiến: Writer assistant với streaming

Tình huống

Bạn build assistant cho copywriter — user paste draft, AI suggest revision. Response thường 500-1000 từ, không stream = đợi quá lâu.

Code

def writer_assistant():
    messages = []
    
    while True:
        draft = input("\nPaste draft (or 'quit'):\n").strip()
        if draft == "quit":
            break
        
        add_user_message(messages, 
            f"Review draft này, đề xuất 3 cải thiện cụ thể:\n\n{draft}")
        
        print("\n🖋️ Assistant review:\n")
        print("-" * 40)
        
        with client.messages.stream(
            model=model,
            max_tokens=2000,
            messages=messages,
        ) as stream:
            for text in stream.text_stream:
                print(text, end="", flush=True)
            
            final = stream.get_final_message()
        
        print("\n" + "-" * 40)
        add_assistant_message(messages, final.content[0].text)

UX khác biệt

Không stream
User paste 500 từ → chờ 12 giây → tất cả hiện ra.
Có stream
User paste 500 từ → 0.5 giây thấy chữ đầu → đọc song song lúc Claude viết → tổng "cảm giác" 3 giây.

Case studies theo ngành

Customer Support — Chatbot web

Không stream
Bounce rate 35% khi response > 5s.
Có stream
Bounce rate giảm còn 12%. Customer cảm thấy "được hồi đáp".

Content — Marketing tool

Task
Generate ad copy 3 variants.
Stream
Writer đọc variant 1 xong thì variant 2 bắt đầu stream → parallel consumption, faster iteration.

Developer Tools — Code assistant

Task
Suggest refactor cho function dài.
Stream critical
Code refactor thường 50-200 dòng. Không stream = dev cảm thấy "tool chậm", switch sang Copilot.

Anti-patterns streaming

Stream vào file log

Hiện tượng
Mỗi chunk log 1 dòng → log file có 500 dòng cho 1 response.
Fix
Collect full_text, log 1 entry khi stream xong.

Stream khi không hiển thị UI

Hiện tượng
Backend batch job dùng stream.
Fix
Backend job dùng non-stream. Stream chỉ cần cho realtime UI.

Forget flush=True

Hiện tượng
Text vẫn hiện "all at once" dù đã bật stream.
Nguyên nhân
stdout buffer chưa flush.
Fix
print(text, end="", flush=True).

Không handle None từ text_stream

Hiện tượng: Occasional crash.

Fix:

for text in stream.text_stream:
    if text:  # ← guard
        print(text, end="", flush=True)

Append chunks vào messages thay vì final text

Hiện tượng:

for text in stream.text_stream:
    messages.append({"role": "assistant", "content": text})

Fix: Dùng stream.get_final_message() sau khi stream xong.

Áp dụng ngay

Bài tập 1: Upgrade chatbot với streaming (20 phút)

Quay lại 02_chat.ipynb (bài 6.8). Update chat() thành chat_stream(). Chạy lại, so sánh feel.

Bài tập 2: Đo TTFT (Time To First Token) (15 phút)

Viết script đo TTFT:

import time

messages = [{"role": "user", "content": "Count to 10"}]

start = time.time()

with client.messages.stream(
    model=model,
    max_tokens=100,
    messages=messages,
) as stream:
    for i, text in enumerate(stream.text_stream):
        if i == 0:
            ttft = time.time() - start
            print(f"\n[TTFT: {ttft*1000:.0f}ms]\n")
        print(text, end="", flush=True)

total = time.time() - start
print(f"\n\n[Total: {total*1000:.0f}ms]")

Chạy 3 lần cho Haiku, Sonnet, Opus. Ghi lại:

ModelTTFTTotal
Haiku______
Sonnet______
Opus______

Insight: Haiku TTFT thường nhanh nhất — là lý do Haiku tốt cho realtime chat.

Mẹo nâng cao

Mẹo 1: Stream vào WebSocket cho web app

from fastapi import WebSocket

async def chat_endpoint(websocket: WebSocket):
    await websocket.accept()
    data = await websocket.receive_json()
    
    with client.messages.stream(
        model=model,
        max_tokens=2000,
        messages=data["messages"],
    ) as stream:
        for text in stream.text_stream:
            await websocket.send_text(text)
    
    await websocket.close()

Mẹo 2: Parse tool_use trong stream

Tool use blocks cũng stream. Nếu bạn làm agent, cần parse:

for event in stream:
    if event.type == "content_block_start":
        if event.content_block.type == "tool_use":
            print(f"\n[Tool call: {event.content_block.name}]")

Chi tiết ở Module 5.

Mẹo 3: Cancel stream giữa chừng

Nếu user click "Stop" giữa chừng, đóng stream:

with client.messages.stream(...) as stream:
    for text in stream.text_stream:
        if user_clicked_stop:
            break  # exit cleanly
        print(text, end="", flush=True)

SDK tự đóng connection khi exit context manager.

Tóm tắt bài học

Những điều cốt lõi
  • Streaming là bắt buộc cho chatbot UX. 40% engagement up, 50% bounce down.
  • 2 cách: stream=True (low-level, full control) hoặc client.messages.stream() (high-level, gọn).
  • 6 loại event, quan tâm nhất là ContentBlockDelta. High-level API đã filter sẵn.
  • stream.get_final_message() = best of both worlds — stream realtime + metadata đầy đủ.
  • Không stream cho batch job. Stream chỉ cho realtime UI.

Tài liệu tham khảo

Bạn đã hoàn thành bài này chưa?Tiến độ lưu trên trình duyệt của bạn.