Giới hạn tần suất Phản áp lực Độ tin cậy API LLM

Giới hạn tần suất API LLM và phản áp lực vững vàng

Cách đọc header giới hạn tần suất của LLM, hấp thụ lỗi 429 bằng backoff và jitter, và áp phản áp lực phía client qua hàng đợi có giới hạn cùng trần đồng thời để tải giảm êm ái.

AveMujica API 12 phút đọc

Khi một nhà cung cấp LLM giới hạn tần suất của bạn, nó trả về HTTP 429, và phản ứng ngây thơ — thử lại ngay lập tức — chỉ làm mọi thứ tệ hơn, bởi các yêu cầu thất bại vẫn được tính vào hạn mức mỗi phút của bạn. Cách khắc phục bền vững là hai lớp phối hợp với nhau: đọc các header giới hạn tần suất mà nhà cung cấp gửi để biết còn lại bao nhiêu ngân sách, và áp phản áp lực ở phía bạn — hàng đợi có giới hạn, trần đồng thời, và backoff kèm jitter — để khi cầu vượt cung, hệ thống của bạn chậm lại thay vì tan chảy. Bài viết này bao trùm cả hai lớp và mã để triển khai chúng.

Giới hạn tần suất không phải là một trạng thái lỗi cần che đậy; chúng là một tín hiệu về sức chứa. Một hệ thống coi chúng là tín hiệu sẽ suy giảm theo cách dự đoán được. Một hệ thống phớt lờ chúng sẽ dao động giữa việc dồn dập nhà cung cấp và việc vứt bỏ yêu cầu của người dùng.

Đọc header trước khi tinh chỉnh bất cứ điều gì

OpenAI trả về trạng thái giới hạn tần suất trên mọi phản hồi qua các header mà bạn có thể kiểm tra trực tiếp. Bộ dựa trên số yêu cầu là:

  • x-ratelimit-limit-requests — trần số yêu cầu trong cửa sổ
  • x-ratelimit-remaining-requests — bạn còn lại bao nhiêu
  • x-ratelimit-reset-requests — thời gian đến khi cửa sổ yêu cầu được đặt lại

Có một bộ song song dựa trên token — x-ratelimit-limit-tokens, x-ratelimit-remaining-tokens, x-ratelimit-reset-tokens — và các biến thể theo phạm vi dự án như x-ratelimit-remaining-project-tokens. Hầu hết các giới hạn thực tế cắn vào token, chứ không phải số yêu cầu, bởi một lệnh gọi ngữ cảnh dài đơn lẻ có thể ngốn một lát lớn ngân sách token của bạn. Ghi log các header này trên mọi phản hồi và bạn sẽ thấy hiện tượng bóp băng thông đến trước khi nó trở thành một bức tường 429:

resp = client.chat.completions.with_raw_response.create(
    model="gpt-5.6",
    messages=[{"role": "user", "content": prompt}],
)
h = resp.headers
print(h.get("x-ratelimit-remaining-requests"), h.get("x-ratelimit-remaining-tokens"))

Ghi lại các header này là nền tảng vận hành: khi lưu x-ratelimit-remaining-requests và x-ratelimit-remaining-tokens cho từng phản hồi, bạn có thể thấy upstream nào đang tiến gần trần trước khi lỗi 429 xuất hiện. Gateway là nơi thích hợp để tập trung việc ghi log vì mọi phản hồi đều đi qua đó — nhưng đường cong ngân sách còn lại theo từng kênh là thứ bạn xây dựng từ các header thô, không phải số liệu đã được phản hồi tính sẵn.

Thử lại 429 với backoff theo cấp số nhân và jitter

Khi một 429 thực sự đến, hãy lùi lại trước khi thử lại. Backoff theo cấp số nhân nhân thời gian chờ lên sau mỗi lần thất bại; jitter ngẫu nhiên hóa nó để cả một đội client không cùng thử lại ở một nhịp và va chạm lại — một hiệu ứng bầy đàn (thundering herd) tái tạo chính sự quá tải mà bạn đang phục hồi khỏi.

import random, time

def backoff_sleep(attempt, base=0.5, cap=20.0):
    # full jitter: random point in [0, min(cap, base * 2**attempt))
    ceiling = min(cap, base * (2 ** attempt))
    time.sleep(random.uniform(0, ceiling))

