Learn > Claude > Tool use với Claude API — JSON Schema, multi-block messages, conversation loop

Tool use với Claude API — JSON Schema, multi-block messages, conversation loop

Phần 3/7 ghi chú Building with the Claude API: cho Claude gọi hàm qua tool schema (JSON Schema), multi-block messages, tool result, conversation loop nhiều tool, streaming tool use và built-in tools (text editor, web search).

  • Tool use 5 bước: viết hàm → mô tả bằng JSON Schema → gọi Claude kèm schema → Claude trả tool_use block → chạy hàm và trả tool_result về Claude.
  • Conversation loop xử lý nhiều tool nối tiếp (multi-turn tool pattern); streaming tool use qua InputJsonEvent + fine-grained tool calling.
  • Built-in tools: text editor (schema có sẵn, hàm tự viết) và web search (chạy hoàn toàn phía server).

TL;DR — Cho Claude "vươn ra thế giới bên ngoài": mô tả hàm bằng JSON Schema, xử lý vòng lặp tool_use → tool_result (kể cả nhiều tool nối tiếp + streaming), và tận dụng built-in tools text editor · web search.

Phần 3/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.

Tool use — mở màn: cho Claude vươn ra thế giới bên ngoài

TL;DR mini. Mặc định Claude chỉ biết training data → mù với mọi thứ real-time (weather, stock, giá hôm nay, dữ liệu trong DB của anh). Tool use giải bài này bằng một vòng back-and-forth 4 bước giữa server của anhClaude. Mấu chốt: Claude KHÔNG tự chạy code — nó chỉ "xin" anh chạy tool; server của anh mới là đứa gọi API/DB thật.

Vấn đề khi không có tool. Hỏi "What's the weather in San Francisco?" → Claude buộc phải đáp "I'm sorry, but I don't have access to up-to-date weather information." UX bực mình, vì đây là thứ Claude dư sức giúp nếu có data tươi.

Cơ chế — 4 bước (back-and-forth pattern):

# Bước Hướng Chuyện gì xảy ra
1 Initial Request Server → Claude Gửi câu hỏi + kèm chỉ dẫn cách lấy data ngoài (mô tả tool). Lưu ý: đưa CÁCH lấy data, chưa phải data.
2 Tool Request Claude → Server Claude phân tích, tự quyết là cần thêm info → xin, nói rõ cần gì (vd: thời tiết thành phố nào).
3 Data Retrieval Server (chạy code) Anh chạy code gọi external API / database lấy data thật. Claude không làm bước này.
4 Final Response Server → Claude → client Gửi data về Claude → Claude ghép câu hỏi gốc + data tươi → câu trả lời hoàn chỉnh, chính xác.

Ví dụ weather (cụ thể hoá). User hỏi thời tiết → server gửi câu hỏi kèm mô tả tool weather → Claude nhận ra cần data hiện tại, xin thời tiết cho đúng location → server gọi weather API lấy điều kiện real-time → trả về Claude → Claude ghép data + câu hỏi ra câu trả lời chuẩn, cập nhật.

Key benefits: Real-time Information (data không có lúc training) · External System Integration (nối DB, API, dịch vụ khác) · Dynamic Responses (trả lời theo thông tin mới nhất) · Structured Interaction (Claude biết chính xác cần info gì và cách xin).

⚠️ Mấy chỗ dễ nhầm. (1) Claude KHÔNG tự chạy code / gọi API — nó chỉ xin; server của anh chạy (bước 3). (2) Claude là đứa QUYẾT ĐỊNH có dùng tool không ("Claude decides to ask us") — anh chỉ mô tả tool có sẵn. (3) Bước 1 đưa "how to get data" (mô tả tool), KHÔNG phải data; data thật chỉ tới ở bước 4. (4) Một câu trả lời có tool cần ≥ 2 lần gọi Claude API (lần xin tool + lần chốt) → tốn token/chi phí hơn câu thường.

Góc Delivery Manager. Đây là cú chuyển hệ hình: Claude từ bách khoa tĩnh (đóng băng ở training cutoff) thành trợ lý động nối được hệ thống thật — nền móng của agent · RAG · MCP về sau. Hai thứ phải canh ngay từ thiết kế: (1) chi phí & latency — mỗi tool call là thêm ≥1 round-trip API, hội thoại có tool phình nhanh hơn; (2) ranh giới tin cậy — code chạy tool nằm ở server của mình, nên validate mọi tham số Claude xin (đừng cho nó tự ý gọi DELETE/đụng tiền) và bọc lỗi khi API ngoài fail. Tool use mở năng lực, nhưng cũng mở bề mặt tấn công — coi tool như một endpoint thật, có auth + rate-limit + log.

Project reminder — goal & 3 tools cần xây

TL;DR mini. Module tool use dựng một project thực hành: dạy Claude đặt reminder cho ngày tương lai. Mục tiêu — user gõ "Set a reminder for my doctor's appointment. It's a week from Thursday" → Claude đáp "OK, I will remind you." Để làm được, phải vá 3 limitation của Claude bằng 3 custom tools. Nguyên tắc lõi: model có giới hạn → mở rộng bằng tool, KHÔNG cố lách bằng prompt.

Vì sao khó — 3 limitation:

# Limitation Nội dung
1 Limited time awareness Claude có thể biết ngày hiện tại, nhưng không biết giờ chính xác.
2 Date calculation issues Claude làm toán ngày-giờ không đáng tin, nhất là cộng nhiều ngày về tương lai.
3 No reminder capability Claude không có cơ chế built-in để thật sự đặt một reminder.

3 tools để vá (mỗi limitation một tool):

Tool Vá cho Vai trò
Get the current date time #1 Cho Claude biết ngày + giờ chính xác.
Add duration to date time #2 Làm phép cộng ngày-giờ đáng tin thay Claude.
Set a reminder #3 Thật sự ghi reminder vào hệ thống.

Cách triển khai: build từng tool một, bắt đầu từ cái đơn giản nhất → hiểu cơ chế tool calling trước khi ghép. Cuối cùng Claude kết hợp cả 3 tool để giải một câu như "remind me in a week" (lấy giờ → cộng duration → đặt reminder).

⚠️ Mấy chỗ dễ nhầm. (1) "Biết ngày" ≠ "biết giờ" — Claude có thể có ngày nhưng không có đồng hồ thật, nên cần tool #1. (2) 1 task user có thể cần NHIỀU tool ghép lại (reminder = 3 tool phối hợp). (3) Slide ghi "3–4 custom tools" nhưng bài làm 3 — cốt là mỗi limitation → một tool, đừng kẹt con số. (4) Câu chốt ra thi: khi model có limitation, extend bằng tool chứ không "work around in prompts".

