AI-Native Solutions Architect

AI Integration Patterns: Cách nhúng LLM/Agents vào ứng dụng của khách hàng

7/19/2026 · 16p đọc


title: "AI Integration Patterns: Cách nhúng LLM/Agents vào ứng dụng của khách hàng"
series: "AI-Native Solutions Architect: Từ Coder đến Kiến trúc sư AI"
season: "Season 2 — AI-Augmented Architecture & System Design"
order: 20
audience: "Software Engineer hướng tới Solutions Architect"
reading_time: "13 phút"
tags: ["ai integration", "llm", "streaming", "background job", "human-in-the-loop", "fallback", "cost control", "solutions architect", "ai-native"]

AI Integration Patterns: Cách nhúng LLM/Agents vào ứng dụng của khách hàng

Có một khoảnh khắc chuyển pha mà gần như đội kỹ thuật nào cũng đi qua: sau vài tháng dùng AI để tự hỗ trợ mình — viết code nhanh hơn, review kỹ hơn, thiết kế hệ thống chắc hơn (đúng như 19 bài trước trong series này) — sản phẩm manager quay sang hỏi "vậy khách hàng của mình có được hưởng AI không?". Câu trả lời tự nhiên nhất là thêm một nút "Tóm tắt bằng AI" vào đâu đó trong UI, gọi thẳng API của một LLM provider, và ship.

Vấn đề là: dùng AI để hỗ trợ chính mình và nhúng AI vào sản phẩm cho hàng nghìn người dùng cuối là hai bài toán kiến trúc hoàn toàn khác nhau. Khi bạn tự dùng ChatGPT hay Claude để debug, nếu API chậm 10 giây hoặc lỗi, bạn chỉ mất thời gian cá nhân, thử lại là xong. Nhưng khi tính năng đó nằm trong sản phẩm bạn bán cho khách hàng, một cuộc gọi LLM chậm hoặc lỗi giờ là downtime của khách, một request tốn kém giờ nhân với hàng chục nghìn người dùng đồng thời, và một câu trả lời sai giờ là uy tín công ty bạn — không còn là chuyện "thử lại cho vui".

Phần lớn các đội mắc lỗi ở đúng chỗ dễ nhất: họ coi lệnh gọi LLM như một hàm nội bộ bình thường — await llm.complete(prompt) — và bọc nó thẳng vào request handler như thể nó có SLA giống một database query nội bộ. Nhưng LLM API là một dependency bên ngoài, mạng WAN, thời gian phản hồi dao động từ vài trăm ms đến hàng chục giây, chi phí tính theo token, và có thể bị rate-limit hoặc down bất cứ lúc nào. Bài này không nói về prompt engineering hay chọn model — nó nói về kiến trúc tích hợp: chọn đúng pattern cho đúng loại tác vụ, và không để một tính năng AI trở thành single point of failure cho cả sản phẩm.

Vấn đề

Giả sử module CRM của bạn cần thêm tính năng "Tóm tắt đánh giá khách hàng" — khách hàng B2B của bạn (ví dụ chủ shop dùng CRM này) muốn xem nhanh 200 đánh giá của người mua thành một đoạn tóm tắt 3 câu, thay vì đọc từng cái. Đội kỹ thuật implement phiên bản đầu tiên như sau:

// Cách làm sai điển hình — gọi LLM trực tiếp trong request handler,
// không timeout, không fallback, không giới hạn chi phí
@Post(':productId/summarize-reviews')
async summarizeReviews(@Param('productId') productId: string) {
  const reviews = await this.reviewService.findAllByProduct(productId);
  const prompt = `Tóm tắt các đánh giá sau thành 3 câu: ${reviews.map(r => r.content).join('\n')}`;
  const summary = await this.llmClient.complete(prompt); // không timeout, không retry, không cache
  return { summary };
}

