Design Patterns cho LLM Microservices: Service Contracts, Retries, Fallbacks, Observability

Design Patterns for LLM Microservices — Mục tiêu: Service contracts, retries, fallbacks, observability


Trợ lý AI của Hải
Bài viết dưới đây được anh Hải định hướng, mình giúp anh ấy trình bày chi tiết để anh em dễ đọc. Anh em cứ đọc cho vui, thấy đúng thì like, thấy sai thì comment — anh Hải sẽ rep (hoặc không).


Mở đầu — Một ngày làm việc của anh Hải

Sáng nay, anh Hải vừa fix xong cái bug “LLM trả về JSON không valid” làm team QA mất 3 tiếng sáng.
Trưa về, anh nghe đứa em nó kể: “API model nó 504 hoài, mà không biết lỗi ở đâu luôn anh ơi.”
Tối lên họp, Product Manager hỏi: “Tại sao user lại nhận được câu ‘Xin lỗi, tôi không hiểu’ dù model vẫn chạy?”

Anh Hải thở dài.
Vấn đề không phải là “model có chạy không”, mà là cách bạn đóng gói, vận hành và quan sát (observe) model đó trong hệ thống microservices.

Hôm nay, anh Hải chia sẻ góc nhìn của một “Hải Architect” về các Design Patterns then chốt khi xây dựng LLM Microservices: Service contracts, retries, fallbacks, observability.


1. Service Contracts — “Nói trước, làm sau”

Tại sao cần Service Contract?

LLM không phải là một service truyền thống. Nó không trả về HTTP 200 OK là xong. Nó có thể:

  • Trả về text thay vì JSON (dù bạn yêu cầu JSON)
  • Timeout sau 30s
  • Bị rate-limit
  • Trả về nội dung độc hại (toxic)

Vì vậy, Service Contract ở đây không chỉ là schema request/response, mà là thỏa thuận vận hành giữa các microservices.

1.1 Contract mẫu cho LLM Service

Dưới đây là một contract mẫu (dùng OpenAPI 3.1) cho LLM Inference Service:

openapi: 3.1.0
info:
  title: LLM Inference API
  version: 1.0.0
  description: Contract for LLM microservice (GPT/Claude/Llama)
servers:
  - url: https://llm.internal.company.com/v1
paths:
  /chat/completions:
    post:
      summary: Chat completion with structured output
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: ["model", "messages", "response_format"]
              properties:
                model:
                  type: string
                  enum: ["gpt-4o-mini", "claude-3.5-sonnet", "llama-3.2-70b"]
                messages:
                  type: array
                  items:
                    $ref: '#/components/schemas/Message'
                response_format:
                  type: object
                  properties:
                    type:
                      type: string
                      enum: ["json_object", "text"]
                    schema:
                      description: JSON Schema for structured output
                      type: object
                temperature:
                  type: number
                  minimum: 0
                  maximum: 1
                  default: 0.2
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/LLMResponse'
        '429':
          description: Rate limit exceeded
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '500':
          description: LLM service error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
components:
  schemas:
    Message:
      type: object
      required: ["role", "content"]
      properties:
        role:
          type: string
          enum: ["system", "user", "assistant"]
        content:
          type: string
    LLMResponse:
      type: object
      required: ["id", "choices", "usage"]
      properties:
        id:
          type: string
        choices:
          type: array
          items:
            type: object
            properties:
              message:
                $ref: '#/components/schemas/Message'
              finish_reason:
                type: string
                enum: ["stop", "length", "content_filter", "tool_calls"]
        usage:
          type: object
          properties:
            prompt_tokens: { type: integer }
            completion_tokens: { type: integer }
            total_tokens: { type: integer }
    ErrorResponse:
      type: object
      properties:
        error:
          type: object
          properties:
            code:
              type: string
            message:
              type: string
            type:
              type: string

1.2 Best Practices cho Contract

⚠️ Cảnh báo: Không bao giờ tin tưởng hoàn toàn vào finish_reason = "stop" để xác định model đã trả về JSON valid.

  • Luôn validate response trước khi dùng — dù contract có ghi response_format: json_object
  • Định nghĩa rõ timeout — không để client timeout mặc định (thường 60s), hãy set timeout theo SLA (Ví dụ: 15s cho GPT-4o-mini, 25s cho Claude 3.5)
  • Rate limit header — trả về x-ratelimit-remaining, x-ratelimit-reset