Góc Delivery Manager. Bài này dạy đúng một phản xạ thiết kế: gặp chỗ LLM yếu (toán ngày, không có clock, không side-effect được) thì đừng nhồi prompt cho nó "ráng đúng" — kết quả vẫn xác suất, không audit được. Hãy đẩy phần xác định ra code tất định (tool): add_duration luôn cộng đúng, set_reminder luôn ghi đúng chỗ. Mô hình này = chia vai: LLM lo phần hiểu ý + điều phối, tool lo phần chính xác + tác động thật. Đây cũng là lý do agent thật cần nhiều tool nhỏ-rõ-ràng thay vì một prompt khổng lồ.

Tool function — bước 1/5: viết hàm tool đầu tiên

TL;DR mini. Xây một tool đi qua pipeline 5 bước: Write a tool function → Write a JSON Schema → Call Claude with JSON Schema → Run Tool → Add Tool Result and call Claude again. Bài này là bước 1. Một tool function chỉ là hàm Python bình thường sẽ chạy khi Claude quyết định cần thêm info. ⭐ Insight lõi: Claude ĐỌC được error message bạn raise và có thể GỌI LẠI hàm với tham số đã sửa — nên validate input + báo lỗi rõ nghĩa rất quan trọng.

Pipeline 5 bước (thanh tiến trình — bản đồ cả module):

# Bước Làm gì
1 Write a tool function Viết hàm Python thật sẽ thực thi (bài này).
2 Write a JSON Schema Mô tả hàm cho Claude hiểu (tên · mục đích · tham số).
3 Call Claude with JSON Schema Gửi schema kèm request để Claude biết tool có sẵn.
4 Run Tool Khi Claude xin, server chạy hàm lấy kết quả.
5 Add Tool Result and call Claude again Gửi kết quả về → Claude chốt câu trả lời.

Tool function là gì? Hàm Python plain (không decorator/thư viện đặc biệt) được thực thi khi Claude quyết định cần thêm info. Hỏi "What time is it?" → Claude sẽ gọi tool date/time. Ở bước 1, Claude còn CHƯA biết hàm tồn tại — phải đợi schema (bước 2) mới "giới thiệu" được.

3 best practices:

Best practice Vì sao
Descriptive names Cả tên hàm lẫn tên tham số phải nói rõ mục đích — Claude đọc tên (qua schema) để biết truyền gì.
Validate inputs Check tham số bắt buộc không rỗng/sai → raise lỗi nếu hỏng.
Meaningful error messages ⭐ Claude thấy error → có thể retry với tham số đúng. "Location cannot be empty" > lỗi trống.

Ví dụ tool weather (slide) minh hoạ cả 3: validate location không rỗng → raise ValueError("Location cannot be empty"), đặt tên rõ, gọi API rồi return response.json().

Tool đầu tiên — get_current_datetime:

def get_current_datetime(date_format="%Y-%m-%d %H:%M:%S"):
    if not date_format:
        raise ValueError("date_format cannot be empty")
    return datetime.now().strftime(date_format)
  • Nhận date_format (default %Y-%m-%d %H:%M:%S = năm-tháng-ngày giờ:phút:giây) → Claude xin giờ theo format tuỳ ý.
  • datetime.now().strftime(date_format) lấy giờ hiện tại + format theo chuỗi truyền vào.
  • Test: get_current_datetime()"2024-01-15 14:30:25"; get_current_datetime("%H:%M")"14:30".
  • if not date_format minh hoạ pattern validate (lỗi này hiếm, nhưng dạy đúng thói quen).

⚠️ Mấy chỗ dễ nhầm. (1) ⭐ Claude HỌC TỪ LỖI — error raise quay về cho Claude đọc → nó retry với tham số sửa. Đây là điểm dễ ra thi nhất. (2) Bước 1 tool function chỉ là hàm Python THƯỜNG — không có gì của Anthropic; cái biến nó thành tool Claude gọi được là JSON schema (bước 2) + vòng run (bước 4–5). (3) Tên tham số cũng là tài liệu cho Claude (khác code thường, nơi tên chỉ cho người đọc). (4) Tạo hàm mới chỉ là bước đầu — chưa tích hợp gì.

Góc Delivery Manager. "Claude learns from errors" đổi cách mình viết tool: error message không còn cho dev đọc log mà là kênh giao tiếp ngược lại model — nó là một phần "API" của tool. Lỗi mơ hồ ("invalid input") = Claude retry mù; lỗi rõ ("date_format cannot be empty, expected a strftime string") = Claude tự sửa đúng lượt sau. Validate + raise rõ nghĩa vì thế vừa là đỡ đòn an toàn (chặn input rác) vừa là cơ chế tự chữa của agent. Với team mình: coi mỗi tool như một micro-service có contract rõ — tên tham số mô tả, validate ở biên, lỗi human-readable — thì agent ghép nhiều tool mới chạy bền.

Tool schema — bước 2/5: mô tả tool cho Claude bằng JSON Schema

TL;DR mini. Hàm tool (bước 1) đã có, nhưng Claude vẫn chưa biết nó tồn tại. JSON Schema là tờ "hướng dẫn sử dụng" anh đưa cho Claude đọc để biết tool làm gì · khi nào gọi · cần tham số gì. Một tool spec đầy đủ gồm 3 phần: name · description · input_schema. ⭐ description là thứ Claude dựa vào để quyết định có gọi tool không → viết kỹ. Mẹo: nhờ chính Claude sinh schema từ code hàm + tài liệu tool use.

JSON Schema là gì (và KHÔNG phải gì). JSON Schema không phải phát minh riêng cho AI / tool calling — nó là một chuẩn validate dữ liệu (data validation specification) đã có nhiều năm, dùng rộng rãi ngoài giới AI. Cộng đồng AI chỉ mượn lại vì nó là cách tiện để mô tả tham số hàm và kiểm tra (validate) dữ liệu. Hiểu vậy để khỏi dính bẫy "JSON Schema sinh ra cho tool calling".

Một tool specification đầy đủ — 3 phần:

Phần Vai trò
name Tên tool rõ nghĩa, mô tả đúng việc (vd get_weather). Claude đọc tên để đoán công dụng.
description Quan trọng nhất. Tool làm gì, khi nào Claude nên dùng, và trả về data loại gì. Đây là "tài liệu" Claude đọc để quyết định gọi tool.
input_schema Chính là JSON schema mô tả các tham số (arguments) của hàm: kiểu dữ liệu, mô tả từng tham số, default, cái nào bắt buộc (required).

