Các lỗi của AI API khác với lỗi API thông thường. Phản hồi 200 không có nghĩa là lần sinh nội dung của bạn đã thành công. Trường content là null không phải lúc nào cũng là lỗi. Và cùng một prompt hoạt động hôm qua có thể thất bại hôm nay vì nhà cung cấp cập nhật chính sách nội dung.
Hướng dẫn này trình bày cách đọc lỗi AI API, ý nghĩa thực sự của từng chế độ lỗi, và cách xây dựng xử lý lỗi để cho bạn biết cái gì hỏng thay vì chỉ biết rằng có thứ gì đó hỏng.
Note: Tên model như gpt-5.4 và gpt-5.4-mini được dùng trong bài viết này là định danh nền tảng của CometAPI. Chúng chỉ hoạt động qua https://api.cometapi.com/v1 — không hoạt động trực tiếp với API của OpenAI hoặc Anthropic. Xem danh sách model đầy đủ.
Tại sao debug AI API khó hơn debug API thông thường
Với một REST API điển hình, mã 200 nghĩa là thành công và 4xx nghĩa là bạn làm sai. AI API thêm một loại thứ ba: các lỗi mềm — phản hồi trả về 200 nhưng không có nội dung sử dụng được.
Có ba điều có thể xảy ra lỗi:
- Lỗi cứng — Lỗi HTTP (4xx, 5xx). Yêu cầu không hoàn tất.
- Lỗi mềm — HTTP 200, nhưng
finish_reasonlàcontent_filterhoặclength, hoặccontentlànull. - Lỗi im lặng — HTTP 200, nội dung trông có vẻ ổn, nhưng đầu ra sai theo cách mà bạn chỉ phát hiện được ở tầng ứng dụng.
Hầu hết xử lý lỗi chỉ bao phủ trường hợp 1. Trường hợp 2 và 3 mới là nơi đa số lỗi sản xuất xuất hiện.
Hiểu định dạng phản hồi lỗi
Endpoint text completions trả về một cấu trúc lỗi nhất quán:
{ "error": { "message": "human-readable description (often includes request id)", "type": "comet_api_error", "param": "the_problematic_parameter_or_null", "code": "error_code_or_null" }}
Các endpoint hình ảnh và video trả về các định dạng lỗi khác — luôn phân tích phần thân phản hồi thô thay vì giả định một cấu trúc cố định giữa các endpoint.
Trường message thường cho bạn biết chính xác điều gì sai. Trường param cho biết tham số nào gây ra lỗi. Luôn ghi log cả hai.
Biết mỗi mã trạng thái HTTP có ý nghĩa gì
| Status | Ý nghĩa | Nguyên nhân phổ biến | Cách khắc phục |
|---|---|---|---|
| 400 | Yêu cầu sai | Thiếu model, tham số không phù hợp với model | Kiểm tra error.param trong phản hồi |
| 401 | Chưa xác thực | Sai hoặc thiếu API key | Xác minh định dạng Authorization: Bearer |
| 429 | Bị giới hạn tần suất | Quá nhiều yêu cầu | Backoff theo cấp số nhân (xem Bước 4) |
| 500 | Lỗi máy chủ | Vấn đề phía nhà cung cấp, hoặc body yêu cầu sai định dạng | Thử lại với backoff; kiểm tra định dạng yêu cầu |
| 504 | Gateway timeout | Nhà cung cấp mất quá nhiều thời gian | Thử lại; cân nhắc model nhanh hơn |
Nguồn**: Tài liệu chat completions của CometAPI
Sự khác biệt giữa 400 và 500 quan trọng đối với logic retry. 400 nghĩa là yêu cầu của bạn sai — thử lại cùng yêu cầu sẽ không giúp ích. 500 hoặc 504 nghĩa là máy chủ gặp vấn đề — retry là hợp lý.
Kiểm tra finish_reason — trường bị bỏ qua nhiều nhất
Phản hồi 200 với finish_reason: "content_filter" nghĩa là lần sinh nội dung của bạn bị chặn. Trường content sẽ là null hoặc rỗng. Nếu bạn không kiểm tra điều này, ứng dụng của bạn sẽ âm thầm trả về không có gì.
| finish_reason | Ý nghĩa | Cần làm gì | Cách khắc phục |
|---|---|---|---|
| stop | Hoàn tất bình thường | Không cần làm gì — đây là thành công | Kiểm tra error.param trong phản hồi |
| length | Chạm giới hạn token | Tăng max_tokens hoặc rút ngắn prompt | Xác minh định dạng Authorization: Bearer |
| content_filter | Bị chặn bởi chính sách an toàn | Diễn đạt lại prompt; tránh tên/chủ đề cụ thể | Backoff theo cấp số nhân (xem Bước 4) |
| tool_calls | Model gọi một công cụ thay vì trả văn bản | Xử lý lời gọi công cụ; content sẽ là null | Thử lại với backoff; kiểm tra định dạng yêu cầu |
| 504 | Gateway timeout | Nhà cung cấp mất quá nhiều thời gian | Thử lại; cân nhắc model nhanh hơn |
Nguồn**: Tài liệu chat completions của CometAPI
import osimport loggingfrom openai import OpenAI, APIStatusError, APIConnectionError, APITimeoutErrorfrom dotenv import load_dotenvload_dotenv()api_key = os.environ.get("COMETAPI_KEY")if not api_key: raise ValueError("COMETAPI_KEY is not set")client = OpenAI( base_url="https://api.cometapi.com/v1", api_key=api_key,)def safe_complete(messages: list, model: str = "gpt-5.4-mini", **kwargs) -> dict: """ Complete a chat request with full error and finish_reason handling. Returns {"content": str, "finish_reason": str, "tool_calls": list | None} Raises on API errors. """ try: response = client.chat.completions.create( model=model, messages=messages, **kwargs ) except APIStatusError as e: error_body = {} try: error_body = e.response.json().get("error", {}) except Exception: pass logging.error( f"API error status={e.status_code} " f"message={error_body.get('message')} " f"param={error_body.get('param')}" ) raise except (APIConnectionError, APITimeoutError) as e: logging.error(f"Network/timeout error: {e}") raise choice = response.choices[0] finish_reason = choice.finish_reason if finish_reason == "content_filter": raise ValueError( f"Generation blocked by content filter. " f"Model: {model}. Rephrase the prompt." ) if finish_reason == "length": used = response.usage.completion_tokens if response.usage else "unknown" logging.warning(f"Output truncated at token limit. Used {used} tokens.") # Return structured result so callers can handle tool_calls explicitly return { "content": choice.message.content or "", "finish_reason": finish_reason, "tool_calls": choice.message.tool_calls, }# Usageresult = safe_complete( messages=[{"role": "user", "content": "Summarize this article: [text]"}], model="gpt-5.4-mini")if result["finish_reason"] == "tool_calls": # Handle tool call — content will be empty print("Model wants to call a tool:", result["tool_calls"])else: print(result["content"])
Phát hiện lỗi im lặng ở tầng ứng dụng
Lỗi im lặng là khó bắt nhất. API trả về 200, finish_reason là stop, nhưng đầu ra sai về mặt ngữ nghĩa. Bạn chỉ có thể bắt chúng ở tầng ứng dụng.
Các mẫu thường gặp:
def validate_completion(result: dict, task: str) -> str: """ Application-layer validation for silent failures. Raises ValueError if the output doesn't meet basic expectations. """ content = result["content"].strip() # Empty output that isn't a tool call if not content and result["finish_reason"] != "tool_calls": raise ValueError(f"Empty output for task '{task}' with finish_reason='{result['finish_reason']}'") # Task-specific checks if task == "classify": valid_labels = {"positive", "negative", "neutral"} if content.lower() not in valid_labels: logging.warning( f"Unexpected classification output: '{content}'. " f"Expected one of {valid_labels}. " f"Model may have returned explanation instead of label." ) if task == "json_extract": import json try: json.loads(content) except json.JSONDecodeError: raise ValueError( f"Expected JSON output but got: '{content[:100]}...'. " f"Try adding 'respond with valid JSON only' to the prompt, " f"or use response_format={{\"type\": \"json_object\"}}." ) if task == "summarize" and len(content.split()) < 10: logging.warning( f"Suspiciously short summary ({len(content.split())} words). " f"Check if the input was too short or the model misunderstood the task." ) return content# Full flow with silent failure detectionresult = safe_complete( messages=[{"role": "user", "content": "Classify as positive/negative/neutral: 'Great product!'"}], model="claude-haiku-4-5")label = validate_completion(result, task="classify")
Các lỗi im lặng thường đến từ một trong ba nguồn: prompt mơ hồ, model bỏ qua hướng dẫn định dạng của bạn, hoặc đầu vào quá ngắn/dài cho tác vụ. Ghi log đầy đủ đầu ra khi xác thực thất bại là cách nhanh nhất để chẩn đoán đâu là nguyên nhân.
Thêm backoff theo cấp số nhân cho lỗi giới hạn tần suất
Lỗi giới hạn tần suất (429) là tạm thời. Cách đúng là chờ và retry với độ trễ tăng dần — một thực hành tiêu chuẩn cho bất kỳ API nào có giới hạn tần suất:
import timeimport randomfrom openai import RateLimitErrordef complete_with_retry( messages: list, model: str = "gpt-5.4-mini", max_retries: int = 3, **kwargs) -> dict: """Retry on rate limits and server errors with exponential backoff.""" last_error = None for attempt in range(max_retries): try: return safe_complete(messages, model=model, **kwargs) except APIStatusError as e: if e.status_code < 500: raise # 4xx: don't retry, request is wrong last_error = e except RateLimitError as e: last_error = e except (APIConnectionError, APITimeoutError) as e: last_error = e if attempt < max_retries - 1: wait = (2 ** attempt) + random.random() # jitter prevents thundering herd logging.warning(f"Attempt {attempt + 1} failed. Waiting {wait:.1f}s before retry.") time.sleep(wait) raise RuntimeError(f"All {max_retries} attempts failed") from last_error
Đừng retry với 400 hoặc 401 — đó là lỗi phía client sẽ không tự hết. Ngoại lệ là 401 nếu bạn đang xoay vòng API key.
Debug lỗi sinh ảnh
Sinh ảnh có các chế độ lỗi riêng bên cạnh lỗi HTTP tiêu chuẩn:
import base64import requestsdef generate_image_safe(prompt: str, model: str = "dall-e-3") -> dict: """ Generate an image with full error handling. Returns {"url": str | None, "bytes": bytes | None, "blocked": bool} """ api_key = os.environ.get("COMETAPI_KEY") if not api_key: raise ValueError("COMETAPI_KEY is not set") BASE64_MODELS = {"gpt-image-2", "qwen-image"} headers = { "Authorization": f"Bearer {api_key}", "Content-Type": "application/json" } payload = {"model": model, "prompt": prompt, "size": "1024x1024"} if model in BASE64_MODELS: payload["output_format"] = "png" else: payload["response_format"] = "url" try: response = requests.post( "https://api.cometapi.com/v1/images/generations", json=payload, headers=headers, timeout=60 ) response.raise_for_status() except requests.exceptions.HTTPError as e: logging.error(f"Image generation HTTP error: {e.response.status_code} {e.response.text}") raise except requests.exceptions.Timeout: logging.error("Image generation timed out after 60s") raise data = response.json().get("data", []) if not data: logging.warning("Image generation returned empty data — prompt may have been filtered.") return {"url": None, "bytes": None, "blocked": True} item = data[0] if "revised_prompt" in item: logging.info(f"Provider revised prompt to: {item['revised_prompt']}") if "url" in item: return {"url": item["url"], "bytes": None, "blocked": False} return { "url": None, "bytes": base64.b64decode(item["b64_json"]), "blocked": False }
Các vấn đề riêng của ảnh cần lưu ý:
| Triệu chứng | Nguyên nhân | Cách khắc phục |
|---|---|---|
| Mảng data rỗng | Prompt bị lọc | Kiểm tra revised_prompt; diễn đạt lại |
| Lỗi response_format trên GPT Image 2 | Tham số không được hỗ trợ | Dùng output_format thay thế |
| n > 1 lỗi trên Qwen Image | Giới hạn của model | Lặp lại yêu cầu thay vì dùng n > 1 |
| URL trả về 403 sau đó | URL hết hạn | Tải xuống ngay sau khi sinh |
Nguồn**: Tài liệu sinh ảnh của CometAPI
Debug lỗi sinh video
Sinh video lỗi khác vì là bất đồng bộ. Khởi tạo các biến trạng thái trước vòng lặp để thông báo lỗi timeout luôn đầy đủ:
def submit_and_poll_video( prompt: str, model: str = "veo3-fast", max_wait: int = 600) -> str: """Submit video task and poll to completion. Returns video URL.""" api_key = os.environ.get("COMETAPI_KEY") if not api_key: raise ValueError("COMETAPI_KEY is not set") headers = {"Authorization": f"Bearer {api_key}"} try: response = requests.post( "https://api.cometapi.com/v1/videos", headers=headers, files={ "prompt": (None, prompt), "model": (None, model), "size": (None, "16x9") }, timeout=30 ) response.raise_for_status() except requests.exceptions.HTTPError as e: logging.error(f"Video submit failed: {e.response.status_code} {e.response.text}") raise task_id = response.json()["id"] logging.info(f"Video task submitted: {task_id}") poll_url = f"https://api.cometapi.com/v1/videos/{task_id}" elapsed = 0 interval = 10 status = "unknown" # initialize before loop progress = 0 # initialize before loop while elapsed < max_wait: try: poll_response = requests.get(poll_url, headers=headers, timeout=30) poll_response.raise_for_status() except requests.exceptions.HTTPError as e: logging.error(f"Poll request failed: {e.response.status_code}") raise result = poll_response.json() status = result.get("status", "unknown") progress = result.get("progress", 0) logging.info(f"Task {task_id}: status={status} progress={progress}%") if status == "succeeded": return result["output"][0] elif status in ("failed", "cancelled"): error_detail = result.get("error", "no error detail returned") raise RuntimeError(f"Video task {task_id} failed: {error_detail}") time.sleep(interval) elapsed += interval raise TimeoutError( f"Video task {task_id} did not complete within {max_wait}s. " f"Last status: {status}, progress: {progress}%" )
Các vấn đề riêng của video:
| Triệu chứng | Nguyên nhân | Cách khắc phục |
|---|---|---|
| Tác vụ kẹt ở trạng thái queued > 10 phút | Tải máy chủ cao | Thử lại với model khác |
| failed không có chi tiết lỗi | Prompt bị lọc hoặc lỗi model | Diễn đạt lại prompt |
| URL video trả về 403 | URL hết hạn | Tải xuống ngay |
| task_not_exist ở lần poll đầu của Runway | Tác vụ vẫn đang khởi tạo (hành vi được CometAPI ghi nhận) | Chờ 5s và thử lại |
| Kling trả về "succeed" không phải "succeeded" | API của Kling dùng chuỗi trạng thái không chuẩn | Xử lý cả hai trong logic poll |
Nguồn**: Tài liệu sinh video của CometAPI**, Tài liệu Kling Video
Phiên bản Node.js
import OpenAI from 'openai';const apiKey = process.env.COMETAPI_KEY;if (!apiKey) throw new Error('COMETAPI_KEY is not set');const client = new OpenAI({ baseURL: 'https://api.cometapi.com/v1', apiKey,});async function safeComplete(messages, model = 'gpt-5.4-mini', options = {}) { let response; try { response = await client.chat.completions.create({ model, messages, ...options }); } catch (err) { if (err.status && err.status < 500) { console.error(`Client error ${err.status}: ${err.message}`); } else { console.error(`Server/network error: ${err.message}`); } throw err; } const choice = response.choices[0]; const finishReason = choice.finish_reason; if (finishReason === 'content_filter') { throw new Error(`Generation blocked by content filter. Model: ${model}`); } if (finishReason === 'length') { console.warn(`Output truncated. Used ${response.usage?.completion_tokens ?? 'unknown'} tokens.`); } return { content: choice.message.content ?? '', finishReason, toolCalls: choice.message.tool_calls ?? null, };}async function completeWithRetry(messages, model = 'gpt-5.4-mini', maxRetries = 3) { let lastError; for (let attempt = 0; attempt < maxRetries; attempt++) { try { return await safeComplete(messages, model); } catch (err) { // Don't retry 4xx client errors if (err.status && err.status < 500) throw err; lastError = err; if (attempt < maxRetries - 1) { const wait = (2 ** attempt + Math.random()) * 1000; console.warn(`Attempt ${attempt + 1} failed. Retrying in ${(wait / 1000).toFixed(1)}s`); await new Promise(r => setTimeout(r, wait)); } } } throw new Error(`All ${maxRetries} attempts failed: ${lastError?.message}`);}// Usageconst result = await safeComplete( [{ role: 'user', content: 'Classify as positive/negative/neutral: "Great product!"' }], 'claude-haiku-4-5');if (result.finishReason === 'tool_calls') { console.log('Tool call requested:', result.toolCalls);} else { console.log(result.content);}
Danh sách kiểm tra debug
Khi một lần sinh thất bại và bạn không chắc bắt đầu từ đâu:
Đối với sinh văn bản:
- API key đã được đặt và theo định dạng
Authorization: Bearer <key>chưa? finish_reasoncó khácstopkhông?contentcó null không? Kiểm tra xemfinish_reasoncó phảitool_callskhông- Đầu ra có bị cắt ngắn không? Kiểm tra
finish_reason: "length"vàusage.completion_tokens - Lỗi là 4xx (sửa yêu cầu) hay 5xx (retry)?
- Đầu ra có vượt qua xác thực ở tầng ứng dụng của bạn không? (lỗi im lặng)
Đối với sinh ảnh:
- Mảng
datacó rỗng không? (bộ lọc nội dung) - Bạn có dùng
response_formattrên GPT Image 2 không? (không được hỗ trợ — dùngoutput_format) - Bạn có đặt
n > 1trên Qwen Image không? (không được hỗ trợ) - Bạn có tải ảnh xuống trước khi URL hết hạn không?
Đối với sinh video:
- Tác vụ có bị kẹt ở
queuedkhông? (thử model khác) - Bạn có kiểm tra trường
errortrong phản hồi tác vụ thất bại không? - Bạn có tải video xuống trước khi URL hết hạn không?
- Bạn có xử lý cả
"succeed"(Kling) và"succeeded"(Veo, Runway) không?
FAQ
Q: Yêu cầu của tôi trả về 200 nhưng không có nội dung. Chuyện gì xảy ra?
Kiểm tra finish_reason. Nếu là content_filter, lần sinh nội dung bị chặn — yêu cầu thành công nhưng đầu ra bị ẩn. Nếu là tool_calls, model đã gọi một công cụ thay vì trả văn bản, và content là null theo thiết kế. Nếu finish_reason là stop nhưng nội dung vẫn rỗng, đó là lỗi im lặng — ghi log toàn bộ phản hồi và kiểm tra prompt của bạn.
Q: Làm sao biết prompt của tôi đang bị lọc?
Đối với văn bản: kiểm tra finish_reason === "content_filter". Đối với ảnh: kiểm tra xem mảng data có rỗng không. Đối với video: kiểm tra xem tác vụ chuyển sang trạng thái failed ngay sau khi gửi mà không có chi tiết lỗi. Trong mọi trường hợp, thử diễn đạt prompt trung tính hơn.
Q: Khi nào tôi nên retry một yêu cầu thất bại?
Retry với 429 và 5xx bằng backoff theo cấp số nhân. Đừng retry với 4xx — một yêu cầu sai sẽ không tự sửa. Ngoại lệ là 401 nếu bạn đang xoay vòng API key.
Q: Backoff theo cấp số nhân là gì và tại sao quan trọng?
Thay vì retry ngay lập tức, bạn chờ lâu dần: 1s, 2s, 4s. Thêm jitter ngẫu nhiên (+ random.random()) để ngăn nhiều client retry cùng lúc. Đây là thực hành tiêu chuẩn cho bất kỳ API nào có giới hạn tần suất — không riêng CometAPI.
Q: Tác vụ video bị kẹt ở queued trong 10 phút. Có phải đã thất bại?
Không nhất thiết — hàng đợi có thể bị dồn khi tải cao. Chờ đến ngưỡng max_wait của bạn, sau đó ném TimeoutError và thử lại với model khác. Ghi log ID tác vụ để bạn có thể kiểm tra trạng thái thủ công nếu cần.
Q: Làm sao bắt lỗi im lặng?
Lỗi im lặng cần xác thực ở tầng ứng dụng — API sẽ không nói cho bạn biết đầu ra sai về mặt ngữ nghĩa. Kiểm tra đầu ra có khớp định dạng mong đợi không (JSON hợp lệ, nhãn kỳ vọng, độ dài tối thiểu). Ghi log đầy đủ đầu ra khi xác thực thất bại. Nguyên nhân phổ biến nhất là prompt mơ hồ, hướng dẫn định dạng bị bỏ qua, hoặc đầu vào quá ngắn hoặc quá dài cho tác vụ.
