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ức anthropic-ai/sdk v0.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 map toolUseID/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ề block tool_use gồm tên tool và input JSON, stop_reason = tool_use. Code của tôi chạy tool, gửi lại tool_result cùng tool_use_id, lặp tới khi end_turn. Mảng messages chí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ằng is_error thay 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 sinh tool_use_id mớ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 callingModel 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ả.
harnessCode 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.
idempotentGọi 1 lần hay N lần cho cùng một ý định thì kết quả cuối như nhau.
checkpointLưu state (ở đây là mảng messages) ra bộ nhớ bền để resume sau khi process chết.
durable executionMô 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ảng messages — 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:

3. Internals và quyết định thiết kế

3.1 Giao thức trên dây

Thành phầnHình dạngGhi 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ọiBlock {type: "tool_use", id, name, input} trong content của assistantMộ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 userTất cả kết quả của một lượt nằm chung một user message
stop_reasonend_turn, tool_use, max_tokens, pause_turn, refusal, stop_sequenceKiểm trước khi đọc content
tool_choiceauto (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: trueTrê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áchBạn viếtAi hostDùng khi
Vòng lặp tự viếtToàn bộ vòng lặp (như trên)BạnCầ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 toolBạ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êngAnthropic (vòng lặp + sandbox)Agent chạy lâu, cần sandbox bash/file, chạy theo lịch
Claude Agent SDKPrompt + tuỳ chọnBạnMuố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ắcLàmTránhVì sao
Gộp theo ý địnhfind_customer_orders(customer_ref, status?)list_users + list_orders + joinMỗ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ẹpenum, định dạng rõ, ví dụquery: string tự doGiảm input sai; strict: true chặn input lệch schema
Kết quả gọn, có ngữ nghĩaTên, trạng thái, ID cần cho bước sau; phân trangDump 5MB JSON thô, UUID vô nghĩaKế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à ghipreview_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ừ sessionuser_id, tenant_id do server gắnĐể model truyền user_idModel 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ạnhGiao thức đảm bảoKhông đảm bảo — bạn phải tự lo
Định dạng input toolVới strict: true: khớp JSON SchemaGiá 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ượcResume từ checkpointRetry từ đầuNhận xét
Không khoá2 lần hoàn tiền2 lầnMặc định của hầu hết code demo
Khoá = tool_use_id1 lần2 lầnAn toàn giả: chỉ đúng khi assistant turn đã được lưu
Khoá nghiệp vụ1 lần1 lầnChọ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 PHP fn() => 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ằng function () 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ỗiBiểu hiệnXử lý
Retry chồng retrySDK retry 2 lần × job retry 3 lần × agent tự gọi lại tool → 1 lỗi thành 18 requestChỉ 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_useInput tool bị cắt, parse ra object thiếu trườngKhông chạy tool khi bị cắt; tăng max_tokens, gọi lại
pause_turnServer 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ớnContext phình, chất lượng giảm, hoá đơn tăngPhâ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ếtModel chỉ thấy "Error executing tool" nên không tự sửa đượcPhâ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

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ụcMột lần gọiWorkflow (code điều phối)Agent loop tự viếtFramework (LangGraph, Prism PHP…)Managed Agents
Độ linh hoạtThấpTrung bìnhCaoCaoCao
Dễ đoán chi phí/latencyRất caoCaoThấpThấpThấp (có budget theo phiên)
DebugDễDễKhó (đọc transcript)Khó + phải hiểu frameworkQua event stream
Kiểm soát state/transaction—Toàn quyềnToàn quyềnTheo frameworkState ở phía Anthropic
Lock-inThấpThấpThấp–trung bìnhGắn frameworkCao
Tuyển người / onboardingDễDễTrung bìnhPhải học frameworkTrung 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

9. Anti-patterns và bài học thực tế

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