Trong demo, tính năng này chạy mượt. Nhưng ba tuần sau khi lên production, ba việc xảy ra gần như đồng thời:

  1. LLM provider bị chậm bất thường trong 20 phút (P99 latency tăng từ 2s lên 25s) — không phải lỗi hoàn toàn, chỉ chậm. Toàn bộ request summarize-reviews treo, chiếm hết connection pool của Node.js event loop, và kéo theo cả các API khác trong cùng service bị timeout dây chuyền.
  2. Một khách hàng lớn có sản phẩm với 5.000 đánh giá bấm nút tóm tắt liên tục vì UI không disable nút trong lúc chờ — mỗi lần bấm là một request mới, mỗi request là hàng chục nghìn token đầu vào, hóa đơn LLM tháng đó tăng đột biến.
  3. Một tính năng khác trong roadmap — "AI tự động trả lời đánh giá 1 sao thay mặt shop" — được đề xuất, và ai đó nhận ra: nếu để AI tự động post câu trả lời mà không ai duyệt, một câu trả lời sai giọng điệu hoặc sai sự thật sẽ được đăng công khai dưới tên thương hiệu của khách hàng, không thể thu hồi.

Ba tình huống trên tương ứng với ba lỗi kiến trúc khác nhau: thiếu cơ chế chống nghẽn khi dependency bên ngoài chậm, thiếu kiểm soát chi phí khi traffic không được rate-limit, và thiếu điểm kiểm soát của con người cho hành động có hệ quả không thể đảo ngược. Không có pattern tích hợp AI nào giải quyết cả ba cùng lúc — nhưng biết chọn đúng pattern cho đúng loại tác vụ sẽ ngăn được phần lớn các sự cố này ngay từ thiết kế, thay vì phải vá sau khi đã cháy.

Kỹ thuật cốt lõi

Có bốn pattern tích hợp LLM/AI vào sản phẩm mà một Solutions Architect cần phân biệt rõ, không phải vì chúng là công thức cứng nhắc, mà vì mỗi pattern gắn với một bộ ràng buộc khác nhau về latency chấp nhận được, chi phí, và mức độ rủi ro của hành động.

flowchart TD
    A[Yêu cầu người dùng cần AI xử lý] --> B{Có cần phản hồi ngay không?}
    B -->|Có, và nhanh <3s| C[Request-Response đồng bộ]
    B -->|Có, nhưng nội dung dài| D[Streaming Response]
    B -->|Không cần ngay| E{Tác vụ có nặng/tốn không?}
    E -->|Có| F[Background Job]
    B -->|Hành động có hệ quả| G{Kết quả có thể tự động thực thi không?}
    G -->|Không - cần người duyệt| H[Human-in-the-loop]
    G -->|Có, rủi ro thấp| C

    style C fill:#2d5,color:#000
    style D fill:#59d,color:#000
    style F fill:#d95,color:#000
    style H fill:#d55,color:#fff

1. Request-response đồng bộ

Client gọi API, chờ, nhận kết quả trong một round-trip HTTP. Phù hợp với tác vụ ngắn, đầu ra nhỏ, latency chấp nhận được dưới vài giây — ví dụ phân loại một câu ngắn, trích xuất entity từ một đoạn text nhỏ, kiểm tra chính tả. Đây là pattern đơn giản nhất để implement, nhưng cũng dễ bị lạm dụng nhất — nhiều đội áp dụng nó cho cả những tác vụ không hề "nhanh" (như tóm tắt 200 đánh giá), và kết quả là request treo, timeout dây chuyền như ví dụ trên.

Ràng buộc bắt buộc đi kèm pattern này: timeout tường minh (không dựa vào timeout mặc định của HTTP client), circuit breaker để ngắt mạch khi LLM provider lỗi liên tục, và một giá trị fallback rõ ràng khi không thể trả lời kịp.

2. Streaming response

Thay vì chờ toàn bộ câu trả lời rồi trả về một lần, server đẩy từng token/chunk về client ngay khi model sinh ra, thường qua Server-Sent Events (SSE) hoặc WebSocket. Đây là pattern chuẩn cho trải nghiệm "gõ chữ" kiểu chatbot — người dùng thấy AI "đang suy nghĩ" thay vì nhìn một spinner trống trong 8 giây.

