Learn > Claude > Claude API cơ bản — vòng đời request, multi-turn, streaming & structured data

Claude API cơ bản — vòng đời request, multi-turn, streaming & structured data

Phần 1/7 ghi chú Building with the Claude API: vòng đời request 5 bước, vì sao cần server riêng, API key, hội thoại multi-turn, system prompt, temperature, response streaming và ép Claude trả structured data.

  • Mọi request đi qua server riêng (giữ API key bí mật) với 4 field bắt buộc; Claude xử lý qua tokenization → embedding → contextualization → generation rồi dừng theo stop_reason.
  • Claude stateless — phải tự gửi lại toàn bộ message history mỗi lượt; input token tích luỹ ~O(n²) khi hội thoại dài.
  • System prompt điều khiển CÁCH trả lời, temperature chỉnh ngẫu nhiên ↔ xác định, streaming hiện chữ dần; prefilling + stop_sequences ép Claude trả raw data sạch.

TL;DR — Nền tảng gọi Claude API: request đi 5 bước qua server riêng (giữ API key bí mật) với 4 field bắt buộc; hội thoại multi-turn là stateless; điều khiển đầu ra bằng system prompt · temperature · streaming; ép raw data bằng prefilling + stop sequences.

Phần 1/7 của ghi chú khoá Building with the Claude API — đề thi thử 589 câu (chấm điểm + giải thích) nằm ở tab "Đề thi thử" trên trang tổng quan.

Vòng đời một request (5 bước)

Hiểu trọn vòng đời một request giúp bạn ra quyết định kiến trúc tốt hơn và debug nhanh hơn — biết lỗi nằm ở khúc nào trong pipeline.

# Bước Chuyện gì xảy ra
1 Request to server Web/mobile app gửi input người dùng tới server của bạn (không gọi thẳng Anthropic).
2 Request to Anthropic API Server gắn API key + tham số rồi gọi Anthropic API (qua SDK hoặc HTTP thuần).
3 Model processing Claude xử lý input qua 4 giai đoạn (xem dưới) và sinh response.
4 Response to server Anthropic trả về một response có cấu trúc (message · usage · stop_reason).
5 Response to client Server chuyển text sinh ra về app, hiển thị cho người dùng.

Vì sao bắt buộc có server riêng

KHÔNG bao giờ gọi Anthropic API trực tiếp từ client-side code.

  • Mỗi request cần một secret API key để xác thực.
  • Nhúng key vào code client = lỗ hổng bảo mật nghiêm trọng — ai cũng trích xuất được.
  • Kẻ xấu lấy key xong có thể gọi API trái phép, đốt tiền trên tài khoản của bạn.

Thay vào đó: app gửi request tới server của bạn, server giữ key an toàn (biến môi trường / secret manager) rồi mới nói chuyện với Anthropic API.

Gọi API: SDK hay HTTP, và các field bắt buộc

Server có thể gọi Anthropic API bằng SDK chính thức hoặc HTTP request thuần. Anthropic cung cấp SDK cho Python, TypeScript, JavaScript, Go, Ruby.

Mọi request đều phải có 4 field cốt lõi:

Field Mục đích
API Key Xác thực request của bạn với Anthropic.
Model Tên model cần dùng (vd một model id họ claude-…; id tiến hoá theo thời gian nên luôn tra id hiện hành).
Messages Danh sách "messages" chứa input người dùng. Text người dùng được bọc trong một message role: "user", rồi đặt vào list messages.
Max Tokens Giới hạn số token tối đa Claude được sinh ra.

Bên trong Claude xử lý — 4 giai đoạn

Khi Anthropic nhận request, Claude chạy qua 4 giai đoạn: tokenization → embedding → contextualization → generation.

1. Tokenization — Claude cắt input thành các mẩu nhỏ gọi là token (có thể là cả từ, một phần từ, dấu cách, hay ký hiệu). Để đơn giản, cứ hình dung mỗi từ ≈ một token. Ví dụ "What is quantum computing?"What · is · quantum · computing · ?.

