Dùng AI

Xử lý lỗi khi gọi API AI: giới hạn tốc độ, thử lại, hết giờ

Chương trình gọi API mô hình AI sớm muộn sẽ gặp lỗi: khóa sai, yêu cầu sai định dạng, vượt giới hạn tốc độ (lỗi 429), máy chủ quá tải (529), mạng chập chờn. Có lỗi phải sửa code, có lỗi chỉ cần chờ rồi thử lại. Bài này phân loại các lỗi theo tài liệu của Anthropic, giải thích giới hạn tốc độ, cách chờ lũy thừa có ngẫu nhiên, và một đoạn Python thử lại đúng chỗ.

Ý chính
  • Lỗi do yêu cầu (400, 401, 403, 404, 413) phải sửa code hoặc cấu hình; thử lại chỉ tốn thời gian.
  • Lỗi tạm thời (429 giới hạn tốc độ, 500, 529 quá tải, lỗi mạng) nên thử lại, chờ theo retry-after nếu có, nếu không thì chờ lũy thừa có thêm ngẫu nhiên.
  • Một lỗi 429 không có retry-after và có mã hết hạn mức chi tiêu tháng thì thử lại vô ích cho tới khi hạn mức được mở.
  • Thư viện anthropic đã tự thử lại 2 lần. Chỉ tự viết logic thử lại khi cần kiểm soát riêng, và khi đó tắt thử lại của thư viện.

Bài này dựa trên hai trang tài liệu của Anthropic về lỗi API và giới hạn tốc độ (đọc ngày 11/11/2026). Mã lỗi ở các hãng khác tương tự nhưng không giống hệt, nên đọc tài liệu của hãng bạn dùng. Nếu mới bắt đầu, đọc trước bài gọi API AI bằng Python.

Phân loại lỗi

Theo trang lỗi của Anthropic, mỗi lỗi trả về một mã HTTP và một đối tượng JSON có kiểu lỗi và lời nhắn, kèm mã yêu cầu để báo hỗ trợ. Có thể chia thành hai nhóm.

Nhóm phải sửa, không thử lại. 400 là yêu cầu sai định dạng hoặc tham số (cũng dùng khi chạm hạn mức chi tiêu do bạn tự đặt). 401 là khóa API sai, bị thu hồi hoặc hết hạn. 402 là vấn đề thanh toán. 403 là khóa không có quyền. 404 là sai đường dẫn hoặc tên mô hình. 413 là yêu cầu quá lớn; với Messages API giới hạn là 128 MB. Gửi lại y nguyên chỉ nhận lại lỗi y nguyên.

Nhóm tạm thời, nên thử lại. 429 là vượt giới hạn tốc độ. 500 là lỗi bất ngờ bên trong hệ thống. 529 là API đang quá tải do lưu lượng chung. Lỗi mạng và hết giờ chờ cũng thuộc nhóm này. Tài liệu ghi rằng thư viện chính thức tự thử lại các lỗi tạm thời này với thời gian chờ lũy thừa, mặc định hai lần, và tôn trọng retry-after khi có.

Lỗi nào thì thử lại Thử lại sai chỗ chỉ tốn thêm thời gian Sửa code, đừng thử lại400, 401, 403, 404, 413yêu cầu sai, khóa sai Thử lại có chờ429, 500, 529, mạngchờ lũy thừa, ngẫu nhiên Có retry-afterChờ đúng số giâykhông thử sớm hơn Hết hạn mức tháng429 không retry-afterdừng, báo người quản lý Thư viện anthropic tự thử lại 2 lần; đừng thử lại hai tầng
Lỗi nào thì thử lại Sửa code, đừng thử lại400, 401, 403, 404, 413yêu cầu sai, khóa sai Thử lại có chờ429, 500, 529, mạngchờ lũy thừa, ngẫu nhiên Có retry-afterChờ đúng số giâykhông thử sớm hơn Hết hạn mức tháng429 không retry-afterdừng, báo người quản lý Thư viện anthropic tự thử lại 2 lần; đừng thử lại hai tầng
Hình 1. Lỗi nào thì thử lại: 400, 401, 403, 404, 413 là yêu cầu hoặc khóa sai, sửa code, đừng thử lại; 429, 500, 529 và lỗi mạng thì thử lại có chờ lũy thừa và ngẫu nhiên; nếu có retry-after thì chờ đúng số giây đó; 429 không có retry-after do hết hạn mức tháng thì dừng và báo người quản lý. Thư viện anthropic tự thử lại 2 lần, đừng thử lại hai tầng.

Giới hạn tốc độ hoạt động thế nào