Về mặt kiến trúc, streaming giải quyết vấn đề cảm nhận latency (perceived latency) chứ không giảm tổng thời gian xử lý — tổng thời gian model sinh xong toàn bộ output vẫn y hệt, chỉ là người dùng bắt đầu đọc được nội dung sau vài trăm ms đầu tiên thay vì phải chờ hết. Cái giá phải trả: kết nối phải giữ mở lâu hơn (ảnh hưởng tới connection pool, load balancer timeout config), và xử lý lỗi giữa chừng (model bắt đầu trả lời rồi mới lỗi) phức tạp hơn nhiều so với request-response một lần.

3. Background job

Với các tác vụ nặng — xử lý hàng nghìn bản ghi, gọi nhiều lượt LLM tuần tự (multi-step reasoning), hoặc phân tích tài liệu lớn — client không nên chờ trực tiếp. Request ban đầu chỉ tạo một job, trả về ngay một job_id, và client poll hoặc nhận thông báo (qua WebSocket/Socket.IO có sẵn trong stack, hoặc qua RabbitMQ worker) khi job hoàn tất.

Đây chính xác là pattern phù hợp cho ví dụ "tóm tắt 5.000 đánh giá" ở trên — không có lý do gì để giữ một HTTP connection mở trong lúc LLM xử lý một khối lượng dữ liệu lớn như vậy. Pattern này cũng tự nhiên giải quyết một phần bài toán kiểm soát chi phí: job được xếp hàng, có thể giới hạn số job chạy đồng thời, và dễ áp dụng cơ chế dedup (nếu đã có job đang chạy cho cùng productId, không tạo job mới).

4. Human-in-the-loop

Khi đầu ra của AI dẫn tới một hành động có hệ quả thật — gửi email cho khách hàng, đăng công khai một câu trả lời, tự động duyệt/từ chối một giao dịch, thay đổi dữ liệu quan trọng — AI không nên có quyền thực thi trực tiếp. Pattern đúng là: AI tạo ra một đề xuất (draft), hệ thống lưu đề xuất đó ở trạng thái chờ duyệt, và một người có thẩm quyền xác nhận hoặc chỉnh sửa trước khi hành động thật sự xảy ra.

Đây cũng chính là điểm nối sang khái niệm AI Agent — thứ mà Season 3 của series này sẽ đào sâu. Về bản chất, một Agent chỉ là một hệ thống có khả năng tự quyết định chuỗi hành động để đạt mục tiêu (thay vì chỉ trả lời một câu hỏi), và càng tự chủ bao nhiêu, ranh giới human-in-the-loop càng quan trọng bấy nhiêu — vì agent có thể tự chọn sai hành động ở một bước trung gian mà không ai kiểm soát kịp.

Thực hành

Bảng chọn pattern theo loại use case

Đây là bảng quyết định thực dụng — dùng để review thiết kế bất kỳ tính năng AI nào trước khi code, trả lời ba câu hỏi: latency chấp nhận được là bao nhiêu, tác vụ có nặng không, và hậu quả sai có đảo ngược được không.

Use case Latency chấp nhận Hậu quả nếu sai Pattern phù hợp Lý do
Phân loại ticket hỗ trợ vào 1 trong 5 category < 2s Thấp, dễ sửa sau Request-response đồng bộ Tác vụ nhỏ, output ngắn, sai thì sửa category thủ công không tốn kém
Chatbot hỏi-đáp cho khách hàng cuối Người dùng chờ được vài giây nếu thấy tiến trình Thấp-trung bình Streaming response UX cần cảm giác phản hồi tức thì; nội dung dài nên stream token
Tóm tắt 200-5.000 đánh giá sản phẩm Vài giây tới vài phút, không cần real-time Thấp Background job Khối lượng dữ liệu lớn, không nên giữ HTTP connection; kết quả cache lại dùng nhiều lần
Phân tích tài liệu hợp đồng dài, trích rủi ro pháp lý Vài phút Trung bình-cao (quyết định kinh doanh dựa trên đó) Background job + review UI Multi-step reasoning, cần thời gian; kết quả nên hiển thị kèm nguồn trích dẫn để người review kiểm tra
AI tự động trả lời đánh giá 1 sao thay mặt shop Không cần ngay Cao (đăng công khai dưới tên thương hiệu khách hàng) Human-in-the-loop Sai giọng điệu/sự thật không thể thu hồi sau khi đăng; bắt buộc người duyệt trước
AI đề xuất tự động duyệt hoàn tiền dưới 500k Vài giây Cao nếu sai (mất tiền, gian lận) Human-in-the-loop (hoặc auto-approve có ngưỡng + audit log) Hành động tài chính; cần ít nhất log + khả năng revert, tốt nhất là duyệt thủ công dưới một ngưỡng rõ ràng