for attempt in range(6):
    r = call_model(prompt)
    if r.status_code != 429:
        break
    backoff_sleep(attempt)

Hai quy tắc giữ cho việc này trung thực. Thứ nhất, giới hạn cả độ trễ lẫn số lần thử — thử lại vô hạn chỉ đẩy sự cố xuống hạ nguồn. Thứ hai, chỉ thử lại những gì có thể thử lại: 429 và các 5xx/timeout tạm thời đáng để thử lại; còn 400 cho một yêu cầu sai định dạng sẽ thất bại y hệt mỗi lần. Vì một thao tác ghi được thử lại có thể nhân đôi công việc, hãy ghép việc thử lại với tính lũy đẳng — phần xử lý đầy đủ nằm trong thử lại và tính lũy đẳng cho LLM API.

Nếu một phản hồi có kèm header Retry-After, hãy ưu tiên nó hơn backoff bạn tự tính — máy chủ đang nói cho bạn biết chính xác khi nào cửa sổ mở lại, và tôn trọng giá trị đó vừa phục hồi nhanh hơn vừa tránh lãng phí những lần thử chắc chắn thất bại. Hãy xem lịch trình theo cấp số nhân của bạn là phương án dự phòng cho khi máy chủ không đưa ra gợi ý rõ ràng.

Phản áp lực: làm chậm nguồn, đừng buông sàn

Thử lại xử lý một lệnh gọi thất bại đơn lẻ. Phản áp lực xử lý tình huống khi tốc độ tiếp nhận của bạn đơn giản là vượt quá mức nhà cung cấp chấp nhận. Ý tưởng cốt lõi từ lý thuyết hàng đợi: đặt một bộ đệm có giới hạn giữa các nhà sản xuất và nhà cung cấp, và khi bộ đệm đầy, hãy bắt nhà sản xuất chờ thay vì nhận công việc vô hạn mà bạn không thể phục vụ.

Ba cơ chế kết hợp thành một chiến lược phản áp lực:

Cơ chếNó giới hạn điều gìChế độ hỏng nó ngăn chặn
Giới hạn đồng thời (semaphore)Yêu cầu đang bayMở hàng nghìn socket mà tất cả đều 429
Hàng đợi có giới hạnCông việc đang chờ khởi độngBộ nhớ tăng vô hạn dưới một đợt tăng vọt
Kiểm soát tiếp nhận / giảm tảiCông việc mới được chấp nhậnChấp nhận yêu cầu bạn không bao giờ kịp phục vụ

Giới hạn đồng thời là thay đổi có đòn bẩy cao nhất và dễ thêm nhất. Giới hạn các lệnh gọi đang bay đồng thời để khớp, chứ không vượt, mức chịu đựng của nhà cung cấp:

import asyncio

sem = asyncio.Semaphore(20)  # never more than 20 concurrent upstream calls

async def guarded_call(prompt):
    async with sem:
        return await call_model_async(prompt)

Khi semaphore đầy, các bên gọi mới sẽ chờ — sự chờ đợi đó chính là phản áp lực. Nó truyền ràng buộc ngược về nguồn thay vì để nó bùng nổ tại ranh giới nhà cung cấp.

Giảm tải một cách có chủ đích khi hàng đợi đầy

Một hàng đợi có giới hạn rốt cuộc sẽ đầy trong một đợt tăng vọt kéo dài. Điều gì xảy ra khi đó là một quyết định sản phẩm, và làm cho nó tường minh chính là toàn bộ trọng điểm:

  • Từ chối nhanh — trả về 503 kèm Retry-After để bên gọi thử lại sau. Tốt nhất cho lưu lượng tương tác, nơi một câu trả lời chậm còn tệ hơn một lời "hãy thử lại" rõ ràng.
  • Giảm tải theo ưu tiên — bỏ hoặc hoãn công việc ưu tiên thấp (theo lô, nền) để yêu cầu tương tác vẫn được thông suốt.
  • Hạ cấp — định tuyến phần tràn sang một mô hình rẻ hơn, nhanh hơn, nhiều khả năng còn dư địa hơn.

