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

6.15 · Bài tập — Structured data cho 3 use case

Bài viếtTrung cấp45 phút đọc

Hoàn thành 3 task. Mỗi task tự code + test trong notebook 03_structured.ipynb.

Sau bài này, bạn sẽ
  1. Áp dụng pattern prefill + stop_sequence cho 3 use case khác nhau
  2. Viết validation function cho output structured
  3. Handle edge case parse fail
  4. Build reusable utility function cho structured extraction

Task 1: Recipe extractor

Input: Text mô tả món ăn (paragraph tiếng Việt).

Output JSON:

{
  "dish_name": "Phở bò",
  "prep_time_minutes": 30,
  "cook_time_minutes": 120,
  "servings": 4,
  "ingredients": [
    {"name": "xương bò", "amount": 1, "unit": "kg"},
    {"name": "bánh phở", "amount": 400, "unit": "g"}
  ],
  "steps": [
    "Ninh xương 2 giờ",
    "Nêm gia vị..."
  ]
}

Test input:

"Phở bò truyền thống Hà Nội. Chuẩn bị 30 phút, nấu 2 tiếng. 
Cho 4 người ăn. Cần 1kg xương bò, 400g bánh phở, hành tím, gừng nướng. 
Đầu tiên ninh xương 2 giờ với gừng hành nướng, sau đó lọc lấy nước dùng..."

Validation:

  • prep_time_minutes và cook_time_minutes là int
  • ingredients có ít nhất 3 phần tử, mỗi phần tử có name, amount, unit
  • steps là list strings

Starter code:

def extract_recipe(text: str) -> dict:
    messages = [
        {"role": "user", "content": f"Extract recipe data:\n\n{text}"},
        {"role": "assistant", "content": "```json\n"}
    ]
    # TODO: call client.messages.create with stop_sequences
    # TODO: parse json
    # TODO: validate
    pass

Task 2: SQL query generator

Input
Schema + natural language question (tiếng Việt).
Output
Pure SQL string (không markdown, không comment).

Test input:

Schema:
- users(id, name, email, created_at, country)
- orders(id, user_id, amount, status, created_at)

Question: "Liệt kê top 5 user ở Việt Nam có tổng order lớn nhất tháng 3/2026"

Expected output (ví dụ):

SELECT u.id, u.name, SUM(o.amount) as total
FROM users u
JOIN orders o ON u.id = o.user_id
WHERE u.country = 'Vietnam'
  AND o.created_at >= '2026-03-01'
  AND o.created_at < '2026-04-01'
  AND o.status = 'completed'
GROUP BY u.id, u.name
ORDER BY total DESC
LIMIT 5;

Validation:

  • Output chứa keyword SELECT
  • Không chứa ` ` ` hoặc explain
  • Kết thúc với ;

Gợi ý:

  • Prefill ` `sql\n `
  • stop_sequences=["`"]
  • Temperature=0

Task 3: Tweet sentiment batch classifier

Input
List 10 tweet.
Output
List 10 object {tweet, sentiment, confidence}.
[
  {"tweet": "Yêu app này quá!", "sentiment": "positive", "confidence": 0.95},
  {"tweet": "Lại crash nữa rồi", "sentiment": "negative", "confidence": 0.92},
  ...
]

Validation:

  • Length list output = length input
  • Mỗi sentiment thuộc {"positive", "negative", "neutral"}
  • confidence là float 0-1

Challenge: Batch-process 10 tweet trong 1 call (tiết kiệm token vs 10 calls).

