Agent gọi một API, nhận lỗi 429, rồi thử lại ngay lập tức. Lần thử tiếp theo lại là 429, và vòng lặp cứ tiếp diễn cho đến khi process bị dừng hoặc chi phí tăng vọt. Đây không phải lỗi mà ai chủ động muốn viết; nó thường xuất hiện từ một nhánh “xử lý lỗi” quá đơn giản, đồng thời là vấn đề phổ biến trong các thảo luận về Anthropic SDK.
Khôi phục lỗi là phần phân biệt một agent demo với một agent có thể chạy production. Vấn đề không chỉ nằm ở mô hình, mà ở cách mã của bạn phản ứng khi tool call chậm, bị rate limit hoặc trả lỗi. Bài viết này triển khai bốn cơ chế cốt lõi: retry với exponential backoff và jitter, timeout, circuit breaker và idempotency key. Sau đó, bạn sẽ kiểm tra chúng bằng mock API trước khi người dùng gặp sự cố. Để hiểu rõ hơn các điểm lỗi của agent, hãy xem vì sao agent AI thất bại trong production.
Bạn không thể kiểm tra khả năng khôi phục với một API khỏe mạnh
Trong môi trường development, dependency thường hoạt động bình thường. Bạn gọi API thành công, demo chạy tốt và triển khai. Nhưng code retry, timeout hoặc circuit breaker chưa từng chạy.
Lần đầu logic khôi phục được kích hoạt lại là trong production, khi dependency thực sự gặp sự cố và người dùng đang chờ kết quả. Đây là lúc tệ nhất để phát hiện vòng lặp retry sai hoặc timeout không tồn tại.
Quy tắc thực tế:
Muốn kiểm tra recovery, hãy chủ động tạo lỗi.
Mock API mà agent gọi và lập trình các phản hồi như:
429 Too Many Requests500 Internal Server Error- timeout hoặc phản hồi chậm
- body JSON không hợp lệ
- response thiếu field bắt buộc
Sau đó, trỏ tool của agent vào mock server và xác nhận hành vi. Với Apidog, bạn có thể mock dependency, script response theo thứ tự và kiểm tra request mà agent gửi đến.
Retry với exponential backoff và jitter
Retry là tuyến phòng thủ đầu tiên, nhưng retry ngay lập tức thường khiến lỗi nghiêm trọng hơn.
Ví dụ không nên dùng:
while True:
try:
return call_external_api()
except Exception:
continue
Đoạn mã trên có thể:
- gửi hàng nghìn request trong vài giây;
- làm dependency quá tải hơn;
- đốt token và thời gian chạy của agent;
- không bao giờ kết thúc khi lỗi là vĩnh viễn.
Thay vào đó, dùng:
- Exponential backoff: tăng thời gian chờ sau mỗi lần lỗi.
- Jitter: thêm độ ngẫu nhiên để nhiều client không retry cùng lúc.
- Retry limit: giới hạn số lần thử.
- Delay cap: giới hạn thời gian chờ tối đa.
Ví dụ Python:
import random
import time
from typing import Callable, TypeVar
T = TypeVar("T")
def retry_with_backoff(
fn: Callable[[], T],
max_attempts: int = 4,
base_delay_seconds: float = 1.0,
max_delay_seconds: float = 8.0,
) -> T:
last_error = None
for attempt in range(max_attempts):
try:
return fn()
except Exception as error:
last_error = error
if attempt == max_attempts - 1:
break
exponential_delay = min(
base_delay_seconds * (2 ** attempt),
max_delay_seconds,
)
jitter = random.uniform(0, exponential_delay * 0.25)
delay = exponential_delay + jitter
time.sleep(delay)
raise RuntimeError(
f"API vẫn thất bại sau {max_attempts} lần thử"
) from last_error
Chuỗi chờ có thể là:
Lần 1 thất bại → chờ ~1 giây
Lần 2 thất bại → chờ ~2 giây
Lần 3 thất bại → chờ ~4 giây
Lần 4 thất bại → dừng và trả lỗi rõ ràng
Thông thường, 3–5 lần thử là đủ cho lỗi tạm thời. Retry nhiều hơn thường chỉ kéo dài thời gian thất bại.
Anthropic SDK đã xử lý một phần retry cho các lệnh gọi SDK của chính nó, bao gồm một số lỗi kết nối và status code cụ thể. Bạn có thể kiểm soát giới hạn qua max_retries. Tuy nhiên, retry đó không tự áp dụng cho API khác mà tool của agent gọi.
Đặc biệt với API có tác động tài chính, retry thiếu kiểm soát có thể gây hậu quả thực. Xem thêm logic retry cho API rủi ro cao.
Đặt timeout cho mọi lệnh gọi
Retry chỉ hoạt động khi request trả về lỗi. Trường hợp nguy hiểm hơn là request không bao giờ hoàn tất:
- TCP connection được mở;
- server không trả response;
- socket bị treo;
- toàn bộ agent run bị chặn.
Không có timeout nghĩa là không có lỗi để retry, không có recovery và không có điểm dừng rõ ràng.
Mỗi external call nên có ba mức ngân sách:
- Connect timeout: thời gian tối đa để thiết lập kết nối.
- Read timeout: thời gian tối đa để chờ response.
- Run budget: ngân sách thời gian cho toàn bộ tác vụ của agent.
Ví dụ với httpx:
import httpx
timeout = httpx.Timeout(
connect=3.0,
read=15.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()
Ví dụ kết hợp timeout với retry:
import httpx
def fetch_customer(customer_id: str) -> dict:
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(
f"https://api.example.com/customers/{customer_id}"
)
response.raise_for_status()
return response.json()
customer = retry_with_backoff(
lambda: fetch_customer("cus_123"),
max_attempts=3,
)
Chọn timeout theo số liệu thực tế, không theo cảm tính:
- đặt timeout cao hơn p99 của dependency với một khoảng an toàn;
- không đặt quá thấp, nếu không bạn sẽ hủy request vốn có thể thành công;
- không đặt quá cao, nếu không agent sẽ bị giữ quá lâu bởi dependency treo.
Với streaming response, dùng ngân sách riêng. Một response stream dài có thể hợp lệ, trong khi read timeout cố định quá ngắn sẽ cắt ngang quá trình giữa chừng.
Kích hoạt circuit breaker khi dependency bị lỗi liên tục
Backoff phù hợp với lỗi ngắn hạn. Nhưng nếu dependency đã chết trong nhiều phút, tiếp tục retry mọi request là lãng phí.
Circuit breaker giúp agent thất bại nhanh khi một dependency đang không khả dụng.
Circuit breaker có ba trạng thái:
| Trạng thái | Hành vi |
|---|---|
closed |
Request đi qua bình thường, hệ thống đếm lỗi. |
open |
Không gửi request tới dependency; trả lỗi nhanh trong thời gian cool-down. |
half-open |
Cho phép một request thăm dò để kiểm tra dependency đã phục hồi chưa. |
Luồng hoạt động:
closed
↓ nhiều lỗi liên tiếp
open
↓ hết cool-down window
half-open
├─ request thành công → closed
└─ request thất bại → open
Ví dụ implementation đơn giản:
import time
from dataclasses import dataclass
@dataclass
class CircuitBreaker:
failure_threshold: int = 3
recovery_timeout_seconds: float = 30.0
failures: int = 0
state: str = "closed"
opened_at: float | None = None
def allow_request(self) -> bool:
if self.state == "closed":
return True
if self.state == "open":
elapsed = time.monotonic() - (self.opened_at or 0)
if elapsed >= self.recovery_timeout_seconds:
self.state = "half-open"
return True
return False
# half-open: cho phép request thăm dò
return True
def record_success(self) -> None:
self.failures = 0
self.state = "closed"
self.opened_at = None
def record_failure(self) -> None:
self.failures += 1
if self.failures >= self.failure_threshold:
self.state = "open"
self.opened_at = time.monotonic()
Dùng breaker cho từng dependency riêng biệt:
payment_breaker = CircuitBreaker()
search_breaker = CircuitBreaker()
Không dùng một breaker global. Nếu API tìm kiếm lỗi, agent vẫn có thể gọi API thanh toán hoặc API dữ liệu khác đang khỏe mạnh.
Ví dụ wrapper:
def call_payment_api(payload: dict) -> dict:
if not payment_breaker.allow_request():
raise RuntimeError("Payment API tạm thời không khả dụng")
try:
result = retry_with_backoff(
lambda: send_payment_request(payload),
max_attempts=3,
)
payment_breaker.record_success()
return result
except Exception:
payment_breaker.record_failure()
raise
Với agent, circuit breaker biến hàng chục timeout chậm thành một lỗi nhanh, rõ ràng và có thể xử lý được.
Giúp retry an toàn với idempotency key
Retry không phải lúc nào cũng an toàn.
Ví dụ:
- Agent gửi
POST /charge. - Server xử lý thanh toán thành công.
- Response bị timeout trên đường về client.
- Agent không biết request đã thành công.
- Agent retry.
- Khách hàng bị tính phí hai lần.
Retry logic đã làm đúng nhiệm vụ. Thiết kế request mới là vấn đề.
Giải pháp là idempotency key.
Client tạo một key duy nhất cho mỗi hành động nghiệp vụ và gửi key đó trong request:
POST /charge
Idempotency-Key: 8f2d579d-1fd1-45cf-8107-72ba8498ac29
Content-Type: application/json
Server cần lưu key của request đầu tiên. Nếu nhận request khác có cùng key, server trả lại kết quả trước đó thay vì thực hiện hành động thêm lần nữa.
Ví dụ:
import uuid
import httpx
def create_charge(payload: dict) -> dict:
idempotency_key = str(uuid.uuid4())
def send_request() -> dict:
response = httpx.post(
"https://api.example.com/charge",
json=payload,
headers={
"Idempotency-Key": idempotency_key,
},
timeout=httpx.Timeout(connect=3.0, read=15.0),
)
response.raise_for_status()
return response.json()
return retry_with_backoff(send_request, max_attempts=3)
Điểm quan trọng: tạo key trước vòng retry.
Không làm như sau:
def send_request_wrong(payload: dict) -> dict:
return httpx.post(
"https://api.example.com/charge",
json=payload,
headers={
# Sai: mỗi lần retry lại sinh key mới
"Idempotency-Key": str(uuid.uuid4()),
},
).json()
Mỗi retry phải mang cùng key cho cùng một hành động logic.
Dùng idempotency key cho mọi endpoint có thay đổi trạng thái:
- tính phí;
- tạo đơn hàng;
- gửi email hoặc tin nhắn;
- tạo bản ghi;
- cập nhật trạng thái;
- kích hoạt workflow bên ngoài.
Các lệnh chỉ đọc như GET /users/:id thường không cần idempotency key. Hướng dẫn đầy đủ về idempotency keys trình bày cả phía client lẫn server.
Xử lý rate limit và tránh vòng lặp RateLimitError
Rate limit cần được xử lý riêng vì server thường gửi hướng dẫn cụ thể qua header Retry-After.
Một phản hồi rate limit thường có dạng:
HTTP/1.1 429 Too Many Requests
Retry-After: 30
Khi nhận response này, hãy chờ ít nhất 30 giây trước khi retry.
Không làm như sau:
if response.status_code == 429:
time.sleep(2)
retry()
Nếu server yêu cầu 30 giây mà client chỉ đợi 2 giây, bạn gần như chắc chắn nhận thêm 429. Đây là nguyên nhân phổ biến của vòng lặp:
429 → retry quá sớm → 429 → retry quá sớm → 429
Một thảo luận khác của Anthropic SDK cũng đề cập đến vấn đề này.
Ví dụ đọc Retry-After:
from datetime import datetime, timezone
from email.utils import parsedate_to_datetime
def parse_retry_after(value: str | None) -> float | None:
if not value:
return None
try:
# Retry-After: 30
return max(float(value), 0)
except ValueError:
pass
try:
# Retry-After: Wed, 21 Oct 2026 07:28:00 GMT
retry_at = parsedate_to_datetime(value)
now = datetime.now(timezone.utc)
return max((retry_at - now).total_seconds(), 0)
except (TypeError, ValueError):
return None
Áp dụng vào retry:
import random
import time
import httpx
def request_with_rate_limit_handling(url: str, max_attempts: int = 4) -> dict:
for attempt in range(max_attempts):
response = httpx.get(
url,
timeout=httpx.Timeout(connect=3.0, read=10.0),
)
if response.status_code == 429:
if attempt == max_attempts - 1:
raise RuntimeError("Đã vượt giới hạn rate limit quá lâu")
retry_after = parse_retry_after(
response.headers.get("Retry-After")
)
if retry_after is None:
fallback = min(2 ** attempt, 8)
retry_after = fallback + random.uniform(0, 0.5)
time.sleep(retry_after)
continue
response.raise_for_status()
return response.json()
raise RuntimeError("Không thể hoàn tất request")
Ngoài recovery, hãy chủ động điều tiết lưu lượng. Nếu provider giới hạn request mỗi phút, token bucket là một cách phổ biến để giữ client dưới ngưỡng thay vì chờ bị 429.
import time
class TokenBucket:
def __init__(self, capacity: int, refill_per_second: float):
self.capacity = capacity
self.tokens = capacity
self.refill_per_second = refill_per_second
self.updated_at = time.monotonic()
def acquire(self) -> bool:
now = time.monotonic()
elapsed = now - self.updated_at
self.tokens = min(
self.capacity,
self.tokens + elapsed * self.refill_per_second,
)
self.updated_at = now
if self.tokens < 1:
return False
self.tokens -= 1
return True
Recovery xử lý giới hạn bạn đã chạm phải. Rate limiting phía client giúp bạn tránh chạm vào chúng.
Cách kiểm tra recovery path
Các cơ chế trên chỉ có giá trị khi có test chứng minh chúng hoạt động. Một API khỏe mạnh không thể tạo đủ tình huống để kiểm tra các nhánh lỗi.
Dùng quy trình sau.
Mock dependency
Tạo mock API cho tool mà agent gọi. Bạn cần kiểm soát status code, header, body và độ trễ mà không tạo giao dịch, email hoặc thay đổi dữ liệu thật.Script chuỗi response
Lập trình mock trả response theo thứ tự:
Request 1 → 429 + Retry-After: 2
Request 2 → 500
Request 3 → 200 + JSON hợp lệ
Trỏ agent vào mock URL
Cấu hình tool dùng mock server thay vì endpoint production.-
Xác nhận hành vi quan trọng
Kiểm tra agent:- đợi ít nhất 2 giây sau
429; - retry sau
500; - thành công ở request thứ ba;
- không vượt quá retry limit;
- gửi đúng idempotency key nếu endpoint thay đổi trạng thái.
- đợi ít nhất 2 giây sau
Ví dụ test với pytest:
def test_agent_recovers_from_429_then_500(mock_api, agent):
mock_api.script(
"/payment",
[
{
"status": 429,
"headers": {"Retry-After": "2"},
"body": {"error": "rate_limited"},
},
{
"status": 500,
"body": {"error": "temporary_failure"},
},
{
"status": 200,
"body": {"status": "paid"},
},
],
)
started_at = time.monotonic()
result = agent.run(
payment_api_url=mock_api.url("/payment"),
)
elapsed = time.monotonic() - started_at
assert result["status"] == "paid"
assert elapsed >= 2
assert mock_api.request_count("/payment") == 3
Thêm test cho failure path:
def test_agent_stops_after_retry_limit(mock_api, agent):
mock_api.script(
"/payment",
[
{"status": 500, "body": {"error": "failed"}},
{"status": 500, "body": {"error": "failed"}},
{"status": 500, "body": {"error": "failed"}},
],
)
with pytest.raises(RuntimeError, match="sau 3 lần thử"):
agent.run(payment_api_url=mock_api.url("/payment"))
assert mock_api.request_count("/payment") == 3
Test circuit breaker:
def test_circuit_breaker_fails_fast_after_threshold(mock_api, agent):
mock_api.always_return(
"/search",
status=500,
body={"error": "service_down"},
)
for _ in range(3):
with pytest.raises(RuntimeError):
agent.search(mock_api.url("/search"))
started_at = time.monotonic()
with pytest.raises(RuntimeError, match="tạm thời không khả dụng"):
agent.search(mock_api.url("/search"))
elapsed = time.monotonic() - started_at
# Request mới bị chặn nhanh, không phải chờ timeout.
assert elapsed < 0.5
Test idempotency là phần thường bị bỏ qua nhưng giúp tránh mất tiền.
Kịch bản:
- Mock chấp nhận request
POST /charge. - Mock xử lý hành động nhưng cố tình không gửi response.
- Agent timeout và retry.
- Mock nhận request lần hai.
- Test xác nhận cả hai request dùng cùng
Idempotency-Key. - Test xác nhận mock chỉ ghi nhận một hành động nghiệp vụ.
def test_retry_reuses_idempotency_key(mock_api, agent):
mock_api.script(
"/charge",
[
{"action": "process_then_timeout"},
{"status": 200, "body": {"charge_id": "ch_123"}},
],
)
result = agent.charge(
api_url=mock_api.url("/charge"),
amount=5000,
)
requests = mock_api.requests("/charge")
assert result["charge_id"] == "ch_123"
assert len(requests) == 2
assert (
requests[0].headers["Idempotency-Key"]
== requests[1].headers["Idempotency-Key"]
)
assert mock_api.logical_action_count("/charge") == 1
Nếu retry tạo key mới hoặc mock thấy hai thao tác logic, bạn đã phát hiện lỗi double-charge trước khách hàng. Xem thêm hướng dẫn kiểm tra agent gọi API.
Danh sách kiểm tra khôi phục lỗi
Trước khi triển khai agent lên production, kiểm tra các mục sau:
- [ ] Mọi external call có connect timeout, read timeout và run budget.
- [ ] Retry dùng exponential backoff với jitter.
- [ ] Retry có giới hạn số lần thử và thời gian chờ tối đa.
- [ ] Response
429đọc và tuân thủRetry-After. - [ ] Có fallback backoff khi
Retry-Afterkhông tồn tại. - [ ] Mỗi dependency có circuit breaker riêng.
- [ ] Endpoint thay đổi trạng thái dùng idempotency key ổn định qua các lần retry.
- [ ] Failure path trả về lỗi rõ ràng thay vì chờ vô hạn.
- [ ] Từng cơ chế được kiểm tra bằng mock API chủ động tạo lỗi.
Hoàn thành danh sách này giúp agent phục hồi có chủ đích thay vì chỉ “may mắn” chạy được trong demo.
Apidog phù hợp ở đâu, và không phù hợp ở đâu
Apidog không phải agent framework, model server hay runtime điều phối agent. Nó không xây dựng agent, chạy agent hoặc đánh giá output của model.
Vai trò của Apidog nằm ở API layer mà agent gọi — nơi retry, timeout, rate limit và idempotency thường phát sinh lỗi.
Cụ thể, Apidog có thể hỗ trợ ba việc:
- Mock các dependency mà agent truy cập.
- Script các response lỗi như
429vớiRetry-After,500, timeout và malformed body. - Xác thực request mà mock nhận được, bao gồm:
- idempotency key có tồn tại không;
- key có giữ nguyên qua retry không;
- request body có đúng cấu trúc không;
- agent có gửi quá số request dự kiến không.
Đây là phạm vi phù hợp: mô phỏng lỗi mà agent phải sống sót và xác nhận hành vi request của agent khi lỗi xảy ra.
Các câu hỏi thường gặp
Anthropic SDK có tự xử lý retry không?
Có, cho các lệnh gọi của chính SDK. SDK retry một số lỗi với exponential backoff và tuân thủ Retry-After; bạn có thể cấu hình giới hạn qua max_retries. Tuy nhiên, điều này không bao gồm API khác mà tool của agent gọi. Bạn vẫn cần triển khai timeout, retry, circuit breaker và idempotency cho các dependency đó.
Khi nào cần idempotency key?
Dùng cho mọi request tạo hoặc thay đổi trạng thái, chẳng hạn như thanh toán, tạo đơn hàng, gửi tin nhắn hoặc tạo bản ghi. Request chỉ đọc thường có thể retry mà không cần key. Tạo key một lần cho mỗi hành động logic và tái sử dụng nó trong mọi lần retry.
Diễn tập một lỗi trong tuần này
Bạn không cần triển khai cả bốn cơ chế cùng lúc. Hãy chọn lỗi gây thiệt hại lớn nhất trong hệ thống hiện tại, thường là:
- vòng lặp
429retry quá sớm; - timeout không có giới hạn;
- request thanh toán không có idempotency key;
- dependency chết nhưng agent vẫn retry liên tục.
Tạo mock, script một lỗi 429 hoặc một response bị mất, rồi quan sát request mà agent thực sự gửi. Khi bạn thấy agent backoff đúng cách và tái sử dụng một idempotency key thay vì tạo giao dịch trùng lặp, bạn có bằng chứng đáng tin cậy hơn nhiều so với một demo xanh.

Top comments (0)