Trang giới hạn tốc độ mô tả ba loại giới hạn cho mỗi nhóm mô hình: số yêu cầu mỗi phút, số token đầu vào mỗi phút và số token đầu ra mỗi phút. Vượt bất kỳ giới hạn nào đều nhận 429. Mức giới hạn tùy hạng sử dụng của tổ chức, tăng dần theo lịch sử dùng; xem mức hiện tại trong trang quản lý tài khoản.

Ba chi tiết hay bị hiểu sai. Thứ nhất, giới hạn dùng thuật toán thùng token: sức chứa được bù liên tục, không phải reset vào đầu mỗi phút. Thứ hai, giới hạn có thể áp ở khoảng ngắn hơn: 60 yêu cầu mỗi phút có thể được áp như 1 yêu cầu mỗi giây, nên gửi dồn một lúc 30 yêu cầu vẫn có thể bị chặn dù cả phút chưa tới 60. Thứ ba, token đọc từ cache không tính vào giới hạn token đầu vào, nên prompt caching vừa giảm chi phí vừa tăng sức chứa.

Tài liệu còn nhắc: khi lưu lượng của tổ chức tăng vọt đột ngột, có thể gặp 429 do giới hạn tăng tốc. Cách tránh là tăng tải từ từ.

Hết hạn mức chi tiêu không phải giới hạn tốc độ

Các hạng Start, Build và Scale đều có một mức chi tiêu tối đa mỗi tháng; riêng hạng Custom thì không. Khi chạm mức này, API trả 429 cùng kiểu lỗi với giới hạn tốc độ, nhưng không có retry-after, và chi tiết lỗi có mã enforced_spend_limit_reached. Theo tài liệu, thử lại, kể cả thử lại tự động của thư viện, đều thất bại cho tới khi quyền truy cập được mở lại. Code cần phân biệt trường hợp này để dừng và báo người quản lý, thay vì thử lại hàng giờ.

Nếu bạn tự đặt hạn mức chi tiêu thấp hơn mức của hạng, chạm hạn mức đó sẽ nhận 400 với lời nhắn đã đạt hạn mức bạn đặt.

Chờ bao lâu

Nếu phản hồi có retry-after, chờ đúng số giây đó; thử sớm hơn sẽ thất bại. Nếu không có, dùng chờ lũy thừa: lần thứ nhất chờ khoảng 1 giây, lần hai 2 giây, lần ba 4 giây, có một mức trần như 60 giây. Thêm một phần ngẫu nhiên (ví dụ lấy từ 50% đến 100% thời gian chờ) để nhiều tiến trình cùng bị chặn không cùng thử lại một lúc rồi lại cùng bị chặn.

Code thử lại đúng chỗ

Đoạn Python dưới đây có ba hàm chính. nen_thu_lai quyết định lỗi nào đáng thử lại, có xử lý riêng trường hợp hết hạn mức tháng. thoi_gian_cho ưu tiên retry-after, nếu không thì tính chờ lũy thừa có trần và ngẫu nhiên. goi_co_thu_lai bọc một lời gọi bất kỳ, thử lại tối đa một số lần và ghi nhật ký. Phần chạy thử dùng lỗi giả lập có cùng thuộc tính với lỗi của thư viện, nên không cần mạng.

"""Gọi API mô hình có thử lại: chỉ thử lại lỗi tạm thời, chờ theo retry-after hoặc chờ lũy thừa có ngẫu nhiên.

Lưu ý: thư viện anthropic đã tự thử lại 2 lần (max_retries). Code này để hiểu cơ chế, hoặc dùng cho việc chạy lô
dài cần nhật ký và giới hạn riêng; khi đó tắt thử lại của thư viện (max_retries=0) để không thử lại hai tầng.
"""
import os
import random
import sys
import time

THU_LAI_DUOC = {408, 409, 429, 500, 502, 503, 504, 529}


def nen_thu_lai(loi):
    ma = getattr(loi, "status_code", None)
    if ma is None:  # lỗi mạng, hết giờ: không có mã HTTP
        return type(loi).__name__ in ("APIConnectionError", "APITimeoutError")
    if ma == 429:
        # body có thể là CHUỖI thô: thư viện đặt body = response.text rồi mới thử json.loads, parse hỏng thì
        # giữ nguyên chuỗi (ví dụ 429 trả trang HTML của lớp proxy). Gọi .get trên chuỗi sẽ ném AttributeError
        # ngay trong khối except, làm sập cả vòng thử lại thay vì thử lại.
        than = getattr(loi, "body", None)
        chi_tiet = than.get("error", {}).get("details") or {} if isinstance(than, dict) else {}
        if chi_tiet.get("error_code") == "enforced_spend_limit_reached":
            return False  # hết hạn mức chi tiêu tháng: thử lại vô ích
    return ma in THU_LAI_DUOC


