DEV Community

Cover image for การกู้คืนข้อผิดพลาด AI Agent: รูปแบบ Retry, Timeout, Backoff และ Circuit Breaker
Thanawat Wongchai
Thanawat Wongchai

Posted on • Originally published at apidog.com

การกู้คืนข้อผิดพลาด AI Agent: รูปแบบ Retry, Timeout, Backoff และ Circuit Breaker

เอเจนต์ของคุณเรียกใช้ API แล้วได้รับสถานะ 429 Too Many Requests จึงพยายามเรียกซ้ำทันที ได้ 429 อีกครั้ง แล้ววนซ้ำต่อไปจนงานหยุดลงหรือค่าใช้จ่ายพุ่งสูงขึ้น ไม่มีใครตั้งใจเขียนวงจรแบบนี้ แต่มันมักเกิดจากการจัดการข้อผิดพลาดแบบตรงไปตรงมาเกินไป และเป็นปัญหาที่พบบ่อยในกระดานสนทนา Anthropic SDK

ลองใช้ Apidog วันนี้

การกู้คืนข้อผิดพลาดคือสิ่งที่แยกเดโมที่ดูดีออกจากเอเจนต์ที่พร้อมใช้งานจริง ปัญหาไม่ได้อยู่ที่โมเดล แต่อยู่ที่โค้ดของคุณทำอะไรเมื่อ tool call ช้า ถูกจำกัดอัตรา หรือใช้งานไม่ได้ หากออกแบบถูกต้อง dependency ที่ไม่เสถียรจะกลายเป็นเพียงการหยุดชั่วคราวที่ผู้ใช้แทบไม่สังเกตเห็น แต่หากออกแบบผิด สถานะ 500 ครั้งเดียวอาจลุกลามเป็น incident ได้

บทความนี้ครอบคลุม 4 รูปแบบหลักสำหรับการกู้คืน:

  1. Retry ด้วย Exponential Backoff และ Jitter
  2. Timeout สำหรับทุกการเรียกออก
  3. Circuit Breaker
  4. Idempotency Key

จากนั้นจะนำทั้งหมดไปทดสอบกับ Mock API ก่อนที่ผู้ใช้จริงจะพบช่องโหว่ สำหรับภาพรวมเพิ่มเติมเกี่ยวกับความล้มเหลวของเอเจนต์ AI ใน production โปรดดูสาเหตุที่เอเจนต์ AI พังในการใช้งานจริง

คุณไม่สามารถทดสอบการกู้คืนกับ API ที่ทำงานปกติได้

นี่คือกับดักที่พบบ่อย: dependency ทำงานปกติใน development คุณเขียนเอเจนต์ การเรียกสำเร็จ เดโมผ่าน แล้วจึง deploy แต่โค้ด recovery ไม่เคยถูกเรียกใช้งานเลย เพราะ API ไม่ได้ส่ง error ที่คุณต้องจัดการ

ผลคือครั้งแรกที่ retry, timeout หรือ circuit breaker ทำงาน คือระหว่าง incident จริงที่มีผู้ใช้กำลังรออยู่

แนวทางที่ถูกต้องคือต้องสร้าง failure โดยตั้งใจ:

  1. สร้าง Mock ของ API ที่เอเจนต์เรียกใช้
  2. กำหนดให้ Mock ส่งกลับ 429, 500, timeout หรือ response body ที่ผิดรูปแบบ
  3. ชี้ tool ของเอเจนต์ไปยัง Mock แทน service จริง
  4. ตรวจสอบว่าเอเจนต์ 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")
Enter fullscreen mode Exit fullscreen mode

ควรจำกัด 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 ระดับ:

  1. Connect timeout — เวลาสูงสุดสำหรับการเปิด connection
  2. Read timeout — เวลาสูงสุดสำหรับการรอ response
  3. 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()
Enter fullscreen mode Exit fullscreen mode

หากคุณมี 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"
Enter fullscreen mode Exit fullscreen mode

