เอเจนต์ของคุณเรียกใช้ API แล้วได้รับสถานะ 429 Too Many Requests จึงพยายามเรียกซ้ำทันที ได้ 429 อีกครั้ง แล้ววนซ้ำต่อไปจนงานหยุดลงหรือค่าใช้จ่ายพุ่งสูงขึ้น ไม่มีใครตั้งใจเขียนวงจรแบบนี้ แต่มันมักเกิดจากการจัดการข้อผิดพลาดแบบตรงไปตรงมาเกินไป และเป็นปัญหาที่พบบ่อยในกระดานสนทนา Anthropic SDK
การกู้คืนข้อผิดพลาดคือสิ่งที่แยกเดโมที่ดูดีออกจากเอเจนต์ที่พร้อมใช้งานจริง ปัญหาไม่ได้อยู่ที่โมเดล แต่อยู่ที่โค้ดของคุณทำอะไรเมื่อ tool call ช้า ถูกจำกัดอัตรา หรือใช้งานไม่ได้ หากออกแบบถูกต้อง dependency ที่ไม่เสถียรจะกลายเป็นเพียงการหยุดชั่วคราวที่ผู้ใช้แทบไม่สังเกตเห็น แต่หากออกแบบผิด สถานะ 500 ครั้งเดียวอาจลุกลามเป็น incident ได้
บทความนี้ครอบคลุม 4 รูปแบบหลักสำหรับการกู้คืน:
- Retry ด้วย Exponential Backoff และ Jitter
- Timeout สำหรับทุกการเรียกออก
- Circuit Breaker
- Idempotency Key
จากนั้นจะนำทั้งหมดไปทดสอบกับ Mock API ก่อนที่ผู้ใช้จริงจะพบช่องโหว่ สำหรับภาพรวมเพิ่มเติมเกี่ยวกับความล้มเหลวของเอเจนต์ AI ใน production โปรดดูสาเหตุที่เอเจนต์ AI พังในการใช้งานจริง
คุณไม่สามารถทดสอบการกู้คืนกับ API ที่ทำงานปกติได้
นี่คือกับดักที่พบบ่อย: dependency ทำงานปกติใน development คุณเขียนเอเจนต์ การเรียกสำเร็จ เดโมผ่าน แล้วจึง deploy แต่โค้ด recovery ไม่เคยถูกเรียกใช้งานเลย เพราะ API ไม่ได้ส่ง error ที่คุณต้องจัดการ
ผลคือครั้งแรกที่ retry, timeout หรือ circuit breaker ทำงาน คือระหว่าง incident จริงที่มีผู้ใช้กำลังรออยู่
แนวทางที่ถูกต้องคือต้องสร้าง failure โดยตั้งใจ:
- สร้าง Mock ของ API ที่เอเจนต์เรียกใช้
- กำหนดให้ Mock ส่งกลับ
429,500, timeout หรือ response body ที่ผิดรูปแบบ - ชี้ tool ของเอเจนต์ไปยัง Mock แทน service จริง
- ตรวจสอบว่าเอเจนต์ retry, back off, หยุด retry หรือส่ง error กลับอย่างไร
ด้วยวิธีนี้ failure จะเป็นสิ่งที่คุณเรียกใช้ได้ใน test suite แทนที่จะเป็นสิ่งที่ปลุกคุณตอนตี 3 คุณสามารถใช้Apidogเพื่อสร้าง Mock และกำหนด response sequence สำหรับสถานการณ์เหล่านี้
Retry ด้วย Exponential Backoff และ Jitter
Retry คือแนวป้องกันแรก แต่ retry แบบ “เจอ error แล้วเรียกใหม่ทันที” คือกับดัก
เมื่อบริการกำลังรับภาระสูง ไคลเอ็นต์ที่ล้มเหลวทุกตัวจะ retry พร้อมกัน ทำให้ dependency ถูกถล่มหนักกว่าเดิม ปัญหานี้มักเรียกว่า retry storm
ให้ใช้ 2 เทคนิคคู่กัน:
- Exponential Backoff: เพิ่มเวลารอเป็นลำดับ เช่น 1, 2, 4, 8 วินาที
- Jitter: เพิ่มค่าสุ่มในเวลารอ เพื่อไม่ให้ไคลเอ็นต์จำนวนมาก retry พร้อมกัน
ตัวอย่าง Python สำหรับ retry ที่มีเพดานจำนวนครั้งและเพดาน delay:
import random
import time
import requests
MAX_RETRIES = 4
MAX_DELAY_SECONDS = 30
def retry_delay(attempt: int) -> float:
base_delay = min(2 ** attempt, MAX_DELAY_SECONDS)
jitter = random.uniform(0, base_delay * 0.25)
return base_delay + jitter
def call_api(url: str, payload: dict):
for attempt in range(MAX_RETRIES):
try:
response = requests.post(
url,
json=payload,
timeout=(3, 10), # connect timeout, read timeout
)
if response.status_code == 429:
retry_after = response.headers.get("Retry-After")
if retry_after:
delay = float(retry_after)
else:
delay = retry_delay(attempt)
time.sleep(delay)
continue
if 500 <= response.status_code < 600:
time.sleep(retry_delay(attempt))
continue
response.raise_for_status()
return response.json()
except requests.exceptions.Timeout:
time.sleep(retry_delay(attempt))
raise RuntimeError("API call failed after maximum retry attempts")
ควรจำกัด 2 ค่าเสมอ:
- จำนวนครั้งที่ retry เพื่อไม่ให้ permanent failure กลายเป็น loop
- เวลารอสูงสุด เพื่อไม่ให้ผู้ใช้ต้องรอหลายนาที
โดยทั่วไป retry ประมาณ 3–5 ครั้งมักเพียงพอสำหรับ transient error หากมากกว่านั้น คุณอาจกำลัง retry สิ่งที่ไม่มีทางสำเร็จ
Anthropic SDK มีการ retry สำหรับบาง error ของการเรียก SDK เองด้วย Exponential Backoff และกำหนดจำนวนครั้งได้ผ่าน max_retries แต่ไม่ได้ครอบคลุม API อื่นที่ tool ของเอเจนต์เรียกใช้ คุณยังต้องนำรูปแบบนี้ไปใช้กับ dependency ของคุณเอง
สำหรับตัวอย่างความเสียหายที่เกิดจาก retry ที่ออกแบบไม่ดี โปรดดูตรรกะการลองใหม่สำหรับ API ที่มีความเสี่ยงสูง
ตั้งค่า Timeout ในทุกการเรียกใช้
Retry ช่วยได้เมื่อ request ล้มเหลว แต่กรณีที่แย่กว่าคือ request ที่ไม่ตอบกลับเลย
Dependency อาจรับ connection แล้วค้างอยู่โดยไม่ส่ง response หากไม่มี timeout tool call จะ block และ workflow ทั้งหมดอาจหยุดอยู่บน socket เดียว ไม่มี error ไม่มี retry และไม่มี recovery
ทุก outbound call ควรมีอย่างน้อย 3 ระดับ:
- Connect timeout — เวลาสูงสุดสำหรับการเปิด connection
- Read timeout — เวลาสูงสุดสำหรับการรอ response
- Operation deadline — งบเวลารวมของงานหรือ workflow ทั้งหมด
ตัวอย่างด้วย httpx:
import httpx
timeout = httpx.Timeout(
connect=3.0,
read=10.0,
write=10.0,
pool=5.0,
)
with httpx.Client(timeout=timeout) as client:
response = client.get("https://api.example.com/search")
response.raise_for_status()
หากคุณมี agent workflow หลายขั้นตอน ให้กำหนด deadline ระดับงานด้วย ตัวอย่างเช่น หากผู้ใช้รอได้สูงสุด 30 วินาที อย่าปล่อยให้ tool call เดียวใช้เวลา 25 วินาที แล้วให้ retry อีกหลายครั้งจนเกินงบ
เลือก timeout จาก latency จริง ไม่ใช่การเดา:
- ใช้ค่า p99 ของ dependency เป็นจุดเริ่มต้น
- เพิ่ม buffer สำหรับ network variability
- อย่าตั้งสั้นเกินไปจนตัด request ที่ควรสำเร็จ
- อย่าตั้งนานเกินไปจน dependency ที่ค้างทำให้ agent ใช้งานไม่ได้
สำหรับ streaming response ควรมี timeout แยกต่างหาก เพราะงาน streaming ที่ใช้เวลานานอาจเป็นพฤติกรรมปกติ
ใช้ Circuit Breaker เมื่อ Dependency ล่ม
Backoff เหมาะสำหรับบริการที่กำลังยุ่งชั่วคราว แต่ไม่เหมาะกับบริการที่ล่มต่อเนื่อง
หาก dependency ล้มเหลวมาหลายนาที request ถัดไปก็มักจะล้มเหลวเช่นกัน การ retry ต่อไปจึงเพิ่มภาระให้ระบบที่เสียอยู่แล้ว และบังคับให้ผู้ใช้รอ timeout ที่คาดเดาได้
Circuit Breaker มี 3 สถานะ:
- Closed: สถานะปกติ Request ผ่านได้ และระบบนับ failure
- Open: เมื่อ failure เกินเกณฑ์ ระบบหยุดส่ง request ไปยัง dependency และ fail fast
- Half-open: หลังช่วง cool-down ระบบอนุญาต probe request หนึ่งครั้ง หากสำเร็จจึงกลับไป Closed หากล้มเหลวจะกลับไป Open
แนวคิดแบบย่อ:
class CircuitBreaker:
def __init__(self, failure_threshold=5):
self.failure_threshold = failure_threshold
self.failures = 0
self.state = "closed"
def allow_request(self):
return self.state != "open"
def record_success(self):
self.failures = 0
self.state = "closed"
def record_failure(self):
self.failures += 1
if self.failures >= self.failure_threshold:
self.state = "open"
ใน production คุณต้องเพิ่ม cool-down timer และ half-open probe แต่หลักสำคัญคือ:
สร้าง Circuit Breaker แยกตาม dependency ไม่ใช่สร้างตัวเดียวทั่วทั้งเอเจนต์
ตัวอย่าง: หาก Search API ล่ม เอเจนต์ยังควรเรียก Billing API ที่ทำงานปกติได้
สำหรับเอเจนต์ Circuit Breaker จะเปลี่ยน “Payment API ล่ม” ให้เป็น error ที่รวดเร็วและวิเคราะห์ได้ แทนที่จะปล่อยให้ workflow เสียเวลา timeout ซ้ำหลายครั้งจนใช้งบ token และเวลาหมด
ทำให้ Retry ปลอดภัยด้วย Idempotency Key
Retry จะปลอดภัยก็ต่อเมื่อ operation นั้นทำซ้ำได้อย่างปลอดภัย แต่ request ที่เปลี่ยนแปลงสถานะมักไม่เป็นเช่นนั้น
ตัวอย่าง:
- เอเจนต์ส่ง
POST /charge - Server ประมวลผลการชำระเงินสำเร็จ
- Response timeout ระหว่างส่งกลับ
- เอเจนต์คิดว่า request ล้มเหลวและ retry
- ลูกค้าถูกเรียกเก็บเงิน 2 ครั้ง
ปัญหาไม่ใช่ retry ทำงานผิด แต่เป็น request ถูกออกแบบโดยไม่มี idempotency
ให้สร้าง Idempotency Key หนึ่งค่า สำหรับหนึ่ง logical operation แล้วส่งไปใน header:
import uuid
import requests
idempotency_key = str(uuid.uuid4())
response = requests.post(
"https://api.example.com/charge",
json={
"customer_id": "cus_123",
"amount": 500,
},
headers={
"Idempotency-Key": idempotency_key,
},
timeout=(3, 10),
)
ข้อสำคัญคือสร้าง key ก่อนเข้าสู่ retry loop:
import uuid
idempotency_key = str(uuid.uuid4())
for attempt in range(3):
response = requests.post(
"https://api.example.com/charge",
json=payload,
headers={"Idempotency-Key": idempotency_key},
timeout=(3, 10),
)
อย่าสร้าง key ใหม่ในการ retry แต่ละครั้ง:
# ไม่ถูกต้อง: retry แต่ละครั้งมี key ใหม่
for attempt in range(3):
requests.post(
"https://api.example.com/charge",
headers={"Idempotency-Key": str(uuid.uuid4())},
)
Server ต้องบันทึก key ที่พบครั้งแรก และเมื่อได้รับ key เดิมอีกครั้ง ต้องส่งผลลัพธ์เดิมกลับแทนการทำ operation ซ้ำ
ใช้ Idempotency Key กับทุก request ที่สร้างหรือแก้ไข state เช่น:
- การชำระเงิน
- การสร้างคำสั่งซื้อ
- การส่งอีเมลหรือข้อความ
- การสร้าง record
- การอัปเดตข้อมูลสำคัญ
สำหรับการเรียก read-only เช่น GET /users/123 โดยทั่วไป retry ได้โดยไม่ต้องใช้ key
ดูรายละเอียดฝั่ง server ได้ในคู่มือIdempotency Keys
รับมือ Rate Limits และวงจร RateLimitError
Rate limit ต้องได้รับการจัดการเป็นกรณีพิเศษ เพราะ server มักส่งคำแนะนำมาให้แล้ว
Response 429 มักมี header Retry-After ซึ่งระบุเวลาที่ควรรอก่อน retry เช่น:
HTTP/1.1 429 Too Many Requests
Retry-After: 30
หาก server บอกให้รอ 30 วินาที แต่คุณ retry ใน 2 วินาที คุณก็จะได้ 429 อีกครั้ง และสร้าง RateLimitError loop:
- ได้
429 - retry เร็วเกินไป
- ได้
429อีกครั้ง - ถูกจำกัดหนักขึ้น
- วนซ้ำจน workflow ล้มเหลว
ดูรายละเอียดเพิ่มเติมได้ที่คู่มือแก้ไข Rate Limit Exceededและเธรด Anthropic SDK นี้
ลำดับการจัดการที่ควรใช้คือ:
def get_retry_delay(response, attempt):
retry_after = response.headers.get("Retry-After")
if retry_after is not None:
return float(retry_after)
return retry_delay(attempt)
หลักการคือ:
- ถ้ามี
Retry-Afterให้เคารพค่าจาก server - ถ้าไม่มี ให้ fallback ไปใช้ Exponential Backoff + Jitter
- จำกัดจำนวน retry เพื่อให้ sustained rate limit จบด้วย error ที่ชัดเจน
- อย่ารอแบบไม่มีที่สิ้นสุด
นอกจากนี้ควรป้องกันเชิงรุกด้วย rate limiter ฝั่ง client เช่น Token Bucket เพื่อควบคุมความเร็ว request ของตัวเองให้อยู่ภายใต้ quota ของ provider
Recovery ช่วยจัดการ rate limit ที่เกิดขึ้นแล้ว ส่วน pacing ช่วยไม่ให้คุณไปชน rate limit ตั้งแต่แรก
วิธีทดสอบเส้นทางการกู้คืน
รูปแบบ recovery จะมีค่าเมื่อคุณพิสูจน์ได้ว่ามันทำงานจริง โดยต้องใช้ Mock API ที่สร้าง failure ตามคำสั่งได้
นำ flow ต่อไปนี้ไปใช้กับทุก dependency:
สร้าง Mock ของ dependency
Mock API ที่ tool ของเอเจนต์เรียกใช้ เพื่อควบคุม status code, header, response body และ delay ได้ทั้งหมด โดยไม่ต้องเรียกเก็บเงินจริงหรือส่งอีเมลจริง-
กำหนด response sequence
ตั้งค่าให้ endpoint เดียวตอบกลับตามลำดับ:- ครั้งที่ 1:
429พร้อมRetry-After: 2 - ครั้งที่ 2:
500 - ครั้งที่ 3:
200พร้อม body ที่ถูกต้อง
- ครั้งที่ 1:
ชี้เอเจนต์ไปยัง Mock
เปลี่ยน base URL ของ tool จาก service จริงเป็น Mock URL-
ยืนยันพฤติกรรม
ตรวจสอบว่าเอเจนต์:- รออย่างน้อย 2 วินาทีหลัง
429 - retry หลัง
500 - สำเร็จในการเรียกครั้งที่ 3
- ไม่เกินจำนวน retry ที่กำหนด
- รออย่างน้อย 2 วินาทีหลัง
ตัวอย่าง pseudo-test:
def test_agent_recovers_from_429_then_500(mock_api, agent):
mock_api.sequence([
{
"status": 429,
"headers": {"Retry-After": "2"},
},
{
"status": 500,
"body": {"error": "temporary failure"},
},
{
"status": 200,
"body": {"result": "success"},
},
])
started_at = time.time()
result = agent.run()
elapsed = time.time() - started_at
assert result["result"] == "success"
assert elapsed >= 2
assert mock_api.request_count == 3
เพิ่ม test อีกอย่างน้อย 3 กลุ่ม:
1. Give-up path
กำหนดให้ Mock ล้มเหลวทุกครั้ง แล้วตรวจสอบว่าเอเจนต์หยุดเมื่อถึง retry limit และส่ง error ที่ชัดเจน
def test_agent_stops_after_retry_limit(mock_api, agent):
mock_api.always_return(status=500)
with pytest.raises(RuntimeError):
agent.run()
assert mock_api.request_count == 4
2. Circuit Breaker path
กำหนดให้ request ติดต่อกันล้มเหลว แล้วตรวจสอบว่า circuit เปิดและ request ถัดไป fail fast แทนการรอ timeout ทุกครั้ง
3. Idempotency path
กำหนดให้ Mock รับ mutating request สำเร็จ แต่ตัด response ทิ้งเพื่อให้ client คิดว่า timeout จากนั้นให้เอเจนต์ retry แล้วตรวจสอบว่า:
- Request ทั้งสองมี
Idempotency-Keyเดียวกัน - Server ประมวลผล logical operation เพียงครั้งเดียว
- ไม่มีการสร้าง charge, order หรือ record ซ้ำ
แนวทางที่ครอบคลุมกว่านี้อยู่ในคู่มือทดสอบเอเจนต์ที่เรียกใช้ API ของคุณ
รายการตรวจสอบการกู้คืนข้อผิดพลาด
ก่อน deploy เอเจนต์ ให้ตรวจสอบรายการนี้:
- [ ] Outbound call ทุกตัวมี connect timeout, read timeout และ operation deadline
- [ ] Retry ใช้ Exponential Backoff พร้อม Jitter
- [ ] Retry มีเพดานทั้งจำนวนครั้งและเวลารอสูงสุด
- [ ] Response
429อ่านและเคารพRetry-After - [ ] Circuit Breaker แยกตาม dependency
- [ ] Mutating request ทุกตัวมี Idempotency Key ที่คงที่ตลอด retry
- [ ] Give-up path ส่ง error ที่ชัดเจน ไม่ใช่ loop หรือการรอไม่สิ้นสุด
- [ ] Recovery flow ทุกแบบมี test ที่บังคับให้เกิด failure ผ่าน Mock
หากทำครบทั้งหมดนี้ เอเจนต์ของคุณจะกู้คืนจากความล้มเหลวได้โดยการออกแบบ ไม่ใช่ด้วยโชค
Apidog เหมาะกับอะไร และไม่เหมาะกับอะไร
กำหนดขอบเขตของเครื่องมือให้ชัดเจน: Apidog ไม่ใช่ agent framework, model host หรือ runtime สำหรับเอเจนต์ ไม่ได้สร้าง รัน หรือจัดการเอเจนต์ของคุณ และไม่ได้ประเมินคุณภาพ output ของโมเดล
สิ่งที่ Apidog ช่วยได้คือ API layer ที่เอเจนต์เรียกใช้ ซึ่งเป็นจุดที่ recovery สำเร็จหรือล้มเหลว
สำหรับ use case นี้ Apidog มีบทบาทหลัก 3 อย่าง:
Mock dependency
สร้าง stand-in ที่ควบคุมได้สำหรับ API ภายนอกจำลอง failure response
กำหนด429พร้อมRetry-After,500, timeout หรือ malformed body เพื่อทดสอบ recovery pathตรวจสอบ request ที่ Mock ได้รับ
ตรวจว่าIdempotency-Keyมีอยู่และคงที่, request shape ถูกต้อง และจำนวน request เป็นไปตามที่คาดไว้
กล่าวคือ Apidogช่วยจำลอง failure ที่เอเจนต์ต้องรับมือ และตรวจสอบสิ่งที่เอเจนต์ส่งกลับไป
คำถามที่พบบ่อย
Anthropic SDK ไม่จัดการ retry ให้แล้วหรือ?
สำหรับการเรียกผ่าน SDK เอง ใช่ Anthropic SDK จะ retry error บางประเภทด้วย Exponential Backoff และเคารพ Retry-After โดยกำหนดจำนวนครั้งสูงสุดผ่าน max_retries ได้
แต่ SDK ไม่ได้จัดการ API อื่นที่ tool ของเอเจนต์เรียกใช้ คุณต้องใช้ retry, timeout, circuit breaker และ idempotency pattern เดียวกันกับ dependency เหล่านั้นเอง
เมื่อไรที่ต้องใช้ Idempotency Key?
ใช้กับทุก operation ที่สร้างหรือเปลี่ยนแปลง state เช่น charge, order, message ที่ส่งออก หรือ record ใหม่
สำหรับ read-only call เช่น GET โดยทั่วไป retry ได้โดยไม่จำเป็นต้องมี key
สร้าง key หนึ่งครั้งต่อ logical action และใช้ key เดิมในทุก retry ของ action นั้น
ฝึกซ้อม Failure หนึ่งครั้งในสัปดาห์นี้
คุณไม่จำเป็นต้องสร้างทั้ง 4 รูปแบบพร้อมกัน เริ่มจาก failure ที่สร้างความเสียหายมากที่สุดในระบบของคุณ ซึ่งมักเป็น rate-limit loop หรือ mutating request ที่ไม่มี idempotency
ลองทำตามนี้:
- สร้าง Mock endpoint
- ตั้งค่าให้ตอบ
429พร้อมRetry-After - จำลอง response timeout หลัง server ประมวลผล request
- ตรวจดูว่าเอเจนต์ back off อย่างถูกต้องหรือไม่
- ตรวจว่า retry ใช้ Idempotency Key เดิมหรือไม่
- ยืนยันว่าไม่มี operation ซ้ำเกิดขึ้น
เมื่อคุณเห็น retry ที่เว้นจังหวะถูกต้อง และเห็น Idempotency Key เดิมตลอดการ retry คุณจะมีเหตุผลที่ดีกว่าเดโมที่ราบรื่นในการเชื่อมั่นว่าเอเจนต์พร้อมรับมือกับ production failure

Top comments (0)