Hội thoại nhiều lượt (Multi-Turn Conversations)

2 — Gọi Claude qua APICơ bản20 phút

Hãy tưởng tượng bạn đang trò chuyện với một người bạn có chứng mất trí nhớ từng giây — mỗi khi bạn dứt lời, họ reset hoàn toàn. Bạn hỏi:

Bạn sẽ học được
  • Giải thích tại sao Claude API stateless — không tự nhớ conversation
  • Tự quản lý message history trong code Python
  • Viết 3 helper function tái sử dụng: add_user_message, add_assistant_message, chat
  • Nhận diện và tránh các lỗi phổ biến với multi-turn (role alternating, history bloat)

Tại sao Claude lại stateless?

3 lý do thiết kế

1. Scalability

Stateful system cần lưu state cho từng user — scale ra triệu user = khó khăn ngàn lần. Stateless = mỗi request độc lập, có thể phân phối đến bất kỳ server nào.

2. Privacy

Nếu Anthropic tự lưu history, họ phải lưu data của bạn. Stateless = data về user ở server của bạn, Anthropic chỉ thấy trong 1 request rồi xóa.

3. Flexibility

Bạn tự quyết định gửi history gì:

Stateful system không cho bạn control này.

Trade-off

Vấn đề cuối — token cost — là chi phí bạn phải trả. Bài 6.47-6.49 (Prompt caching) sẽ giải quyết phần lớn.

  • Full history? (đắt)
  • Chỉ 10 turn gần nhất? (tiết kiệm)
  • Tóm tắt phần cũ + chi tiết phần mới? (hybrid)
┌────────────────────────────────────┐
│  STATELESS (Anthropic API)         │
├────────────────────────────────────┤
│  ✅ Scale dễ                        │
│  ✅ Privacy rõ ràng                 │
│  ✅ Flexibility cao                 │
│  ❌ Dev phải tự quản history        │
│  ❌ Token cost dồn lên user lâu     │
└────────────────────────────────────┘

Cách multi-turn hoạt động

Nguyên tắc:

  • Sau mỗi response từ Claude, append vào messages với role=assistant
  • Khi user gửi tiếp, append với role=user
  • Gửi TOÀN BỘ list qua mỗi request
┌──────────────────────────────────────────────────────┐
│                                                      │
│   Turn 1:                                            │
│   messages = [                                       │
│     { role: "user",       content: "What is 2+2?" }  │
│   ]                                                  │
│   ── gửi lên Anthropic ──                            │
│   ◀── "2+2 = 4"                                      │
│                                                      │
│   Turn 2:                                            │
│   messages = [                                       │
│     { role: "user",       content: "What is 2+2?" }, │
│     { role: "assistant",  content: "2+2 = 4" },      │
│     { role: "user",       content: "What about 3+3?"}│
│   ]                                                  │
│   ── gửi lên Anthropic ──                            │
│   ◀── "3+3 = 6"                                      │
│                                                      │
│   Turn 3: (tiếp tục append)                          │
│   ...                                                │
│                                                      │
└──────────────────────────────────────────────────────┘

3 helper function chuẩn

Viết 3 function sau ở đầu notebook, dùng lại cho mọi bài sau:

Cách dùng

from dotenv import load_dotenv
load_dotenv()
from anthropic import Anthropic

client = Anthropic()
model = "claude-sonnet-5-20260205"


def add_user_message(messages: list, text: str):
    """Append user message vào list."""
    messages.append({"role": "user", "content": text})


def add_assistant_message(messages: list, text: str):
    """Append assistant message vào list."""
    messages.append({"role": "assistant", "content": text})


def chat(messages: list) -> str:
    """Gửi messages lên Claude, trả về text response."""
    msg = client.messages.create(
        model=model,
        max_tokens=1000,
        messages=messages,
    )
    return msg.content[0].text

Cách dùng

Output dự kiến:

messages = []

# Turn 1
add_user_message(messages, "Define quantum computing in one sentence")
answer = chat(messages)
print("Claude:", answer)
add_assistant_message(messages, answer)

# Turn 2 — Claude BIẾT context nhờ history
add_user_message(messages, "Write another sentence")
answer = chat(messages)
print("Claude:", answer)
add_assistant_message(messages, answer)

