- 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 anh và Claude. 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_durationluôn cộng đúng,set_reminderluô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
raisevà 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_formatminh 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
raisequay 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. ⭐descriptionlà 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_schemacódescriptionriêng).
Nguyên tắc:
descriptionkhô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:
- Copy code tool function của anh.
- Đưa cho Claude, bảo nó viết JSON schema cho tool calling.
- Kèm tài liệu tool use của Anthropic làm ngữ cảnh (context).
- Để 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_datetime→get_current_datetime_schema) → dễ ghép cặp hàm ↔ schema. date_formatcódefaultnên không nằm trongrequired(mảngrequiredrỗ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ầminput_schemavớ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ódefaultthì không liệt kê trongrequired. (5)ToolParamlà 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à
descriptionlà 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 coidescription+input_schemangang 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àdescriptionphả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.ToolParamthì 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àoclient.messages.create(). Khi Claude quyết định dùng tool, response không còn là một text block đơn lẻ —contentthà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):
- Gửi user message + tool schema cho Claude.
- Nhận assistant message chứa text block + tool_use block.
- 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).
- Gửi tool result về Claude kèm toàn bộ lịch sử.
- 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)
toolsnhận một LIST schema, không phải một schema lẻ. (3) ⭐ Lưuresponse.contentnguyên cả list, KHÔNGresponse.content[0].text— quên là mấtid→ 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].textcứ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ênresponse.contentvà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áiidcủa tool_use là khoá để ghép kết quả ở vòng sau; (2) duyệt block theotypethay 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: validatename/inputtrước khi thực thi, và luôn trả kết quả kèm đúngid.
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
inputtừ 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ộttool_resultblock (đặt trong mộtusermessage) 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:
user— câu hỏi gốc.assistant— chứa text block + ToolUse block.user— chứatool_resultblock.
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_resultblock). 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_resultblock thuộcusermessage, không phảiassistant(dễ tưởng "kết quả là của mình nên để assistant"). (2)tool_use_idphải khớpid— sai/thiếu là API lỗi hoặc Claude ghép nhầm. (3)contentphải là string — số/dict phảistr(...)hoặcjson.dumps(...)trước. (4) Nhiều tool → mộtusermessage chứa nhiềutool_result, KHÔNG phải mỗi kết quả một message. (5) Vòng cuối vẫn kèmtools. (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 theoblock.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èmtools" mới chỉ là một vòng. Sản phẩm thật cần agent loop: bọc trongwhile True, mỗi vòng nhìnstop_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.appendmỗi vòng; "khớptool_use_id" = vì một message có thể gom nhiều kết quả; "vòng cuối vẫn kèmtools" = vì loop gọicreatemỗi vòng với cùngtools. Với team em chốt: duyệt block theotype+ lặp theostop_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?":
- User hỏi.
- Claude xin tool
get_current_datetime. - Server chạy → trả kết quả (hôm nay).
- Claude nhận ra cần thêm → xin tool
add_duration_to_datetime. - Server chạy → trả kết quả.
- 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àMessageobject thì bóc.content(list block); còn nếu đã là string/list thì giữ nguyên. Nhờ vậyadd_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 object → mọi chỗ gọichat()cũ phải sửa (không cònanswer = chat(...)là string; giờ phảitext_from_message(chat(...))). (2)text_from_messagejoin nhiều text block, không phảicontent[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ảMessageobject,isinstancelo 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_conversationlà skeleton của mọi agent — và đây là chỗ phải cài circuit breaker. Loopwhile Truekhô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 quencontent[0].textlà 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áctool_useblock, chạy từng cái, trả về listtool_resultblock;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ảistr(...)).json.dumpsserialize 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àocontent", nhưngjson.dumpsan 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 calculator10+10và30+30). Đây là ví dụ nhiều tool song song trong 1 response —run_toolstrả 2tool_resultblock một lượt.
⚠️ Điểm dễ sai chí mạng: trong
except, vẫn phảiappendmộttool_resultblock (kèmis_error=True) cho mọitool_useblock. Nếu tool lỗi mà anh bỏ qua không trả block, Claude sẽ treo chờ kết quả của cáiidđó → API báo thiếu tool_result. Luật sắt: cứ có mộttool_useblock thì PHẢI có đúng mộttool_resultblock khớpid, 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:
- Viết tool function (hàm Python thật).
- Định nghĩa schema (JSON Schema mô tả cho Claude).
- Thêm schema vào
toolslist trongrun_conversation:response = chat(messages, tools=[ get_current_datetime_schema, add_duration_to_datetime_schema, set_reminder_schema, ]) - 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àtoolslist +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/12mình dùng dict router nên còn gọn hơn: thêm tool chỉ cần đăng ký vàoTOOL_FUNCTIONS+TOOL_SCHEMAS,run_toolkhô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:
- Tính ngày bằng
add_duration_to_datetime(1/1/2050 + 177 ngày = 27/6/2050). - Đặt reminder bằng
set_remindercho 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_resultmessage(s) — kết quả trả về (trongusermessage).assistanttiế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 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). - Nhiều tool song song (parallel). "100 USD ra VND và JPY?" → Claude bung 2
tool_useblock trong MỘT lượt; code gom 2tool_resultvào mộtusermessage, khớp 2id. Vẫn chỉ 2 API call dù chạy 2 tool. 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.- 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àoconvert_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òngstop_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_amountchỉ 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ấystop_reason+whileloop (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ậtfine_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ùngsnapshot. (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
viewfile → đọc → tóm tắt. - "...write a function to calculate pi to the 5th digit. Then create a ./test.py to test it" → Claude
viewmain.py →str_replacenội dung mới (hàm pi) →createtest.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áccreate/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 implementation — Anthropic 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" vì server Anthropic thực thi, khácToolUseBlockthườ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) cost —
max_useslà van 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á.