Viết description cho tốt (best practices):

  • Khoảng 3–4 câu giải thích tool làm gì.
  • Nói rõ khi nào Claude nên dùng nó.
  • Giải thích nó trả về loại data gì.
  • Mô tả chi tiết từng tham số (mỗi property trong input_schemadescription riêng).

Nguyên tắc: description không phải comment cho người — nó là prompt Claude thật sự đọc để chọn tool và điền tham số. Mơ hồ ở đây = Claude gọi sai tool hoặc truyền sai đối số.

Cách "lười mà đúng" để sinh schema. Đừng gõ JSON schema bằng tay — nhờ chính Claude viết:

  1. Copy code tool function của anh.
  2. Đưa cho Claude, bảo nó viết JSON schema cho tool calling.
  3. Kèm tài liệu tool use của Anthropic làm ngữ cảnh (context).
  4. Để Claude sinh ra schema đúng định dạng, theo best practices.

Prompt mẫu: "Write a valid JSON schema spec for the purposes of tool calling for this function. Follow the best practices listed in the attached documentation."

Gắn schema vào code — quy ước đặt tên <hàm> + <hàm>_schema:

def get_current_datetime(date_format="%Y-%m-%d %H:%M:%S"):
    if not date_format:
        raise ValueError("date_format cannot be empty")
    return datetime.now().strftime(date_format)

get_current_datetime_schema = {
    "name": "get_current_datetime",
    "description": "Returns the current date and time formatted according to the specified format",
    "input_schema": {
        "type": "object",
        "properties": {
            "date_format": {
                "type": "string",
                "description": "A string specifying the format of the returned datetime. Uses Python's strftime format codes.",
                "default": "%Y-%m-%d %H:%M:%S"
            }
        },
        "required": []
    }
}
  • Đặt tên schema theo đúng tên hàm + hậu tố _schema (get_current_datetimeget_current_datetime_schema) → dễ ghép cặp hàm ↔ schema.
  • date_formatdefault nên không nằm trong required (mảng required rỗng).

Type safety — bọc bằng ToolParam:

from anthropic.types import ToolParam

get_current_datetime_schema = ToolParam({
    "name": "get_current_datetime",
    "description": "Returns the current date and time formatted according to the specified format",
    # ... phần còn lại của schema
})

ToolParam (từ anthropic.types) không bắt buộc để chạy được, nhưng giúp type-check lúc dùng schema với API → bắt lỗi sai cấu trúc sớm, code chắc tay hơn.

⚠️ Mấy chỗ dễ nhầm. (1) JSON Schema KHÔNG phải đồ riêng của AI — là chuẩn validate có sẵn từ lâu, AI mượn lại (câu bẫy hay gặp). (2) 3 phần của tool spec là name · description · input_schema — đừng nhầm input_schema với cả spec. (3) description ăn vào quyết định của Claude (chọn tool + điền đối số), không phải comment trang trí → viết 3–4 câu + tả từng tham số. (4) Tham số có default thì không liệt kê trong required. (5) ToolParam là tùy chọn (chỉ để type-check), bỏ đi vẫn chạy. (6) Quy ước tên: <hàm>_schema.

Góc Delivery Manager. Schema chính là interface contract giữa model và code của mình — và description là phần "tài liệu cho một consumer rất lạ": người đọc tài liệu chính là model sẽ tự quyết gọi hay không. Nên với team, em coi description + input_schema ngang hàng với OpenAPI spec của một endpoint: phải rõ khi nào dùng, trả về gì, mỗi tham số nghĩa gì — viết ẩu thì Claude gọi nhầm tool / điền sai đối số, y như client gọi nhầm API. Mẹo "nhờ Claude sinh schema" tiện, nhưng vẫn phải review tay: tên tham số, required, và nhất là description phải khớp đúng hành vi thật của hàm — schema lệch với code là nguồn bug âm thầm. ToolParam thì cứ bật, vì bắt lỗi schema lúc compile-time/IDE rẻ hơn nhiều so với debug một tool call hỏng lúc runtime.

Multi-block messages — bước 3/5: gọi Claude kèm tool schema

TL;DR mini. Bật tool bằng cách thêm tham số tools=[...schema...] vào client.messages.create(). Khi Claude quyết định dùng tool, response không còn là một text block đơn lẻcontent thành list nhiều block (block còn gọi là "parts"): một Text Block (câu nói cho người) + một ToolUse Block (chỉ dẫn cho code: gọi tool nào, tham số gì). ⭐ Lưu lịch sử phải giữ NGUYÊN cả response.content (cả 2 block), KHÔNG bóc lấy .text.

Bật tool — thêm tham số tools. Để Claude được phép dùng tool, truyền list các JSON schema (bước 2) qua tham số tools:

messages = []
messages.append({
    "role": "user",
    "content": "What is the exact time, formatted as HH:MM:SS?"
})

response = client.messages.create(
    model=model,
    max_tokens=1000,
    messages=messages,
    tools=[get_current_datetime_schema],   # list các schema tool có sẵn
)

tools nhận một list các schema mô tả những hàm Claude được phép gọi. (Cho phép ≠ bắt buộc — Claude vẫn tự quyết có dùng không.)

Response giờ là multi-block message. Trước đây content chỉ có một text block (response.content[0].text). Khi Claude muốn dùng tool, content trở thành list nhiều block:

Block Là gì Nội dung
Text Block Câu nói cho người đọc "I can help you find out the current time. Let me find that information for you."
ToolUse Block Chỉ dẫn cho code của anh Gọi tool nào + tham số gì.

ToolUse Block gồm 4 field:

Field Ý nghĩa
id Mã theo dõi lệnh gọi tool (vd toolu_01QtKMDNkU1FBj73NP2FbF8w) — cần để khớp tool result ở bước sau.
name Tên hàm cần gọi (vd get_current_datetime).
input Tham số ở dạng dict (vd {'date_format': '%H:%M:%S'}).
type Luôn là 'tool_use' → cách nhận diện đây là ToolUse block.
ToolUseBlock(
    id='toolu_01QtKMDNkU1FBj73NP2FbF8w',
    input={'date_format': '%H:%M:%S'},
    name='get_current_datetime',
    type='tool_use'
)

⭐ Lưu lịch sử — giữ NGUYÊN cả content list. Claude vẫn stateless → anh tự lưu. Nhưng giờ phải bảo toàn toàn bộ cấu trúc content (cả text block lẫn tool_use block):

messages.append({
    "role": "assistant",
    "content": response.content        # cả LIST block, KHÔNG phải .content[0].text
})

Bóc lấy .content[0].text như trước = mất ToolUse block (mất id) → lượt sau gửi tool result không khớp được → vỡ vòng tool. Phải giữ cả list để Claude có đủ ngữ cảnh ở các request kế tiếp.