Gợi ý:

  • Pass list tweet vào user message dạng numbered list
  • Prefill ` `json\n[ `
  • Stop sequence ` ` `

Skeleton hoàn chỉnh

import json
from anthropic import Anthropic

client = Anthropic()
model = "claude-sonnet-4-6-20260205"


def call_json(user_prompt: str, max_retries: int = 2) -> dict:
    """Utility chung: call Claude, get JSON."""
    for attempt in range(max_retries + 1):
        messages = [
            {"role": "user", "content": user_prompt},
            {"role": "assistant", "content": "```json\n"}
        ]
        msg = client.messages.create(
            model=model,
            max_tokens=2000,
            messages=messages,
            stop_sequences=["```"],
            temperature=0
        )
        raw = msg.content[0].text.strip()
        try:
            return json.loads(raw)
        except json.JSONDecodeError as e:
            if attempt == max_retries:
                print(f"Failed after {max_retries} retries: {raw}")
                raise
            print(f"Retry {attempt + 1}")
    return None


def extract_recipe(text: str) -> dict:
    prompt = f"""Extract recipe từ text Vietnamese sau:

<text>
{text}
</text>

Schema:
- dish_name: string
- prep_time_minutes: int
- cook_time_minutes: int
- servings: int
- ingredients: [{{name, amount, unit}}]
- steps: [string]"""
    return call_json(prompt)


def validate_recipe(recipe: dict) -> bool:
    assert isinstance(recipe["prep_time_minutes"], int)
    assert isinstance(recipe["cook_time_minutes"], int)
    assert len(recipe["ingredients"]) >= 3
    for ing in recipe["ingredients"]:
        assert all(k in ing for k in ["name", "amount", "unit"])
    return True


def generate_sql(schema: str, question: str) -> str:
    messages = [
        {"role": "user", "content": f"""Schema:
{schema}

Question: {question}

Generate clean SQL query."""},
        {"role": "assistant", "content": "```sql\n"}
    ]
    msg = client.messages.create(
        model=model,
        max_tokens=500,
        messages=messages,
        stop_sequences=["```"],
        temperature=0
    )
    return msg.content[0].text.strip()


def classify_tweets(tweets: list) -> list:
    numbered = "\n".join(f"{i+1}. {t}" for i, t in enumerate(tweets))
    prompt = f"""Classify sentiment của mỗi tweet sau. Return JSON array.

Tweets:
{numbered}

Schema cho mỗi element: {{tweet: string, sentiment: "positive|negative|neutral", confidence: 0-1}}"""
    messages = [
        {"role": "user", "content": prompt},
        {"role": "assistant", "content": "```json\n"}
    ]
    msg = client.messages.create(
        model=model,
        max_tokens=2000,
        messages=messages,
        stop_sequences=["```"],
        temperature=0
    )
    return json.loads(msg.content[0].text.strip())


recipe = extract_recipe("Phở bò truyền thống Hà Nội...")
print(recipe)
validate_recipe(recipe)

sql = generate_sql("users(id, name, country)\norders(id, user_id, amount)",
                    "Top 5 user Vietnam có tổng order nhiều nhất")
print(sql)

tweets = [
    "Yêu app này quá!",
    "Lại crash nữa rồi",
    "OK, không tệ",
    # ... thêm 7 tweets
]
results = classify_tweets(tweets)
print(json.dumps(results, ensure_ascii=False, indent=2))

Self-review checklist

  • 3 task đều chạy được không crash
  • Output match schema mong đợi
  • Validation catch malformed output
  • Retry logic triggered ít nhất 1 lần (có thể test bằng sabotage temperature=1)
  • call_json là utility tái sử dụng được cho task khác

Mẹo debug

Nếu JSON parse fail liên tục

  1. In raw output xem Claude trả về gì:
print("Raw:", repr(raw))
  1. Check quote: đôi khi Claude dùng smart quote "..." thay vì ASCII "...":
raw = raw.replace(""", '"').replace(""", '"')
  1. Check trailing comma: JSON không chấp nhận {"a": 1,}:
import re
raw = re.sub(r",\s*([}\]])", r"\1", raw)

Nếu SQL có markdown

  • Check stop_sequences đã set đúng chưa
  • Check prefill là ` `sql\n ` (có newline)

Nếu batch classification output sai length

  • Prompt explicit: "Return exactly 10 items"
  • Include test count trong prompt

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

Những điều cốt lõi
  • 3 task = 3 pattern structured output — JSON nested, SQL plain, JSON array batch.
  • call_json utility — abstract pattern common, dùng cho mọi task JSON sau.
  • Validation là safety net — catch output không đúng trước khi dùng downstream.
  • Batch processing tiết kiệm token — 10 tweet / 1 call < 10 calls / 1 tweet.
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.