Kiến trúc tích hợp cho tính năng "Tóm tắt đánh giá khách hàng"

Áp dụng bảng trên, tính năng tóm tắt đánh giá nên đi theo pattern background job (vì khối lượng dữ liệu biến thiên lớn, không đoán trước được) kết hợp cache (vì đánh giá không đổi liên tục, không cần tóm tắt lại mỗi lần bấm nút):

sequenceDiagram
    participant U as User (Frontend)
    participant API as NestJS API
    participant Q as RabbitMQ Queue
    participant W as Worker (LLM caller)
    participant LLM as LLM Provider
    participant Cache as Redis Cache

    U->>API: POST /products/:id/summarize-reviews
    API->>Cache: Check cache theo product_id + review_count_hash
    alt Cache hit
        Cache-->>API: Trả tóm tắt đã cache
        API-->>U: 200 { summary, cached: true }
    else Cache miss
        API->>API: Kiểm tra rate limit theo tenant_id
        API->>Q: Enqueue job { productId, reviewIds }
        API-->>U: 202 { jobId, status: "processing" }
        Q->>W: Worker nhận job
        W->>LLM: Gọi API với timeout 30s + retry có giới hạn
        alt LLM thành công
            LLM-->>W: Trả tóm tắt
            W->>Cache: Lưu cache TTL 24h
            W->>U: Emit event qua Socket.IO "summary.ready"
        else LLM timeout/lỗi sau retry
            W->>W: Fallback: trả tóm tắt rule-based (top 3 keyword + rating trung bình)
            W->>U: Emit event "summary.ready" kèm flag degraded: true
        end
    end

Ba quyết định kiến trúc quan trọng trong sơ đồ trên: cache theo hash của tập review (tránh tóm tắt lại khi không có đánh giá mới), rate limit theo tenant trước khi enqueue (chặn tình huống một khách hàng bấm nút liên tục), và fallback rule-based khi LLM không phản hồi kịp (tính năng vẫn hoạt động, chỉ ở mức chất lượng thấp hơn, thay vì báo lỗi trắng).

Snippet minh họa: rate limit + circuit breaker trước khi gọi LLM

// review-summary.service.ts — kiểm soát chi phí và chống single point of failure
import { Injectable, ServiceUnavailableException } from '@nestjs/common';
import { InjectRedis } from '@nestjs-modules/ioredis';
import Redis from 'ioredis';

@Injectable()
export class ReviewSummaryService {
  constructor(@InjectRedis() private readonly redis: Redis) {}

  private readonly RATE_LIMIT_PER_TENANT = 20; // request/giờ, tránh spam nút bấm
  private readonly CIRCUIT_BREAKER_KEY = 'llm:circuit:review-summary';
  private readonly CIRCUIT_FAIL_THRESHOLD = 5; // 5 lỗi liên tiếp -> mở circuit
  private readonly CIRCUIT_OPEN_DURATION = 120; // giây, tạm ngắt gọi LLM

  async checkRateLimit(tenantId: string): Promise<void> {
    const key = `ratelimit:review-summary:${tenantId}`;
    const count = await this.redis.incr(key);
    if (count === 1) await this.redis.expire(key, 3600);
    if (count > this.RATE_LIMIT_PER_TENANT) {
      throw new ServiceUnavailableException(
        'Đã vượt giới hạn tóm tắt trong giờ này, vui lòng thử lại sau.',
      );
    }
  }

  async isCircuitOpen(): Promise<boolean> {
    const state = await this.redis.get(this.CIRCUIT_BREAKER_KEY);
    return state === 'open';
  }

