Structured data — Ép Claude trả JSON/code sạch

3 — Prompt EngineeringTrung cấp20 phút

Bạn build web app tạo AWS EventBridge rule. Flow: 1.

Bạn sẽ học được
  • Giải thích vì sao Claude mặc định hay wrap output trong markdown + giải thích
  • Dùng kỹ thuật assistant prefill + stop sequence để lấy output sạch
  • Áp dụng pattern cho JSON, Python code, CSV, bulleted list
  • Parse và validate output structured trong Python
  • Handle edge case khi output không đúng format

Kỹ thuật: Assistant message prefill

Insight

Claude trong Anthropic API có thể nhận assistant message như "Claude đã bắt đầu viết, bây giờ tiếp tục":

Claude "nghĩ" mình đã bắt đầu code block JSON, và tiếp tục viết JSON (không nói chuyện khác).

Kết hợp với stop sequence

Thêm stop sequence để dừng khi Claude thử đóng code block:

messages = [
    {"role": "user", "content": "Generate EventBridge rule"},
    {"role": "assistant", "content": "```json\n"}  # ← Prefill!
]

Kết hợp với stop sequence

Flow:

Output = pure JSON, không preamble không postamble.

  • User: "Generate EventBridge rule"
  • Assistant (prefill): ` `json\n `
  • Claude tiếp tục: {... JSON content ...}\n
  • Claude định đóng ` ` ` → gặp stop sequence, dừng ngay
msg = client.messages.create(
    model=model,
    max_tokens=1000,
    messages=messages,
    stop_sequences=["```"]
)

Code hoàn chỉnh

def generate_json(user_prompt: str) -> dict:
    """Generate JSON từ user prompt, trả về dict."""
    messages = [
        {"role": "user", "content": user_prompt},
        {"role": "assistant", "content": "```json\n"}
    ]
    
    msg = client.messages.create(
        model=model,
        max_tokens=1000,
        messages=messages,
        stop_sequences=["```"]
    )
    
    raw_text = msg.content[0].text.strip()
    
    import json
    return json.loads(raw_text)


# Dùng
result = generate_json("Generate EventBridge rule for EC2 state change")
print(result)
# {'source': ['aws.ec2'], 'detail-type': ['EC2 Instance State-change Notification']}

Áp dụng cho các format khác

Pattern chung: prefill opener → Claude viết content → stop sequence at closer.

Python code

Bulleted list (no explanation)

messages = [
    {"role": "user", "content": "Write a Python function to calculate fibonacci"},
    {"role": "assistant", "content": "```python\n"}
]
stop_sequences = ["```"]

Bulleted list (no explanation)

Output:

messages = [
    {"role": "user", "content": "List 5 popular JavaScript frameworks"},
    {"role": "assistant", "content": "- "}  # prefill để Claude tiếp tục bullet
]
# Không cần stop sequence — Claude tự dừng sau list

Áp dụng cho các format khác (tiếp)

(Ghi chú: cần re-prepend "- " vào dòng đầu khi parse)

CSV

React
- Vue
- Angular
- Svelte
- Ember

CSV

Claude sẽ tiếp tục viết data rows, không có explanation.

XML

messages = [
    {"role": "user", "content": "Create CSV of 5 countries with capital, population"},
    {"role": "assistant", "content": "country,capital,population\n"}
]

XML

messages = [
    {"role": "user", "content": "Generate product description"},
    {"role": "assistant", "content": "<product>\n  "}
]
stop_sequences = ["</product>"]

Pattern production: Retry + validate

Real-world, cần handle case Claude output không đúng JSON:

Validation schema

Dùng Pydantic để validate output structure:

import json
from typing import Optional

def generate_json_robust(user_prompt: str, max_retries: int = 2) -> Optional[dict]:
    """Generate JSON với retry khi parse fail."""
    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=1000,
            messages=messages,
            stop_sequences=["```"],
            temperature=0  # ← deterministic cho parsing
        )
        
        raw = msg.content[0].text.strip()
        
        try:
            return json.loads(raw)
        except json.JSONDecodeError as e:
            if attempt == max_retries:
                raise
            # Retry với hint về format
            print(f"Retry {attempt + 1}: JSON parse failed: {e}")
    
    return None

Validation schema

from pydantic import BaseModel
from typing import List

class EventBridgeRule(BaseModel):
    source: List[str]
    detail_type: List[str]
    detail: dict = {}

def generate_rule(user_prompt: str) -> EventBridgeRule:
    raw = generate_json_robust(user_prompt)
    return EventBridgeRule(**raw)

# Dùng
rule = generate_rule("EC2 state change")
# Nếu JSON không match schema → ValidationError

Ví dụ thực chiến: Extract dữ liệu từ CV

Tình huống

HR manager nhận 200 CV. Cần extract: tên, email, experience, skills → file Excel.

Setup

Kết quả

200 CV extract trong 10 phút thay vì 2 giờ làm tay. Chi phí: ~$0.50 tổng. Output pandas DataFrame sẵn để analyze.

import json

