Năm ngoái, team tôi deploy một hệ thống multi-agent để tự động hóa pipeline phân tích dữ liệu khách hàng. Ba tuần sau go-live, hệ thống sụp hoàn toàn lúc 2 giờ sáng. Nguyên nhân? Một agent timeout, agent tiếp theo retry, tạo ra một chuỗi domino mà chúng tôi không lường trước. Đó là bài học đắt giá nhất trong 15 năm làm kỹ sư của tôi.

Bài này tôi sẽ chia sẻ những gì thực sự xảy ra khi multi-agent systems gặp production — không phải lý thuyết trong demo, mà là những sự cố đã xảy ra, và cách chúng tôi fix.

Tại Sao Hệ Thống Multi-Agent Thất Bại

1. Retry Storms

Đây là killer phổ biến nhất. Khi một LLM call thất bại, mọi người đều nghĩ đến retry — hợp lý. Nhưng khi bạn có 5 agent cùng retry đồng thời, mỗi agent gọi ra ngoài 3-5 lần, bạn tạo ra tải gấp 15-25 lần bình thường. OpenAI rate limit bắt đầu fire, càng làm retry nhiều hơn. Vòng xoáy tử thần.

Tôi đã thấy hệ thống từ 10 req/s leo lên 200 req/s trong vòng 30 giây chỉ vì một upstream timeout nhỏ.

2. Context Loss Giữa Các Agent

Multi-agent architecture thường pass context qua shared memory hoặc message queue. Vấn đề: khi một agent fail giữa chừng, context đã mutate một nửa. Agent tiếp theo nhận state không nhất quán và đưa ra quyết định sai — mà không có error log nào cảnh báo bạn.

Tôi gọi đây là “silent corruption” — nguy hiểm hơn crash vì khó phát hiện.

3. Cascading Timeouts

Trong distributed system, timeout là nguyên tắc cơ bản. Nhưng nhiều team khi xây agent system lại set timeout quá dài (vì LLM vốn chậm), hoặc quên set timeout cho inter-agent communication. Kết quả: một agent treo, kéo theo thread pool của orchestrator, cả pipeline freeze.

Các Pattern Hiệu Quả

Circuit Breaker cho Agent Calls

Circuit breaker là pattern kinh điển từ microservices, nhưng ít người áp dụng vào agent calls. Ý tưởng đơn giản: đếm failure, nếu vượt ngưỡng thì “mở mạch” — không gọi nữa, trả về fallback ngay.

Dưới đây là implementation Python tôi đang dùng:

import time
from enum import Enum
from functools import wraps

class CircuitState(Enum):
    CLOSED = "closed"       # Hoạt động bình thường
    OPEN = "open"           # Đang fail, không gọi
    HALF_OPEN = "half_open" # Thử lại một lần

class AgentCircuitBreaker:
    def __init__(self, failure_threshold=5, recovery_timeout=60, half_open_max=1):
        self.failure_threshold = failure_threshold
        self.recovery_timeout = recovery_timeout
        self.half_open_max = half_open_max
        self.failure_count = 0
        self.last_failure_time = None
        self.state = CircuitState.CLOSED
        self.half_open_calls = 0

    def call(self, func, *args, **kwargs):
        if self.state == CircuitState.OPEN:
            if time.time() - self.last_failure_time > self.recovery_timeout:
                self.state = CircuitState.HALF_OPEN
                self.half_open_calls = 0
            else:
                raise Exception("Circuit OPEN — agent call blocked")

        if self.state == CircuitState.HALF_OPEN:
            if self.half_open_calls >= self.half_open_max:
                raise Exception("Circuit HALF_OPEN — max probe calls reached")
            self.half_open_calls += 1

        try:
            result = func(*args, **kwargs)
            self._on_success()
            return result
        except Exception as e:
            self._on_failure()
            raise e

    def _on_success(self):
        self.failure_count = 0
        self.state = CircuitState.CLOSED

    def _on_failure(self):
        self.failure_count += 1
        self.last_failure_time = time.time()
        if self.failure_count >= self.failure_threshold:
            self.state = CircuitState.OPEN


# Usage
breaker = AgentCircuitBreaker(failure_threshold=3, recovery_timeout=30)

def call_summarizer_agent(text: str) -> str:
    return breaker.call(_invoke_llm, prompt=f"Summarize: {text}")

Với C#, bạn có thể dùng Polly — library battle-tested cho resilience patterns:

using Polly;
using Polly.CircuitBreaker;

var circuitBreaker = Policy
    .Handle<HttpRequestException>()
    .Or<TimeoutException>()
    .CircuitBreakerAsync(
        exceptionsAllowedBeforeBreaking: 3,
        durationOfBreak: TimeSpan.FromSeconds(30),
        onBreak: (ex, duration) =>
            logger.LogWarning("Agent circuit OPEN for {Duration}s: {Error}", duration.TotalSeconds, ex.Message),
        onReset: () =>
            logger.LogInformation("Agent circuit CLOSED — resuming calls")
    );

var retryPolicy = Policy
    .Handle<HttpRequestException>()
    .WaitAndRetryAsync(
        retryCount: 3,
        sleepDurationProvider: attempt => TimeSpan.FromSeconds(Math.Pow(2, attempt)),
        onRetry: (ex, delay, attempt, _) =>
            logger.LogWarning("Retry {Attempt} after {Delay}s", attempt, delay.TotalSeconds)
    );