  async recordLlmFailure(): Promise<void> {
    const failKey = `${this.CIRCUIT_BREAKER_KEY}:fails`;
    const fails = await this.redis.incr(failKey);
    await this.redis.expire(failKey, 60);
    if (fails >= this.CIRCUIT_FAIL_THRESHOLD) {
      await this.redis.set(
        this.CIRCUIT_BREAKER_KEY,
        'open',
        'EX',
        this.CIRCUIT_OPEN_DURATION,
      );
    }
  }

  async recordLlmSuccess(): Promise<void> {
    await this.redis.del(`${this.CIRCUIT_BREAKER_KEY}:fails`);
  }

  // Fallback không phụ thuộc LLM — luôn chạy được, chất lượng thấp hơn nhưng không bao giờ chết
  buildFallbackSummary(reviews: { content: string; rating: number }[]): string {
    const avgRating = (
      reviews.reduce((sum, r) => sum + r.rating, 0) / reviews.length
    ).toFixed(1);
    return `Điểm trung bình ${avgRating}/5 dựa trên ${reviews.length} đánh giá. (Tóm tắt chi tiết tạm thời không khả dụng, vui lòng thử lại sau.)`;
  }
}

Prompt mẫu: yêu cầu AI thiết kế lại một tích hợp AI thiếu an toàn

Dùng prompt sau khi review một tính năng AI đã tồn tại hoặc đang ở giai đoạn thiết kế, để ép AI (hoặc chính bạn) rà soát đủ các góc cạnh vận hành, không chỉ phần "gọi model":

Vai trò: Bạn là Solutions Architect review thiết kế tích hợp AI vào sản phẩm production.

Tính năng: "Tóm tắt đánh giá khách hàng" — người dùng bấm nút, hệ thống gọi LLM
tóm tắt N đánh giá của một sản phẩm thành 3 câu.

Ràng buộc thực tế:
- Traffic: trung bình 200 request/ngày, nhưng có thể tăng đột biến 20 lần khi
  một khách hàng lớn chạy campaign marketing.
- LLM provider hiện tại: SLA công bố 99.5% nhưng đã có 2 lần down/chậm bất thường
  trong 3 tháng qua (mỗi lần 15-30 phút).
- Chi phí: mỗi lần tóm tắt tốn khoảng input token tương đương 200 đánh giá x độ
  dài trung bình 50 từ.
- Ngân sách AI hàng tháng cho tính năng này: giới hạn cứng, không được vượt.

Yêu cầu phân tích và đề xuất, theo đúng cấu trúc:

1. PATTERN: Xác nhận pattern tích hợp phù hợp nhất (request-response / streaming /
   background job / human-in-the-loop) cho tính năng này, giải thích rõ vì sao,
   dựa trên đặc điểm traffic và độ nặng của tác vụ.

2. FALLBACK: Đề xuất cụ thể điều gì xảy ra khi LLM provider timeout hoặc trả lỗi
   sau khi đã retry (không được để tính năng trả lỗi trắng cho người dùng cuối) —
   nêu rõ fallback là gì và chất lượng suy giảm ra sao so với bình thường.

3. COST CONTROL: Đề xuất cơ chế giới hạn chi phí cụ thể (rate limit theo tenant,
   cache, giới hạn ngân sách hàng ngày/hàng tháng với circuit breaker khi chạm
   ngưỡng) — nêu rõ điều gì xảy ra với người dùng khi ngân sách bị chạm.

4. MONITORING: Liệt kê 3-5 metric bắt buộc phải theo dõi cho tích hợp này (ví dụ
   latency P50/P99 của lệnh gọi LLM, tỷ lệ fallback được kích hoạt, chi phí theo
   tenant, tỷ lệ lỗi) và ngưỡng cảnh báo hợp lý cho mỗi metric.

Không chấp nhận câu trả lời kiểu "thêm try-catch và cache là đủ" — yêu cầu phân
tích rủi ro vận hành cụ thể gắn với các con số traffic và chi phí đã cho.

Cạm bẫy thường gặp

