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

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

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

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:

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

Mở đầu: Cuộc trò chuyện với người mất trí nhớ mỗi giây

Bạn: "Giải thích quantum computing." Họ: "Quantum computing là..." Bạn: "Viết 1 câu nữa đi." Họ: "Về cái gì?

Đó chính xác là tình trạng mặc định của Claude API — stateless hoàn toàn. Mỗi request là một cuộc gặp mới, Claude không nhớ đã nói gì trước đó.

Bài này dạy bạn cách "ghi nhớ giùm Claude" — tự duy trì history trong code, gửi lại mỗi lần request. Đây là nền tảng cho mọi app chat, RAG, agent bạn xây sau này.

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ì:

  • 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)

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

Trade-off

┌────────────────────────────────────┐
│  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     │
└────────────────────────────────────┘

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.

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

┌──────────────────────────────────────────────────────┐
│                                                      │
│   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)                          │
│   ...                                                │
│                                                      │
└──────────────────────────────────────────────────────┘

Nguyên tắc:

  1. Sau mỗi response từ Claude, append vào messages với role=assistant
  2. Khi user gửi tiếp, append với role=user
  3. Gửi TOÀN BỘ list qua mỗi request

3 helper function chuẩn

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

from dotenv import load_dotenv
load_dotenv()
from anthropic import Anthropic

client = Anthropic()
model = "claude-sonnet-4-6-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

messages = []

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

add_user_message(messages, "Write another sentence")
answer = chat(messages)
print("Claude:", answer)
add_assistant_message(messages, answer)

print(f"\nTotal turns: {len(messages)}")

Output dự kiến:

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

Total turns: 4

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

Pattern nâng cao: Interactive loop

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

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()

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ữ.

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ộ:

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

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:

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:]

Đơ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 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."
    }]

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.

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

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

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)

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)

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

Output dự kiến

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!

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ì.

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:

messages = []
add_user_message(messages, "Hi")
answer = chat(messages)

add_user_message(messages, "Remember what I said?")

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

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

messages.append({"role": "system", "content": "..."})

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

messages.append({"role": "user", "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

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)

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

Áp dụng ngay

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

Trong notebook mới 02_chat.ipynb:

  1. Define 3 helper functions (copy từ bài)
  2. Tạo messages rỗng
  3. Làm 5 turn với Claude về chủ đề yêu thích (ví dụ: bóng đá, nấu ăn)
  4. In messages cuối để xem đầy đủ history
  5. Tính tổng input_tokens và output_tokens qua 5 turn

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

Mở rộng helper chat():

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

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

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

Những điều cốt lõi
  • 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

Kiểm tra nhanh1 câu hỏi · chọn đáp án để xem giải thích
    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.