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:
- 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)
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:
- 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
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:
- 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
messagescuối để xem đầy đủ history - Tính tổng
input_tokensvàoutput_tokensqua 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
- 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.