spring ai và kiến trúc rag
Tung Bui Thanh (TungDaDev)17 mins read

Ở hệ thống doanh nghiệp, một thư viện AI dùng được không thể chỉ là cái vỏ bọc HTTP client gọi sang OpenAI hay Gemini. Nó còn phải cho đổi model dễ dàng (model portability), giữ được ngữ cảnh hội thoại (context management), có lớp chặn bảo mật (guardrails), làm RAG theo chuẩn, và cắm vừa vào hệ thống observability / resilience mà tổ chức đang có.
Làn sóng Generative AI kéo theo cả một hệ sinh thái Python nở rộ (LangChain, LlamaIndex). Có điều, ở ngân hàng, công ty tài chính hay sàn thương mại điện tử lớn, backend lõi gần như toàn chạy Java / Spring Boot với cả trăm microservices. Dựng thêm một service Python chỉ để làm "cầu nối AI" ở đó sẽ vướng mấy chỗ khá đau:
- Thêm một network hop: mỗi lượt gọi phải serialize, đi qua mạng sang Python gateway rồi quay về. So với vài giây chờ LLM thì không đáng kể, nhưng là thêm một service phải deploy, scale và trực sự cố.
- Vận hành tách rời: phải đồng bộ lại distributed tracing (W3C TraceContext / OpenTelemetry), security (OAuth2/JWT), cách quản lý cấu hình và secret giữa hai stack khác nhau.
- Hai bộ kỹ năng: team Java phải đọc, review và trực sự cố cả code Python, còn domain model và DTO thì phải định nghĩa lặp lại ở hai bên.
Spring AI (1.0 GA tháng 5/2025, bài này viết theo nhánh 1.1) ra đời để gỡ đúng mấy nút thắt này. Phần dưới đi từ các abstraction chính cho tới mấy blueprint dùng được cho production.
NOTE
Tới tháng 10/2026, Spring AI đã lên 2.0. Phần lớn khái niệm trong bài vẫn đúng, nhưng 2.0 bỏ cách đăng ký tool bằng tên Function bean (xem mục function calling). Đọc kèm release notes nếu bạn dùng 2.x.
# bản đồ kiến trúc tổng thể (architectural topology)
Spring AI bám khá sát Dependency Inversion Principle (DIP): code nghiệp vụ chỉ phụ thuộc vào các abstraction (ChatClient, ChatModel, VectorStore, EmbeddingModel), tách khỏi SDK riêng của OpenAI, Anthropic, Google Vertex AI, AWS Bedrock hay Ollama. Tách được tới đâu thì còn tuỳ: tính năng riêng của từng provider (tham số đặc thù, prompt caching, các kiểu tool riêng) vẫn phải đi qua ChatOptions riêng của provider đó.
# so sánh hai cách dùng: chatmodel vs chatclient
| Tiêu chí | ChatModel (Low-Level Driver) | ChatClient (High-Level Enterprise API) |
|---|---|---|
| Cách hoạt động | Interface mỏng, gần sát với Provider API | Fluent API dựng trên Builder Pattern, cấu hình mặc định sẵn |
| Advisor Pipeline | Không có sẵn; muốn can thiệp thì tự viết code | Có chuỗi interceptor (Advisors), na ná Spring Security Filters |
| Quản lý Context | Tự tay append danh sách Message (System, User, Assistant) | Qua memory advisor + ChatMemory (In-Memory, JDBC, Cassandra) |
| Output Parsing | Trả về ChatResponse thô, tự convert JSON sang DTO | Có entity(Class<T>), tự bơm schema và kiểm tra type |
| Nên dùng khi nào | Chỉ khi cần đụng sâu vào driver hoặc viết custom connector | Mặc định dùng cái này cho toàn bộ code nghiệp vụ |
# deep dive: chuỗi advisors (advisor interceptor chain)
Thiết kế mình thấy đáng giá nhất của Spring AI là Advisor API. Ý tưởng lấy thẳng từ ClientHttpRequestInterceptor của Spring và AOP (Aspect-Oriented Programming).
Request gửi qua ChatClient không đi thẳng tới LLM, mà phải đi qua một chuỗi advisor. Mỗi advisor (interface CallAdvisor / StreamAdvisor) nhận ChatClientRequest, có thể sửa nó, gọi advisor kế tiếp trong chain, rồi xử lý ChatClientResponse trên đường về. Thứ tự do getOrder() quyết định: order nhỏ chạy trước ở chiều đi và chạy sau ở chiều về, giống filter của Servlet.
Tracing và metrics thì không cần advisor riêng: ChatClient và từng advisor đều tự sinh observation qua Micrometer.
# cơ chế hoạt động của questionansweradvisor
Khi bật RAG, QuestionAnswerAdvisor sẽ:
Lấy câu hỏi của user ra từ
ChatClientRequest.Tạo vector embedding cho câu hỏi đó qua
EmbeddingModel.Gửi truy vấn tới
VectorStore, kèm metadata filter,topKvà ngưỡng similarity (similarityThreshold).Nhét các đoạn văn tìm được vào template ngữ cảnh. Template mặc định ở bản 1.1:
{query} Context information is below, surrounded by --------------------- --------------------- {question_answer_context} --------------------- Given the context and provided history information and not prior knowledge, reply to the user comment. If the answer is not in the context, inform the user that you can't answer the question.Thay user message bằng bản đã ghép context (system prompt giữ nguyên) trước khi payload được gửi ra mạng. Muốn đổi template thì dùng
.promptTemplate(...), nhớ giữ hai placeholderqueryvàquestion_answer_context.
Advisor này chỉ là RAG "ngây thơ": embed nguyên câu hỏi rồi tìm. Câu hỏi kiểu "thế còn cái kia thì sao?" ở lượt thứ năm của cuộc hội thoại sẽ tìm ra rác. Spring AI có thêm RetrievalAugmentationAdvisor với các bước rewrite/transform query cho những ca như vậy.
# kiến trúc RAG doanh nghiệp (enterprise RAG pipeline)
Muốn dựng pipeline RAG chịu tải cao trên Spring AI (giả sử đặt mục tiêu P99 phần truy xuất dưới 800ms, chưa tính thời gian LLM sinh câu trả lời) thì cần để ý mấy thành phần sau:
# triển khai pgvector tối ưu với Spring Boot 3.4+
SimpleVectorStore (lưu trên RAM) chỉ hợp để chạy thử và unit test. Lên production thì phải dùng database chuyên dụng. Ví dụ cấu hình PgVector với HNSW index, loại index cho phép tìm láng giềng gần đúng (ANN) với độ trễ vài ms tới vài chục ms ngay cả khi có hàng triệu vector, tuỳ phần cứng và tham số:
-- Migration script V1__init_vector_store.sql
CREATE EXTENSION IF NOT EXISTS vector;
CREATE TABLE IF NOT EXISTS enterprise_vector_store (
id UUID PRIMARY KEY, -- PgVectorStore mặc định dùng id kiểu uuid
content TEXT,
metadata JSON, -- schema mặc định của Spring AI dùng json
embedding VECTOR(1536) -- Khớp với chiều không gian của embedding model
);
-- HNSW Index: Tối ưu triệt để tốc độ tìm kiếm láng giềng gần nhất (ANN)
CREATE INDEX IF NOT EXISTS enterprise_vector_store_hnsw_idx
ON enterprise_vector_store
USING hnsw (embedding vector_cosine_ops)
WITH (m = 16, ef_construction = 64);Cấu hình trong application.yml:
spring:
ai:
vectorstore:
pgvector:
index-type: HNSW
distance-type: COSINE_DISTANCE
dimensions: 1536
table-name: enterprise_vector_store
initialize-schema: false # tự quản lý schema bằng Flyway/Liquibase như trênTừ bản 1.0, initialize-schema mặc định là false: Spring AI không tự tạo bảng nữa, nên hoặc bật nó lên, hoặc tự tạo bằng migration như trên. Tự tạo thì giữ đúng cấu trúc cột mà PgVectorStore mong đợi. Tham số HNSW m = 16, ef_construction = 64 là giá trị mặc định của pgvector; lúc truy vấn có thể tăng hnsw.ef_search (mặc định 40) để đổi tốc độ lấy độ chính xác.
# Function calling (tool execution) & agentic workflows
Spring AI biến class Java bình thường thành "công cụ" (tools) để mô hình tự gọi khi cần đụng tới thế giới bên ngoài.
Spring AI dựa vào type system của Java (qua thư viện victools JSON Schema Generator kèm module Jackson) để sinh JSON Schema khớp với kiểu tham số của tool. Model chỉ đề xuất lời gọi tool kèm tham số; chính ứng dụng của bạn mới là bên chạy tool:
# cách viết Function calling chuẩn cho enterprise
Ở Spring AI 1.x có hai cách khai báo tool: method gắn @Tool, hoặc một Function bean kèm @Description rồi gọi theo tên bean. Ví dụ dưới dùng cách thứ hai. Lưu ý Spring AI 2.0 đã bỏ cách gọi Function bean theo tên; ở 2.x phải khai báo bean ToolCallback (vd FunctionToolCallback.builder(...)) hoặc dùng @Tool.
@Configuration
public class BankingToolsConfiguration {
public record TransferRequest(
@JsonProperty(required = true, value = "sourceAccount")
String sourceAccount,
@JsonProperty(required = true, value = "targetAccount")
String targetAccount,
@JsonProperty(required = true, value = "amount")
BigDecimal amount,
@JsonProperty(value = "currency", defaultValue = "VND")
String currency
) {}
public record TransferResponse(String status, String transactionId, String message) {}
@Bean
@Description("Thực hiện chuyển tiền giữa hai tài khoản ngân hàng nội bộ khi khách hàng yêu cầu rõ ràng.")
public Function<TransferRequest, TransferResponse> transferMoneyTool(BankingService bankingService) {
return request -> {
// Business validation & Transaction isolation
var txId = bankingService.executeTransfer(
request.sourceAccount(),
request.targetAccount(),
request.amount(),
request.currency()
);
return new TransferResponse("SUCCESS", txId, "Chuyển tiền thành công");
};
}
}Gắn vào Service qua ChatClient:
@Service
public class FinancialAdvisorService {
private final ChatClient chatClient;
public FinancialAdvisorService(ChatClient.Builder chatClientBuilder, ChatMemory chatMemory) {
this.chatClient = chatClientBuilder
.defaultToolNames("transferMoneyTool") // Tham chiếu Function bean theo tên (Spring AI 1.x)
.defaultAdvisors(MessageChatMemoryAdvisor.builder(chatMemory).build())
.build();
}
public String processBankingRequest(String userPrompt, String userId) {
return chatClient.prompt()
.user(userPrompt)
// Chỉ có tác dụng khi đã đăng ký memory advisor ở trên
.advisors(advisorSpec -> advisorSpec.param(ChatMemory.CONVERSATION_ID, userId))
.call()
.content();
}
}CAUTION
Ví dụ chuyển tiền chỉ để minh hoạ cơ chế. Đừng để model tự quyết định một thao tác không đảo ngược được như chuyển tiền. Nội dung đi vào model (tin nhắn của user, tài liệu RAG, kết quả tool khác) đều có thể chứa prompt injection, và model có thể điền sai tham số. Tool nên chỉ tạo lệnh chờ xác nhận; việc xác thực (OTP, xác nhận trên UI) và phân quyền phải do code của bạn kiểm tra theo user đang đăng nhập, không phải theo tham số model đưa vào.
# structured outputs & type-safe extraction
Đưa LLM vào backend thì một trong những rủi ro lớn nhất là dữ liệu trả về dạng text tự do. LLM chỉ cần chêm một câu chào ("Chắc chắn rồi, đây là dữ liệu của bạn: ...") là parser JSON ném exception ngay.
Spring AI có BeanOutputConverter<T> (chính là thứ chạy sau .entity(...)): sinh JSON Schema từ class rồi nhét hướng dẫn định dạng vào prompt, sau đó dọn bớt phần thừa (vd dấu rào code block markdown bọc quanh JSON) và parse bằng Jackson. Model vẫn có thể trả sai, khi đó parse ném exception. Với provider có hỗ trợ structured output gốc (ép model sinh đúng schema), bật nó lên sẽ chắc hơn nhiều:
public record LoanEvaluationResult(
boolean approved,
BigDecimal creditLimit,
double riskScore,
List<String> rejectionReasons,
RiskTier riskTier
) {
public enum RiskTier { LOW, MEDIUM, HIGH, CRITICAL }
}
@Service
public class CreditUnderwritingService {
private final ChatClient chatClient;
public CreditUnderwritingService(ChatClient.Builder chatClientBuilder) {
this.chatClient = chatClientBuilder.build();
}
public LoanEvaluationResult evaluateProfile(String customerFinancialDossier) {
return chatClient.prompt()
.system("Bạn là hệ thống thẩm định tín dụng tự động. Đánh giá hồ sơ và trả về kết quả cấu trúc chuẩn.")
.user(customerFinancialDossier)
.call()
.entity(LoanEvaluationResult.class); // Parse sai định dạng thì ném RuntimeException (bọc JsonProcessingException)
}
}# resilience, fallback & multi-model routing
Model AI trên cloud lúc nào cũng có thể chậm vì mạng, hoặc dính rate limit gắt (HTTP 429 Too Many Requests). Hệ thống của bạn không nên sập theo chỉ vì API của OpenAI trục trặc.
# kiến trúc dự phòng active-passive failover
# code cấu hình resilience & dynamic routing
@Slf4j
@Service
public class ResilientAIService {
private final ChatModel openAiChatModel;
private final ChatModel vertexAiChatModel;
private final ChatModel ollamaChatModel;
// Inject theo kiểu cụ thể, không phải đoán tên bean do auto-config sinh ra
public ResilientAIService(
OpenAiChatModel openAiChatModel,
VertexAiGeminiChatModel vertexAiChatModel,
OllamaChatModel ollamaChatModel
) {
this.openAiChatModel = openAiChatModel;
this.vertexAiChatModel = vertexAiChatModel;
this.ollamaChatModel = ollamaChatModel;
}
// Resilience4j mặc định bọc Retry ở ngoài CircuitBreaker. Nếu đặt fallback ở @CircuitBreaker,
// fallback "nuốt" exception ngay lần lỗi đầu và Retry không bao giờ được chạy.
// Đặt fallback ở @Retry: thử lại xong vẫn lỗi (hoặc circuit OPEN) mới chuyển sang Vertex AI.
@Retry(name = "aiServiceRetry", fallbackMethod = "fallbackToVertexAI")
@CircuitBreaker(name = "aiServiceCircuitBreaker")
public String executeAIInference(String prompt) {
return openAiChatModel.call(prompt);
}
public String fallbackToVertexAI(String prompt, Exception ex) {
log.warn("OpenAI gặp sự cố ({}), kích hoạt dự phòng Google Vertex AI", ex.getMessage());
try {
return vertexAiChatModel.call(prompt);
} catch (Exception vertexEx) {
return fallbackToLocalOllama(prompt, vertexEx);
}
}
public String fallbackToLocalOllama(String prompt, Exception ex) {
log.error("Cả 2 Cloud Model đều sập, chuyển về Local SLM (Ollama)", ex);
return ollamaChatModel.call(prompt);
}
}Hai cái bẫy khi làm theo mẫu này. Thứ nhất, các model client của Spring AI (OpenAI, Vertex...) đã tự retry sẵn qua module spring-ai-retry, mặc định tới 10 lần với exponential backoff bắt đầu từ 2 giây. Chồng thêm @Retry của Resilience4j là số lần thử nhân lên và một request có thể treo vài phút. Nên giảm spring.ai.retry.max-attempts hoặc chỉ giữ một lớp retry. Thứ hai, có nhiều ChatModel bean thì auto-config ChatClient.Builder không biết chọn cái nào; phải tắt nó (spring.ai.chat.client.enabled=false) rồi tự tạo ChatClient cho từng model.
Fallback sang model khác cũng không "miễn phí": prompt viết cho model này chưa chắc chạy tốt trên model kia, nhất là model nhỏ tự host. Nên có bộ test prompt cho từng model dự phòng.
# observability: giám sát chi phí & độ trễ với OpenTelemetry
Không đo được thì cũng không tối ưu được. Spring AI tích hợp sẵn với Micrometer Observation API, tự sinh metrics và distributed trace spans, chạy được với OpenTelemetry, Grafana Tempo và Prometheus. Tên dưới đây là tên observation/metric theo tài liệu Spring AI; khi export sang Prometheus, dấu chấm thành gạch dưới và thêm hậu tố (_seconds_count, _total...).
# metrics nên có dashboard & alert
| Tên metric | Kiểu | Ý nghĩa với SRE / Architect |
|---|---|---|
gen_ai.client.operation | Timer | Phân bố độ trễ (P50, P90, P99) của từng lượt gọi model, theo provider |
gen_ai.client.token.usage | Counter | Tổng số token (input vs output) đã dùng, để tính chi phí theo thời gian thực |
db.vector.client.operation | Timer | Tốc độ truy vấn Vector DB (đặt ngưỡng cảnh báo theo SLO của bạn) |
spring.ai.chat.client.active | LongTaskTimer | Số request AI đang chạy đồng thời (để không bị cạn Thread Pool) |
# checklist trước khi lên production
Trước khi bấm deploy một service Spring AI lên production, nên rà lại 7 câu sau:
- 1. Vector DB Indexing: bảng vector đã có index
HNSWhoặcIVFFlatchưa, và operator class của index (vdvector_cosine_ops) có khớp với kiểu khoảng cách lúc query không? Lệch nhau là PostgreSQL bỏ qua index và quét cả bảng. - 2. Semantic Caching: mấy câu hỏi lặp lại của user đã được chặn ở tầng cache (Redis) với ngưỡng cosine similarity chưa?
- 3. Token Quota & Guardrails: đã đặt giới hạn
maxTokenscho response chưa, để LLM không sinh chữ vô tận đốt sạch ngân sách? - 4. Circuit Breaker: các lượt gọi API bên ngoài đã được bọc bằng Resilience4j chưa, để lỗi không lan dây chuyền (cascading failure) làm nghẽn Tomcat worker threads?
- 5. Prompt Injection Defense: đã có lớp lọc đầu vào để chặn mấy kiểu jailbreak như "Ignore all previous instructions" chưa? Lọc theo từ khoá chỉ chặn được kiểu thô; quan trọng hơn là giới hạn quyền của tool và không tin tham số model đưa ra.
- 6. Distributed Tracing: đã truyền header W3C
traceparentđể trace một mạch từ Frontend -> API Gateway -> Spring AI -> Model Provider chưa? - 7. PII Redaction: đã có bộ lọc che thông tin cá nhân (số CCCD, số thẻ ngân hàng, số điện thoại) trước khi gửi payload lên model cloud của bên thứ ba chưa?
# lời kết
Spring AI không phải kiểu chữa cháy cho có để "mang AI vào Java". Kiến trúc chia tầng rõ ràng, chuỗi advisor linh hoạt, abstraction tách khỏi nhà cung cấp, lại dùng lại được những gì hệ sinh thái Spring đang có (Security, Observability, Data). Với team đã chạy Spring Boot, đó là điểm xuất phát hợp lý. Có điều framework đổi khá nhanh (1.0 tới 2.0 chỉ khoảng một năm), nên ghim phiên bản và đọc release notes trước mỗi lần nâng cấp.
Chỉ là những ghi chép cá nhân với hy vọng mang lại chút giá trị. Nếu thấy hữu ích, đừng ngại chia sẻ cho bạn bè & đồng nghiệp nhé!
Happy coding 😎 👍🏻 🚀 🔥.