Bài tập — Xây chatbot đầu tiên của bạn

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

Bạn đã học 7 bài đầu tiên. Kiến thức chắc về: - Cách gọi API (bài 6.6) - Multi-turn conversation (bài 6.7) - Helper functions chuẩn

Bạn sẽ học được
  • Ship một chatbot hoàn chỉnh có thể chạy end-to-end
  • Áp dụng pattern multi-turn từ bài 6.7 vào use case thật
  • Debug các lỗi phổ biến khi chạy chatbot (role alternating, token limit)
  • Đo performance của chatbot: latency, token usage, cost

Đề bài

Xây một chatbot "Travel Assistant" trong Jupyter notebook. Yêu cầu:

Tính năng bắt buộc

Tính năng optional (điểm cộng)

  • Multi-turn: nhớ context xuyên suốt phiên chat
  • Interactive: user gõ input, Claude trả lời, lặp lại đến khi user gõ "quit"
  • Giới hạn scope: Chatbot chỉ trả lời về du lịch (xin gợi ý địa điểm, lịch trình, ẩm thực). Nếu user hỏi off-topic, Claude phải từ chối lịch sự.
  • Log: In token usage mỗi turn để theo dõi cost
  • Sliding window history (giữ 10 turn gần nhất)
  • Xử lý edge case: input rỗng, stop_reason=max_tokens
  • Thống kê cuối session: tổng token, số turn, cost ước tính

Hướng dẫn step-by-step

Bước 1: Setup boilerplate

Bước 2: Viết hàm chat với logging

from dotenv import load_dotenv
load_dotenv()
from anthropic import Anthropic

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

def add_user_message(messages, text):
    messages.append({"role": "user", "content": text})

def add_assistant_message(messages, text):
    messages.append({"role": "assistant", "content": text})

Bước 2: Viết hàm chat với logging

Bước 3: Viết hàm main loop

total_input_tokens = 0
total_output_tokens = 0

def chat(messages):
    global total_input_tokens, total_output_tokens
    
    msg = client.messages.create(
        model=model,
        max_tokens=1000,
        messages=messages,
    )
    
    # Log token
    total_input_tokens += msg.usage.input_tokens
    total_output_tokens += msg.usage.output_tokens
    
    print(f"  [tokens: in={msg.usage.input_tokens}, out={msg.usage.output_tokens}]")
    
    # Check truncation
    if msg.stop_reason == "max_tokens":
        print("  ⚠️ Response bị cắt! Cần tăng max_tokens.")
    
    return msg.content[0].text

Bước 3: Viết hàm main loop

Bước 4: Chạy

def travel_chatbot():
    messages = []
    
    # Giới thiệu scope qua prompt đầu
    intro = """Bạn là Travel Assistant. Chỉ trả lời câu hỏi về du lịch:
- Gợi ý địa điểm
- Lập lịch trình
- Gợi ý ẩm thực địa phương
- Mẹo du lịch

Nếu user hỏi chủ đề khác (code, finance, relationship...), 
hãy lịch sự từ chối và hướng về chủ đề du lịch."""
    
    # Cách "hack": đưa intro vào user message đầu + assistant ack
    add_user_message(messages, intro)
    add_assistant_message(messages, 
        "Chào bạn! Tôi là Travel Assistant. Hãy hỏi tôi về du lịch — điểm đến, lịch trình, ẩm thực. Bạn cần tư vấn chuyến đi nào?")
    
    print("🧭 Travel Assistant đã sẵn sàng. Gõ 'quit' để thoát.\n")
    print(messages[-1]["content"])
    print()
    
    while True:
        user_input = input("You: ").strip()
        if user_input.lower() in ("quit", "exit", "bye"):
            break
        if not user_input:
            print("  (input rỗng, skip)")
            continue
        
        add_user_message(messages, user_input)
        
        try:
            answer = chat(messages)
            add_assistant_message(messages, answer)
            print(f"\nTravel Assistant: {answer}\n")
        except Exception as e:
            print(f"  ❌ Lỗi: {e}")
            # Remove user message cuối để giữ history hợp lệ
            messages.pop()
    
    # Thống kê cuối
    print("\n" + "=" * 50)
    print(f"Session ended.")
    print(f"Total turns: {(len(messages) - 2) // 2}")  # trừ 2 intro message
    print(f"Total tokens: input={total_input_tokens}, output={total_output_tokens}")
    
    # Cost estimate (Sonnet 5)
    cost = (total_input_tokens * 3 + total_output_tokens * 15) / 1_000_000
    print(f"Estimated cost: ${cost:.4f}")

Bước 4: Chạy

Bước 5: Test flow