2. Embedding — Mỗi token được đổi thành một embedding: một danh sách dài các con số (vector) biểu diễn mọi nghĩa khả dĩ của từ đó — như một "định nghĩa bằng số" nắm bắt quan hệ ngữ nghĩa.

Một từ thường đa nghĩa. Ví dụ "quantum" có thể là: (1) đơn vị rời rạc của một đại lượng vật lý; (2) khái niệm cơ học/vật lý lượng tử; (3) thứ cực nhỏ ở mức hạ nguyên tử; (4) liên quan tới quantum computing. Embedding ban đầu mang theo tất cả các nghĩa này.

3. Contextualization — Claude tinh chỉnh từng embedding dựa trên các từ xung quanh để xác định nghĩa khả dĩ nhất trong ngữ cảnh. Cùng từ "quantum" nhưng đứng cạnh "computing" sẽ được kéo về nghĩa "quantum computing", loại bớt các nghĩa kia.

4. Generation — Các embedding đã ngữ cảnh hoá đi qua một output layer tính xác suất cho mỗi từ kế tiếp khả dĩ. Claude không phải lúc nào cũng chọn từ xác suất cao nhất — nó pha trộn xác suất với một chút ngẫu nhiên có kiểm soát để câu trả lời tự nhiên, đa dạng. Chọn xong một từ, nó thêm vào chuỗi và lặp lại toàn bộ quá trình cho từ tiếp theo.

Khi nào Claude dừng sinh

Sau mỗi token, Claude kiểm tra để quyết định có sinh tiếp không:

  1. Max tokens reached — đã chạm giới hạn max_tokens bạn đặt chưa?
  2. Natural ending — đã sinh ra token kết thúc tự nhiên (end-of-sequence) chưa?
  3. Stop sequence — có gặp một cụm dừng (stop sequence) bạn định nghĩa trước không?

Response trả về

Khi sinh xong, API trả về một response có cấu trúc:

Field Nội dung
Message Text Claude sinh ra.
Usage Số token input và output (cơ sở để tính chi phí).
Stop Reason Vì sao dừng (xem 3 điều kiện trên).

Server nhận response này, lấy text và chuyển về client để hiển thị.

Góc Delivery Manager. Cái sơ đồ "5 bước" này không chỉ là kiến thức học thuộc — nó là bản đồ rủi ro để mình thiết kế và vận hành. (1) Bước "server riêng" không thương lượng: với team mình, key luôn nằm ở secret manager phía server, không bao giờ ở repo/client — và đây cũng là chỗ đặt rate-limit + logging tập trung. (2) max_tokensusageđòn bẩy chi phí: đặt trần token đúng theo use case (chatbot ngắn vs tổng hợp tài liệu) tránh vừa đốt tiền vừa để câu trả lời lan man. (3) stop_reason phải được xử lý tường minh trong code: dừng vì max_tokens (output bị cắt giữa chừng — cần xử lý khác) hoàn toàn khác dừng tự nhiên — nuốt lỗi này là nguồn của những bug "câu trả lời cụt" khó tái hiện. Cuối cùng, biết pipeline 5 bước giúp khoanh vùng sự cố nhanh: lỗi ở server mình, ở request gửi đi, hay ở phía model.

Tạo API key (5 bước)

Trước khi gọi API, cần một secret API key. Lấy qua console.anthropic.com:

  1. Đăng nhập console.
  2. Bấm Get API Keys (góc trên phải dashboard).
  3. Bấm Create Key.
  4. Chọn workspace Default, đặt tên cho key (để nhận diện sau này — vd "Anthropic Course").
  5. Copy key trong popup và cất kỹ.

⚠️ Key chỉ hiển thị MỘT LẦN. Lỡ đóng cửa sổ trước khi copy → xoá key cũ và tạo lại (không có cách xem lại key đã tạo).

Request đầu tiên bằng Python

1. Cài thư viện (trong Jupyter notebook):

%pip install anthropic python-dotenv

2. Cất key vào file .env (cùng thư mục notebook), KHÔNG nhét thẳng vào code:

ANTHROPIC_API_KEY="your-api-key-here"