Phản mẫu là một hàng đợi vô hạn nhận mọi thứ và lặng lẽ thổi phồng độ trễ cho đến khi các yêu cầu dù sao cũng timeout. Điều đó biến một sự từ chối nhìn thấy được, phục hồi được thành một sự đình trệ vô hình trên toàn hệ thống. Hãy giới hạn hàng đợi, và quyết định "rơi khỏi đuôi" nghĩa là gì.

Nơi một gateway tập trung hóa chính sách

Phản áp lực được triển khai riêng trong từng dịch vụ sẽ dần lệch nhau — mỗi nơi chọn một giới hạn khác và chúng tương tác kém. Gateway là nơi tự nhiên để thực thi một chính sách thống nhất vì mọi lưu lượng đều đã đi qua đó. Trong dự án này, kết nối nhà cung cấp được đăng ký thành các kênh, mỗi kênh có hạn mức và độ ưu tiên riêng; gateway cũng đã chuyển dự phòng sang kênh khỏe mạnh khi một kênh bắt đầu bị giới hạn (chiến lược chuyển dự phòng cho LLM API) và ghi lại mức sử dụng của từng yêu cầu để bạn tinh chỉnh. Các cơ chế tiếp nhận nâng cao hơn là kiến trúc bạn xây thêm tại chính điểm kiểm soát này:

  • theo dõi ngân sách token và số yêu cầu còn lại theo từng kênh bằng cách lưu các header phản hồi,
  • áp giới hạn đồng thời theo từng kênh để một kênh nóng không làm các kênh khác thiếu tài nguyên,
  • duy trì một ngân sách thử lại tổng hợp để lưu lượng phục hồi không khuếch đại sự cố.

Hãy coi ba mục này là thiết kế cần triển khai tại gateway, không phải các công tắc có sẵn theo mặc định; giá trị của gateway là bạn chỉ có một nơi để bổ sung chúng. Việc tập trung hóa quan trọng vì một lý do cụ thể: nếu mười dịch vụ đều tự thử lại khi gặp 429, tổng lưu lượng thử lại có thể tự vượt giới hạn, và chỉ một ngân sách thử lại dùng chung mới ngăn được sự khuếch đại do chính hệ thống gây ra.

Một tuyến phòng thủ nhiều lớp, theo thứ tự tác động

Áp dụng chúng theo thứ tự; mỗi cái mua thời gian cho cái kế tiếp:

  1. Đo lường — ghi log các header giới hạn tần suất để hiện tượng bóp băng thông hiện rõ sớm.
  2. Giới hạn đồng thời — một semaphore khớp với mức chịu đựng của nhà cung cấp.
  3. Giới hạn hàng đợi — một bộ đệm cố định, không bao giờ vô hạn.
  4. Backoff kèm jitter — với 429/5xx, không bao giờ với lỗi client 4xx.
  5. Giảm tải hoặc hạ cấp — một chính sách tường minh cho hàng đợi đầy.
  6. Phân tán tải — nhiều kênh và chuyển đổi dự phòng để nâng trần hiệu dụng.

Hai thay đổi đầu tiên thường loại bỏ phần lớn nỗi đau 429. Phần còn lại biến những đợt tăng vọt còn sót thành sự chậm lại êm ái thay vì gián đoạn.

Lập ngân sách cho token, không chỉ cho yêu cầu

Vì giới hạn token thường chạm trần trước giới hạn yêu cầu, hãy ước lượng chi phí token của một lệnh gọi trước khi bạn gửi và dành ngân sách tương ứng. Một yêu cầu tiêu thụ cả token đầu vào (prompt của bạn, cộng bất kỳ lịch sử được nối vào) lẫn token đầu ra (bị giới hạn bởi max_tokens/max_output_tokens). Nếu bạn tiếp nhận công việc chỉ dựa trên số yêu cầu, một loạt các lệnh gọi ngữ cảnh dài có thể phá vỡ trần token trong khi bộ đếm yêu cầu của bạn trông vẫn khỏe mạnh — và mỗi lệnh gọi đó đều quay về với 429.

