Integrating AI into Architecture: Thiết kế RAG (Retrieval-Augmented Generation) như một module hệ thống
7/19/2026 · 15p đọc
title: "Integrating AI into Architecture: Thiết kế RAG (Retrieval-Augmented Generation) như một module hệ thống"
series: "Software Architecture Masterclass"
part: "Phần 5 — AI-Augmented & Future-Proofing"
order: 38
audience: "Solutions Architect & Technical Lead"
reading_time: "14 phút"
tags: ["rag", "retrieval-augmented-generation", "llm", "vector-database", "ai-architecture", "system-design"]
Integrating AI into Architecture: Thiết kế RAG (Retrieval-Augmented Generation) như một module hệ thống
Một đội sản phẩm được giao chỉ tiêu "thêm trợ lý AI trả lời câu hỏi về tài liệu nội bộ" trong hai tuần sprint. Cách nhanh nhất để demo được: một hàm askAI(question) gọi thẳng API của một nhà cung cấp LLM, nhét toàn bộ context có thể lấy được vào prompt, nhận về câu trả lời, hiển thị lên UI. Demo chạy tốt. Sếp gật đầu. Hàm đó được gọi trực tiếp từ ba controller khác nhau trong vòng một tháng, mỗi chỗ tự ghép prompt theo cách riêng, tự quyết định lấy dữ liệu nào để nhét vào context theo cảm tính của người viết code hôm đó.
Sáu tháng sau, không ai trong đội trả lời được ba câu hỏi cơ bản: khi trợ lý trả lời sai, dữ liệu nào đã được đưa vào context để sinh ra câu trả lời đó? Khi nhà cung cấp LLM đổi giá hoặc đổi giới hạn rate limit, bao nhiêu chỗ trong codebase phải sửa? Khi cần thay vector database vì lý do chi phí, có bao nhiêu điểm gọi trực tiếp API của vector database đó rải rác khắp business logic? Không ai biết, vì không có ai từng vẽ ranh giới cho "module AI" — nó chỉ là một tập lời gọi HTTP rải rác, được gọi chung là "tính năng AI", nhưng về kiến trúc là một mớ hỗn độn không có interface, không có trách nhiệm rõ ràng, không có điểm nào để test độc lập.
Đây không phải vấn đề của AI. Đây là vấn đề kiến trúc kinh điển — tích hợp một dependency ngoại vi (external dependency) vào hệ thống mà không qua một lớp trừu tượng nào — chỉ đang xảy ra dưới một cái tên mới và với một loại rủi ro mới: đầu ra không xác định (non-deterministic output). RAG (Retrieval-Augmented Generation — sinh câu trả lời có tăng cường bằng truy xuất dữ liệu) là kỹ thuật phổ biến nhất để làm cho LLM trả lời đúng ngữ cảnh nội bộ của một tổ chức, và nó xứng đáng được đối xử như bất kỳ module hạ tầng nào khác trong kiến trúc — không phải một phép màu được dán tạm vào business logic.
The Problem
Khi một đội bắt đầu tích hợp AI vào sản phẩm, áp lực thời gian luôn đẩy về hướng con đường ngắn nhất: gọi thẳng API của LLM ngay tại nơi cần dùng nó — trong controller, trong một service nghiệp vụ đã có sẵn, thậm chí trong một cron job. Cách này có vẻ hợp lý ở giai đoạn prototype: cần câu trả lời, gọi API, nhận kết quả, xong việc.
Vấn đề xuất hiện khi tính năng đó phải sống lâu hơn một demo. Prompt bị viết cứng (hardcode) rải rác ở nhiều nơi, mỗi nơi tự quyết định cách lấy dữ liệu ngữ cảnh (context) theo cách riêng — có chỗ query thẳng database, có chỗ đọc file tĩnh, có chỗ gọi một vector database không ai nhớ vì sao chọn. Không có ranh giới rõ ràng giữa "lấy dữ liệu liên quan" (retrieval) và "sinh câu trả lời" (generation), nên không thể kiểm thử hay tối ưu riêng từng phần — muốn cải thiện độ chính xác của việc tìm tài liệu liên quan buộc phải đụng vào cùng một khối code với phần gọi LLM sinh văn bản.
Hệ quả kỹ thuật cụ thể: đổi nhà cung cấp LLM (vì giá, vì hiệu năng, vì chính sách dữ liệu) đòi hỏi sửa ở hàng chục điểm gọi rải khắp codebase. Đổi vector database (vì chi phí vận hành, vì giới hạn quy mô) là một dự án rủi ro cao vì API của nó bị gọi trực tiếp từ business logic thay vì qua một interface trung gian. Và nghiêm trọng hơn cả: khi hệ thống trả lời sai hoặc "ảo giác" (hallucination — LLM sinh ra thông tin không có thật), không có log có cấu trúc nào ghi lại chính xác dữ liệu nào đã được truy xuất và đưa vào context để tạo ra câu trả lời đó — nghĩa là không thể debug, không thể audit, không thể cải thiện có hệ thống. Đội gọi hiện tượng này là "AI spaghetti": logic AI trộn lẫn vào business logic đến mức không còn ranh giới module nào để bảo trì hay thay thế độc lập.
The Theory
Cách khắc phục không phải là một công cụ hay framework cụ thể — đó là áp dụng chính kỷ luật kiến trúc mà bạn đã áp dụng cho bất kỳ external dependency nào khác: định nghĩa RAG như một module có ranh giới rõ ràng, với interface tường minh giữa các thành phần con, thay vì một hộp đen được gọi tùy tiện.
RAG là gì, ở tầng kiến trúc
RAG giải quyết một giới hạn cố hữu của LLM: mô hình được huấn luyện trên một tập dữ liệu tĩnh, đóng băng tại thời điểm huấn luyện, không biết gì về tài liệu nội bộ, dữ liệu nghiệp vụ, hay thông tin cập nhật sau đó của tổ chức bạn. RAG giải quyết việc này bằng cách, tại thời điểm xử lý câu hỏi, chủ động truy xuất (retrieve) những đoạn dữ liệu liên quan nhất từ kho tri thức riêng của tổ chức, rồi đưa chúng vào context của prompt trước khi yêu cầu LLM sinh câu trả lời. Nói cách khác: thay vì hy vọng mô hình "biết" câu trả lời, bạn chủ động cho nó đọc tài liệu liên quan ngay trước khi trả lời — giống hệt việc đưa tài liệu tham khảo cho một chuyên gia trước khi hỏi ý kiến, thay vì bắt họ trả lời hoàn toàn bằng trí nhớ.
Về mặt kiến trúc, một pipeline RAG có ba thành phần con tách biệt, mỗi thành phần nên có interface (port) riêng để có thể hoán đổi độc lập:
- Retriever (bộ truy xuất). Nhận câu hỏi/truy vấn đầu vào, trả về tập ứng viên là các đoạn dữ liệu (chunk) có khả năng liên quan cao nhất — thường bằng cách chuyển câu hỏi thành vector embedding rồi tìm các vector gần nhất trong một vector database (Qdrant, Pinecone, hoặc tương đương — kho lưu trữ được tối ưu cho tìm kiếm tương đồng ngữ nghĩa). Trách nhiệm duy nhất của thành phần này: tìm đúng dữ liệu, không phải sinh văn bản.
- Ranker (bộ xếp hạng, tùy chọn nhưng nên có). Vector search theo độ tương đồng ngữ nghĩa thô thường trả về ứng viên nhiễu — liên quan bề mặt nhưng không thực sự hữu ích cho câu hỏi cụ thể. Một bước re-ranking (dùng mô hình chuyên biệt hoặc heuristic nghiệp vụ) lọc và sắp xếp lại tập ứng viên trước khi đưa vào context, giảm nhiễu và giảm số token phải gửi cho LLM.
- Generator (bộ sinh câu trả lời). Nhận câu hỏi gốc cộng với context đã được retriever và ranker chọn lọc, gọi LLM để sinh câu trả lời cuối cùng. Đây là thành phần duy nhất thực sự "gọi AI" — và nó không nên biết gì về cách dữ liệu được lấy ra, chỉ nhận context đã sẵn sàng.
Ba thành phần này cộng thêm một pipeline ingest (nạp dữ liệu nguồn, chia nhỏ thành chunk, sinh embedding, ghi vào vector store — chạy độc lập, thường theo lịch hoặc theo sự kiện, không nằm trên đường request-response của người dùng) tạo thành một module RAG hoàn chỉnh, tách biệt khỏi mọi business logic khác trong hệ thống.
Vì sao interface tường minh quan trọng hơn công nghệ cụ thể
Điểm cốt lõi không phải "dùng vector database nào" hay "dùng LLM của nhà cung cấp nào" — đó là quyết định có thể thay đổi theo thời gian và không nên ảnh hưởng đến phần còn lại của hệ thống. Điều thực sự quan trọng là định nghĩa contract rõ ràng cho từng thành phần: Retriever nhận gì, trả về gì (một danh sách chunk có kèm điểm số liên quan, không phải văn bản thô không cấu trúc); Generator nhận gì (câu hỏi + context đã chuẩn hóa), trả về gì (câu trả lời + metadata về nguồn đã dùng). Khi contract này rõ ràng, việc thay Qdrant bằng một vector database khác, hay thay nhà cung cấp LLM này bằng nhà cung cấp khác, chỉ là việc viết một adapter mới tuân theo cùng interface — đúng nguyên lý Ports & Adapters đã bàn ở Bài 16, áp dụng cho một loại dependency mới: dependency có đầu ra không xác định.
The Trade-off
Đây là phần phải rõ ràng: kiến trúc hóa RAG đúng cách không miễn phí, và cách làm nhanh (gọi thẳng API) cũng không miễn phí — chỉ là cái giá được trả ở hai thời điểm khác nhau.
Gọi thẳng API LLM, không qua module — được gì:
- Tốc độ triển khai nhanh nhất cho prototype hoặc proof-of-concept: từ ý tưởng đến demo có thể tính bằng ngày.
- Không cần đầu tư hạ tầng vector database, pipeline ingest, hay logic đánh giá chất lượng câu trả lời — phù hợp khi mục tiêu chỉ là kiểm chứng giả thuyết sản phẩm trước khi đầu tư nghiêm túc.
- Không cần đội ngũ hiểu sâu về embedding, chunking strategy, hay retrieval evaluation — một lập trình viên bất kỳ có thể tích hợp trong một buổi.
Gọi thẳng API LLM, không qua module — mất gì, cụ thể:
- Không thể thay nhà cung cấp LLM mà không sửa nhiều nơi. Prompt, format request, cách parse response viết cứng rải rác ở mọi điểm gọi — đổi nhà cung cấp vì giá hoặc chính sách dữ liệu trở thành một dự án rà soát toàn bộ codebase, không phải một thay đổi cấu hình.
- Không có điểm nào để chèn caching, rate limiting, hay cost control tập trung. Mỗi điểm gọi tự chịu trách nhiệm (hoặc thường là không chịu trách nhiệm) về việc gọi lại API cho cùng một câu hỏi nhiều lần, dẫn đến chi phí API tăng không kiểm soát khi lưu lượng tăng.
- Không có fallback nhất quán khi LLM lỗi hoặc trả lời không đáng tin. Mỗi nơi tự xử lý lỗi theo cách riêng — có chỗ crash, có chỗ trả về chuỗi rỗng, có chỗ hiển thị lỗi kỹ thuật thẳng cho người dùng cuối.
- Không thể audit hay debug khi trả lời sai. Không có log có cấu trúc ghi lại dữ liệu nào đã được đưa vào context — khi người dùng khiếu nại câu trả lời sai, không ai tái hiện được vì sao mô hình trả lời như vậy.
- Không có hook đánh giá chất lượng (evaluation). Không đo được retrieval có đang tìm đúng tài liệu hay không, không phát hiện được khi chất lượng câu trả lời suy giảm dần theo thời gian (data drift trong kho tri thức nguồn).
RAG được kiến trúc hóa như một module — được gì:
- Mọi thành phần (Retriever, Ranker, Generator) có thể hoán đổi độc lập qua adapter — đổi vector database, đổi nhà cung cấp LLM là thay implementation sau một interface, không phải rà soát toàn hệ thống.
- Caching có thể chèn ở đúng một điểm (ví dụ cache theo cặp câu hỏi + phiên bản context) để giảm chi phí gọi LLM lặp lại cho câu hỏi tương tự.
- Fallback nhất quán: khi điểm tin cậy retrieval thấp hoặc LLM lỗi, toàn hệ thống phản ứng theo cùng một chính sách đã định nghĩa (trả lời "không đủ dữ liệu để trả lời chính xác" thay vì đoán liều), thay vì mỗi nơi tự xử lý một kiểu.
- Có log có cấu trúc ghi lại chunk nào được truy xuất, điểm liên quan bao nhiêu, prompt cuối cùng gửi đi là gì — cho phép audit và debug khi có khiếu nại về câu trả lời.
- Có thể gắn hook đánh giá (evaluation) đo chất lượng retrieval và chất lượng câu trả lời một cách hệ thống, phát hiện suy giảm chất lượng trước khi người dùng phàn nàn.
RAG được kiến trúc hóa như một module — mất gì, cụ thể:
- Overhead kỹ thuật thật, không phải tưởng tượng. Cần xây pipeline ingest (chunking, embedding, đồng bộ khi dữ liệu nguồn thay đổi), vận hành một vector database (dù managed hay tự host), định nghĩa và bảo trì interface cho từng thành phần — tất cả là công việc kỹ thuật có chi phí thời gian thực, không đến "miễn phí" cùng với việc gọi LLM.
- Thời gian đến demo đầu tiên chậm hơn đáng kể. Nếu mục tiêu chỉ là kiểm chứng ý tưởng sản phẩm trong một tuần, đầu tư dựng module RAG đầy đủ là quá tay — chi phí kiến trúc chưa hoàn vốn ở giai đoạn khám phá.
- Cần năng lực đội ngũ về chunking strategy, chọn mô hình embedding, và đánh giá retrieval — đây là kỹ năng khác với việc gọi API thông thường, và làm sai (chunk quá lớn, quá nhỏ, embedding không phù hợp domain) có thể khiến retrieval tệ hơn cả không có retrieval.
- Thêm một tầng hạ tầng phải vận hành và giám sát — vector database cần backup, cần giám sát độ trễ truy vấn, cần đồng bộ khi dữ liệu nguồn cập nhật; đây là một thành phần hạ tầng mới, không phải chỉ là một API call thêm vào.
Nói ngắn gọn: gọi thẳng API đổi tốc độ ban đầu lấy nợ kiến trúc tăng dần và rủi ro không kiểm soát được khi quy mô tăng — chi phí bị hoãn lại, không bị xóa bỏ. Kiến trúc hóa RAG đổi tốc độ ban đầu lấy khả năng bảo trì, khả năng audit, và khả năng kiểm soát chi phí về lâu dài — chỉ đáng giá khi tính năng AI đó thực sự sẽ sống trong sản phẩm lâu dài, không phải một thử nghiệm hai tuần.
The Architecture Diagram
flowchart TB
subgraph Ingest["Ingest Pipeline (offline, theo lịch/sự kiện)"]
direction LR
Src[("Tài liệu nguồn\nDB, file, wiki nội bộ")]
Chunk["Chunking\n(chia nhỏ tài liệu)"]
Embed["Embedding Model\n(port — hoán đổi được)"]
Src --> Chunk --> Embed
end
VDB[("Vector Store\nport: VectorStorePort\nadapter: Qdrant / Pinecone / ...")]
Embed --> VDB
subgraph RAGModule["RAG Module (ranh giới rõ ràng, gọi qua interface)"]
direction TB
Retriever["Retriever\nnhận câu hỏi -> vector\ntìm top-K chunk liên quan"]
Ranker["Ranker\nlọc + xếp hạng lại\ntheo mức độ liên quan thật"]
ConfCheck{"Điểm tin cậy\nretrieval đủ cao?"}
Generator["Generator\nport: LLMProviderPort\nadapter: nhà cung cấp LLM cụ thể"]
Fallback["Fallback Handler\ntrả lời an toàn:\n'không đủ dữ liệu' /\nchuyển người hỗ trợ"]
Cache[("Response Cache\ntheo câu hỏi + phiên bản context")]
EvalHook["Evaluation Hook\nlog: chunk dùng, điểm số,\nprompt cuối, câu trả lời"]
Retriever --> Ranker --> ConfCheck
ConfCheck -- "đủ tin cậy" --> Generator
ConfCheck -- "thấp" --> Fallback
Generator --> Cache
Generator --> EvalHook
Fallback --> EvalHook
end
Client["Business Logic / Controller\n(chỉ gọi RAGModule qua 1 interface,\nkhông biết chi tiết bên trong)"]
Client -->|"query(question)"| Retriever
Retriever -->|"vector search"| VDB
Cache -->|"trả lời"| Client
Fallback -->|"trả lời an toàn"| Client
style RAGModule fill:#0d3b66,stroke:#333,color:#fff
style Retriever fill:#1565c0,stroke:#333,color:#fff
style Ranker fill:#1565c0,stroke:#333,color:#fff
style Generator fill:#1565c0,stroke:#333,color:#fff
style Fallback fill:#b71c1c,stroke:#333,color:#fff
style VDB fill:#2e7d32,stroke:#333,color:#fff
style Cache fill:#2e7d32,stroke:#333,color:#fff
style EvalHook fill:#6a1b9a,stroke:#333,color:#fff
Điểm cấu trúc cốt lõi trong sơ đồ: business logic phía Client chỉ nói chuyện với RAG Module qua đúng một điểm vào (query(question)), không biết và không cần biết vector database nào đang chạy phía sau, nhà cung cấp LLM nào đang được gọi, hay chiến lược ranking cụ thể là gì. Mọi thành phần bên trong module đều đứng sau một port riêng — VectorStorePort, LLMProviderPort — nên thay adapter không đụng đến Client. Nhánh ConfCheck là điểm then chốt thường bị bỏ qua trong triển khai vội vàng: khi retrieval không tìm được dữ liệu đủ liên quan, hệ thống phải rẽ sang Fallback thay vì ép Generator "đoán" câu trả lời từ context nghèo nàn — đây là ranh giới giữa RAG có kỷ luật và RAG tạo ảo giác.
Implementation Guardrails
- Không được gọi thẳng SDK của vector database hay nhà cung cấp LLM từ business logic. Mọi truy cập phải đi qua interface (
VectorStorePort,LLMProviderPorthoặc tương đương) được định nghĩa trong module RAG; implementation cụ thể (Qdrant, một nhà cung cấp LLM cụ thể) chỉ là adapter sau interface đó. Vi phạm quy tắc này là dấu hiệu chắc chắn của "AI spaghetti" sẽ phải trả giá khi cần thay đổi nhà cung cấp. - Bắt buộc có ngưỡng tin cậy (confidence threshold) và hành vi fallback rõ ràng, được định nghĩa trước, không phải ứng biến tại chỗ. Khi điểm liên quan của kết quả retrieval thấp hơn ngưỡng đã thống nhất, hệ thống phải trả lời theo một chính sách đã định nghĩa (thông báo không đủ dữ liệu, chuyển sang kênh hỗ trợ con người, hoặc trả lời giới hạn phạm vi) — không được để Generator tự "đoán" câu trả lời từ context nghèo, vì đó chính xác là kịch bản sinh ảo giác dễ xảy ra nhất.
- Mọi lần truy vấn phải ghi log có cấu trúc: câu hỏi gốc, danh sách chunk được truy xuất kèm điểm số, prompt cuối cùng gửi tới LLM, và câu trả lời nhận về. Không có log này, không thể điều tra khi người dùng khiếu nại câu trả lời sai, và không thể xây dựng vòng lặp đánh giá (evaluation loop) để cải thiện chất lượng retrieval theo thời gian.
- Không hardcode một vector database hay một nhà cung cấp LLM cụ thể vào contract của module. Interface phải được thiết kế trung lập với nhà cung cấp ngay từ đầu — kể cả khi chỉ có một adapter được triển khai trong giai đoạn đầu — vì chi phí chuyển đổi sau này (do đổi giá, do giới hạn quy mô, do yêu cầu compliance về nơi lưu trữ dữ liệu) luôn cao hơn chi phí thiết kế interface đúng ngay từ đầu.
Architect's Verdict
RAG không phải một tính năng AI được dán vào hệ thống — nó là một bài toán kiến trúc pipeline dữ liệu, có ingest, có storage, có truy vấn, có fallback, giống hệt bất kỳ hệ thống tìm kiếm hay hệ thống đề xuất nào bạn đã từng thiết kế trước khi LLM xuất hiện. Sự khác biệt duy nhất là thành phần cuối của pipeline sinh ra đầu ra không xác định, và chính vì vậy nó càng cần kỷ luật kiến trúc chặt hơn, không phải lỏng hơn. Nếu đội của bạn đang gọi thẳng API LLM từ business logic vì "chỉ là một tính năng AI nhỏ", hãy hỏi lại: nếu đây là một tích hợp thanh toán hay một tích hợp bên thứ ba bất kỳ khác, bạn có chấp nhận thiết kế này không? Câu trả lời thường là không — và AI không xứng đáng được một tiêu chuẩn thấp hơn.
🔗 Bài viết liên quan: Hexagonal Architecture (Ports & Adapters) · LLM Gateway & Governance
Bài trước: Technical Debt Management · Bài tiếp theo: LLM Gateway & Governance