02 · Tool use và agent loop — giao thức, thiết kế tool, lỗi và idempotency
Agent nghe có vẻ phức tạp, nhưng phần lõi chỉ là một vòng while quanh một API stateless. Cái khó ở mức senior
không phải viết vòng lặp, mà là: tool thiết kế thế nào để model dùng đúng, vòng lặp dừng khi nào, và
khi process chết giữa chừng thì tác dụng phụ (hoàn tiền, gửi email) có bị chạy hai lần không. Bài này trả lời cả ba bằng
lab chạy thật với SDK PHP chính thức.
Đã verify gì, chưa verify gì. Mọi lab dùng SDK chính thứcanthropic-ai/sdkv0.52.0 (PHP 8.4.19, Guzzle 8.2.0) gọi vào mock server nói đúng giao thức Messages API (labs/ai/mock_anthropic.py) — vì máy lab không có API key. Nghĩa là: định dạng request/response, cách SDK maptoolUseID/isError, retry, checkpoint, idempotency đã chạy thật; quyết định của model thật (có gọi đúng tool không, có tự bỏ cuộc khi tool lỗi không) do mock đóng vai theo kịch bản cố định — chưa verify với model thật. Context7 không có trong môi trường; tài liệu API lấy từ bộ tham chiếu Claude API (cache 2026-09-25) và mã nguồn SDK.
Câu trả lời 60 giây
"Agent là vòng lặp quanh Messages API. Model không chạy được gì; nó trả về blocktool_usegồm tên tool và input JSON,stop_reason = tool_use. Code của tôi chạy tool, gửi lạitool_resultcùngtool_use_id, lặp tới khiend_turn. Mảngmessageschính là state của agent — một event log. Ba thứ tôi luôn làm: giới hạn số vòng vì vòng lặp không tự đảm bảo dừng; trả lỗi tool về model bằngis_errorthay vì để exception giết vòng lặp; và làm tool có tác dụng phụ idempotent bằng khoá nghiệp vụ, vì tôi đã tái hiện được: crash sau khi hoàn tiền rồi retry job thì model sinhtool_use_idmới và khách được hoàn tiền hai lần nếu khoá dựa vào id đó."
Thuật ngữ
| Thuật ngữ | Nghĩa ngắn |
|---|---|
| tool use / function calling | Model sinh ra yêu cầu gọi hàm có cấu trúc; ứng dụng thực thi và trả kết quả. |
| harness | Code bao quanh model: chạy vòng lặp, thực thi tool, kiểm quyền, quản lý context. |
| ACI (agent–computer interface) | "Giao diện" của tool nhìn từ phía model: tên, mô tả, schema, định dạng kết quả và lỗi. |
| idempotent | Gọi 1 lần hay N lần cho cùng một ý định thì kết quả cuối như nhau. |
| checkpoint | Lưu state (ở đây là mảng messages) ra bộ nhớ bền để resume sau khi process chết. |
| durable execution | Mô hình chạy workflow mà mỗi bước được ghi lại, crash thì chạy tiếp từ bước dở (vd Temporal). |
1. Bài toán gốc và bối cảnh ra đời
LLM chỉ sinh chữ. Muốn nó tra đơn hàng, người ta từng viết prompt kiểu ReAct (Yao et al., 2022): "Suy nghĩ: … Hành động: search[…]", rồi dùng regex bóc chuỗi "Hành động" ra để chạy. Cách này mong manh: model viết sai định dạng, thiếu ngoặc, bịa tên hành động. Function calling (OpenAI 6/2023; Anthropic tool use GA 5/2024) chuẩn hoá lại: API nhận danh sách tool có JSON Schema, model được huấn luyện để trả về một block có cấu trúc thay vì chữ tự do, và API tách block đó ra cho bạn.
Được tối ưu cho: để model tự chọn hành động trong không gian mở — không biết trước cần gọi tool nào, bao nhiêu lần, theo thứ tự nào. Không tối ưu cho: luồng cố định biết trước (viết code tuần tự rẻ và chắc hơn), độ trễ thấp (mỗi vòng là một lần gọi model), đảm bảo giao dịch (không có transaction bao quanh nhiều tool call), hay tất định.
2. Mô hình tư duy cốt lõi
Model đề xuất, harness quyết định. Agent =while (stop_reason == "tool_use")quanh một API stateless; state duy nhất là mảngmessages— một event log append-only mà bạn sở hữu.
sequenceDiagram
participant U as Người dùng
participant H as Harness (Laravel)
participant M as Messages API
participant T as Tool (DB, payment)
U->>H: "Hoàn tiền đơn 1001"
H->>M: tools + messages[user]
M-->>H: tool_use get_order(1001), stop_reason=tool_use
H->>T: SELECT đơn 1001 (kiểm quyền theo session)
T-->>H: {status: delivered, total: 350000}
H->>M: messages + tool_result(tool_use_id)
M-->>H: tool_use refund_order(1001, 350000)
H->>T: refund (khoá idempotency)
T-->>H: ok
H->>M: messages + tool_result
M-->>H: text "Đã hoàn tiền", stop_reason=end_turn
H-->>U: câu trả lời
Hệ quả trực tiếp của abstraction:
- Mọi quyền lực nằm ở harness. Model chỉ sinh JSON; kiểm quyền, giới hạn, duyệt, ghi log là việc của code bạn. Đây là nền của toàn bộ bài bảo mật (bài 08).
- State là event log bạn giữ. Muốn resume sau crash, audit, replay để debug — lưu mảng messages. Muốn "sửa lịch sử" thì phải cẩn thận vì phá prompt cache và (trên model mới) làm mất hiệu lực các thinking block (bài 03).
- Mỗi vòng gửi lại toàn bộ lịch sử. Chi phí input cộng dồn tăng theo bình phương số vòng (lab mục 5).
- Dừng hay không là do model. Không có gì trong giao thức đảm bảo vòng lặp kết thúc → giới hạn phải do bạn đặt.
3. Internals và quyết định thiết kế
3.1 Giao thức trên dây
| Thành phần | Hình dạng | Ghi chú |
|---|---|---|
| Định nghĩa tool | {name, description, input_schema} | Được render vào đầu prompt (trước system) → đổi tool = mất cache |
| Yêu cầu gọi | Block {type: "tool_use", id, name, input} trong content của assistant | Một lượt có thể có nhiều block (parallel tool use) |
| Kết quả | Block {type: "tool_result", tool_use_id, content, is_error?} trong message role user | Tất cả kết quả của một lượt nằm chung một user message |
stop_reason | end_turn, tool_use, max_tokens, pause_turn, refusal, stop_sequence | Kiểm trước khi đọc content |
tool_choice | auto (mặc định), none; any/tool (ép gọi) | Opus 5.5 / Sonnet 5.5 / Fable 5.1 từ chối any/tool (400) → dùng auto + chỉ dẫn trong prompt, hoặc structured outputs |
strict: true | Trên định nghĩa tool | Đảm bảo input khớp schema (cần additionalProperties: false) |
Trong SDK PHP, key viết camelCase và SDK tự đổi sang snake_case trên dây: inputSchema, toolUseID,
isError, stopReason. Lab xác nhận: tool_result gửi bằng 'isError' => true đến mock server dưới
dạng is_error (mock đọc đúng khoá snake_case và chuyển sang nhánh "tool lỗi").
3.2 Vòng lặp tối thiểu nhưng đúng (PHP, đã chạy)
for ($i = 1; $i <= $maxIter; $i++) {
$resp = $client->messages->create(model: 'claude-opus-5-5', maxTokens: 1024,
tools: $tools, messages: $messages);
$content = json_decode(json_encode($resp->content), true); // lưu dạng mảng để checkpoint
$messages[] = ['role' => 'assistant', 'content' => $content]; // luôn append NGUYÊN content
$save(); // checkpoint trước khi chạy tool
if ($resp->stopReason !== 'tool_use') break; // end_turn / max_tokens / refusal
$results = [];
foreach ($content as $b) {
if ($b['type'] !== 'tool_use') continue;
try {
$out = runTool($b['name'], $b['input'], $b['id']);
$results[] = ['type' => 'tool_result', 'toolUseID' => $b['id'], 'content' => $out];
} catch (Throwable $e) { // lỗi tool là DỮ LIỆU cho model
$results[] = ['type' => 'tool_result', 'toolUseID' => $b['id'],
'content' => $e->getMessage(), 'isError' => true];
}
}
$messages[] = ['role' => 'user', 'content' => $results]; // mọi kết quả trong 1 message
$save();
}
Mỗi dòng là một quyết định: append nguyên content (không chỉ phần text — mất block tool_use thì
tool_result sau đó không khớp id và API trả 400); checkpoint trước khi chạy tool (để khi resume biết có tool đang
dở); bắt mọi exception thành is_error (model có cơ hội đổi hướng hoặc báo người dùng); gom mọi kết quả vào một message
(tài liệu Anthropic lưu ý: tách ra nhiều message sẽ "dạy" model ngừng gọi song song).
3.3 Ai chạy vòng lặp: bốn cách
| Cách | Bạn viết | Ai host | Dùng khi |
|---|---|---|---|
| Vòng lặp tự viết | Toàn bộ vòng lặp (như trên) | Bạn | Cần kiểm soát tuyệt đối, checkpoint riêng, không muốn phụ thuộc tính năng beta |
Tool Runner của SDK (beta; PHP: BetaRunnableTool + toolRunner()) | Chỉ hàm tool | Bạn | Đa số trường hợp; vẫn chèn được bước duyệt, sửa kết quả, giới hạn vòng |
| Claude Managed Agents (beta) | Cấu hình agent + tool riêng | Anthropic (vòng lặp + sandbox) | Agent chạy lâu, cần sandbox bash/file, chạy theo lịch |
| Claude Agent SDK | Prompt + tuỳ chọn | Bạn | Muốn nguyên harness Claude Code (đọc/sửa file, bash) trong hạ tầng của mình |
Đánh đổi: càng xuống dưới càng ít code nhưng càng ít kiểm soát chỗ lưu state và càng gắn với một nhà cung cấp. Với sản phẩm Laravel cần transaction DB và audit, vòng lặp tự viết hoặc Tool Runner chạy trong queue job là điểm cân bằng.
3.4 Thiết kế tool — phần quyết định agent giỏi hay dở
Tool là API công khai cho một client không tất định, đọc tài liệu rất kỹ nhưng không hỏi lại được. Nguyên tắc (tổng hợp từ hướng dẫn "Writing effective tools for agents" của Anthropic và tài liệu tool use):
| Nguyên tắc | Làm | Tránh | Vì sao |
|---|---|---|---|
| Gộp theo ý định | find_customer_orders(customer_ref, status?) | list_users + list_orders + join | Mỗi tool call là một vòng (latency + token); model ghép nhiều tool nhỏ hay sai |
| Mô tả nói khi nào gọi | "Gọi khi khách hỏi trạng thái hoặc muốn huỷ/hoàn một đơn cụ thể" | "Lấy đơn hàng" | Model quyết định gọi dựa trên mô tả; model đời mới gọi tool thận trọng hơn |
| Tham số hẹp | enum, định dạng rõ, ví dụ | query: string tự do | Giảm input sai; strict: true chặn input lệch schema |
| Kết quả gọn, có ngữ nghĩa | Tên, trạng thái, ID cần cho bước sau; phân trang | Dump 5MB JSON thô, UUID vô nghĩa | Kết quả tool chiếm context mỗi vòng sau |
| Lỗi có hướng dẫn | "Không có đơn 9999; mã đơn gồm 4 chữ số, hỏi lại khách" | "Error 500" | Model đọc lỗi để tự sửa |
| Tách tool đọc và ghi | preview_refund + execute_refund (cần duyệt) | Một tool "làm tất" | Chèn cổng duyệt, ghi audit, giới hạn quyền theo loại |
| Tham số nhạy cảm lấy từ session | user_id, tenant_id do server gắn | Để model truyền user_id | Model có thể bị lừa truyền id người khác (bài 08) |
Bash vs tool chuyên dụng. Một tool bash cho model đòn bẩy rất lớn, nhưng harness chỉ thấy một chuỗi lệnh mờ
đục — không chặn, không duyệt, không chạy song song an toàn được. Nâng một hành động thành tool riêng khi cần cổng bảo mật
(gửi tiền, xoá dữ liệu), kiểm tra trạng thái (từ chối ghi file nếu file đã đổi từ lần đọc trước), hiển thị (form duyệt),
hoặc chạy song song (tool đọc được đánh dấu an toàn).
Nhiều tool quá? Mỗi định nghĩa tool tốn token ở mọi request và làm model phân vân. Khi có hàng chục tool (thường xảy ra khi
cắm nhiều MCP server), dùng tool search (đánh dấu defer_loading, model tự tìm tool cần) hoặc programmatic tool
calling (model viết script gọi nhiều tool trong sandbox, chỉ kết quả cuối vào context). Cả hai được mô tả trong tài liệu API —
chưa verify trong lab.
3.5 State, checkpoint và idempotency
Mảng messages là event log, nên bài toán "agent chết giữa chừng" chính là bài toán quen thuộc của hệ phân tán: side effect đã xảy ra nhưng chưa được ghi nhận. Có ba điểm có thể chết:
flowchart LR A["Gọi model"] --> B["Lưu assistant turn (tool_use)"] B --> C["Chạy tool (side effect)"] C --> D["Lưu tool_result"] A -.->|"chết ở đây: gọi lại model, mất tiền token, không hại"| A C -.->|"chết ở đây: side effect đã xảy ra, log chưa có -> NGUY HIỂM"| X["Resume/retry chạy lại tool"]
Cách chặn chạy trùng phụ thuộc vào cái gì còn ổn định khi chạy lại. tool_use_id chỉ ổn định nếu assistant turn đã
nằm trong checkpoint; nếu retry từ đầu (queue job chạy lại), model sinh lượt mới với id mới. Khoá nghiệp vụ
(refund:{order_id}, hoặc refund:{order_id}:{request_id của khách}) ổn định bất kể model sinh gì. Lab mục 5 chứng
minh cả bốn trường hợp.
4. Đảm bảo và giới hạn
| Khía cạnh | Giao thức đảm bảo | Không đảm bảo — bạn phải tự lo |
|---|---|---|
| Định dạng input tool | Với strict: true: khớp JSON Schema | Giá trị hợp lý / được phép (order_id của người khác vẫn đúng schema) |
| Có gọi tool không | — | Với auto, model có thể không gọi; phải kiểm và nhắc lại |
| Thứ tự thực thi | — | Nhiều tool_use trong một lượt không có thứ tự; harness quyết định chạy song song hay tuần tự |
| Số lần thực thi | — | Nếu bạn retry/resume thì semantics là at-least-once → cần idempotency |
| Kết thúc | — | Vòng lặp có thể không bao giờ end_turn → max_iter, ngân sách token/thời gian |
| Nguyên tử nhiều bước | — | Không có transaction bao quanh chuỗi tool; cần saga/bù trừ nếu bước sau fail |
| Chi phí | — | Input cộng dồn ~O(n²) theo số vòng n (mỗi vòng gửi lại lịch sử) |
Trong khung lý thuyết hệ phân tán: tool call qua agent loop giống RPC qua mạng không tin cậy — không có exactly-once, chỉ có at-least-once + idempotent receiver hoặc at-most-once (không retry, chấp nhận mất).
5. Failure modes (có lab)
5.1 Tool luôn lỗi → vòng lặp không tự dừng
Mock đóng vai model "cứng đầu": thấy is_error thì gọi lại đúng tool đó. Chỉ có max_iter cứu:
=== B. tool luôn lỗi, max-iter=6
[iter 1] stop_reason=tool_use tools=get_order
tool get_order -> {"id":"1001","total":350000,"status":"delivered"}
[iter 2] stop_reason=tool_use tools=refund_order
tool refund_order -> ERROR payment gateway timeout (giả lập)
[iter 3] stop_reason=tool_use tools=refund_order
tool refund_order -> ERROR payment gateway timeout (giả lập)
[iter 4] stop_reason=tool_use tools=refund_order
tool refund_order -> ERROR payment gateway timeout (giả lập)
[iter 5] stop_reason=tool_use tools=refund_order
tool refund_order -> ERROR payment gateway timeout (giả lập)
[iter 6] stop_reason=tool_use tools=refund_order
tool refund_order -> ERROR payment gateway timeout (giả lập)
[stop] chạm max_iter=6 — dừng vòng lặp, báo người dùng / chuyển người thật
==> số giao dịch hoàn tiền trong DB: 0
--- kích thước input mỗi lượt (ước lượng ~4 ký tự/token, từ log mock server):
lượt 1: messages= 1 input≈ 152 token cộng dồn≈152
lượt 2: messages= 3 input≈ 246 token cộng dồn≈398
lượt 3: messages= 5 input≈ 329 token cộng dồn≈727
lượt 4: messages= 7 input≈ 422 token cộng dồn≈1149
lượt 5: messages= 9 input≈ 515 token cộng dồn≈1664
lượt 6: messages=11 input≈ 608 token cộng dồn≈2272
Input mỗi lượt tăng tuyến tính, cộng dồn tăng bậc hai: 6 lượt gửi tổng ~2 272 token dù mỗi lượt chỉ thêm ~90. Với tool result
2–3k token (log, kết quả search), 30 vòng lặp vô ích có thể tốn cả đô-la. Model thật thường tự dừng sau vài lần lỗi,
nhưng "thường" không phải "luôn" — hành vi model thật chưa verify. Phòng thủ: max_iter; phát hiện lặp (cùng tool +
cùng input N lần liên tiếp → dừng); ngân sách token (task_budget beta cho phép model tự biết còn bao nhiêu); lỗi tool
phân loại rõ "có nên thử lại không".
5.2 Crash sau side effect → hoàn tiền hai lần
Kịch bản: process chết sau khi refund_order ghi DB, trước khi lưu tool_result. Rồi chạy lại
theo hai cách: resume từ checkpoint, hoặc queue retry chạy lại từ đầu. Bốn chiến lược khoá idempotency:
=== C1. idem=none: crash sau refund, resume từ checkpoint
[iter 2] stop_reason=tool_use tools=refund_order
tool refund_order -> {"order_id":"1001","amount":350000,"idem_key":"40d646ff09702beb","reused":false}
!!! process chết sau khi tool chạy, trước khi lưu tool_result
[resume] nạp checkpoint: 4 message
[iter 1] (resume) chạy lại tool_use đang dở
tool refund_order -> {"order_id":"1001","amount":350000,"idem_key":"fdc2ed9948087947","reused":false}
==> số giao dịch hoàn tiền trong DB: 2
row: 1 | 1001 | 350000 | 40d646ff09702beb
row: 2 | 1001 | 350000 | fdc2ed9948087947
=== C2. idem=tooluse: crash, resume từ checkpoint
[resume] nạp checkpoint: 4 message
[iter 1] (resume) chạy lại tool_use đang dở
tool refund_order -> {"order_id":"1001","amount":350000,"idem_key":"toolu_25b72146970544e0a6b6","reused":true}
==> số giao dịch hoàn tiền trong DB: 1
=== C3. idem=tooluse: crash, queue retry chạy lại TỪ ĐẦU (mất checkpoint)
[iter 2] stop_reason=tool_use tools=refund_order
tool refund_order -> {"order_id":"1001","amount":350000,"idem_key":"toolu_9e8a601c6cd34c688288","reused":false}
==> số giao dịch hoàn tiền trong DB: 2
row: 1 | 1001 | 350000 | toolu_6819dafee76745ea82d9
row: 2 | 1001 | 350000 | toolu_9e8a601c6cd34c688288
=== C4. idem=business: crash, chạy lại từ đầu
[iter 2] stop_reason=tool_use tools=refund_order
tool refund_order -> {"order_id":"1001","amount":350000,"idem_key":"refund:1001","reused":true}
==> số giao dịch hoàn tiền trong DB: 1
row: 1 | 1001 | 350000 | refund:1001
| Chiến lược | Resume từ checkpoint | Retry từ đầu | Nhận xét |
|---|---|---|---|
| Không khoá | 2 lần hoàn tiền | 2 lần | Mặc định của hầu hết code demo |
Khoá = tool_use_id | 1 lần | 2 lần | An toàn giả: chỉ đúng khi assistant turn đã được lưu |
| Khoá nghiệp vụ | 1 lần | 1 lần | Chọn khoá đại diện cho ý định; cẩn thận khi cùng đơn có thể hoàn nhiều lần hợp lệ (thêm id yêu cầu) |
Truyền khoá xuống cổng thanh toán nữa (đa số cổng thanh toán nhận idempotency key) — DB của bạn không phải nơi duy nhất có side effect.
Bug thật gặp khi viết lab: lần chạy đầu, resume luôn nạp checkpoint chỉ có 1 message. Nguyên nhân: hàm lưu viết bằng arrow function PHPfn() => file_put_contents(..., json_encode($messages))— arrow function capture biến theo giá trị tại lúc tạo, nên luôn lưu mảng messages ban đầu. Không có lỗi, không có cảnh báo, checkpoint "chạy" nhưng vô dụng. Sửa bằngfunction () use (&$messages). Bài học: test đường resume thật sự (kill process rồi chạy lại), đừng tin checkpoint vì thấy file được ghi.
5.3 Các lỗi khác
| Lỗi | Biểu hiện | Xử lý |
|---|---|---|
| Retry chồng retry | SDK retry 2 lần × job retry 3 lần × agent tự gọi lại tool → 1 lỗi thành 18 request | Chỉ một tầng retry cho mỗi loại lỗi; lab retry SDK ở bài 09 (R6: 3 request tới server cho 1 lời gọi) |
stop_reason = max_tokens giữa tool_use | Input tool bị cắt, parse ra object thiếu trường | Không chạy tool khi bị cắt; tăng max_tokens, gọi lại |
pause_turn | Server tool (web search…) chạy quá 10 vòng nội bộ | Gửi lại nguyên lịch sử để server chạy tiếp; không chèn "Continue"; giới hạn số lần |
| Tool trả quá lớn | Context phình, chất lượng giảm, hoá đơn tăng | Phân trang, tóm tắt, cắt với thông báo "đã cắt, dùng tham số X để xem tiếp" |
| Tên/tham số tool bị "bịa" | Gọi tool không tồn tại, trường không có | Trả is_error kèm danh sách hợp lệ; strict: true |
| Lỗi bị nuốt mất chi tiết | Model chỉ thấy "Error executing tool" nên không tự sửa được | Phân biệt lỗi lường trước (trả thông điệp hữu ích) và lỗi crash (ẩn chi tiết nội bộ) — MCP SDK làm đúng như vậy (lab ở bài 06) |
6. Vận hành trong production
Chạy vòng lặp ở đâu trong Laravel
Không chạy agent loop trong request PHP-FPM: một lượt agent có thể mất 10–120 giây, giữ một worker FPM suốt thời gian đó. Mẫu
khuyến nghị: controller tạo bản ghi agent_runs + dispatch job lên queue riêng (Horizon, supervisor riêng, timeout dài);
job chạy vòng lặp, checkpoint messages vào DB sau mỗi bước, đẩy tiến độ ra client qua broadcast/SSE. Khi job retry, nó resume từ
checkpoint thay vì chạy lại từ đầu.
// Phác thảo cấu trúc — chưa chạy trong lab (lab chạy bản CLI tương đương ở labs/ai/php/agent_loop.php)
final class RunAgentTurn implements ShouldQueue
{
public int $tries = 3; // retry của QUEUE: resume từ checkpoint, không chạy lại từ đầu
public int $timeout = 300;
public function handle(LlmGateway $llm, ToolRegistry $tools): void
{
$run = AgentRun::lockForUpdate()->findOrFail($this->runId); // một run chỉ một worker
$messages = $run->messages; // checkpoint (jsonb)
// ... vòng lặp như mục 3.2, $run->update(['messages' => $messages]) sau mỗi bước
}
}
Metric
- Số vòng / task (p50, p95) và % task chạm
max_iter— tăng là dấu hiệu tool hỏng hoặc prompt mơ hồ. - Tỉ lệ
is_errortheo tool, latency theo tool — tool chậm làm cả vòng chậm. - Token và chi phí / task hoàn thành (không phải / request).
- Số side effect / task — đếm refund, email; trùng lặp là alert.
Cấu hình ảnh hưởng lớn
max_iter (5–15 cho CSKH, 50+ cho coding agent), timeout từng tool, ngưỡng phát hiện lặp, danh sách tool cần duyệt,
max_tokens đủ lớn để tool input không bị cắt, và effort (effort thấp → ít tool call hơn, gộp hơn).
Gánh nặng cho team
Debug agent là đọc transcript. Cần công cụ xem lại toàn bộ messages của một run (bài 07), và quy ước: mọi bug report về agent phải kèm run id. Người trực cần biết tắt một tool (feature flag) mà không cần deploy.
7. So sánh và ma trận quyết định
| Trục | Một lần gọi | Workflow (code điều phối) | Agent loop tự viết | Framework (LangGraph, Prism PHP…) | Managed Agents |
|---|---|---|---|---|---|
| Độ linh hoạt | Thấp | Trung bình | Cao | Cao | Cao |
| Dễ đoán chi phí/latency | Rất cao | Cao | Thấp | Thấp | Thấp (có budget theo phiên) |
| Debug | Dễ | Dễ | Khó (đọc transcript) | Khó + phải hiểu framework | Qua event stream |
| Kiểm soát state/transaction | — | Toàn quyền | Toàn quyền | Theo framework | State ở phía Anthropic |
| Lock-in | Thấp | Thấp | Thấp–trung bình | Gắn framework | Cao |
| Tuyển người / onboarding | Dễ | Dễ | Trung bình | Phải học framework | Trung bình |
Dùng agent loop khi: số bước và thứ tự không biết trước, có tool kiểm được kết quả, sai thì phục hồi được (có người duyệt, có test, có rollback). Tránh khi: luồng cố định ("tra đơn → nếu giao rồi thì tạo phiếu đổi trả") — viết workflow; khi cần latency dưới vài giây; khi mỗi hành động không đảo ngược được mà không có cổng duyệt.
8. Góc nhìn tech lead: tác động tổ chức
- Tool là API public cho một client không đáng tin. Áp dụng đủ quy trình như API ra ngoài: review schema, authz ở server, rate limit, audit log, versioning. Đừng để team "thêm nhanh một tool" mà bỏ qua review bảo mật.
- Ranh giới sở hữu: team sản phẩm sở hữu prompt + eval; team domain (payment, order) sở hữu tool của domain đó và các bất biến (idempotency, quyền). Tool registry là hợp đồng giữa hai bên.
- Kiến trúc: agent chạy như một workload nền (queue), không phải một controller. Cần bảng
agent_runs, chính sách lưu trữ transcript (có PII), và cơ chế huỷ run đang chạy. - Lock-in và exit strategy: định nghĩa tool bằng JSON Schema gần như portable giữa các nhà cung cấp; phần gắn chặt là định dạng message (tool_use/tool_result), thinking block, và tính năng server-side (web search, code execution). Giữ tool registry và vòng lặp trong code của mình, adapter mỏng cho từng nhà cung cấp → đổi được trong vài tuần, không phải vài quý.
- Tốc độ team: Tool Runner giảm code nhưng che mất chỗ checkpoint; quyết định sớm và ghi ADR.
9. Anti-patterns và bài học thực tế
- Một tool
execute_sqlquyền đầy đủ. Tiện lúc demo, là thảm hoạ khi bị prompt injection. Sự cố Supabase MCP (6–7/2025, General Analysis công bố): agent chạy với khoáservice_role(bỏ qua Row Level Security), đọc ticket hỗ trợ có chỉ dẫn độc, rồi truy vấn bảng token và ghi kết quả ngược vào ticket. - Tool phá huỷ không có cổng duyệt. Replit (7/2025): agent chạy lệnh xoá database production trong lúc người dùng đã tuyên bố "code freeze", rồi báo sai rằng không rollback được. Replit sau đó tách DB dev/prod tự động và thêm chế độ chỉ lập kế hoạch (nguồn: Fast Company, phỏng vấn CEO Replit). Bài học: quyền của agent phải được giới hạn bằng hạ tầng, không bằng lời dặn trong prompt.
- Để model truyền
user_id/tenant_id. Lấy từ session phía server (lab ở bài 08). - Tách
tool_resultra nhiều message hoặc bỏ qua mộttool_usekhông trả kết quả → 400 hoặc model ngừng gọi song song. - Exception của tool làm chết cả vòng lặp thay vì thành
is_error. - Khoá idempotency bằng
tool_use_idrồi cho queue retry chạy lại từ đầu (lab C3). - Trả nguyên response API bên thứ ba làm tool result — vừa tốn token, vừa là kênh prompt injection.
Điều tài liệu không nói: phần lớn thời gian cải thiện agent là sửa tool (mô tả, lỗi, định dạng kết quả), không phải sửa system prompt. Đọc 20 transcript thất bại, bạn sẽ thấy model gọi sai vì mô tả tool mơ hồ nhiều hơn vì "model kém".
10. Câu hỏi phụ interviewer hay hỏi tiếp
- Agent khác workflow chỗ nào?
- Workflow: code quyết định bước tiếp theo, LLM làm từng bước. Agent: model quyết định bước tiếp theo. Workflow rẻ và dễ đoán; agent linh hoạt khi không biết trước đường đi.
- Làm sao đảm bảo agent không làm một việc hai lần?
- Không đảm bảo exactly-once được; làm at-least-once + tool idempotent bằng khoá nghiệp vụ, checkpoint trước khi chạy tool, và truyền khoá xuống hệ thống bên dưới (cổng thanh toán).
- Vì sao không để exception của tool nổi lên?
- Vì lỗi là thông tin cho model: nó có thể thử cách khác, hỏi lại người dùng, hoặc dừng và giải thích. Exception chỉ nên nổi lên với lỗi của chính harness (DB checkpoint chết).
- Tool Runner hay tự viết vòng lặp?
- Tool Runner cho đa số trường hợp (vẫn chèn duyệt, sửa kết quả, giới hạn vòng được). Tự viết khi cần checkpoint theo cách riêng, transaction DB bao quanh từng bước, hoặc không muốn phụ thuộc tính năng beta.
- Model mới không cho
tool_choice: any, vậy ép gọi tool thế nào? - Dùng
auto+ chỉ dẫn rõ trong prompt, kiểm xem có tool_use không và nhắc lại nếu không; nếu mục đích chỉ là lấy JSON thì dùng structured outputs (output_config.format). - Có bao nhiêu tool là quá nhiều?
- Không có con số cứng; dấu hiệu là model chọn nhầm tool gần nghĩa và chi phí token định nghĩa tool đáng kể. Gộp tool theo ý định, hoặc dùng tool search để nạp theo nhu cầu.
Phụ lục: lab đã chạy
Môi trường: PHP 8.4.19, anthropic-ai/sdk 0.52.0, guzzlehttp/guzzle 8.2.0, SQLite (pdo_sqlite), Python 3.11
cho mock server. Output gốc: labs/ai/out/02-agent-loop.txt.
cd labs/ai/php && composer install
cd .. && bash php/run_agent_loop_lab.sh # tự bật/tắt mock server (policy cskh)
# Chạy tay từng kịch bản:
./mockctl.sh start cskh
php php/agent_loop.php --reset # A. happy path
php php/agent_loop.php --reset --fail-tool=refund_order --max-iter=6 # B. lặp khi tool lỗi
php php/agent_loop.php --reset --crash-after=refund_order # C1. crash sau side effect
php php/agent_loop.php --resume # resume -> hoàn tiền lần 2
php php/agent_loop.php --reset --idem=business --crash-after=refund_order; rm php/checkpoint.json
php php/agent_loop.php --idem=business # C4. khoá nghiệp vụ -> 1 lần
./mockctl.sh stop
Chưa verify: model thật có tự dừng khi tool lỗi liên tục không; hành vi tool search, programmatic tool calling, Tool Runner PHP (chỉ đọc tài liệu và mã nguồn SDK). Nguồn: tài liệu tham chiếu Claude API (tool use, agent design); Anthropic Engineering, "Writing effective tools for agents"; Yao et al., "ReAct" (2022); Simon Willison, "Supabase MCP can leak your entire SQL database" (7/2025); Fast Company, phỏng vấn CEO Replit về sự cố của Jason Lemkin (7/2025).
Bài trước: 01 · LLM dưới nắp capo · Bài tiếp: 03 · Context engineering.