// Wrap: retry bên trong circuit breaker
var resilientPolicy = Policy.WrapAsync(circuitBreaker, retryPolicy);

var result = await resilientPolicy.ExecuteAsync(() => agentClient.InvokeAsync(request));

Idempotent Task Design

Mỗi task agent nhận phải có thể chạy lại mà không gây side effect. Nguyên tắc:

  1. Task ID là bất biến — generate UUID từ input content, không phải timestamp
  2. Check-then-act — trước khi execute, kiểm tra task đã complete chưa
  3. Write kết quả atomic — dùng upsert thay vì insert
import hashlib
import json

def generate_task_id(agent_name: str, input_data: dict) -> str:
    """Task ID được derive từ nội dung — cùng input luôn cho cùng ID."""
    content = f"{agent_name}:{json.dumps(input_data, sort_keys=True)}"
    return hashlib.sha256(content.encode()).hexdigest()[:16]

async def process_with_idempotency(task_id: str, func, *args):
    # Kiểm tra đã xử lý chưa
    existing = await task_store.get(task_id)
    if existing and existing["status"] == "completed":
        return existing["result"]  # Trả kết quả cũ, không gọi lại

    # Mark in-progress
    await task_store.upsert(task_id, {"status": "in_progress"})

    try:
        result = await func(*args)
        await task_store.upsert(task_id, {"status": "completed", "result": result})
        return result
    except Exception as e:
        await task_store.upsert(task_id, {"status": "failed", "error": str(e)})
        raise

Graceful Degradation

Khi một agent không available, hệ thống không nên crash — nên fallback sang behavior đơn giản hơn.

Pattern tôi hay dùng: Capability Tiers. Mỗi pipeline có 3 tier:

  • Tier 1 (Full): Tất cả agent hoạt động, output chất lượng cao nhất
  • Tier 2 (Degraded): Một số agent fail, dùng cached result hoặc simplified logic
  • Tier 3 (Minimal): Chỉ trả raw output không qua processing

Khi circuit breaker của một agent mở, orchestrator tự động drop xuống tier thấp hơn và log để alert.

Naive vs Resilient: So Sánh Thực Tế

Khía cạnhNaive ArchitectureResilient Architecture
RetryImmediate, unlimitedExponential backoff, max 3 lần
Failure handlingException propagatesCircuit breaker + fallback
ContextPass by referenceImmutable snapshot per task
TimeoutMặc định hoặc quá dàiExplicit, per-agent, per-call
ObservabilityLog khi crashTrace mọi agent call + latency
Task IDAuto-increment DBContent-derived hash

Hệ thống naive hoạt động tốt trong demo, đẹp trong staging, và sụp trong production lúc 2 giờ sáng. Hệ thống resilient đòi hỏi effort ban đầu nhiều hơn khoảng 30-40%, nhưng mean time between failures cao hơn nhiều lần.

Checklist Thực Tế Cho Team

Trước khi ship bất kỳ agent system nào ra production, team tôi chạy qua checklist này:

Resilience

  • Mọi LLM/agent call đều có timeout tường minh (không dựa vào default)
  • Circuit breaker được cấu hình cho mỗi external agent dependency
  • Retry dùng exponential backoff với jitter (không fixed interval)
  • Có fallback behavior khi agent không available

Idempotency

  • Mỗi task có ID derive từ content, không phải time
  • Task store có check trước khi re-execute
  • Side effects (write DB, send email) chỉ xảy ra sau khi idempotency check pass

Observability

  • Mỗi agent call được trace với agent_name, task_id, latency, status
  • Circuit state được expose qua health endpoint
  • Alert khi failure rate > 10% trong 5 phút

Context Management

  • Context được serialize thành immutable snapshot trước khi pass cho agent
  • Không có shared mutable state giữa concurrent agent runs
  • Context size được validate (không để overflow token limit âm thầm)

Load Testing

  • Đã simulate failure của từng agent độc lập — hệ thống degrade đúng không?
  • Đã test retry storm scenario với 10x normal load
  • Đã đo memory/thread usage khi tất cả circuit breaker mở đồng thời

Bài Học Rút Ra

Sau nhiều lần đau, tôi nhận ra multi-agent systems về bản chất là distributed systems — và distributed systems failures không phải “nếu” mà là “khi nào”. Sự khác biệt giữa team ship được và team luôn xử lý incident là: design for failure từ ngày đầu, không phải sau khi production sụp.

Ba nguyên tắc tôi giờ áp dụng cho mọi agent project:

1. Treat agent calls như external HTTP calls — timeout, retry, circuit breaker đầy đủ, không ngoại lệ.

2. Context là immutable — khi agent nhận task, nó nhận một snapshot. Không mutate shared state.

3. Mọi task phải idempotent — giả sử mọi task sẽ được chạy ít nhất hai lần, design cho trường hợp đó.

Hệ thống chúng tôi build sau sự cố năm ngoái đã chạy 8 tháng mà không có một production incident nào. Không phải vì may mắn — mà vì chúng tôi đã chấp nhận rằng failure là mặc định, và thiết kế xung quanh điều đó.

Nếu bạn đang chuẩn bị ship agent system lần đầu, đừng học bài học này theo cách tôi đã học. Dùng checklist trên, và chúc may mắn.

Xuất nội dung

Bình luận