# In để check
print(f"\nTotal turns: {len(messages)}")

3 helper function chuẩn (tiếp)

Claude hiểu "another sentence" nghĩa là "về quantum computing" — vì nó thấy full context trong messages.

Claude: Quantum computing is a form of computation...
Claude: Unlike classical computers that use bits...

Total turns: 4

Pattern nâng cao: Interactive loop

Chatbot thật cần vòng lặp đọc input → gửi → in response → lặp.

Chạy trong terminal hoặc Jupyter. Gõ câu hỏi, nhấn Enter, Claude trả lời, gõ tiếp — context được giữ.

def chat_loop():
    messages = []
    print("Chat with Claude. Type 'quit' to exit.\n")
    
    while True:
        user_input = input("You: ")
        if user_input.lower() in ("quit", "exit"):
            break
        
        add_user_message(messages, user_input)
        answer = chat(messages)
        add_assistant_message(messages, answer)
        print(f"Claude: {answer}\n")

chat_loop()

Vấn đề: History dài vô tận

Sau 50 turn, messages có 100 element (50 user + 50 assistant). Mỗi request gửi toàn bộ:

Chi phí scale O(n²) với số turn — không sustainable.

3 chiến lược quản lý history

Chiến lược 1: Sliding window

Chỉ giữ N turn gần nhất:

Turn 1:  input_tokens = 10
Turn 2:  input_tokens = 30
Turn 10: input_tokens = 500
Turn 50: input_tokens = 5000
Turn 100: input_tokens = 15000

3 chiến lược quản lý history

Đơn giản, mất context xa. Phù hợp chatbot casual.

Chiến lược 2: Summarization

Mỗi 20 turn, yêu cầu Claude tóm tắt phần cũ:

def trim_history(messages: list, max_turns: int = 20):
    """Giữ max_turns × 2 messages gần nhất."""
    max_messages = max_turns * 2
    if len(messages) > max_messages:
        messages[:] = messages[-max_messages:]

Vấn đề: History dài vô tận (tiếp)

Giữ context xa, tốn thêm API call. Phù hợp chatbot nghiêm túc.

Chiến lược 3: Prompt caching

Bật prompt caching cho system prompt + static context → chỉ tính tiền incremental. Chi tiết ở bài 6.47-6.49.

Production-grade. Dùng khi traffic lớn.

def summarize_history(messages: list):
    """Nén history cũ thành 1 system-like message."""
    if len(messages) < 40:
        return  # Chưa cần
    
    old_messages = messages[:20]
    # Gọi Claude tóm tắt
    summary = chat(old_messages + [{
        "role": "user",
        "content": "Summarize the above conversation in 200 words."
    }])
    # Replace 20 cũ bằng 1 summary message
    messages[:20] = [{
        "role": "user",
        "content": f"[Context tóm tắt từ conversation cũ: {summary}]"
    }, {
        "role": "assistant",
        "content": "Understood, I'll continue with this context."
    }]

Ví dụ thực chiến: Tutor AI toán

Tình huống

Bạn xây AI tutor cho học sinh cấp 2 giải toán. Học sinh hỏi, AI gợi ý từng bước.

Code

Output dự kiến

messages = []
system = None  # sẽ học ở bài 6.10

# Turn 1
add_user_message(messages, "Giúp tôi giải 5x + 2 = 3")
ans = chat(messages)
print(f"Tutor: {ans}\n")
add_assistant_message(messages, ans)

# Turn 2 — học sinh tiếp theo step
add_user_message(messages, "Tôi trừ 2 cả 2 vế → 5x = 1")
ans = chat(messages)
print(f"Tutor: {ans}\n")
add_assistant_message(messages, ans)

# Turn 3
add_user_message(messages, "Chia cả 2 cho 5")
ans = chat(messages)
print(f"Tutor: {ans}\n")

Output dự kiến

Tutor hiểu progression vì có full history. Nếu không có history, turn 2 "Tôi trừ 2 cả 2 vế" → Claude không biết đang nói về phương trình gì.

Tutor: Gợi ý: để giải 5x + 2 = 3, bạn cần isolate x. 
       Bước đầu tiên có thể làm gì để loại bỏ số 2?

