Vấn đề
Cuối năm 2024, tôi tham gia một dự án AI cho cơ quan nhà nước. Yêu cầu: "Hệ thống hỏi đáp trên văn bản pháp luật - nhân viên có thể hỏi bằng ngôn ngữ tự nhiên, hệ thống trả lời dựa trên văn bản luật."
Nghe như RAG tutorial chuẩn. Rồi họ nói thêm:
- "Data không được rời khỏi data center của chúng tôi"
- "Không dùng cloud AI API - không dùng OpenAI, không dùng Claude API"
- "Network isolated - không có internet access"
- "Phải audit log mọi query và response"
- "Model phải có khả năng explain tại sao đưa ra câu trả lời đó"
Đây là lúc tôi biết: Đây không phải tutorial RAG. Đây là enterprise RAG với constraints cực kỳ khắt khe.
Giải thích đơn giản: RAG là gì và tại sao nó phù hợp
RAG (Retrieval-Augmented Generation) là pattern: thay vì "train" LLM với data của bạn (đắt, chậm, không flexible), bạn:
- Lưu documents vào vector database
- Khi user hỏi, tìm documents liên quan (retrieval)
- Đưa documents + câu hỏi vào LLM → LLM trả lời dựa trên context đó
Nó như là: "Này LLM, đây là 5 trang văn bản luật liên quan. Dựa vào đó, trả lời câu hỏi này."
Lợi thế lớn nhất: Data không cần "train vào model". Bạn cập nhật vector database là xong - không cần retrain model.
Nhưng constraint của dự án này làm mọi thứ phức tạp hơn nhiều.
Kiến trúc on-premise RAG pipeline
Lớp 1: Document Processing Pipeline
Văn bản pháp luật (PDF, DOCX)
↓
Text Extraction (Apache Tika)
↓
Chunking Strategy
↓
Embedding Generation (on-premise model)
↓
Vector Database (Qdrant on-premise)
public class DocumentProcessor
{
private readonly IEmbeddingModel _embeddingModel; // On-premise
private readonly IVectorStore _vectorStore; // Qdrant local
private readonly ITextChunker _chunker;
public async Task ProcessDocumentAsync(LegalDocument document)
{
// Extract text
var rawText = await _textExtractor.ExtractAsync(document.FilePath);
// Chunk với overlap để không mất context ở ranh giới chunk
var chunks = _chunker.Chunk(rawText, new ChunkOptions
{
MaxTokens = 512,
OverlapTokens = 50,
SplitStrategy = SplitStrategy.SemanticBoundary // Ưu tiên split ở ranh giới câu/đoạn
});
// Generate embeddings - on-premise model
foreach (var (chunk, index) in chunks.WithIndex())
{
var embedding = await _embeddingModel.GetEmbeddingAsync(chunk.Text);
await _vectorStore.UpsertAsync(new VectorDocument
{
Id = $"{document.Id}_chunk_{index}",
Vector = embedding,
Metadata = new Dictionary<string, object>
{
["documentId"] = document.Id,
["documentName"] = document.Name,
["chunkIndex"] = index,
["text"] = chunk.Text,
["pageNumber"] = chunk.PageNumber,
["effectiveDate"] = document.EffectiveDate
}
});
}
}
}
Lớp 2: On-premise LLM Setup
Đây là phần khác biệt nhất so với cloud setup.
Model choice: Qwen2.5-7B-Instruct - lý do:
- Hỗ trợ tiếng Việt tốt
- 7B parameters - chạy được trên server 4x RTX 3090 (48GB VRAM total)
- License cho phép commercial use
- Performance acceptable cho legal Q&A
# docker-compose.yml cho Ollama on-premise
version: '3.8'
services:
ollama:
image: ollama/ollama:latest
volumes:
- ollama_data:/root/.ollama
deploy:
resources:
reservations:
devices:
- driver: nvidia
count: 4
capabilities: [gpu]
environment:
- OLLAMA_NUM_PARALLEL=4
- OLLAMA_MAX_LOADED_MODELS=2
public class OnPremiseLLMClient : ILLMClient
{
private readonly HttpClient _httpClient;
private readonly string _modelName = "qwen2.5:7b-instruct";
public async Task<string> CompleteAsync(string prompt)
{
var request = new
{
model = _modelName,
prompt = prompt,
stream = false,
options = new
{
temperature = 0.1, // Low temperature cho legal context - cần chính xác
num_ctx = 4096,
top_p = 0.9
}
};
var response = await _httpClient.PostAsJsonAsync("/api/generate", request);
var result = await response.Content.ReadFromJsonAsync<OllamaResponse>();
return result?.Response ?? string.Empty;
}
}
Lớp 3: Query Pipeline với Audit Logging
public class LegalRAGService
{
private readonly IVectorStore _vectorStore;
private readonly ILLMClient _llmClient;
private readonly IAuditLogger _auditLogger;
private readonly IEmbeddingModel _embeddingModel;
public async Task<RAGResponse> QueryAsync(string userId, string userQuery)
{
var auditEntry = new AuditEntry
{
UserId = userId,
Query = userQuery,
Timestamp = DateTime.UtcNow
};
try
{
// 1. Convert query thành vector
var queryVector = await _embeddingModel.GetEmbeddingAsync(userQuery);
// 2. Retrieve relevant chunks
var relevantChunks = await _vectorStore.SearchAsync(queryVector, topK: 5,
minScore: 0.7); // Threshold để filter noise
if (!relevantChunks.Any())
{
return new RAGResponse
{
Answer = "Không tìm thấy văn bản pháp luật liên quan đến câu hỏi này.",
Sources = Array.Empty<SourceReference>(),
Confidence = 0
};
}
// 3. Build prompt với retrieved context
var context = BuildContext(relevantChunks);
var prompt = BuildLegalPrompt(userQuery, context);
// 4. Generate answer
var answer = await _llmClient.CompleteAsync(prompt);
// 5. Extract source references for explainability
var sources = relevantChunks.Select(c => new SourceReference
{
DocumentName = c.Metadata["documentName"].ToString(),
PageNumber = (int)c.Metadata["pageNumber"],
RelevanceScore = c.Score,
ExcerptText = c.Metadata["text"].ToString()[..Math.Min(200, c.Metadata["text"].ToString().Length)]
}).ToList();
auditEntry.Response = answer;
auditEntry.Sources = sources;
auditEntry.Success = true;
return new RAGResponse { Answer = answer, Sources = sources, Confidence = relevantChunks.Average(c => c.Score) };
}
catch (Exception ex)
{
auditEntry.Error = ex.Message;
throw;
}
finally
{
await _auditLogger.LogAsync(auditEntry); // LUÔN log, dù success hay fail
}
}
private string BuildLegalPrompt(string query, string context)
{
return $"""
Bạn là trợ lý pháp luật. Chỉ trả lời dựa trên văn bản pháp luật được cung cấp.
Nếu thông tin không có trong văn bản, hãy nói rõ "Tôi không tìm thấy thông tin này trong văn bản được cung cấp."
KHÔNG suy đoán hoặc thêm thông tin ngoài văn bản.
VĂN BẢN PHÁP LUẬT THAM KHẢO:
{context}
CÂU HỎI: {query}
TRẢ LỜI:
""";
}
}
So sánh: Cloud RAG vs On-premise Government RAG
| Khía cạnh | Cloud RAG | Government On-premise RAG |
|---|---|---|
| Model chất lượng | GPT-4o, Claude | Qwen2.5-7B (lower quality) |
| Setup complexity | Thấp | Cao (GPU infra, networking) |
| Latency | ~2-3s | ~3-8s (phụ thuộc hardware) |
| Cost model | Pay-per-token | CapEx (hardware upfront) |
| Data security | Trust provider | Hoàn toàn kiểm soát |
| Updates | Automatic | Manual, cần process |
| Scalability | Unlimited | Giới hạn bởi hardware |
Best practices từ kinh nghiệm thực
1. Chunking strategy là critical. Tôi đã thử 3 strategies:
- Fixed-size chunking: đơn giản nhưng cắt đứt context pháp luật
- Sentence-based: tốt hơn nhưng câu luật thường dài
- Paragraph-based với overlap: tốt nhất cho văn bản pháp luật
2. Hybrid search luôn tốt hơn pure vector search. Kết hợp BM25 + vector search:
// Reciprocal Rank Fusion
var hybridResults = RRFMerge(keywordResults, vectorResults, k: 60);
3. Confidence threshold là quan trọng. Đặt minimum similarity score. Nếu không có chunk nào score đủ cao, trả về "không tìm thấy" thay vì hallucinate.
4. Audit log không thể thiếu. Không chỉ để compliance - còn để debug khi model trả lời sai.
Kết
On-premise RAG cho government systems phức tạp hơn nhiều so với cloud demo. Nhưng khi constraints là absolute (data sovereignty, network isolation), đây là con đường duy nhất.
Bài học lớn nhất: Đừng pick model trước, pick constraints trước. Khi constraints rõ ràng, model và architecture sẽ tự nhiên follow.
Tham khảo
- Qdrant documentation: qdrant.tech/documentation
- Ollama on-premise deployment: ollama.ai
- LangChain RAG patterns: python.langchain.com
/Son Do - believe in basic
#1percentbetter #AIArchitecture #RAG #LLM #Enterprise #OnPremise