Cách này giữ key ra khỏi code và tránh vô tình commit lên version control. Luôn thêm .env vào .gitignore (ăn khớp với luật "key không lộ" ở phần trên).

3. Nạp env + tạo client:

from dotenv import load_dotenv
load_dotenv()

from anthropic import Anthropic
client = Anthropic()            # tự đọc ANTHROPIC_API_KEY từ biến môi trường
model = "claude-sonnet-4-0"     # id ví dụ của khoá; id hiện hành: claude-opus-4-8 / claude-sonnet-4-6 (id tiến hoá theo thời gian)

4. Gọi client.messages.create() — hàm lõi, cần 3 tham số:

Tham số Vai trò
model Tên model cần dùng.
max_tokens Van an toàn giới hạn độ dài response — KHÔNG phải mục tiêu. Đặt 1000 nghĩa là Claude dừng khi chạm 1000 token; nó viết vừa đủ rồi dừng, không cố viết cho đủ.
messages Lịch sử hội thoại gửi cho Claude — một list các dict {role, content}.
message = client.messages.create(
    model=model,
    max_tokens=1000,
    messages=[
        {"role": "user", "content": "What is quantum computing? Answer in one sentence"}
    ]
)

Messages — 2 loại role: "user" (nội dung người dùng gửi) và "assistant" (response Claude sinh ra). Mỗi message là dict có role + content (text thật).

5. Lấy text từ response:

message.content[0].text

Response chứa nhiều thông tin (usage, stop_reason…), nhưng thường chỉ cần text. content là một list block → lấy block đầu [0] rồi .text.

Hội thoại nhiều lượt (multi-turn) — Claude KHÔNG nhớ

Claude là stateless (không lưu trạng thái): mỗi request hoàn toàn độc lập, không có trí nhớ về các lượt trước.

Hệ quả: hỏi "What is quantum computing?" xong, nếu hỏi tiếp "Write another sentence" trong một request mới, Claude không biết anh đang nói về cái gì → viết một câu ngẫu nhiên chẳng liên quan. Muốn nó nhớ ngữ cảnh, anh phải tự quản lý conversation state:

  1. Tự giữ một list tất cả messages trong code.
  2. Gửi TOÀN BỘ lịch sử kèm mỗi request.

Vòng lặp đúng:

  1. Gửi user message đầu tiên cho Claude.
  2. Lấy response của Claude → thêm vào list dưới dạng assistant message (bước hay quên).
  3. Thêm câu hỏi tiếp theo (user message).
  4. Gửi toàn bộ lịch sử đi.

3 helper functions cho gọn việc quản lý:

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

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

def chat(messages):
    message = client.messages.create(
        model=model,
        max_tokens=1000,
        messages=messages,
    )
    return message.content[0].text

Ghép lại:

messages = []                                               # sổ rỗng
add_user_message(messages, "Define quantum computing in one sentence")
answer = chat(messages)                                     # lượt 1
add_assistant_message(messages, answer)                     # ghi câu trả lời vào sổ
add_user_message(messages, "Write another sentence")        # follow-up
final_answer = chat(messages)                               # lượt 2 — CÓ ngữ cảnh

Giờ Claude hiểu "Write another sentence" là viết tiếp về quantum computing, vì anh đã đưa toàn bộ ngữ cảnh cho nó.

Chatbot — vòng lặp vô hạn (exercise)

Bài exercise lấy đúng kịch bản 2 lượt ở trên rồi bọc trong while True + input() → biến thành chatbot tương tác vô hạn lượt trong terminal. 6 bước lặp: (1) input() lấy câu người dùng → (2) add_user_message → (3) chat(messages) gọi API → (4) add_assistant_message ghi câu trả lời vào sổ → (5) in ra → (6) while True tự quay lại bước 1. Thêm một đường thoát (quit/exitbreak).

⭐ Bẫy số 1 — messages = [] phải nằm NGOÀI vòng lặp. Đây là "quyển sổ" giữ trí nhớ: nó lớn dần qua mỗi lượt, và mỗi lần chat() gửi lại toàn bộ sổ. Đặt messages = [] bên trong loop → xé sổ làm quyển mới mỗi lượt → bot mất trí nhớ, tụt về stateless như chưa làm gì.