Tutor: Chính xác! Bây giờ bạn có 5x = 1. 
       Làm sao để lấy chỉ riêng x?

Tutor: Đúng rồi! x = 1/5 = 0.2. Bạn đã giải được!

Case studies theo ngành

💼 Sales — Discovery call assistant

Tình huống: Sales rep chat với AI trước call để brainstorm câu hỏi discovery.

Setup: Multi-turn với history. Rep mô tả company, AI gợi ý questions. Rep refine, AI adjust.

Kết quả: 30 phút prep → 10 phút, với question bank tùy chỉnh theo prospect.

🎧 Customer Support — Contextual resolution

Tình huống: Customer báo issue, AI hỏi clarify, customer trả lời, AI gợi fix.

Key: Mỗi turn AI cần nhớ issue gốc + clarifications. Không có history → hỏi lại vô nghĩa.

📝 Content — Iterative writing

Tình huống: Writer feedback "phần 2 viết lại theo hướng casual hơn", AI làm, writer ok phần 2 nhưng muốn fix phần 3.

Key: AI cần nhớ bản nháp hiện tại + feedback trước đó. Multi-turn cứu.

Anti-patterns với multi-turn

❌ Quên append assistant message

Hiện tượng:

Fix: Luôn có cặp add_user_message + chat + add_assistant_message.

❌ Thêm role khác ngoài user/assistant

messages = []
add_user_message(messages, "Hi")
answer = chat(messages)
# ← QUÊN add_assistant_message!

add_user_message(messages, "Remember what I said?")
# messages = [user1, user2] → 2 user liên tiếp → API error

❌ Thêm role khác ngoài user/assistant

Fix: System prompt đi qua tham số system riêng (bài 6.10), không vào messages.

❌ Gửi empty string làm content

# ❌ Không có role "system" trong messages
messages.append({"role": "system", "content": "..."})

❌ Gửi empty string làm content

Fix: Validate input trước khi append.

❌ Không trim history, bill blowup

Hiện tượng: Chatbot public, user chat 200 turn → 1 user đốt $5.

Fix: Trim history, hoặc dùng caching.

❌ Append response mà chưa check stop_reason

# ❌ Không hợp lệ
messages.append({"role": "user", "content": ""})

❌ Append response mà chưa check stop_reason

Fix: Skip append nếu truncated, hoặc retry với max_tokens lớn hơn.

msg = client.messages.create(...)
if msg.stop_reason == "max_tokens":
    # Response bị cắt — append vào history sẽ misleading
    pass
add_assistant_message(messages, msg.content[0].text)

Áp dụng ngay

Bài tập 1: Xây chatbot 5-turn (20 phút)

Trong notebook mới 02_chat.ipynb:

Bài tập 2: Implement sliding window (15 phút)

Mở rộng helper chat():

Test bằng cách chat 30 turn, verify Claude "quên" turn 1-10 nhưng nhớ turn 20-30.

  • Define 3 helper functions (copy từ bài)
  • Tạo messages rỗng
  • Làm 5 turn với Claude về chủ đề yêu thích (ví dụ: bóng đá, nấu ăn)
  • In messages cuối để xem đầy đủ history
  • Tính tổng input_tokens và output_tokens qua 5 turn
def chat(messages: list, max_history: int = 20) -> str:
    """Chat với sliding window cho history."""
    # Giữ max_history turns gần nhất (mỗi turn = 2 messages)
    if len(messages) > max_history * 2:
        trimmed = messages[-max_history * 2:]
    else:
        trimmed = messages
    
    msg = client.messages.create(
        model=model,
        max_tokens=1000,
        messages=trimmed,
    )
    return msg.content[0].text

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

🎯 Claude API stateless — bạn tự quản lý history, gửi toàn bộ qua mỗi request.

🎯 3 helper function đủ cho 95% chatbot: add_user_message, add_assistant_message, chat.

🎯 Role phải alternating user ↔ assistant. Quên append assistant là bug phổ biến #1.

🎯 History dài → cost scale O(n²). Chiến lược: sliding window, summarization, caching.

🎯 Multi-turn = foundation cho mọi bài sau: streaming, tool use, RAG, agents. Không skip.

Tài liệu tham khảo
  • Conversation history best practices
  • Managing long conversations
Nội dung này có hữu ích không?