Full flow 5 bước (bản chi tiết của pipeline):

  1. Gửi user message + tool schema cho Claude.
  2. Nhận assistant message chứa text block + tool_use block.
  3. Bóc thông tin tool (name + input) rồi chạy hàm thật (bước 4 của pipeline — bài sau).
  4. Gửi tool result về Claude kèm toàn bộ lịch sử.
  5. Nhận câu trả lời cuối từ Claude.

Hệ quả — nâng cấp helper. add_user_message() / add_assistant_message() cũ chỉ nhận một string text → phải sửa để nhận cả list block (string text hoặc response.content đa block), nếu không sẽ không lưu nổi tool_use block.

⚠️ Mấy chỗ dễ nhầm. (1) block = "parts" (cùng một thứ, hai tên gọi). (2) tools nhận một LIST schema, không phải một schema lẻ. (3) ⭐ Lưu response.content nguyên cả list, KHÔNG response.content[0].text — quên là mất id → không gửi tool result đúng được. (4) Claude không tự chạy hàm ở bước này — nó mới chỉ xin (trả ToolUse block); code của anh chạy ở bước 4 (bài sau). (5) type='tool_use' là dấu nhận diện ToolUse block (phân biệt với text block). (6) Một response có thể có cả text lẫn tool_use cùng lúc — đừng chỉ tìm một loại.

Góc Delivery Manager. Khúc này là chỗ nhiều bug "tốn tiền mà khó tái hiện" sinh ra: response đổi từ scalar (một text) sang một cấu trúc list nhiều block — code nào đang content[0].text cứng sẽ âm thầm sai khi tool bật lên (lúc thì có text trước, lúc tool_use đứng đầu). Với team em chốt 2 luật: (1) lưu nguyên response.content vào lịch sử, coi message như một cấu trúc bất biến — đừng "dọn gọn" lấy mỗi text, vì cái id của tool_use là khoá để ghép kết quả ở vòng sau; (2) duyệt block theo type thay vì theo vị trí index — model có thể đổi thứ tự/đếm block. ToolUse block thực chất là một lời gọi hàm có chữ ký rõ ràng (id/name/input) — xử lý nó như một message trong hàng đợi RPC: validate name/input trước khi thực thi, và luôn trả kết quả kèm đúng id.

Tool result — bước 4-5/5: chạy hàm & trả kết quả về Claude

TL;DR mini. Khép vòng tool use bằng 2 nhịp cuối. (4) Run Tool: bóc input từ ToolUse block rồi gọi hàm thật bằng unpacking (fn(**input)). (5) Add Tool Result: gói kết quả trong một tool_result block (đặt trong một user message) rồi gọi Claude lại kèm cả history + tools → Claude ghép data tươi thành câu trả lời cuối cho người dùng.

Bước 4 — chạy hàm tool. ToolUse block chứa input là một dict các đối số Claude muốn truyền. Hàm của anh nhận keyword arguments chứ không nhận nguyên dict → dùng cú pháp unpacking ** để bung dict thành các tham số:

tool_block = response.content[1]            # ⚠️ tài liệu hardcode index [1] — xem cảnh báo dưới
result = get_current_datetime(**tool_block.input)   # vd input = {'date_format': '%H:%M:%S'}

Bước 5 — tool_result block. Sau khi chạy hàm, trả kết quả về Claude bằng một tool_result block. Block này nằm trong một user message (đứng "về phía người dùng" trong hội thoại) và có 3 field:

Field Ý nghĩa
tool_use_id PHẢI khớp id của ToolUse block mà nó trả lời. Đây là khoá ghép "kết quả nào cho lệnh gọi nào".
content Output của tool, serialize thành string (vd "15:04:22").
is_error True nếu có lỗi khi chạy tool, False nếu OK.
messages.append({
    "role": "user",
    "content": [{
        "type": "tool_result",
        "tool_use_id": tool_block.id,     # khớp id của ToolUse block
        "content": str(result),
        "is_error": False,
    }]
})

Nhiều tool call trong một response. Claude có thể xin nhiều tool cùng lúc (vd "10 + 10 và 30 + 30 bằng mấy?"2 ToolUse block). Xử lý: chạy HẾT các block, mỗi kết quả một tool_result, nhưng gom TẤT CẢ vào MỘT user message — và khớp đúng từng id. Nhờ tool_use_id, Claude ghép đúng kết quả cho từng lệnh kể cả khi thứ tự trả về khác thứ tự xin.

History sau khi khép vòng giờ có 3 message:

  1. user — câu hỏi gốc.
  2. assistant — chứa text block + ToolUse block.
  3. user — chứa tool_result block.

Bước cuối — gọi Claude lại. Gửi follow-up request với toàn bộ history ở trên, và vẫn phải kèm tools=[schema] dù anh không còn đợi Claude gọi tool nữa:

client.messages.create(
    model=model,
    max_tokens=1000,
    messages=messages,
    tools=[get_current_datetime_schema],   # ⭐ vẫn bắt buộc kèm
)

⚠️ Vì sao vòng cuối vẫn phải kèm tools? Vì history đang tham chiếu tới tool (có ToolUse block + tool_result block). Claude cần schema để hiểu các tham chiếu đó; bỏ tools đi → API báo lỗi. Kèm schema không ép Claude gọi tool lần nữa — nó chỉ đọc kết quả rồi trả lời.

Claude nhận tool_result, ghép câu hỏi gốc + data tươi thành một câu trả lời tự nhiên cho người dùng. Vòng tool use hoàn tất.

⚠️ Mấy chỗ dễ nhầm. (1) tool_result block thuộc user message, không phải assistant (dễ tưởng "kết quả là của mình nên để assistant"). (2) tool_use_id phải khớp id — sai/thiếu là API lỗi hoặc Claude ghép nhầm. (3) content phải là string — số/dict phải str(...) hoặc json.dumps(...) trước. (4) Nhiều tool → một user message chứa nhiều tool_result, KHÔNG phải mỗi kết quả một message. (5) Vòng cuối vẫn kèm tools. (6) ⚠️ response.content[1] là hardcode mong manh — tài liệu giả định ToolUse luôn ở index 1, nhưng có thể là [tool_use] (index 0) hoặc [text, tool_use, tool_use] (2 tool). Luôn lọc theo block.type == "tool_use", đừng đếm index.