Hai dòng lõi mỗi lượt:

  • answer = chat(messages) — gửi cả lịch sử, bóc lấy message.content[0].text. Lúc này answer chỉ là biến rời, chưa dính vào sổ.
  • add_assistant_message(messages, answer)append vào sổ; sửa list tại chỗ (in-place), không cần return vì list trong Python là mutable. Bỏ dòng này = bot "nghe mà không nhớ" câu chính nó vừa nói.

Popup nhập liệu thấy trong video là do input() chạy trong Jupyter Notebook. Chạy file .py bằng terminal thì gõ thẳng trong terminal, không có popup.

Khác file multi-turn (03) ở đâu? Bản chất multi-turn giống hệt; chỉ thêm while True + input() → biến kịch bản cố định 2 lượt thành chatbot vô hạn lượt. Pain point lộ ra: mỗi lượt sổ phình thêm → gửi lại toàn bộ mỗi lần → chi phí mỗi request tăng tuyến tính O(n), chi phí tích luỹ cả hội thoại ~O(n²) (chỉ input token phình; output không bị gửi lại), và có trần context window. Đây là cửa ngõ dẫn sang prompt caching · sliding window · summarization (compaction) · RAG.

Góc Delivery Manager. Quản lý conversation state nghe đơn giản, nhưng thứ thật sự phải canh ở production là chi phítrần context window — cả hai cùng phình theo độ dài hội thoại. Đây là lý do tồn tại của các kỹ thuật nêu ở pain point trên (prompt caching, sliding window, compaction, RAG): không phải để "cho xịn", mà để hội thoại dài vẫn rẻ và không vỡ ngữ cảnh. Khi thiết kế, hãy chọn trước chiến lược cắt/nén lịch sử theo use case thay vì để token phình tự do.

System prompts — đặt "vai" cho Claude

TL;DR mini. messages quyết định nội dung (Claude trả lời cái gì); system prompt quyết định cách hành xử (trả lời như thế nào — tone, style, vai trò). Truyền qua tham số system= (một plain string) trong client.messages.create() — là tham số riêng, không phải một message role.

System prompt cho Claude guidance về cách phản hồi: nó cố trả lời đúng như người trong vai đó sẽ trả lời, và giúp giữ Claude bám nhiệm vụ (on-task). Dòng đầu thường gán vai ("You are a patient math tutor."), rồi tới các chỉ thị hành vi.

system_prompt = """
You are a patient math tutor.
Do not directly answer a student's questions.
Guide them to a solution step by step.
"""

client.messages.create(
    model=model,
    messages=messages,
    max_tokens=1000,
    system=system_prompt,
)

Cùng câu hỏi "How do I solve 5x + 2 = 3 for x?":

  • Không system prompt: Claude phơi luôn lời giải đầy đủ từng bước.
  • Có system prompt tutor: Claude không đưa đáp án, mà hỏi gợi mở ("What would be a good first step to isolate x?") và dắt học sinh tự nghĩ.

Nguyên tắc lõi: system prompt điều khiển CÁCH trả lời, KHÔNG phải nội dung. Cùng một câu hỏi, gán vai khác nhau → cách xử lý khác nhau.

chat() linh hoạt — nhận system tuỳ chọn

Đừng hard-code system prompt. Cho chat() nhận system như tham số tuỳ chọn để tái dùng:

def chat(messages, system=None):
    params = {
        "model": model,
        "max_tokens": 1000,
        "messages": messages,
    }
    if system:
        params["system"] = system          # CHỈ thêm khi có
    message = client.messages.create(**params)
    return message.content[0].text

⚠️ Chi tiết dễ bug: API không nhận system=None. Nên phải thêm key system có điều kiện (chỉ khi được truyền) — gom vào params dict rồi **params unpack. Truyền thẳng system=None sẽ lỗi.