1. Không có fallback khi LLM external down — biến một dependency bên ngoài thành single point of failure của cả tính năng. Đây là lỗi phổ biến nhất, và nguy hiểm nhất vì nó không xuất hiện trong testing — LLM provider hầu như luôn phản hồi nhanh trong môi trường dev/staging với traffic thấp. Chỉ khi production có traffic thật và provider gặp sự cố (điều chắc chắn sẽ xảy ra ở một thời điểm nào đó, vì bạn không kiểm soát được uptime của bên thứ ba), toàn bộ tính năng sập theo, và nếu tính năng đó được gọi đồng bộ trong một luồng quan trọng hơn (như checkout, hoặc dashboard chính), sự cố lan rộng ra ngoài phạm vi "tính năng AI". Luôn thiết kế một fallback không phụ thuộc LLM — có thể chất lượng thấp hơn (rule-based, cached kết quả cũ, hoặc đơn giản là ẩn tính năng tạm thời) — nhưng không bao giờ được để một request treo vô thời hạn chờ một dependency bạn không kiểm soát.

2. Không kiểm soát chi phí khi traffic tăng đột biến — mỗi request người dùng có thể là một khoản chi phí thật, không giới hạn. Khác với một API nội bộ (chi phí biên gần như 0), mỗi lệnh gọi LLM có chi phí tính theo token, và chi phí này tăng tuyến tính (hoặc tệ hơn) theo traffic. Nếu không có rate limit theo tenant/user, không có cache cho các câu hỏi lặp lại, và không có ngưỡng ngân sách cứng với cơ chế circuit breaker khi chạm ngưỡng, một sự kiện tăng traffic đột ngột (viral, campaign marketing, hoặc đơn giản là bot/abuse) có thể tạo ra hóa đơn bất ngờ trong vài giờ. Luôn thiết kế cost control như một phần bắt buộc của kiến trúc, không phải một việc "làm sau khi có vấn đề".

3. Để AI tự động thực thi hành động có hệ quả mà không qua human-in-the-loop, chỉ vì "demo chạy ổn". Một agent hoặc pipeline AI có thể hoạt động đúng 95% thời gian trong demo và testing — nhưng 5% còn lại, khi nó sai, hành động đã xảy ra rồi (email đã gửi, nội dung đã đăng, giao dịch đã duyệt) và không thể thu hồi. Ranh giới cần đặt ra rõ ràng ngay từ thiết kế: hành động nào là "đề xuất cần người duyệt" và hành động nào đủ an toàn để tự động hóa hoàn toàn — và ranh giới này nên dựa trên mức độ có thể đảo ngược của hậu quả, không dựa trên việc "AI có vẻ đáng tin cậy" trong vài lần thử đầu.

🧭 Góc nhìn Solutions Architect
Nếu LLM provider tôi đang dùng down hoàn toàn trong 1 giờ tới, tính năng nào trong sản phẩm sẽ chết theo, và người dùng cuối sẽ thấy gì — lỗi trắng, spinner treo vô hạn, hay một fallback chấp nhận được? Chi phí AI của tôi có trần cứng nào không, hay về lý thuyết một request duy nhất từ một người dùng ác ý có thể khiến hóa đơn tháng này tăng gấp 10 lần? Và trong toàn bộ luồng tích hợp AI hiện tại, có hành động nào đang được AI tự động thực thi mà nếu nó sai, tôi không thể xoay chuyển lại được hậu quả?

🔗 Bài viết liên quan

  • [Bài 14] RAG for Engineering — kỹ thuật truy xuất ngữ cảnh làm nền cho chất lượng output của các pattern tích hợp LLM ở bài này (ans-14-rag-for-engineering-query-codebase.md)
  • [Bài 15] Event-Driven Design với AI hỗ trợ — nền tảng kiến trúc bất đồng bộ (queue, event) mà pattern background job trong bài này dựa vào (ans-15-event-driven-design-ai-ho-tro.md)

Bài trước: Disaster Recovery · Bài tiếp theo: Introduction to AI Agents

AI Integration Patterns: Cách nhúng LLM/Agents vào ứng dụng của khách hàng