ความทนทาน: เมื่อ tool พัง loop ต้องไม่ล้ม
หกบทที่ผ่านมาเราสร้าง loop ที่ ฉลาดขึ้น — routing, parallelization, orchestrator-workers, reflection บทนี้ถอยกลับมาถามคำถามที่ทุก loop ต้องรอด: เมื่อ tool พัง จะเกิดอะไรขึ้น? network timeout, rate limit, order ที่ไม่มีอยู่จริง, refund ที่ยิงไปแล้วแต่ response หาย — เหตุพวกนี้ไม่ใช่ ข้อยกเว้น ที่จะเกิดนานๆ ครั้ง มันคือ สภาพปกติ ของระบบที่คุยกับโลกภายนอก และงานของ harness คือรักษา loop ให้หมุนต่อได้ทั้งที่ tool ข้างในมันล้ม
กลับไปดู catch ใน hand-rolled loop ของ บท1 🔁 — บรรทัดที่เขียนว่า catch (Exception ex) { output = $"ERROR: {ex.Message}"; } เราจงใจวางมันไว้เป็น ตะเข็บ บทนี้คือบทที่เปิดตะเข็บนั้นออกมาทำให้ครบ: จากบรรทัดเดียวที่กัน crash ให้กลายเป็นสี่ชั้นของความทนทาน — ให้ model เห็น error, retry อย่างมีจังหวะ, ตั้ง timeout, และรักษาการกระทำที่มีผลข้างเคียงไม่ให้ทำซ้ำ
บทนี้ยังทำงานบน agent Order ตัวเดิม (repo kaen-food-ordering — กำลังจัดทำ) tool สามตัวเดิม getOrder / getDeliveryStatus / issueRefund ไม่เพิ่มใหม่ แต่เราห่อ ชั้นความทนทาน คร่อมการเรียกทั้งการเรียก model และการ dispatch tool ทุก snippet ที่แตะ Messages API / IChatClient ยังยึด invariant เดิมของ #15 ทุกข้อ (header x-api-key, anthropic-version: 2023-06-01, ห้ามส่ง temperature, model id เปล่า claude-opus-4-8 ลงวันที่ 2026-07, อ่าน Contents[] ไม่ใช่ .text, Claude อยู่หลัง NuGet Anthropic beta ผ่าน AsIChatClient)
ชั้นที่ 1 — model ต้อง เห็น error ไม่ใช่ให้มัน crash
หัวข้อที่มีชื่อว่า “ชั้นที่ 1 — model ต้อง เห็น error ไม่ใช่ให้มัน crash”สัญชาตญาณของโปรแกรมเมอร์เมื่อ tool โยน exception คือปล่อยให้มัน propagate ขึ้นไปหยุดโปรแกรม แต่ในบริบทของ agent นี่คือความผิดพลาด — เพราะ model คือส่วนที่กู้สถานการณ์ได้ ถ้าคุณให้มันเห็น error การกู้คืนจาก tool errorTool-error Recoverymodel ต้อง 'เห็น' error — คืน error result (raw: `tool_result` ที่ `is_error: true`; ใต้ IChatClient: string จาก catch ป้อนกลับเป็น FunctionResultContent) ไม่ใช่ปล่อยให้ crash ป้อน error ที่ 'สอนได้' (บอกว่าเกิดอะไรและควรลองอะไรต่อ ไม่ใช่ 'failed' เปล่า ๆ) กลับเข้า loop ให้ model retry/re-route เอง — Claude retry tool call ที่ผิด 2–3 ครั้งเมื่อได้ error กลับไป (Anthropic)Process เริ่มจากกฎเดียว: จับ error แล้ว ป้อนกลับเข้า loop ในฐานะผลของ tool ไม่ใช่โยนทิ้งให้ loop ตาย
ใน raw Messages API เรื่องนี้คือการคืน tool_result ที่มี is_error: true — Anthropic ระบุตรงๆ ว่า “If the tool itself throws an error during execution (for example, a network error when fetching weather data), you can return the error message in the content along with "is_error": true… Claude will then incorporate this error into its response to the user.” ใต้ IChatClient ตัวเทียบคือ string ที่จับได้จาก catch แล้วส่งกลับเป็น FunctionResultContent — ก็คือ catch ในบท1
แต่ ข้อความ ที่ป้อนกลับต้องสอน model ว่าจะทำอะไรต่อ ไม่ใช่แค่ code เปล่า Anthropic เขียนไว้ว่า “Write instructive error messages. Instead of generic errors like "failed", include what went wrong and what Claude should try next, e.g., "Rate limit exceeded. Retry after 60 seconds." This gives Claude the context it needs to recover or adapt without guessing.” — และในคู่มือเขียน tool เขาย้ำหลักเดียวกัน: “if a tool call raises an error (for example, during input validation), you can prompt-engineer your error responses to clearly communicate specific and actionable improvements, rather than opaque error codes or tracebacks.” เพราะเมื่อ error บอกทางแก้ Claude แก้เองได้ — “If a tool request is invalid or missing parameters, Claude will retry 2-3 times with corrections before apologizing to the user.”
List<AIContent> results = [];foreach (var call in calls) { object? output; try { output = await Dispatch(call.Name, call.Arguments); } catch (OrderNotFoundException) { // ข้อความสอน: บอกว่าเกิดอะไรและให้ลองอะไรต่อ ไม่ใช่แค่ "failed" output = "ERROR: ไม่พบ order ตาม id นี้ ตรวจรูปแบบ id (เช่น A-1002) แล้วลองใหม่ หรือถามผู้ใช้ให้ยืนยัน id"; } catch (Exception ex) { output = $"ERROR: {ex.Message} — ลองอีกครั้งหรือเปลี่ยนวิธี"; // ป้อนกลับ ไม่ throw } results.Add(new FunctionResultContent(call.CallId, output));}messages.Add(new ChatMessage(ChatRole.Tool, results)); // ผลของ tool อยู่ใน ChatRole.Tool message เสมอ1) ผลของ tool ต้องตามหลัง tool call ทันที — Anthropic ระบุกติกา format ที่ถ้าละเมิดจะ error เงียบๆ: “Tool result blocks must immediately follow their corresponding tool use blocks.” ถ้าคุณแทรกข้อความอื่นคั่นระหว่าง tool_use กับ tool_result request จะถูกปฏิเสธ hand-rolled loop ของบท1 รักษากฎนี้ให้อยู่แล้วโดยผนวก assistant turn ก่อนแล้วจึงตามด้วย ChatRole.Tool message
2) ผลของ tool คือข้อมูล ที่ไม่น่าเชื่อถือ — “Tool results often carry content from sources outside your control… Treat that content as untrusted: an attacker who can influence it may embed instructions that try to redirect Claude (indirect prompt injection). Keep untrusted content inside tool_result blocks rather than system prompts.” อย่าเลื่อน record ออเดอร์ที่ tool คืนมาไปต่อท้าย system prompt — เก็บมันไว้ใน block ผลของ tool เท่านั้น
ชั้นที่ 2 — retry อย่างมีจังหวะ: ชั่วคราว vs ถาวร
หัวข้อที่มีชื่อว่า “ชั้นที่ 2 — retry อย่างมีจังหวะ: ชั่วคราว vs ถาวร”ไม่ใช่ทุก error ควร retry การ retryRetryการลองใหม่เมื่อ call ล้มเหลว ด้วย exponential backoff + jitter และเพดานจำนวนครั้ง — เฉพาะ fault ชั่วคราว (transient) เท่านั้น; ยกเลิกทันทีถ้าเป็นถาวรหรือ error จาก business logic (ไม่ควร retry) ⚠️ 2 layer ทับกัน: SDK `Anthropic` retry ระดับ transport ให้อยู่แล้ว (`MaxRetries`) ส่วน Polly คือระดับ agent-loop (retry ทั้งเทิร์น, per-turn timeout) — อย่า double-retry transportProcess ปลอดภัยเฉพาะกับ fault ชั่วคราว (transient) — timeout, rate limit, 5xx — ที่มีโอกาสหายเองถ้าลองใหม่ ส่วน fault ถาวรอย่าง business-logic error (order นี้คืนเงินไม่ได้เพราะเลยกำหนด) การ retry มีแต่จะเปลืองเปล่า Microsoft Retry pattern วางเส้นแบ่งนี้ชัด — กลยุทธ์ “Cancel. If the fault indicates that the failure isn’t transient or is unlikely to be successful if repeated, the application should cancel the operation and report an exception.” และย้ำว่า retry ไม่ได้มีไว้ “handling failures that aren’t due to transient faults, such as internal exceptions caused by errors in the business logic.”
เมื่อ retry ต้อง ถอยจังหวะ (back off) เพิ่มขึ้นต่อเนื่อง มีเพดาน และมี jitter — “this process can be repeated with increasing delays between retry attempts, until some maximum number of requests have been attempted. The delay can be increased incrementally or exponentially, depending on the type of failure.” เหตุที่ต้อง jitter คือกัน retry storm: “An aggressive retry policy with minimal delay between attempts, and a large number of retries, could further degrade a busy service that’s running close to or at capacity.” ถ้า client ทุกตัว retry พร้อมกันเป๊ะๆ มันจะซัดบริการที่กำลังจะฟื้นให้ล้มซ้ำ
นี่คือกับดักที่คนพลาดกันบ่อยที่สุดในบทนี้ — NuGet Anthropic ตัวทางการ retry ให้อยู่แล้ว ที่ชั้น transport — โดย default มัน retry ด้วย exponential backoff บน 408/409/429/5xx และ connection error (ปรับผ่าน property MaxRetries) ถ้าคุณเอา Polly ไปครอบแล้วตั้ง retry บน error ชุดเดียวกันอีก คุณจะได้ retry คูณกัน — 3 × 2 = 6 ครั้งต่อการเรียกหนึ่งครั้ง ยิงใส่บริการที่ล่มหนักขึ้น
เส้นแบ่งที่ถูก: ปล่อยให้ SDK รักษาชั้น transport (retry ตัว HTTP request รายตัว) ส่วน Polly รักษาชั้น agent-loop — retry ทั้งเทิร์นที่ล้ม หรือคุม timeout ต่อเทิร์น ไม่ใช่ retry HTTP ซ้ำสิ่งที่ SDK ทำแล้ว
Polly v8 (ResiliencePipelineBuilder ผ่าน Microsoft.Extensions.Http.Resilience — GA ไม่ใช่ beta) ประกอบ retry + timeout ที่ชั้น agent-loop แบบนี้:
using Polly;using Polly.Retry;using Polly.Timeout;
ResiliencePipeline pipeline = new ResiliencePipelineBuilder() .AddRetry(new RetryStrategyOptions { ShouldHandle = new PredicateBuilder().Handle<AnthropicRateLimitException>() // 429 .Handle<Anthropic5xxException>() // 5xx .Handle<TimeoutRejectedException>(), // จาก AddTimeout ด้านล่าง MaxRetryAttempts = 3, BackoffType = DelayBackoffType.Exponential, UseJitter = true, // กระจายจังหวะ retry — กัน retry storm }) .AddTimeout(TimeSpan.FromSeconds(60)) // timeout ต่อความพยายาม; ลำดับสำคัญ (retry ครอบ timeout) .Build();
ChatResponse resp = await pipeline.ExecuteAsync( async token => await agent.GetResponseAsync(messages, options, token), ct);ชื่อ type ใน block PredicateBuilder ข้างบน (AnthropicRateLimitException, Anthropic5xxException, และ base AnthropicApiException) เป็น รูปทรงที่ต้องยืนยันกับตาราง error ของ Anthropic 12.8.0 ที่ติดตั้งจริง ก่อนใช้งาน — SDK ตัวนี้เป็น beta ชื่อ exception เป็นพื้นผิวที่ขยับได้ อย่า copy ชื่อไปวางแล้วเชื่อว่าถูก ให้เปิด error table ของ version ที่ pin ไว้เช็กก่อน และอย่าลืมว่า MaxRetries ของ SDK ยังทำงานอยู่ข้างใต้ — ถ้าจะให้ Polly คุมทั้งหมด ให้ลด MaxRetries ของ SDK ลง (เช่น client.WithOptions(o => o with { MaxRetries = 0 })) เพื่อไม่ให้สองชั้นทับกัน
หลักสุดท้ายของ retry: อย่าซ้อน retry หลายชั้นโดยไม่รู้ตัว และบางเส้นทางควร ล้มเร็ว ดีกว่า — “Implement retry logic only where the full context of a failing operation is understood… It might be better to configure the lower-level task to fail fast.” และ “For some noncritical operations, it’s better to fail fast rather than retry several times and affect the throughput of the application.” บนเส้นทางที่ผู้ใช้กำลังรอคำตอบสดๆ การ retry หลายรอบทำให้ latency บวมจนแย่กว่าการยอมบอกว่า “ตอนนี้ทำไม่ได้”
ชั้นที่ 3 — timeout: ล้มเร็วดีกว่าค้าง
หัวข้อที่มีชื่อว่า “ชั้นที่ 3 — timeout: ล้มเร็วดีกว่าค้าง”timeout เป็นกลยุทธ์ resilience ชั้นหนึ่งเต็มตัว ไม่ใช่ของแถม บนเส้นทาง interactive การปล่อยให้เทิร์นหนึ่งค้างสองนาทีแย่กว่าการตัดจบแล้วบอก model ให้ลองใหม่ ใน .NET เครื่องมือคือ CancellationToken — และ Microsoft แยกสองมิติของ timeout ไว้: timeout ต่อความพยายาม (attempt) กับ timeout รวมทั้งงาน (total) — คุณอยากได้ทั้งสอง: เพดานต่อความพยายาม (ใน Polly คือ .AddTimeout) และเพดานเวลารวมทั้งเทิร์น
using var cts = CancellationTokenSource.CreateLinkedTokenSource(ct);cts.CancelAfter(TimeSpan.FromMinutes(2)); // เพดานเวลารวมทั้งเทิร์นvar resp = await agent.GetResponseAsync(messages, options, cts.Token);จุดสำคัญ: เมื่อ timeout สะดุด อย่าปล่อยให้ exception พุ่งขึ้นไปฆ่า loop — จับมันแล้ว แปลงเป็นข้อความสอน ป้อนกลับ เหมือนชั้นที่ 1 นั่นคือ ให้ timeout กลายเป็น observation ที่ model เห็นและตัดสินใจต่อได้
try { output = await Dispatch(call.Name, call.Arguments, cts.Token);}catch (OperationCanceledException) { output = "ERROR: tool ทำงานเกินเวลาที่กำหนด ลองอีกครั้งหนึ่ง หรือเปลี่ยนไปใช้ tool อื่นที่เร็วกว่า";}ชั้นที่ 4 — idempotency: รักษา issueRefund ไม่ให้คืนเงินซ้ำ
หัวข้อที่มีชื่อว่า “ชั้นที่ 4 — idempotency: รักษา issueRefund ไม่ให้คืนเงินซ้ำ”นี่คือชั้นที่อันตรายที่สุด และเป็นเหตุผลที่เส้นแบ่ง read/write จากบท1 สำคัญ getOrder / getDeliveryStatus เป็น read-only — retry ซ้ำกี่ครั้งก็ไม่เจ็บ แต่ issueRefund เป็น write action ที่มีผลข้างเคียงกับเงินจริง และ retry ทำให้เกิดฝันร้ายคลาสสิก Microsoft อธิบายไว้ตรงเป๊ะ:
“Consider whether the operation is idempotent. If so, it’s inherently safe to retry. Otherwise, retries could cause the operation to be executed more than once, with unintended side effects. For example, a service might receive the request, process the request successfully, but fail to send a response. At that point, the retry logic might re-send the request, assuming that the first request wasn’t received.”
แปลเป็นสถานการณ์ของเรา: issueRefund คืนเงินสำเร็จ → แต่ response หายระหว่างทาง (timeout พอดี) → retry ยิง issueRefund ซ้ำ → ลูกค้าได้เงินคืนสองรอบ idempotencyIdempotencyคุณสมบัติที่ทำให้ retry ปลอดภัย — tool ที่มี side effect (issueRefund) ต้องไม่ทำซ้ำเมื่อถูก retry มิฉะนั้น refund สำเร็จ แต่ response หาย แล้ว retry ยิงซ้ำ → refund สองครั้ง วิธีแก้: สร้าง idempotency key 'ก่อน' เข้า retry loop server เช็ค key แล้วคืนผลที่เก็บไว้แทนการประมวลซ้ำ ⚠️ (แปลความ) key ไม่ใช่หลักประกันเต็ม — side effect (event, email, external call) รั่วได้ รูปที่ทนทานคือเขียน intent + ledger ใน transaction เดียวแล้วส่งผ่าน outbox (โยง #10 ledger + #8 outbox)Architecture คือ property ที่ทำให้เรียกซ้ำได้ผลเท่าเดิม และมันคือเงื่อนไข ก่อน ที่ retry บน write action จะปลอดภัย
กลไก idempotency key (เรียบเรียงจาก S14 — Adyen/Stripe/PayPal ใช้รูปแบบนี้): client สร้าง key เฉพาะ (เช่น UUID) ก่อน เข้า loop ของ retry แล้วแนบไปกับ request ผ่าน header อย่าง Idempotency-Key ฝั่ง server เห็น key ซ้ำก็คืน ผลที่เก็บไว้เดิม แทนการประมวลผลใหม่ — retry จึงไม่มีทางคืนเงินสองรอบ กุญแจสำคัญคือ key ต้องเกิด ก่อน retry loop ไม่ใช่ต่อรอบ ไม่งั้นแต่ละ retry จะมี key ใหม่และหมดความหมาย
// tool คืนเงินรับ idempotency key จากผู้เรียก; ledger บังคับ "1 key คืนเงินได้ครั้งเดียว"static RefundResult IssueRefund(string orderId, decimal amount, string idempotencyKey) => RefundLedger.IssueOnce(orderId, amount, idempotencyKey); // key ซ้ำ → คืนผลเดิมที่เก็บไว้ ไม่คืนเงินใหม่กับดักที่ต้องพูดให้ตรง: unique constraint บน key รักษาแค่ การเขียน DB แถวเดียว แต่ผลข้างเคียงที่ ไม่ได้ อยู่ใน transaction เดียวกัน — event ที่ publish, อีเมลแจ้งลูกค้า, การเรียก payment processor ภายนอก — มักหลุดออกนอกร่มการป้องกันนั้น เรียก issueRefund ซ้ำแล้วเจอ key เดิมอาจไม่เขียน DB ซ้ำ แต่ถ้า code publish event หลัง commit มันก็ยิง event ซ้ำได้อยู่ดี
รูปแบบที่ทนทานจริงคือ เขียน intent + ledger ใน1 transaction แล้ว publish ผ่าน outbox ที่ durable — บันทึกความตั้งใจจะคืนเงินลง ledger พร้อมกับ mark ว่าจัดการ key นี้แล้วในธุรกรรมเดียว จากนั้นให้ตัว outbox เป็นคนส่ง event ออกไปแบบ at-least-once ที่ผู้รับ dedupe เอง pattern นี้ตรงกับ ledger ที่คุณสร้างในคอร์ส #10 และ outbox แบบ Wolverine ในคอร์ส #8 — idempotency ของ agent tool ที่แตะเงินจริงต้องพิงโครงสร้างพวกนั้น ไม่ใช่แค่ if-check ใน method
ก่อนจะถึง error: รักษาที่ input และอ่าน stop_reason
หัวข้อที่มีชื่อว่า “ก่อนจะถึง error: รักษาที่ input และอ่าน stop_reason”สองมาตรการที่กัน error ก่อน มันเกิด อยู่คู่กับสี่ชั้นข้างบน
guardrailGuardrailด่านตรวจ/ขีดจำกัดที่วางไว้ก่อนหรือรอบการ execute เพื่อรักษาให้ทำงานอย่างปลอดภัย — validate ก่อนรัน (`strict: true` บน tool schema กัน parameter ขาด/ผิดชนิด), ปฏิบัติต่อผลของ tool เป็น 'untrusted' (indirect prompt injection — เก็บไว้ใน tool_result ไม่ใช่ system prompt), และ cap iteration/error BEA เตือนว่า agent มี 'higher costs, and the potential for compounding errors' จึงต้อง test ใน sandbox พร้อม guardrail ที่เหมาะสมProcess ที่ input — validate ก่อนรัน (เรียบเรียงจาก S17): ตั้ง strict: true บนนิยาม tool เพื่อการันตีว่า input เข้าคู่ schema — กัน parameter ขาดและชนิดผิด — และใส่ input_examples เพื่อลด tool call ที่ malformed นี่คือการลงทุนใน ACI ต่อจากหลัก “ลด input error” ของ คอร์ส #15 🔁 error ที่ดีที่สุดคือ error ที่ไม่เกิด และ Anthropic ก็เตือนไว้ว่าธรรมชาติ autonomous ของ agent “means higher costs, and the potential for compounding errors. We recommend extensive testing in sandboxed environments, along with the appropriate guardrails.”
อ่าน stop_reason ก่อนประมวลผล — ก่อนจะไล่ Contents[] ควร branch บนเหตุที่ model หยุด Anthropic แจกแจง stop_reason ไว้หลายค่าที่ไม่ใช่ end_turn ปกติ: max_tokens (คำตอบถูกตัดกลางคัน — อาจตัด tool call ขาด), model_context_window_exceeded, refusal, pause_turn การเจอ max_tokens แล้วเดินหน้า parse tool call ต่อทั้งที่มันขาดครึ่งคือ bug ที่หายาก — ตรวจ stop_reason แล้วจัดการเคสตัดจบก่อนเสมอ
ภาพรวม: call เปราะ vs call ที่ทน
หัวข้อที่มีชื่อว่า “ภาพรวม: call เปราะ vs call ที่ทน”flowchart TD
subgraph FRAGILE["❌ version ดิบ — call เปราะ"]
F1["dispatch tool"] --> F2{"tool โยน exception"}
F2 -->|"ใช่"| F3["exception พุ่งขึ้น<br/>loop ตาย"]
F2 -->|"timeout"| F4["ค้างยาว<br/>ผู้ใช้รอเก้อ"]
F5["issueRefund retry"] --> F6["คืนเงินซ้ำ<br/>side effect"]
end
subgraph ROBUST["✅ call ที่ทน — 4 ชั้น"]
R1["dispatch tool"] --> R2{"error ไหม"}
R2 -->|"error"| R3["จับ แล้วป้อน is_error<br/>+ ข้อความสอน กลับเข้า loop"]
R2 -->|"transient"| R4["retry: backoff+jitter<br/>เฉพาะ fault ชั่วคราว"]
R2 -->|"ช้าเกิน"| R5["timeout ด้วย CancellationToken<br/>แปลงเป็นข้อความสอน"]
R6["issueRefund retry"] --> R7["idempotency key<br/>คืนผลเดิม ไม่คืนเงินซ้ำ"]
R3 --> R8["model เห็น error<br/>แก้เอง 2-3 ครั้ง"]
R4 --> R8
R5 --> R8
end
classDef bad fill:#7f1d1d,stroke:#450a0a,color:#fca5a5;
classDef good fill:#166534,stroke:#052e16,color:#bbf7d0;
class F3,F4,F6 bad;
class R3,R4,R5,R7,R8 good;
คำบรรยายภาพ: ฝั่งซ้ายคือ call เปราะ — exception พุ่งขึ้นฆ่า loop, timeout ปล่อยให้ค้าง, retry บน issueRefund คืนเงินซ้ำ ฝั่งขวาคือ call เดียวกันที่รักษาไว้สี่ชั้น — จับ error แล้วป้อน is_error + ข้อความสอนกลับเข้า loop, retry เฉพาะ fault ชั่วคราวด้วย backoff+jitter, ตั้ง timeout ด้วย CancellationToken, และรักษา write action ด้วย idempotency key ปลายทางทั้งสามเส้นฝั่งขวาไปรวมที่เดียว: model เห็น สิ่งที่พลาดและแก้เองได้
ถ้าคุณเดินบน middleware loop (.UseFunctionInvocation()) แทนที่จะ hand-roll เอง ตัว FunctionInvokingChatClient มี knob สำหรับเพดาน error ให้ในตัว: MaximumConsecutiveErrorsPerRequest หยุด loop หลังล้มติดกัน N รอบ และ IncludeDetailedErrors ส่งรายละเอียด exception กลับเข้า model ให้มันแก้เอง (แทนที่ข้อความ generic) ใช้สองตัวนี้แทนการ hand-roll เมื่ออยู่บน middleware — แต่ retry แบบ backoff, timeout ต่อเทิร์น และ idempotency ยังต้องประกอบเองอยู่ดี knob พวกนี้เป็นพื้นผิวที่ขยับเร็ว ให้ยืนยันชื่อกับ version Microsoft.Extensions.AI ที่ติดตั้งจริง
สรุปก่อนไปต่อ
หัวข้อที่มีชื่อว่า “สรุปก่อนไปต่อ”tool พังคือสภาพปกติ ไม่ใช่ข้อยกเว้น และความทนทานคือสี่ชั้นที่ห่อ catch บรรทัดเดียวจากบท1: (1) ให้ model เห็น error ผ่าน is_error + ข้อความที่ สอน ให้มันแก้เอง (2) retry เฉพาะ fault ชั่วคราวด้วย backoff+jitter โดยไม่ retry ซ้อนชั้นที่ SDK ทำแล้ว (3) ตั้ง timeout ด้วย CancellationToken แล้วแปลงเป็นข้อความสอน (4) รักษา write action อย่าง issueRefund ด้วย idempotency key ที่พิง ledger + outbox จริง เสริมด้วย guardrail ที่ input และการอ่าน stop_reason ก่อน parse
สังเกตว่าสี่ชั้นนี้คือการเติม clause “error ที่กู้ไม่ได้” เข้าไปในนโยบายหยุดแบบประกอบที่บท1 วางไว้ — เมื่อ retry หมดทุกชั้นแล้วยังล้ม นั่นคือสัญญาณให้ loop หยุดอย่างมีเหตุผลและ escalate ไม่ใช่วนต่อไม่จบ บทหน้า — บทที่ 8 🔁 — เราประกอบทั้ง5 clause (task-complete, step budget, cost budget, error ที่กู้ไม่ได้, human-checkpoint) เข้าเป็น LoopController ตัวเดียว แล้วปิดคอร์สด้วยความจริงเรื่องต้นทุนและ error ที่ทบต้น
บทนี้อิงต้นทางที่ลงวันที่กำกับ อ่านต่อได้โดยตรง:
- Anthropic, “Building Effective Agents” (2024-12-19) — ธรรมชาติ autonomous ของ agent ที่ “means higher costs, and the potential for compounding errors” พร้อมคำแนะนำให้ “extensive testing in sandboxed environments, along with the appropriate guardrails”
- Anthropic, “Handle tool calls” (Claude Docs) (เข้าถึง 2026-07-19) —
is_error: trueที่ model “incorporate” เข้าไป, “Write instructive error messages” + ตัวอย่าง “Rate limit exceeded. Retry after 60 seconds.”, “retry 2-3 times with corrections”, ผลของ tool ที่ untrusted (indirect prompt injection) และกฎ “Tool result blocks must immediately follow their corresponding tool use blocks.” - Anthropic, “Writing effective tools for AI agents” (2025-09-11) — error ในฐานะ feedback ที่สอน “specific and actionable improvements, rather than opaque error codes”
- Microsoft, “Retry pattern” (Azure Architecture Center) (ms.date 2024-07-18, upd. 2025-12-09) — transient vs permanent (กลยุทธ์ Cancel), exponential backoff มีเพดาน, retry storm, “fail fast”, และย่อหน้า idempotency hazard (“process the request successfully, but fail to send a response… re-send”)
- Microsoft Learn, “Build resilient HTTP apps: key development patterns” (เข้าถึง 2026-07-19) — timeout ต่อความพยายาม (attempt) vs รวมทั้งงาน (total) และ standard resilience handler
- Polly (
ResiliencePipelineBuilder) · pollydocs (เข้าถึง 2026-07-19) —.AddRetry(RetryStrategyOptions{ ShouldHandle=PredicateBuilder, BackoffType=Exponential, UseJitter }),.AddTimeout,TimeoutRejectedException,pipeline.ExecuteAsync - NuGet, “Microsoft.Extensions.Http.Resilience 10.8.0” (GA) — Polly v8 resilience เหนือ HTTP (ไม่ใช่ beta)
- Adyen, “API idempotency” (เข้าถึง 2026-07-19) — กลไก idempotency key (เรียบเรียง; ใช้ร่วมกับ Stripe/PayPal)
- InfoWorld, “Why an idempotency key isn’t an idempotency guarantee” (เข้าถึง 2026-07-19) — key รักษาแค่การเขียน DB; ผลข้างเคียง (event/email/external) ต้อง intent + ledger + outbox (เรียบเรียง)
- Anthropic, “Stop reasons and fallback” (Claude Docs) (เข้าถึง 2026-07-19) —
stop_reasonenumeration (end_turn/max_tokens/model_context_window_exceeded/refusal/pause_turn) - Anthropic, “Implement tool use / Define tools” (Claude Docs) (เข้าถึง 2026-07-19) —
strict: trueและinput_examplesที่ลด malformed call (เรียบเรียง) - Microsoft Learn, “FunctionInvokingChatClient” (page upd. 2026-06-12, pkg v10.7.0) —
MaximumConsecutiveErrorsPerRequest,IncludeDetailedErrors - NuGet, “Anthropic 12.8.0” (official C# SDK, beta) —
MaxRetries(SDK retry ชั้น transport), ตาราง exception (AnthropicRateLimitExceptionฯลฯ) ที่ต้องยืนยันก่อน ship — pin version
เช็กความเข้าใจ — บทที่ 7
ข้อ 1 / 3agent เรียก tool แล้ว tool โยน exception ควรทำอย่างไรจึงจะกู้คืนได้ตามหลัก tool-error recovery?