Góc Delivery Manager. System prompt là lớp "policy" của sản phẩm — vai trò, tông giọng, guardrails nhất quán cho mọi user, tách hẳn khỏi nội dung động (messages) của từng người. Tách system ra tham số chat(messages, system=) chính là tách cấu hình hành vi khỏi logic hội thoại: đổi tutor → lawyer → support agent chỉ là đổi một string, không đụng code. Trong production đây là chỗ mình version-control và A/B test (nối thẳng vào module prompt evaluation phía sau).

Temperature — núm chỉnh "ngẫu nhiên ↔ xác định"

TL;DR mini. temperature là số thực 0–1 điều khiển Claude đoán chắc hay sáng tạo. Gần 0 = deterministic (gần như luôn chọn token xác suất cao nhất); gần 1 = trải đều xác suất → đa dạng/sáng tạo. Nó không bảo đảm ra kết quả khác — chỉ đổi xác suất.

Sinh văn bản, rút gọn còn 3 nhịp: Tokenization (cắt token) → Prediction (tính xác suất cho mỗi token kế tiếp — chính là gộp embedding + contextualization + output layer ở sơ đồ 4 giai đoạn phía trên) → Sampling (bốc 1 token theo xác suất đó), rồi lặp. Temperature tác động vào bước Sampling: nó bóp méo phân phối xác suất trước khi bốc.

  • temperature = 0.0: token cao nhất ~100% → gần như deterministic.
  • temperature = 1.0: xác suất trải đều hơn → token xác suất thấp cũng có cửa → đa dạng.

⚠️ Dễ nhầm: temperature không bảo đảm output khác nhau — chỉ tăng xác suất khác. Temp cao vẫn có thể thỉnh thoảng cho ra câu giống nhau.

Chọn temperature theo task

Dải Temp Hợp với
Low 0.0–0.3 Factual responses · coding · data extraction · content moderation
Medium 0.4–0.7 Summarization · educational content · problem-solving · creative writing có ràng buộc
High 0.8–1.0 Brainstorming · creative writing · marketing · joke generation

Trục: trái = less creative / ổn định, phải = more creative / đa dạng.

Thêm vào code

def chat(messages, system=None, temperature=1.0):
    params = {
        "model": model,
        "max_tokens": 1000,
        "messages": messages,
        "temperature": temperature,
    }
    if system:
        params["system"] = system
    message = client.messages.create(**params)
    return message.content[0].text

Chỉ thêm 2 chỗ: tham số temperature=1.0 (mặc định) và "temperature": temperature trong params. Test nhanh: sinh ý tưởng phim ở temperature=0.0 (lặp đi lặp lại một ý) vs temperature=1.0 (đa dạng chủ đề / nhân vật / cốt truyện).

Góc Delivery Manager. Temperature là núm reproducibility ↔ đa dạng. Task cần kiểm chứng/audit (extraction, classification, trích số liệu) → temp thấp cho đầu ra ổn định, dễ test lại — ăn khớp thẳng với module prompt evaluation (eval chỉ đáng tin khi output không nhảy lung tung). Task cần ý tưởng (brainstorm, marketing) → temp cao. Lỗi production hay gặp: để temp cao ở chỗ cần xác định → kết quả "lúc đúng lúc sai" khó tái hiện. Nhớ temperature · max_tokens · system3 núm khác nhau (ngẫu nhiên · độ dài · hành vi) — đừng lẫn.

Response streaming — hiện chữ dần (chunk-by-chunk)

TL;DR mini. Response có thể mất 10–30s; bắt user nhìn spinner là UX tệ. Streaming cho chữ hiện từng mẩu ngay khi Claude sinh → cảm giác phản hồi tức thì. Tất cả mẩu này thuộc một request duy nhất.

Mặc định (non-stream): server đợi response hoàn chỉnh rồi mới trả về → user chờ vô định. Bật streaming: Claude báo "đã nhận, đang sinh" rồi bắn về một loạt event, mỗi event chứa một mẩu nhỏ; server forward xuống client → chữ "mọc dần" theo thời gian thực.

Các loại event (đúng thứ tự)