Góc Delivery Manager. Ba nhịp "chạy hàm → trả tool_result → gọi lại kèm tools" mới chỉ là một vòng. Sản phẩm thật cần agent loop: bọc trong while True, mỗi vòng nhìn stop_reason — còn "tool_use" thì chạy tool và trả kết quả rồi lặp; "end_turn" thì đó là câu trả lời cuối, thoát. Ba thứ tài liệu nhắc rời rạc thực ra là cùng một cơ chế: "gửi lại full history" = messages.append mỗi vòng; "khớp tool_use_id" = vì một message có thể gom nhiều kết quả; "vòng cuối vẫn kèm tools" = vì loop gọi create mỗi vòng với cùng tools. Với team em chốt: duyệt block theo type + lặp theo stop_reason (không hardcode index, không giả định đúng 1 vòng), wrap mọi tool execution trong try/except → trả is_error=True để Claude tự sửa tham số và gọi lại (nối thẳng với luật "meaningful error messages" ở bước 1), và đặt trần số vòng lặp để một tool hỏng không kéo loop chạy vô hạn đốt tiền.

Conversation loop — xử lý nhiều tool nối tiếp (multi-turn tool pattern)

TL;DR mini. Một câu hỏi có thể cần nhiều tool NỐI TIẾP — vd "103 ngày nữa là ngày nào?"get_current_datetime (lấy hôm nay) → add_duration_to_datetime (cộng 103 ngày). App phải tự lặp cho tới khi Claude thôi xin tool. Đây là bản tổng quát hoá của bước 4-5 thành một conversation loop.

Vấn đề: bước 4-5 mới khép một vòng tool. Nhưng Claude có thể cần chuỗi tool trước khi đủ thông tin trả lời. Trình tự "What day is 103 days from today?":

  1. User hỏi.
  2. Claude xin tool get_current_datetime.
  3. Server chạy → trả kết quả (hôm nay).
  4. Claude nhận ra cần thêm → xin tool add_duration_to_datetime.
  5. Server chạy → trả kết quả.
  6. Claude đủ thông tin → trả lời cuối.

Conversation loop — lặp tới khi Claude ngừng xin tool:

def run_conversation(messages):
    while True:
        response = chat(messages)
        add_assistant_message(messages, response)      # lưu nguyên message (cả block)

        if response.stop_reason != "tool_use":         # không xin tool nữa → xong
            break

        tool_result_blocks = run_tools(response)       # chạy HẾT tool_use block
        add_user_message(messages, tool_result_blocks) # trả kết quả → lặp lại
    return messages

Refactor 4 helper để loop chịu được multi-block:

(1) add_user_message / add_assistant_message — nhận cả Message object. Trước chỉ nhận string; giờ phải nhận string · list block · nguyên Message object (để truyền thẳng response):

from anthropic.types import Message

def add_user_message(messages, message):
    messages.append({
        "role": "user",
        "content": message.content if isinstance(message, Message) else message
    })

isinstance(message, Message) → nếu là Message object thì bóc .content (list block); còn nếu đã là string/list thì giữ nguyên. Nhờ vậy add_assistant_message(messages, response) truyền cả object vẫn đúng.

(2) chat() — nhận tools + trả FULL message. Thêm tools=None, stop_sequences=[], và ⭐ return message (nguyên object) thay vì message.content[0].text:

def chat(messages, system=None, temperature=1.0, stop_sequences=[], tools=None):
    params = {"model": model, "max_tokens": 1000, "messages": messages,
              "temperature": temperature, "stop_sequences": stop_sequences}
    if tools:  params["tools"] = tools
    if system: params["system"] = system
    return client.messages.create(**params)     # trả FULL message

(3) text_from_message() — bóc text khi cần hiển thị. Vì giờ giữ nguyên message đa block, cần helper gom text. ⭐ Nó join MỌI text block (một message có thể có nhiều text block), khác hẳn content[0].text (chỉ block đầu):

def text_from_message(message):
    return "\n".join(
        [block.text for block in message.content if block.type == "text"]
    )

4 cải tiến khoá gọi ra: Flexible message handling (helper nhận nhiều dạng) · Tool support in chat (chat nhận & truyền schema) · Full message returns (giữ nguyên mọi block, không mất id) · Text extraction utility (text_from_message lấy text sạch từ message phức tạp).

⚠️ Mấy chỗ dễ nhầm. (1) ⭐ chat() đổi kiểu trả về từ string sang Message objectmọi chỗ gọi chat() cũ phải sửa (không còn answer = chat(...) là string; giờ phải text_from_message(chat(...))). (2) text_from_message join nhiều text block, không phải content[0].text — quen tay lấy [0] sẽ mất chữ khi model trả nhiều text block. (3) Điều kiện break là stop_reason != "tool_use" (pseudo "isn't asking for a tool"). (4) add_assistant_message(messages, response) truyền cả Message object, isinstance lo phần bóc .content. (5) multi-turn tool ≠ multi-turn hội thoại — đây là Claude tự gọi nhiều tool trong một lượt trả lời của user, không phải nhiều lượt user.

Góc Delivery Manager. run_conversationskeleton của mọi agent — và đây là chỗ phải cài circuit breaker. Loop while True không có trần vòng lặp là một quả bom chi phí: model kẹt (xin đi xin lại một tool, hoặc hai tool đá nhau) sẽ quay vô hạn, mỗi vòng một API call tính tiền. Team em luôn kèm max iterations (vd 10) + log số vòng thực tế mỗi request để phát hiện tool nào hay làm loop dài. text_from_message (gom nhiều text block) là tín hiệu nhắc: message của Claude là cấu trúc, đừng bao giờ giả định "một message = một text" — thói quen content[0].text là nguồn bug thầm lặng khi tool bật lên.

run_tools / run_tool — tách "gom kết quả" khỏi "chạy từng tool"

TL;DR mini. Bản hoàn chỉnh của loop tách phần chạy tool thành 2 hàm: run_tools(message) gom — lọc các tool_use block, chạy từng cái, trả về list tool_result block; run_tool(name, input) = router map tên tool → hàm thật. Tách vậy để thêm tool mới không đụng loop lõi.

run_conversation bản đủ — có print text trung gian mỗi vòng:

def run_conversation(messages):
    while True:
        response = chat(messages, tools=[get_current_datetime_schema])
        add_assistant_message(messages, response)
        print(text_from_message(response))       # thấy Claude "nói" gì trước khi xin tool
        if response.stop_reason != "tool_use":
            break
        tool_results = run_tools(response)        # chạy HẾT tool, gom kết quả
        add_user_message(messages, tool_results)
    return messages

run_tools — gom kết quả (lọc theo type, trả LIST):

def run_tools(message):
    tool_requests = [b for b in message.content if b.type == "tool_use"]  # bỏ text block
    tool_result_blocks = []
    for tool_request in tool_requests:
        try:
            tool_output = run_tool(tool_request.name, tool_request.input)
            block = {"type": "tool_result", "tool_use_id": tool_request.id,
                     "content": json.dumps(tool_output), "is_error": False}
        except Exception as e:
            block = {"type": "tool_result", "tool_use_id": tool_request.id,
                     "content": f"Error: {e}", "is_error": True}   # lỗi VẪN trả block
        tool_result_blocks.append(block)
    return tool_result_blocks