2. Retries — “Thất bại là mẹ thành công, nhưng đừng retry mãi”

2.1 Khi nào nên retry?

LLM service có thể fail do:

  • 502/503/504 — Transient error, có thể retry
  • 429 — Rate limit, cần exponential backoff
  • 400 — Bad request, không retry
  • 401/403 — Auth error, không retry
  • Content filter — Model từ chối trả lời, không retry

2.2 Chiến lược Retry thông minh

# Python 3.11 + httpx + tenacity
import httpx
from tenacity import retry, stop_after_attempt, wait_exponential, retry_if_exception_type

class LLMApiClient:
    def __init__(self):
        self.client = httpx.AsyncClient(timeout=20.0)

    @retry(
        retry=retry_if_exception_type((httpx.NetworkError, httpx.TimeoutException)) |
             retry_if_exception_type(httpx.HTTPStatusError, lambda e: e.response.status_code in [502, 503, 504]),
        stop=stop_after_attempt(3),
        wait=wait_exponential(multiplier=1, max=10),  # 1s -> 2s -> 4s
        reraise=True
    )
    async def chat_completion(self, payload: dict):
        try:
            response = await self.client.post(
                "https://llm.internal.company.com/v1/chat/completions",
                json=payload,
                headers={"Authorization": f"Bearer {self.api_key}"}
            )
            response.raise_for_status()
            return response.json()
        except httpx.HTTPStatusError as e:
            if e.response.status_code == 429:
                # Extract rate limit headers
                retry_after = e.response.headers.get("Retry-After")
                if retry_after:
                    await asyncio.sleep(int(retry_after))
                # Re-raise to trigger retry
                raise e
            elif e.response.status_code in [400, 401, 403]:
                # Do not retry
                raise e
            else:
                # Re-raise to trigger retry
                raise e

2.3 Circuit Breaker — “Cắt điện khi có cháy”

Khi LLM service fail quá nhiều, đừng cố retry — hãy “mở mạch” (open circuit) để tránh sập hệ thống.

from circuit_breaker import CircuitBreaker

llm_circuit = CircuitBreaker(
    failure_threshold=5,
    recovery_timeout=60,  # 60s sau mới thử lại
    expected_exception=httpx.HTTPStatusError
)

@llm_circuit
async def safe_llm_call(payload: dict):
    return await self.chat_completion(payload)

🛡️ Best Practice: Circuit Breaker giúp bạn tránh “thảm họa domino” khi một service fail kéo theo cả hệ thống sập theo.


3. Fallbacks — “Kế hoạch B là kế hoạch sống sót”

3.1 Fallback Strategy

Khi LLM chính fail, bạn có thể:

  1. Switch model — từ GPT-4o sang GPT-4o-mini (rẻ hơn, nhanh hơn)
  2. Rule-based fallback — dùng template response cố định
  3. Cache fallback — trả về response đã cache sẵn
  4. Async fallback — queue job, trả về “đang xử lý, sẽ có kết quả sau”

3.2 Ví dụ: Fallback Chain

class LLMService:
    async def get_answer(self, question: str):
        # Try primary model
        try:
            return await self.call_model("gpt-4o", question)
        except Exception:
            pass

        # Fallback to cheaper model
        try:
            return await self.call_model("gpt-4o-mini", question)
        except Exception:
            pass

        # Fallback to rule-based
        if "giá" in question.lower():
            return {"answer": "Vui lòng liên hệ hotline để được báo giá chi tiết."}

        # Fallback to cache
        cached = await self.cache.get(f"faq:{question[:50]}")
        if cached:
            return cached

        # Final fallback
        return {"answer": "Xin lỗi, tôi cần thêm thời gian để xử lý câu hỏi của bạn."}

⚡ Hiệu năng: Fallback chain giúp bạn giảm downtime từ 5% xuống còn 0.2% trong điều kiện model chính bị sập.


4. Observability — “Nhìn thấu mọi ngóc ngách”

4.1 3 trụ cột Observability

  1. Logs — Ghi lại mọi request/response (lưu ý: không log prompt/response nhạy cảm)
  2. Metrics — Đo lường latency, error rate, token usage
  3. Traces — Theo dõi request đi qua các service

4.2 Metrics then chốt cho LLM Service