ใน 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 ที่เปลี่ยนแปลงสถานะมักไม่เป็นเช่นนั้น

ตัวอย่าง:

  1. เอเจนต์ส่ง POST /charge
  2. Server ประมวลผลการชำระเงินสำเร็จ
  3. Response timeout ระหว่างส่งกลับ
  4. เอเจนต์คิดว่า request ล้มเหลวและ retry
  5. ลูกค้าถูกเรียกเก็บเงิน 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),
)
Enter fullscreen mode Exit fullscreen mode

ข้อสำคัญคือสร้าง 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),
    )
Enter fullscreen mode Exit fullscreen mode

อย่าสร้าง key ใหม่ในการ retry แต่ละครั้ง:

# ไม่ถูกต้อง: retry แต่ละครั้งมี key ใหม่
for attempt in range(3):
    requests.post(
        "https://api.example.com/charge",
        headers={"Idempotency-Key": str(uuid.uuid4())},
    )
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

หาก server บอกให้รอ 30 วินาที แต่คุณ retry ใน 2 วินาที คุณก็จะได้ 429 อีกครั้ง และสร้าง RateLimitError loop:

  1. ได้ 429
  2. retry เร็วเกินไป
  3. ได้ 429 อีกครั้ง
  4. ถูกจำกัดหนักขึ้น
  5. วนซ้ำจน 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)
Enter fullscreen mode Exit fullscreen mode

หลักการคือ:

  • ถ้ามี 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:

  1. สร้าง Mock ของ dependency

    Mock API ที่ tool ของเอเจนต์เรียกใช้ เพื่อควบคุม status code, header, response body และ delay ได้ทั้งหมด โดยไม่ต้องเรียกเก็บเงินจริงหรือส่งอีเมลจริง

  2. กำหนด response sequence

    ตั้งค่าให้ endpoint เดียวตอบกลับตามลำดับ:

    • ครั้งที่ 1: 429 พร้อม Retry-After: 2
    • ครั้งที่ 2: 500
    • ครั้งที่ 3: 200 พร้อม body ที่ถูกต้อง
  3. ชี้เอเจนต์ไปยัง Mock

    เปลี่ยน base URL ของ tool จาก service จริงเป็น Mock URL

  4. ยืนยันพฤติกรรม

    ตรวจสอบว่าเอเจนต์:

    • รออย่างน้อย 2 วินาทีหลัง 429
    • retry หลัง 500
    • สำเร็จในการเรียกครั้งที่ 3
    • ไม่เกินจำนวน retry ที่กำหนด

ตัวอย่าง 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
Enter fullscreen mode Exit fullscreen mode

เพิ่ม 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
Enter fullscreen mode Exit fullscreen mode

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 สำเร็จหรือล้มเหลว

ภาพแสดงการทำงานของ Apidog

สำหรับ use case นี้ Apidog มีบทบาทหลัก 3 อย่าง:

  1. Mock dependency

    สร้าง stand-in ที่ควบคุมได้สำหรับ API ภายนอก

  2. จำลอง failure response

    กำหนด 429 พร้อม Retry-After, 500, timeout หรือ malformed body เพื่อทดสอบ recovery path

  3. ตรวจสอบ 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

ลองทำตามนี้:

  1. สร้าง Mock endpoint
  2. ตั้งค่าให้ตอบ 429 พร้อม Retry-After
  3. จำลอง response timeout หลัง server ประมวลผล request
  4. ตรวจดูว่าเอเจนต์ back off อย่างถูกต้องหรือไม่
  5. ตรวจว่า retry ใช้ Idempotency Key เดิมหรือไม่
  6. ยืนยันว่าไม่มี operation ซ้ำเกิดขึ้น

เมื่อคุณเห็น retry ที่เว้นจังหวะถูกต้อง และเห็น Idempotency Key เดิมตลอดการ retry คุณจะมีเหตุผลที่ดีกว่าเดโมที่ราบรื่นในการเชื่อมั่นว่าเอเจนต์พร้อมรับมือกับ production failure

Top comments (0)