Tác giả: Kỹ sư tích hợp tại HolySheep AI — người đã "cháy" ticket production vì một con Agent gọi MCP cứ nổ timeout mỗi đêm.
Kịch bản thực tế: 3 giờ sáng, alert kêu cứu
Lúc đó tôi đang ngủ say, Slack rung liên hồi. Mở console lên thì thấy chuỗi log dài ngoằng của một Agent MCP đang crawl dữ liệu cho khách hàng:
2026-03-15T03:12:44.221Z ERROR step_3_fetcher ConnectionError:
HTTPSConnectionPool(host='mcp.vendor.io', port=443): Max retries exceeded
with url: /tools/invoke (Caused by ConnectTimeoutError(... timeout=10s))
2026-03-15T03:12:44.225Z WARN retry_attempt=1 backoff=2s
2026-03-15T03:12:46.231Z ERROR step_3_fetcher ConnectionError: ... timeout
2026-03-15T03:12:46.234Z WARN retry_attempt=2 backoff=4s
2026-03-15T03:12:50.241Z ERROR step_3_fetcher 429 Too Many Requests
2026-03-15T03:12:50.243Z FATAL pipeline_aborted steps_completed=2/5
Vấn đề không phải là "retry" — Agent của tôi đã retry đúng. Vấn đề là: retry sai cách, không có model routing, và cứng nhắc dùng một model duy nhất. Kết quả: 2 bước đầu tiên đã tiêu tốn 4.200 token với Claude Sonnet 4.5 (rất đắt) trước khi sập ở bước 3. Tổng thiệt hại đêm đó: $11.40 cho một pipeline lẽ ra chỉ tốn $0.60.
Bài viết này là hướng dẫn tôi rút ra sau đêm đó — và từ đó đến nay đã cứu cho khách hàng của tôi hơn $28,000/năm chi phí LLM.
Tại sao Multi-step Agent cần Retry Logic riêng?
Multi-step Agent không giống một cuộc gọi chat.completions() đơn lẻ. Bạn có:
- Nhiều tool call nối tiếp (MCP server, function calling, RAG retrieval)
- Mỗi tool call có thể timeout, rate-limit, hoặc trả về schema không hợp lệ
- Một lỗi ở bước 3 có thể lãng phí token của bước 1 và 2 nếu pipeline abort
- Một số bước cần model mạnh (reasoning), số khác chỉ cần model rẻ (extraction)
Giải pháp: Exponential Backoff với Jitter + Model Routing theo từng bước + Circuit Breaker để không retry một endpoint đã chết.
Triển khai Exponential Backoff có Jitter
Thư viện tenacity xử lý hầu hết logic khó cho bạn. Đây là adapter tôi dùng cho mọi MCP tool call:
from tenacity import (
retry, stop_after_attempt, wait_exponential_jitter,
retry_if_exception_type, before_sleep_log
)
import logging, requests
from requests.exceptions import ConnectionError, Timeout, HTTPError
logger = logging.getLogger("mcp_agent")
class MCPTransientError(Exception):
"""Lỗi mạng / 5xx / 429 — đáng để retry."""
pass
def call_mcp_tool(url: str, payload: dict, timeout: int = 10):
try:
resp = requests.post(url, json=payload, timeout=timeout)
if resp.status_code in (429, 500, 502, 503, 504):
raise MCPTransientError(f"HTTP {resp.status_code}: {resp.text[:120]}")
resp.raise_for_status()
return resp.json()
except (ConnectionError, Timeout) as e:
raise MCPTransientError(str(e)) from e
@retry(
wait=wait_exponential_jitter(initial=1, max=30, jitter=2),
stop=stop_after_attempt(5),
retry=retry_if_exception_type(MCPTransientError),
before_sleep=before_sleep_log(logger, logging.WARNING),
reraise=True,
)
def call_mcp_tool_resilient(url: str, payload: dict, timeout: int = 10):
return