Event Ý nghĩa
MessageStart Mở một message mới (chỉ báo hiệu, chưa có text).
ContentBlockStart Mở một content block (text, tool use, hay loại khác).
ContentBlockDelta ⭐ Mẩu text thật — chính là cái cần hiển thị cho user. Có nhiều event loại này.
ContentBlockStop Content block hiện tại đã xong.
MessageDelta Message hiện tại đã hoàn tất (metadata cuối: stop_reason/usage).
MessageStop Hết thông tin về message này.

Quy luật: luôn mở bằng MessageStart, text nằm trong chuỗi ContentBlockDelta ở giữa, đóng bằng MessageStop. (Tên type đầy đủ trong SDK có tiền tố Raw…Event.)

3 mức triển khai

1. Raw — stream=True (tự parse event):

stream = client.messages.create(
    model=model,
    max_tokens=1000,
    messages=messages,
    stream=True,
)
for event in stream:
    print(event)

2. Gọn — client.messages.stream() + text_stream (SDK tự lọc, chỉ trả text):

with client.messages.stream(
    model=model,
    max_tokens=1000,
    messages=messages,
) as stream:
    for text in stream.text_stream:
        print(text, end="")     # end="" để chữ nối liền, không xuống dòng

3. Lấy message hoàn chỉnh — get_final_message() (để lưu DB / nối lịch sử):

with client.messages.stream(...) as stream:
    for text in stream.text_stream:
        ...                      # đẩy từng chunk xuống client
    final_message = stream.get_final_message()   # ráp lại toàn bộ

→ Best of both: stream realtime cho user + message đầy đủ cho app (lưu trữ, lấy usage/stop_reason, nối vào lịch sử).

⚠️ Hai API khác nhau, đừng lẫn: client.messages.create(..., stream=True) trả về iterator event thô; còn client.messages.stream(...)context manager (with ... as stream) cho text_stream đã lọc sẵn.

Góc Delivery Manager. Streaming không làm Claude nhanh hơn — tổng thời gian sinh y nguyên; nó chỉ hạ perceived latency (time-to-first-token thấp) → UX chat khác hẳn. Hai bẫy production: (1) vẫn phải get_final_message() để có text đầy đủ mà add_assistant_message vào lịch sử — chỉ stream rồi quên ráp là vỡ multi-turn + mất usage/stop_reason; (2) stream có thể đứt giữa chừng (mạng rớt) → cần xử lý partial/timeout, đừng coi như luôn chạy trọn.

Structured data — ép Claude trả raw data (prefilling + stop sequences)

TL;DR mini. Khi cần Claude sinh dữ liệu có cấu trúc (JSON, code, list), theo mặc định nó "lịch sự" bọc kết quả trong ```json … ``` kèm một câu giải thích → app không copy thẳng được. Combo assistant message prefilling (mồi sẵn câu trả lời) + stop_sequences (cụm dừng) cho ra raw data sạch, không thừa chữ nào.

Vấn đề. Hỏi "sinh JSON" → Claude trả đúng JSON nhưng bọc trong markdown code block ```json … ``` và thêm một câu giải thích phía sau. Với web app kiểu user mô tả → bấm generate → copy dán đi luôn (ví dụ khoá: app sinh AWS EventBridge rule), user buộc phải tự bôi đen đúng đoạn JSON → UX vướng víu.

Cách làm — ghép 2 kỹ thuật:

messages = []
add_user_message(messages, "Generate a very short event bridge rule as json")
add_assistant_message(messages, "```json")          # PREFILL: mồi sẵn câu mở code block

text = chat(messages, stop_sequences=["```"])        # STOP: gặp ``` là dừng

Cơ chế từng bước:

  1. user message nói Claude cần sinh gì.
  2. prefilled assistant message = ```json → Claude tưởng nó đã tự mở một markdown code block.
  3. Claude viết tiếp chỉ phần JSON (không lặp lại lời mào đầu nữa).
  4. Khi Claude định đóng block bằng ```, stop sequence khớp → generation dừng ngay, cắt luôn phần giải thích phía sau.