run_tool — router (tên → hàm):

def run_tool(tool_name, tool_input):
    if tool_name == "get_current_datetime":
        return get_current_datetime(**tool_input)
    elif tool_name == "another_tool":
        return another_tool(**tool_input)
    # thêm tool mới ở đây, KHÔNG phải sửa run_conversation / run_tools

Vì sao tách 2 hàm? Phân vai rạch ròi: run_conversation lo vòng lặp, run_tools lo duyệt + gom kết quả + bắt lỗi, run_tool lo "tool này chạy hàm nào". Thêm tool thứ 5, thứ 10 → chỉ đụng run_tool, hai hàm kia bất động (scalable tool routing). (Ở practice/12 mình làm gọn hơn bằng dict TOOL_FUNCTIONS[name](**input) thay if/elif — cùng ý tưởng router, dict đỡ dài khi nhiều tool.)

2 chi tiết mới so với bản trước:

  • content = json.dumps(tool_output) (không phải str(...)). json.dumps serialize chuẩn cho dict/list/số (Claude parse lại được); str() chỉ hợp output đơn giản. Cùng là "đưa string vào content", nhưng json.dumps an toàn hơn cho data có cấu trúc.
  • print(text_from_message(response)) mỗi vòng → thấy phần text Claude nói kèm lúc xin tool (vd "I'll solve these" trước 2 tool_use block calculator 10+1030+30). Đây là ví dụ nhiều tool song song trong 1 responserun_tools trả 2 tool_result block một lượt.

⚠️ Điểm dễ sai chí mạng: trong except, vẫn phải append một tool_result block (kèm is_error=True) cho mọi tool_use block. Nếu tool lỗi mà anh bỏ qua không trả block, Claude sẽ treo chờ kết quả của cái id đó → API báo thiếu tool_result. Luật sắt: cứ có một tool_use block thì PHẢI có đúng một tool_result block khớp id, dù thành công hay lỗi.

Thêm nhiều tool — pattern 4 bước (hoàn tất project reminder)

TL;DR mini. Khi hạ tầng loop (run_conversation · run_tools · run_tool) đã có, thêm tool mới không cần đập lại code cũ — chỉ theo 4 bước cố định. Đây là lúc ráp trọn 3 tool của project reminder.

3 tool của reminder — mỗi cái vá đúng một limitation đã nêu đầu module:

Tool Vá limitation
get_current_datetime Claude không biết hôm nay ngày mấy (limited time awareness)
add_duration_to_datetime Claude tính ngày-giờ không chuẩn (date calculation issues)
set_reminder Claude không có cơ chế đặt reminder (no reminder capability)

⭐ Pattern 4 bước thêm bất kỳ tool nào:

  1. Viết tool function (hàm Python thật).
  2. Định nghĩa schema (JSON Schema mô tả cho Claude).
  3. Thêm schema vào tools list trong run_conversation:
    response = chat(messages, tools=[
        get_current_datetime_schema,
        add_duration_to_datetime_schema,
        set_reminder_schema,
    ])
    
  4. Thêm case trong run_tool (router):
    def run_tool(tool_name, tool_input):
        if tool_name == "get_current_datetime":
            return get_current_datetime(**tool_input)
        elif tool_name == "add_duration_to_datetime":
            return add_duration_to_datetime(**tool_input)
        elif tool_name == "set_reminder":
            return set_reminder(**tool_input)
    

Vì sao đây là điểm mạnh kiến trúc: run_conversation (vòng lặp) và run_tools (gom/bắt lỗi) bất động khi thêm tool — chỉ đụng 2 chỗ là tools list + run_tool. Đó là modular / seamless expansion: mở rộng năng lực mà không tái cấu trúc logic sẵn có. (Ở practice/12 mình dùng dict router nên còn gọn hơn: thêm tool chỉ cần đăng ký vào TOOL_FUNCTIONS + TOOL_SCHEMAS, run_tool không đổi một dòng.)

Test buộc dùng nhiều tool (nối tiếp): "Set a reminder for my doctors appointment. It's 177 days after Jan 1st, 2050." → Claude phải:

  1. Tính ngày bằng add_duration_to_datetime (1/1/2050 + 177 ngày = 27/6/2050).
  2. Đặt reminder bằng set_reminder cho ngày đó.

Đây là chaining (tool 2 cần output tool 1) — Claude giải thích trước sẽ làm gì, rồi gọi tool tuần tự.

Message flow đầy đủ khi soi messages:

  • user — yêu cầu gốc.
  • assistant — chứa cả text block lẫn tool_use block (Claude vừa nói vừa xin tool).
  • tool_result message(s) — kết quả trả về (trong user message).
  • assistant tiếp theo — bước kế / câu trả lời cuối.

→ Minh hoạ lại: một message có thể gồm nhiều block (text + tool_use), và hội thoại là chuỗi qua lại tới khi xong.

Kết quả thực hành — file practice/12_tool_use.py (2 tool: get_exchange_rate + convert_amount)

Chạy thật một agent loop với 2 tool tỉ giá giả lập, thu được 4 quan sát khớp lý thuyết:

  1. 1 tool, 1 vòng. "1 USD ra bao nhiêu VND?" → một tool_use → chạy → trả lời. Tốn 2 API call (1 xin + 1 trả lời).
  2. Nhiều tool song song (parallel). "100 USD ra VND và JPY?" → Claude bung 2 tool_use block trong MỘT lượt; code gom 2 tool_result vào một user message, khớp 2 id. Vẫn chỉ 2 API call dù chạy 2 tool.
  3. is_error → self-correction (multi-vòng). "1 EUR ra VND?" (EUR không hỗ trợ) → Claude đọc câu lỗi, đổi chiến lược: thử EUR→VND (lỗi) → EUR→USD (lỗi, cố route qua USD) → USD→VND (OK) → trả lời. Chạy 4 API call, 3 lần gọi tool cho một câu — nếu code chỉ làm một vòng như tài liệu dạy thì vỡ ngay ở lỗi đầu. ⚠️ Nhưng câu chốt Claude vẫn bịa ước lượng EUR từ training data ("1 EUR ≈ 27.000 VND") dù tool đã từ chối → tool giới hạn được hành động, KHÔNG tự giới hạn lời nói; muốn cấm đoán mò phải xử lý ở system prompt.
  4. Tool chaining (sequential, phụ thuộc). "430.000 VND ra bao nhiêu JPY?"get_exchange_rate('VND','JPY') trả 0.006188118811881188 → Claude nhét đúng con số đó vào convert_amount(amount=430000, rate=0.006188118811881188). Rate ở tool 2 trùng khít output tool 1 = bằng chứng đóng đinh Claude lấy output tool này làm input tool kia, qua 2 vòng stop_reason=="tool_use" nối tiếp. Khác kịch bản 2 (song song, độc lập).