def thoi_gian_cho(lan, loi, co_so=1.0, tran=60.0, rng=random):
    phan_hoi = getattr(loi, "response", None)
    retry_after = phan_hoi.headers.get("retry-after") if phan_hoi is not None else None
    if retry_after:
        return float(retry_after)
    return min(tran, co_so * 2 ** lan) * (0.5 + rng.random() / 2)  # lũy thừa 2, thêm ngẫu nhiên để không dồn cục


def goi_co_thu_lai(ham, so_lan_toi_da=5, ngu=time.sleep, rng=random, nhat_ky=None):
    for lan in range(so_lan_toi_da):
        try:
            return ham()
        except Exception as loi:  # noqa: BLE001 - phân loại ngay bên dưới
            if not nen_thu_lai(loi) or lan == so_lan_toi_da - 1:
                raise
            cho = thoi_gian_cho(lan, loi, rng=rng)
            if nhat_ky is not None:
                nhat_ky.append(f"lần {lan + 1}: {type(loi).__name__} {getattr(loi, 'status_code', '')}, chờ {cho:.1f} giây")
            ngu(cho)


class LoiGia(Exception):
    """Lỗi giả lập có cùng thuộc tính với lỗi của thư viện (status_code, response.headers, body)."""

    def __init__(self, ma, retry_after=None, ma_loi=None):
        super().__init__(f"HTTP {ma}")
        self.status_code = ma
        self.response = type("R", (), {"headers": {"retry-after": retry_after} if retry_after else {}})()
        self.body = {"error": {"details": {"error_code": ma_loi}}} if ma_loi else {"error": {}}


def kich_ban(cac_ket_qua):
    con = list(cac_ket_qua)

    def ham():
        kq = con.pop(0)
        if isinstance(kq, Exception):
            raise kq
        return kq
    return ham


def chay_thu():
    rng, dong = random.Random(7), []
    for ten, kb in [("Quá tải rồi bị giới hạn tốc độ", [LoiGia(529), LoiGia(429, retry_after="3"), "trả lời"]),
                    ("Sai khóa API", [LoiGia(401)]),
                    ("Hết hạn mức chi tiêu tháng", [LoiGia(429, ma_loi="enforced_spend_limit_reached")])]:
        nk = []
        try:
            kq = goi_co_thu_lai(kich_ban(kb), ngu=lambda s: None, rng=rng, nhat_ky=nk)
            dong.append(f"{ten}: thành công ('{kq}') sau {len(nk)} lần thử lại")
        except LoiGia as e:
            dong.append(f"{ten}: dừng ngay, không thử lại ({e})")
        dong += [f"  {d}" for d in nk]
    print("\n".join(dong))
    return dong


def main():
    if not os.environ.get("ANTHROPIC_API_KEY"):
        sys.exit("Chưa có ANTHROPIC_API_KEY. Đặt biến môi trường trước khi chạy.")
    import anthropic
    client = anthropic.Anthropic(max_retries=0, timeout=60.0)  # tự thử lại ở tầng này
    nk = []
    try:
        # Haiku 5.5 suy nghĩ trước khi trả lời, và token suy nghĩ tính vào max_tokens dù không hiện ra, nên để rộng chỗ.
        # max_tokens chỉ là mức trần: bạn trả tiền cho số token thật sự sinh ra.
        tl = goi_co_thu_lai(lambda: client.messages.create(
            model="claude-haiku-5-5", max_tokens=16000,
            messages=[{"role": "user", "content": "Chào bạn"}]), so_lan_toi_da=3, nhat_ky=nk)
    except anthropic.APIStatusError as e:
        sys.exit(f"API báo lỗi {e.status_code}: {e.message}")
    except anthropic.APIConnectionError:
        sys.exit("Không kết nối được tới API sau 3 lần thử. " + "; ".join(nk))
    # Gọi thành công (mã 200) chưa chắc đã có câu trả lời trọn vẹn: xem lý do dừng trước khi in.
    if tl.stop_reason in ("max_tokens", "model_context_window_exceeded"):
        sys.exit(f"Câu trả lời bị cắt giữa chừng ({tl.stop_reason}).")
    if tl.stop_reason == "refusal":
        sys.exit("Mô hình từ chối trả lời.")
    chu = "".join(k.text for k in tl.content if k.type == "text")
    if not chu.strip():
        sys.exit("Mô hình không trả về chữ nào.")
    print(chu)


if __name__ == "__main__":
    main() if "--goi-that" in sys.argv else chay_thu()

Ba kịch bản cho thấy cách hoạt động. Kịch bản một: lần đầu gặp 529, chờ khoảng 0,7 giây (lũy thừa có ngẫu nhiên); lần hai gặp 429 có retry-after là 3, chờ đúng 3 giây; lần ba thành công. Kịch bản hai: khóa sai (401), dừng ngay. Kịch bản ba: 429 do hết hạn mức tháng, cũng dừng ngay.