Một quy tắc tiếp nhận thực dụng là một cổng nhận biết token: theo dõi ngân sách token còn lại từ x-ratelimit-remaining-tokens, ước lượng số token trường hợp xấu nhất của mỗi lệnh gọi đang chờ, và chỉ tiếp nhận những lệnh gọi mà ước lượng vừa với dư địa còn lại. Đây là ý tưởng token bucket áp dụng cho lưu lượng LLM — nạp lại tại khoảng đặt lại, tiêu khi tiếp nhận, và bắt bên gọi chờ khi thùng cạn.

  • Giới hạn max_tokens/max_output_tokens xuống kích cỡ bạn thực sự kỳ vọng; một trần quá lớn dành riêng ngân sách bạn không bao giờ dùng và bỏ đói các lệnh gọi khác.
  • Tính cả lịch sử được nối vào: với trạng thái hội thoại được lưu, các token đầu vào trước đó vẫn tính cước như token đầu vào, nên một luồng dài tốn nhiều hơn mỗi lượt so với vẻ ngoài.
  • Ưu tiên nhiều lệnh gọi nhỏ hơn một lệnh gọi khổng lồ khi một yêu cầu đơn lẻ sẽ dành riêng một lát lớn của cửa sổ token cùng một lúc.

Định đúng kích cỡ ước lượng token thường là đòn bẩy lớn hơn tinh chỉnh mức đồng thời, bởi nó ngăn những lệnh gọi sẽ 429 khỏi việc được tiếp nhận ngay từ đầu. Chi phí và sức chứa ở đây là cùng một núm vặn, đó là lý do việc này kết nối với giảm chi phí API LLM.

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

Mã trạng thái nào nghĩa là tôi đang bị giới hạn tần suất, và tôi có nên thử lại nó không?

HTTP 429. Nó có thể thử lại, nhưng chỉ với backoff và jitter, không bao giờ ngay lập tức — các yêu cầu thất bại vẫn được tính vào hạn mức mỗi phút của bạn, nên các vòng lặp thử lại dồn dập làm sâu thêm hiện tượng bóp băng thông. Hãy giới hạn số lần thử và độ trễ.

Giới hạn tôi thực sự chạm phải là giới hạn yêu cầu hay giới hạn token?

Thường là giới hạn token. Kiểm tra x-ratelimit-remaining-tokens bên cạnh x-ratelimit-remaining-requests. Một khối lượng công việc với prompt dài hoặc max_tokens lớn có thể vắt cạn ngân sách token trong khi hầu như không đụng đến bộ đếm yêu cầu, nên lập ngân sách chỉ dựa trên số yêu cầu sẽ đánh lừa bạn.

Nâng giới hạn đồng thời có tăng thông lượng không?

Chỉ đến mức chịu đựng của nhà cung cấp. Vượt quá điểm đó, mức đồng thời thêm vào tạo ra nhiều 429 hơn, nhiều lần thử lại hơn, và thông lượng hữu ích thấp hơn. Hãy tinh chỉnh semaphore đến giá trị lớn nhất mà tỷ lệ 429 vẫn gần bằng không, rồi dừng lại.

Một hàng đợi lớn hơn có phải là cách chữa cho quá tải kéo dài không?

Không. Một hàng đợi lớn hơn chỉ trì hoãn thời khắc phán xét và thổi phồng độ trễ đuôi trong khi nó đầy dần. Nếu tiếp nhận vượt sức chứa trong một khoảng kéo dài, không bộ đệm nào cứu được bạn — bạn cần giảm tải, một giới hạn tần suất cao hơn, hoặc nhiều kênh hơn. Hàng đợi hấp thụ các đợt bùng, chứ không phải các xu hướng.

Bắt đầu bằng một semaphore và việc ghi header

Thêm một semaphore đồng thời quanh các lệnh gọi mô hình và bắt đầu ghi x-ratelimit-remaining-tokens trên mọi phản hồi. Vẽ các giá trị đã ghi cùng số lỗi 429 trong một ngày: nếu các đợt sụt ngân sách token còn lại trùng với lỗi, hãy hạ giới hạn semaphore cho đến khi chúng dừng lại. Chỉ vòng phản hồi đó đã loại bỏ phần lớn sự cố giới hạn tần suất trước khi bạn cần xây dựng thứ gì phức tạp hơn.

Tham khảo