→ Kết quả: JSON trần, không ```json, không câu giải thích.

Xử lý output. Thường còn dư vài ký tự newline → json.loads(text.strip()) là gọn:

import json
clean_json = json.loads(text.strip())

Không chỉ JSON. Dùng được cho mọi dữ liệu cần "không lời bình": Python snippet, bulleted list, CSV, bất kỳ format nào. Mẹo lõi: nhận diện Claude hay bọc nội dung trong cái gì → lấy chính cái đó làm prefill + stop sequence. Với code thường là markdown code block; với list có thể là marker khác.

⚠️ Hai kỹ thuật phải đi cặp. Chỉ prefill mà không stop → Claude vẫn tự đóng ``` rồi viết giải thích. Chỉ stop mà không prefill → output vẫn dính ```json ở đầu. Lưu ý phần text API trả về là phần Claude sinh **sau** prefill; và stop_sequences chính là điều kiện dừng thứ 3 đã gặp ở bài đầu (max_tokens / end-of-sequence / stop sequence).

Góc Delivery Manager. Đây là bước chuyển từ "Claude như chatbot" sang "Claude như một component trong pipeline": output không còn để người đọc mà để máy parse. Prefilling là đòn bẩy lái đầu ra gần như miễn phí (mình tự viết hộ phần đầu lượt assistant), còn stop_sequences cho ranh giới đầu ra tất định — quan trọng khi đẩy kết quả LLM vào hệ khác (json.loads, gọi API tiếp). Nhưng nhớ: prefill + stop định hình format, không bảo đảm tính hợp lệ — Claude vẫn có thể sinh JSON sai cú pháp. Ở production luôn bọc parse trong try/except và có đường lui (retry/repair) thay vì tin mù vào output.

⚠️⚠️ QUAN TRỌNG — prefill ĐÃ BỊ KHAI TỬ trên model đời mới. Kỹ thuật assistant message prefilling ở trên (mồi ```json) trả lỗi 400 trên Sonnet 4.6, cả họ Opus 4.6/4.7/4.8, và Fable 5 ("This model does not support assistant message prefill"). Hiện chỉ còn Haiku 4.5 nhận prefill (vì là model đời 4.5, ra trước làn sóng bỏ); các đời Haiku sau gần như chắc cũng sẽ bỏ. Lý do tài liệu khoá vẫn dạy prefill là vì nó viết thời model cũ.

Bản thay thế bền vững (model nào cũng chạy) = Structured Outputs. Ép JSON bằng schema thay cho prefill:

JUDGE_SCHEMA = {"type": "object",
    "properties": {"reasoning": {"type": "string"}, "score": {"type": "integer"}},
    "required": ["reasoning", "score"], "additionalProperties": False}

msg = client.messages.create(
    model="claude-sonnet-4-6", max_tokens=400,
    output_config={"format": {"type": "json_schema", "schema": JUDGE_SCHEMA}},  # thay prefill
    messages=[{"role": "user", "content": prompt}],
)
data = json.loads(msg.content[0].text)   # schema BẢO ĐẢM JSON hợp lệ → khỏi lo lỗi parse

Structured outputs còn xịn hơn prefill: schema bảo đảm JSON hợp lệ (prefill chỉ định hình format, không bảo đảm). Cần nhãn cố định → dùng strict tool use (strict: true). Lưu ý: output_config.format thay cho tham số output_format cũ (đã deprecated); và stop_sequences thì mọi model vẫn nhận bình thường — chỉ phần prefill trong combo là vỡ.

Cho kỳ thi: đề Anthropic Academy vẫn hỏi prefill (tài liệu cũ) → vẫn phải thuộc. Nhưng làm thật với model mới → dùng structured outputs. Hiểu cả hai + vì sao đổi mới là nắm vững. (Thực chiến: file 09_prompt_eval.py11_eval_google_review_reply.py đều đã dùng output_config cho judge vì giám khảo chạy Sonnet 4.6.)


Phần tiếp theo: Prompt engineering & evaluation với Claude — workflow 5 bước, graders, 4 kỹ thuật

Nguồn: Building with the Claude API (Anthropic Academy) — Copyright Anthropic. Phần đề thi thử cho khoá này nằm ở tab "Đề thi thử" trên trang tổng quan khoá.