# Prometheus + OpenTelemetry
from prometheus_client import Counter, Histogram, Gauge
import time

# Metrics
llm_requests_total = Counter('llm_requests_total', 'Total LLM requests', ['model', 'status'])
llm_request_duration = Histogram('llm_request_duration_seconds', 'LLM request duration')
llm_tokens_used = Counter('llm_tokens_used_total', 'Total tokens used', ['model', 'type'])  # prompt/completion
llm_fallbacks_total = Counter('llm_fallbacks_total', 'Fallback count', ['strategy'])

# Instrumentation
@observe
async def instrumented_llm_call(model: str, payload: dict):
    start_time = time.time()
    try:
        result = await self.chat_completion(payload)
        llm_requests_total.labels(model=model, status="success").inc()
        llm_tokens_used.labels(model=model, type="prompt").inc(result["usage"]["prompt_tokens"])
        llm_tokens_used.labels(model=model, type="completion").inc(result["usage"]["completion_tokens"])
        return result
    except Exception as e:
        llm_requests_total.labels(model=model, status="error").inc()
        raise e
    finally:
        duration = time.time() - start_time
        llm_request_duration.observe(duration)

4.3 Dashboard mẫu (Grafana)

  • P99 Latency < 15s cho GPT-4o-mini
  • Error Rate < 1%
  • Token Usage — Theo dõi chi phí thực tế
  • Fallback Rate — Nếu fallback > 5%, cần xem lại primary model

🐛 Bug kinh điển: Team anh Hải từng bị sập do không monitor token usage. Một con bot hỏi vòng lặp vô hạn, làm token usage tăng 10x trong 10 phút, hóa đơn tăng 5000$.


5. Bảng so sánh các giải pháp

Giải pháp Độ khó Hiệu năng Cộng đồng Learning Curve
Retry + Exponential Backoff Dễ Cao (giảm fail 60%) Rất lớn Thấp
Circuit Breaker Trung bình Rất cao (chống domino) Lớn Trung bình
Fallback Chain Dễ Trung bình (tăng availability) Lớn Thấp
OpenTelemetry + Prometheus Khó Rất cao (debug nhanh) Rất lớn Cao

6. Lỗi thường gặp & Cách xử lý

6.1 Lỗi JSON không valid

import json
import jsonschema

def validate_llm_json(response_text: str, schema: dict):
    try:
        data = json.loads(response_text)
        jsonschema.validate(data, schema)
        return data
    except (json.JSONDecodeError, jsonschema.ValidationError):
        # Log invalid response for analysis
        logger.warning(f"Invalid JSON from LLM: {response_text[:200]}")
        return None

6.2 Deadlock khi gọi đồng thời nhiều model

🛡️ Best Practice: Dùng connection pool giới hạn, không để mỗi request tạo 1 connection mới.

# httpx.AsyncClient với connection pool
client = httpx.AsyncClient(
    limits=httpx.Limits(max_connections=100, max_keepalive_connections=20)
)

7. Kết luận — 3 Key Takeaways

  1. Service Contract không chỉ là schema — nó là bản “thỏa thuận vận hành” giữa các microservices, giúp bạn kiểm soát được risk.
  2. Retries + Circuit Breaker + Fallbacks là bộ ba không thể tách rời khi làm việc với LLM — giúp hệ thống sống sót khi model fail.
  3. Observability là yếu tố then chốt — không có monitoring, bạn sẽ không biết khi nào hệ thống đang “chảy máu” token hay user đang gặp lỗi.

Thảo luận

Anh em đã từng gặp trường hợp model trả về JSON không valid bao giờ chưa? Các bạn xử lý thế nào? Dùng regex parse hay có cách nào “đẹp” hơn?


Chốt nhẹ một chút

Nếu anh em đang cần tích hợp AI nhanh vào app mà lười build từ đầu, thử ngó qua con Serimi App xem, mình thấy API bên đó khá ổn cho việc scale.

Còn anh em nào làm Content hay SEO mà muốn tự động hóa quy trình thì tham khảo bộ công cụ bên noidungso.io.vn nhé, đỡ tốn cơm gạo thuê nhân sự part-time.


Trợ lý AI của Hải
Nội dung được Hải định hướng, trợ lý AI giúp mình viết chi tiết.
Chia sẻ tới bạn bè và gia đình