Bài học đắt nhất (DM): convert_amount chỉ làm một phép nhân Claude thừa sức tự tính, nhưng ép qua tool đổi lấy tính xác định + audit được (mọi con số đi qua code, log lại được) — đánh đổi bằng số vòng API tăng (chaining tốn 3 call thay vì 2). Tool hoá tới đâu là quyết định thiết kế thật: cần kiểm chứng chặt (tài chính/y tế) thì tool hoá sâu, cần nhanh thì để model tự tính. Và điểm 3 cho thấy stop_reason + while loop (không phải "một vòng") mới là bản dùng được ở production.

Streaming + tool use — InputJsonEvent & fine-grained tool calling

TL;DR mini. Bật streaming lúc dùng tool → Claude nhỏ giọt cả tham số tool (không chỉ text). Event mới = InputJsonEvent (chunk.type == "input_json"), có partial_json (mẩu mới) + snapshot (JSON cộng dồn). Mặc định API buffer + validate từng cặp key-value top-level → "khựng rồi bung một cụm". Cần nhanh hơn thì bật fine_grained=True để tắt validation — đổi lại phải tự xử lý JSON hỏng.

Ghép streaming vào tool use. Ở bài streaming (07) anh đã quen event ContentBlockDelta (mẩu text). Khi Claude dựng tham số tool, có thêm loại event InputJsonEvent, mang 2 thứ:

Thuộc tính Là gì
partial_json Một mẩu JSON của tham số tool (phần mới vừa sinh).
snapshot JSON cộng dồn từ mọi mẩu nhận tới giờ (bản đầy đủ dần).
for chunk in stream:
    if chunk.type == "input_json":
        print(chunk.partial_json)        # mẩu vừa tới
        current_args = chunk.snapshot    # hoặc dùng bản cộng dồn tới giờ

⭐ Vì sao streaming mà vẫn thấy "khựng rồi bung"? Vì API không bắn từng ký tự ngay khi Claude sinh. Mặc định nó buffer + validate: đợi trọn một cặp key-value top-level, kiểm với schema, rồi mới gửi cả cụm đã buffer. Với cấu trúc:

{ "abstract": "This paper presents...", "meta": { "word_count": 847, "review": "..." } }

API sẽ: (1) đợi hết value của abstract → (2) validate cặp đó → (3) bắn cả cụm abstract một lần → (4) lặp lại cho object meta. Đó là lý do thấy trễ rồi bung một loạt dù đã bật streaming — chunk bị giữ lại tới khi có một cặp key-value hợp lệ.

Fine-grained tool calling — đổi validation lấy tốc độ. Muốn mẩu về ngay khi Claude sinh (hiện progress realtime, xử lý partial sớm) thì bật fine_grained=True:

run_conversation(messages, tools=[save_article_schema], fine_grained=True)

Nó làm đúng một việc: tắt JSON validation phía API. Hệ quả:

  • ✅ Nhận chunk ngay khi Claude sinh (không đợi trọn key).
  • ✅ Không có độ trễ buffer giữa các top-level key → streaming "mượt" kiểu truyền thống.
  • ⚠️ Critical: validation bị TẮT → code của anh phải tự lo JSON hỏng.

Xử lý JSON hỏng. Không có validation, Claude có thể sinh JSON không hợp lệ giữa chừng (vd "word_count": undefined thay vì số). Phải bọc try/except:

try:
    parsed_args = json.loads(chunk.snapshot)
except json.JSONDecodeError:
    print("JSON chưa hợp lệ, bỏ qua mẩu này...")   # xử lý êm, đừng để crash

⚠️ Dễ nhầm. (1) Không bật fine-grained, chính validation của API bắt lỗi này — và có thể bọc value lỗi vào string để "cứu", nhưng kết quả có thể không khớp schema anh mong đợi. (2) partial_json = mẩu rời, snapshot = bản cộng dồn — parse thì dùng snapshot. (3) fine-grained không làm Claude sinh nhanh hơn; nó chỉ bỏ hàng đợi buffer để mẩu tới sớm hơn.

Khi nào bật fine-grained: cần progress realtime trên tham số tool · muốn xử lý partial sớm nhất · độ trễ buffer làm hỏng UX · và anh sẵn sàng viết xử lý lỗi JSON chắc tay. Còn hầu hết app: mặc định (có validate) là đủ và an toàn hơn — đừng bật fine-grained nếu không thật sự cần.

Góc Delivery Manager. Đây là một đánh đổi an toàn ↔ độ nhạy kinh điển, đừng bật fine-grained theo phản xạ "cho nhanh". Mặc định, API validate hộ mình = một lớp lưới đỡ miễn phí; tắt nó đi là mình tự nhận nợ kỹ thuật: phải parse JSON dang dở mỗi mẩu, chịu được value rác (undefined), và không để một mẩu hỏng làm sập cả pipeline. Chỉ đáng bật khi UX thật sự cần thấy tham số mọc dần (vd tool sinh một bài viết dài, muốn show live). Với đa số tool ngắn (đổi tiền, tra ngày), buffer chỉ trễ mili-giây — bật fine-grained là chuốc rủi ro không đổi lại lợi ích.

Text editor tool — built-in tool đầu tiên (schema có sẵn, hàm tự viết)

TL;DR mini. Đây là tool đầu tiên anh KHÔNG phải tự viết schema — Claude có sẵn "kiến thức" về một text editor. ⭐ Nhưng bẫy chí mạng: built-in chỉ là SCHEMA, còn IMPLEMENTATION (hàm thật đọc/ghi file) anh vẫn phải tự viết. Claude biết cách xin thao tác file; code của anh mới thật sự đụng ổ đĩa.

Tool này cho Claude làm gì (năng lực của một text editor thực thụ):

Command Việc
view Xem nội dung file hoặc thư mục, hoặc một khoảng dòng cụ thể
str_replace Thay đoạn text trong file
create Tạo file mới
insert Chèn text tại một dòng cụ thể
undo_edit Hoàn tác chỉnh sửa gần đây

→ Về cơ bản, biến Claude thành "kỹ sư phần mềm ngay từ đầu" — đọc/sửa/tạo file như trong editor.