Thử các case:

  • ✅ "Tôi muốn đi Đà Nẵng 3 ngày, có gợi ý gì?"
  • ✅ "Nên ăn gì ở đó?" (test memory)
  • ✅ "Ngân sách khoảng 5 triệu, đủ không?" (test context)
  • ❌ "Giúp tôi viết Python function" (test scope limit)
  • ✅ "Quit"
travel_chatbot()

Phiên bản nâng cao (optional)

Sliding window

Validation

MAX_HISTORY = 20  # 10 turn × 2 messages

def chat_with_window(messages):
    # Giữ 2 message intro + MAX_HISTORY messages gần nhất
    if len(messages) > MAX_HISTORY + 2:
        trimmed = messages[:2] + messages[-MAX_HISTORY:]
    else:
        trimmed = messages
    
    msg = client.messages.create(
        model=model,
        max_tokens=1000,
        messages=trimmed,
    )
    return msg.content[0].text

Validation

Retry logic

def validate_messages(messages):
    """Check role alternating + first=user."""
    if not messages:
        return
    assert messages[0]["role"] == "user", "First message must be user"
    for i in range(1, len(messages)):
        assert messages[i]["role"] != messages[i-1]["role"], \
            f"Role not alternating at index {i}"

Retry logic

import time
from anthropic import RateLimitError, APIError

def chat_with_retry(messages, max_retries=3):
    for attempt in range(max_retries):
        try:
            return chat(messages)
        except RateLimitError:
            wait = 2 ** attempt  # exponential backoff
            print(f"  Rate limited. Retry in {wait}s...")
            time.sleep(wait)
        except APIError as e:
            if attempt == max_retries - 1:
                raise
            print(f"  API error: {e}. Retrying...")
    return None

Case studies tương tự bạn có thể thử sau

📚 Education — Study buddy

Same pattern nhưng scope "giải thích khái niệm học thuật". Tương tác như tutor ở bài 6.7.

💼 Sales — Discovery prep

Scope: gợi ý câu hỏi discovery. Input: thông tin prospect. Output: 10 câu hỏi.

🍳 Cooking assistant

Scope: công thức nấu ăn, thay thế nguyên liệu. "Tôi có gì trong tủ lạnh → món gì?"

💪 Fitness coach

Scope: lập plan tập. Track progress qua history.

Mỗi use case đều dùng chính pattern bạn vừa học — chỉ khác intro prompt và scope.

Debug guide — Khi gặp lỗi

Lỗi 1: BadRequestError: messages: first message must use "user" role

Nguyên nhân: Có thể bạn append assistant trước user.

Fix: Kiểm tra messages[0]["role"].

Lỗi 2: BadRequestError: messages: all messages must have non-empty content

Nguyên nhân: User gõ empty string (enter không có text).

Fix: Validate user_input.strip() trước khi append.

Lỗi 3: AuthenticationError

Nguyên nhân: API key không load.

Fix:

Lỗi 4: Claude "quên" mặc dù có history

Nguyên nhân: Quên append assistant message sau mỗi turn.

Fix: Cẩn thận với flow: add_user → chat → add_assistant.

Lỗi 5: Response bị cắt

Nguyên nhân: max_tokens quá thấp.

Fix: Tăng lên 2000-4000. Check stop_reason.

import os
print("Key loaded:", bool(os.getenv("ANTHROPIC_API_KEY")))

Self-review checklist

Trước khi consider "done", check:

Functional

Code quality

Edge cases

  • [ ] Chatbot trả lời đúng topic (du lịch)
  • [ ] Từ chối off-topic lịch sự
  • [ ] Nhớ context qua nhiều turn
  • [ ] Quit command hoạt động
  • [ ] 3 helper function tách biệt
  • [ ] Có exception handling
  • [ ] In token usage mỗi turn
  • [ ] Thống kê cuối session
  • [ ] Empty input không crash
  • [ ] Long response không bị cắt giữa chừng
  • [ ] Có thể chat 20+ turn mà không lỗi

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

🎯 Đây là milestone đầu tiên — bạn đã ship 1 chatbot production-ready ở mức sơ khai.

🎯 Pattern bạn viết có thể adapt cho mọi use case chatbot: thay intro, thay scope.

🎯 Logging token là thói quen cần duy trì — không có log = không biết cost.

🎯 Validation + retry là 2 layer bảo vệ mà hầu hết app production cần.

🎯 Sliding window đủ cho 80% chatbot casual. Summarization cần cho chatbot dài.

Tài liệu tham khảo
  • anthropic-cookbook/basic_chat — nhiều ví dụ chatbot
  • Rate limit handling
Nội dung này có hữu ích không?