def extract_cv(cv_text: str) -> dict:
    messages = [
        {"role": "user", "content": f"""Extract structured data from this CV:

<cv>
{cv_text}
</cv>

Return JSON with fields:
- name: string
- email: string
- years_experience: integer
- skills: list of strings
- current_role: string"""},
        {"role": "assistant", "content": "```json\n"}
    ]
    
    msg = client.messages.create(
        model="claude-haiku-4-5",  # Haiku đủ cho task này
        max_tokens=500,
        messages=messages,
        stop_sequences=["```"],
        temperature=0
    )
    
    return json.loads(msg.content[0].text.strip())


# Batch process
cvs = load_cvs_from_folder("cvs/")
results = [extract_cv(cv) for cv in cvs]

# Dump to Excel
import pandas as pd
pd.DataFrame(results).to_excel("cv_data.xlsx")

Case studies theo ngành

💻 Developer — API response generator

Tool: Tool cho dev test API. Input: schema mong muốn. Output: mock JSON.

Prefill: ` `json\n `. Dev copy-paste ngay vào Postman.

📊 Data — CSV cleaner

Task: User paste CSV bị lỗi quote, prompt Claude fix.

Prefill: header row. Claude tiếp tục với data rows sạch.

⚖️ Legal — Clause extraction

Task: Trích các điều khoản của contract vào structured format.

Prefill: <clauses>\n<clause>. Claude output XML structured.

🎧 Support — Ticket classification

Task: Input ticket → output JSON {category, priority, action}.

Prefill: ` `json\n `. Parser tự extract, push vào DB.

Anti-patterns

❌ Quên stop sequence

Fix: Luôn kèm stop_sequences=["`"].

❌ Prefill quá cụ thể

messages = [
    {"role": "user", "content": "Gen JSON"},
    {"role": "assistant", "content": "```json\n"}
]
# Không có stop_sequences → Claude tiếp tục viết "```\n\nThis JSON..."

❌ Prefill quá cụ thể

Vấn đề: Ép Claude phải tiếp tục từ "source" — có thể không phù hợp với mọi input.

Fix: Chỉ prefill wrapper (``json\n``), để Claude tự generate structure.

❌ Không handle JSON parse fail

{"role": "assistant", "content": "```json\n{\"source\": "}

❌ Không handle JSON parse fail

Fix: Try-except + retry với hint về format.

❌ Dùng temperature cao cho parsing

result = json.loads(msg.content[0].text.strip())  # ← crash nếu malformed

❌ Dùng temperature cao cho parsing

Fix: temperature=0 cho tác vụ cần deterministic.

❌ Prefill nhưng assistant thấy mất context

temperature=1  # ← random, output không reliable cho parse

❌ Prefill nhưng assistant thấy mất context

OK. Nhưng nếu prefill không ở cuối → Claude confused.

Fix: Prefill luôn là message cuối cùng.

# Multi-turn
messages = [
    {"role": "user", "content": "Topic A"},
    {"role": "assistant", "content": "Response A"},
    {"role": "user", "content": "Now give JSON"},
    {"role": "assistant", "content": "```json\n"}  ← OK
]

Áp dụng ngay

Bài tập 1: Generate 3 loại structured data (25 phút)

Trong notebook, tạo 3 function:

Test mỗi function với 3 input. Verify output parse được không lỗi.

Bài tập 2: Retry logic (15 phút)

Mở rộng generate_json_robust để:

  • Log attempt number + error mỗi lần retry
  • Sau 2 lần fail, gửi user prompt kèm lỗi JSON (hint cho Claude fix)
  • Max 3 attempts, raise exception nếu vẫn fail
def generate_product_json(description: str) -> dict:
    """Return JSON {name, price, features}"""
    # ...

def generate_sql_query(natural_lang: str) -> str:
    """Return clean SQL string"""
    # ...

def generate_csv(topic: str, num_rows: int) -> str:
    """Return CSV string"""
    # ...

Bài tập 2: Retry logic (15 phút)

def generate_json_v2(user_prompt: str, max_retries: int = 3) -> dict:
    # ...

Mẹo nâng cao

Mẹo 1: JSON schema trong prompt

Claude follow schema tốt hơn.

Mẹo 2: Structured outputs (future)

Anthropic có thể thêm feature response_format tương tự OpenAI — force schema ở API level. Check docs cập nhật.

Mẹo 3: Dùng tool_use cho structured output

Thay vì prefill, define 1 tool với schema. Claude buộc phải call tool với args theo schema. Output 100% đúng format. Chi tiết ở Module 5.

user_prompt = f"""Extract data from: {text}

Return JSON matching this exact schema:
{{
  "name": string,
  "age": integer,
  "emails": [string]
}}"""

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

🎯 Claude mặc định wrap output trong markdown + explanation — tốt cho chat, tệ cho app.

🎯 Prefill + stop sequence là pattern chuẩn để lấy raw structured output.

🎯 Áp dụng cho JSON, code, CSV, XML — chỉ đổi prefill content và stop sequence.

🎯 Production phải có validation + retry + temp=0 để reliable.

🎯 Tool use là pattern stronger cho structured output (Module 5).

Tài liệu tham khảo
  • Message prefill docs
  • Stop sequences
Nội dung này có hữu ích không?