Khi gọi thật, hàm main tạo client với số lần thử lại của thư viện bằng 0 rồi để hàm của bài lo việc thử lại, tránh tình trạng thư viện thử 3 lần bên trong mỗi lần bạn thử, nhân thành 9 lần. Gọi thành công cũng chưa chắc có câu trả lời trọn vẹn, nên main xem lý do dừng và báo rõ nếu câu trả lời bị cắt vì chạm max_tokens hay bị từ chối; max_tokens để rộng vì phần suy nghĩ của mô hình cũng tính vào đó. Trong phần lớn ứng dụng nhỏ, cách đơn giản hơn là giữ mặc định của thư viện, chỉ chỉnh số lần thử lại và thời gian chờ tối đa.

Hết giờ và yêu cầu dài

Câu trả lời dài có thể mất nhiều phút. Trang lỗi khuyên dùng chế độ truyền dần (streaming) hoặc xử lý theo lô cho yêu cầu dài, nhất là trên 10 phút, vì một số mạng cắt kết nối im lặng quá lâu. Thư viện cũng kiểm tra và cảnh báo khi một yêu cầu không truyền dần có thể vượt 10 phút. Với việc không cần kết quả ngay, như xử lý hàng nghìn tài liệu, xem bài xử lý hàng loạt bằng batch.

Đừng để người dùng chờ vô hạn

Thử lại là chuyện phía máy, nhưng người dùng đang nhìn màn hình. Với ứng dụng có người chờ, như chatbot, đặt một thời hạn tổng: nếu sau khoảng ba mươi giây vẫn chưa có trả lời, dừng thử lại và báo rõ "hệ thống đang bận, vui lòng thử lại sau", thay vì để vòng quay chạy mãi. Với việc chạy nền, như xử lý một danh sách dài, có thể kiên nhẫn hơn, nhưng nên lưu tiến độ sau mỗi mục để khi chương trình dừng giữa chừng, lần chạy sau tiếp tục từ chỗ dở thay vì làm lại từ đầu và tốn tiền gọi lại những mục đã xong. Hai kiểu ứng dụng này cần hai chính sách thử lại khác nhau, đừng dùng chung một cấu hình.

Ghi lại để dò lỗi

Mỗi phản hồi có một mã yêu cầu trong tiêu đề request-id, và cũng nằm trong nội dung lỗi. Ghi mã này vào nhật ký cùng thời gian, mô hình và mã lỗi. Khi cần hỏi hỗ trợ, gửi mã yêu cầu giúp tra nhanh. Theo dõi tỷ lệ lỗi theo thời gian: 429 tăng đều là dấu hiệu cần giảm tốc độ gửi, dùng cache, hoặc xin nâng hạng.

Cẩn thận
  • Thử lại mọi lỗi, kể cả 400 và 401.
  • Thử lại ngay không chờ, hoặc chờ cố định không có ngẫu nhiên.
  • Bỏ qua retry-after và thử sớm hơn.
  • Thử lại hàng giờ khi đã hết hạn mức chi tiêu tháng.
  • Bọc thêm một tầng thử lại trong khi thư viện vẫn đang tự thử lại.

Câu hỏi thường gặp

429 và 529 khác nhau thế nào?

429 là tổ chức của bạn vượt giới hạn hoặc hạn mức. 529 là API quá tải chung cho mọi người dùng. Cả hai đều chờ rồi thử lại, trừ 429 do hết hạn mức tháng.

Nên thử lại bao nhiêu lần?

Đủ để vượt qua trục trặc ngắn, thường 3 đến 5 lần. Nhiều hơn chỉ kéo dài thời gian người dùng chờ.

Làm sao tránh 429 ngay từ đầu?

Giới hạn số yêu cầu đồng thời phía bạn, gửi đều thay vì dồn, dùng prompt caching, và dùng xử lý theo lô cho việc không gấp.

Lỗi xảy ra giữa lúc truyền dần thì sao?

Tài liệu ghi rằng khi truyền dần, lỗi có thể đến sau khi đã nhận mã 200, dưới dạng sự kiện lỗi trong luồng. Cần xử lý riêng theo hướng dẫn về truyền dần.

Học tiếp

Nếu bạn muốn học Python và AI bài bản cùng giảng viên, tham khảo khóa học AI của Big-O Coding.

Muốn học bài bản cùng giảng viên?

Khóa AI – Introduction to Machine Learning: nền tảng Máy học từ thuật toán cơ bản đến Neural Networks và LLM, học bằng Python. Học thử buổi 1 miễn phí.

Xem Khóa AI

Nguồn tham khảo

Chia sẻ: Facebook LinkedIn
Nhắn Zalo tư vấnZalo Gọi 0937 401 483