⭐ Điểm dễ nhầm nhất — built-in SCHEMA ≠ built-in IMPLEMENTATION. So với custom tool anh đã làm:

Custom tool (get_exchange_rate…) Text editor tool
Schema anh tự viết Claude có sẵn (built-in)
Implementation (hàm thật) anh tự viết anh vẫn phải tự viết

Nghĩa là: Claude gửi request kiểu "view file ./main.py" / "create ./test.py" — nhưng anh phải có sẵn hàm để thật sự mở file, ghi file, liệt kê thư mục… Không có hàm thì request của Claude rơi vào hư không.

Schema stub — vẫn phải khai báo một mẩu nhỏ, tuỳ model. Dù schema đầy đủ nằm trong Claude, request vẫn cần một stub gồm đúng type + name, khác nhau theo model:

def get_text_edit_schema(model):
    if model.startswith("claude-3-7-sonnet"):
        return {"type": "text_editor_20250124", "name": "str_replace_editor"}
    elif model.startswith("claude-3-5-sonnet"):
        return {"type": "text_editor_20241022", "name": "str_replace_editor"}

Claude thấy stub bé xíu này rồi tự bung thành full text-editor spec phía sau. ⚠️ Version string (type) đổi theo từng model — khoá chỉ liệt kê 3.5/3.7-sonnet; model mới hơn (4.x…) dùng chuỗi khác → luôn tra bảng version ở docs Anthropic trước khi dùng, đừng chép cứng giá trị cũ.

Ví dụ thực tế:

  • "Open the ./main.py file and summarize its contents" → Claude view file → đọc → tóm tắt.
  • "...write a function to calculate pi to the 5th digit. Then create a ./test.py to test it" → Claude view main.py → str_replace nội dung mới (hàm pi) → create test.py với unit test.

Vì sao cần tool này (khi editor hiện đại đã có AI sẵn)? — Khi anh tự xây app cần: sửa file bằng code (programmatic) · chạy ở môi trường thiếu editor xịn · nhúng năng lực sửa file thẳng vào app Claude của mình. Tức là tự dựng một "AI code editor" bên trong app của anh, kiểm soát chi tiết cách Claude đụng vào file system.

Góc Delivery Manager. "Schema có sẵn, hàm tự viết" nghe tiện, nhưng cái anh tự viết mới là chỗ rủi ro cao nhất: hàm implementation của anh có toàn quyền đụng ổ đĩa theo lệnh của một model. Đây là bề mặt tấn công phải khoá: giới hạn thư mục (sandbox/allow-list path, chặn ../ thoát ra ngoài), read-only khi chưa cần ghi, không bao giờ để nó chạm file hệ thống / secret, và log mọi thao tác create/str_replace. Model gửi lệnh, nhưng trách nhiệm an toàn nằm ở hàm của anh — đúng tinh thần "Claude chỉ xin, code mình mới thực thi", nhân lên gấp bội vì lần này thứ nó xin là thao tác trên file thật.

Web search tool — built-in tool chạy hoàn toàn phía server

TL;DR mini. Tool cho Claude tự tra internet để trả lời câu cần thông tin mới/chuyên biệt. ⭐ Khác text editor một trời một vực: ở đây anh KHÔNG viết implementationAnthropic chạy trọn quá trình search, anh chỉ cần đưa một schema đơn giản. Prereq: org phải bật Web Search trong console (privacy settings) mới dùng được.

Hai kiểu built-in tool — phân biệt cho kỹ:

Text editor tool Web search tool
Schema Claude có sẵn Claude có sẵn
Implementation anh tự viết (client-side) Anthropic lo hết (server-side)
Ai chạy code của anh server Anthropic

Schema — chỉ vài field:

web_search_schema = {
    "type": "web_search_20250305",
    "name": "web_search",
    "max_uses": 5,          # trần số lần search
}

max_uses giới hạn số lần Claude được search. Claude có thể search follow-up dựa trên kết quả đầu (một lần search trả nhiều kết quả, nhưng nó có thể thấy cần tìm thêm) → max_uses chặn đốt API quá tay.

Response có các block KIỂU MỚI (để anh thấy Claude tra gì, nguồn nào):

Block Chứa gì
Text block Claude giải thích đang làm gì
ServerToolUseBlock câu query chính xác Claude đã search
WebSearchToolResultBlock cụm kết quả search
WebSearchResultBlock từng kết quả lẻ (title + URL)
Citation block đoạn text chống lưng cho câu trả lời + URL nguồn

⭐ Chú ý cái tên ServerToolUseBlock — có tiền tố "Server"server Anthropic thực thi, khác ToolUseBlock thường (client tool, code anh thực thi). Đây là dấu hiệu nhận diện server tool.

Giới hạn domain — allowed_domains:

web_search_schema = {
    "type": "web_search_20250305", "name": "web_search",
    "max_uses": 5,
    "allowed_domains": ["nih.gov"],     # chỉ tra trong các domain này
}

Cực hữu ích khi cần nguồn authoritative: hỏi chuyện y tế/tập luyện mà khoá vào PubMed (nih.gov) → lấy thông tin evidence-based thay vì blog vớ vẩn.

Render kết quả (mỗi block phục vụ một chỗ trên UI): text block = nội dung thường · web search results = list nguồn ở trên cùng · citations = inline trong text (kèm domain · page title · URL · quoted text). Cấu trúc này giúp user thấy Claude lấy tin từ đâu → minh bạch, tạo niềm tin.

Hợp nhất cho: current events · thông tin chuyên biệt ngoài training data · fact-check & tìm nguồn authoritative · research cần dữ liệu cập nhật. Chỉ cần bỏ schema vào tools array, Claude tự quyết khi nào cần search.

Góc Delivery Manager. Web search "nhẹ code" (không phải viết implementation như text editor) nhưng nặng vận hành: (1) costmax_usesvan chi phí bắt buộc, vì Claude tự search follow-up có thể nhân số call; đặt trần theo use case. (2) privacy — bật tool = query của user bị gửi ra web search của Anthropic; phải cân nhắc dữ liệu nhạy cảm (đó là lý do nó nằm sau opt-in trong console privacy). (3) allowed_domains = đòn bẩy chất lượng & thương hiệu: khoá vào nguồn uy tín để tránh Claude trích blog rác — thẳng hàng với thế giới AEO/GEO/EEAT của mình: citation minh bạch (domain/title/URL/quoted) chính là thứ xây trust, và đây là góc nhìn "làm sao để AI trích đúng nguồn mình muốn". Server tool đổi quyền kiểm soát (Anthropic chạy) lấy tiện lợi (không code, không bảo trì).


Phần tiếp theo: RAG với Claude — text chunking, embeddings, BM25 & hybrid search

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á.