Bộ công nghệ "không ai khuyên dùng" nhưng lại cực kỳ hiệu quả cho hệ thống AI cá nhân

05 tháng 4, 2026·12 phút đọc

Sau khi chia sẻ về việc vận hành 22 dịch vụ Docker tại nhà, tác giả giải thích lý do đằng sau các lựa chọn công nghệ quan trọng như FastAPI, PostgreSQL, n8n và Ollama, cân bằng giữa sự quen thuộc và hiệu quả kỹ thuật trong một hệ thống AI tự triển khai.

Bộ công nghệ "không ai khuyên dùng" nhưng lại cực kỳ hiệu quả cho hệ thống AI cá nhân

Sau khi đăng tải phần 1 về hệ thống 22 dịch vụ Docker tại nhà, câu hỏi phổ biến nhất tôi nhận được là "tại sao ông lại chọn X thay vì Y?". Bài viết này sẽ giải thích về điều đó. Mọi lựa chọn công nghệ lớn, những gì tôi thực sự cân nhắc, nơi tôi đã đúng, và nơi tôi đã may mắn.

Tôi sẽ thẳng thắn ngay từ đầu: một số quyết định được đưa ra dựa trên hiểu biết. Một số khác là "tôi đã biết công cụ này và tôi cần di chuyển nhanh". Cả hai đều hợp lệ, nhưng chúng dẫn đến những đánh đổi khác nhau sau này.

Backend: FastAPI

Tôi xuất thân từ JavaScript và TypeScript. Nhiều năm làm việc với React ở frontend, Express và Fastify ở backend. Khi quyết định dự án này sẽ sử dụng Python (bởi đó là nơi hệ sinh thái AI/ML sinh sống), tôi cần một thứ gì đó không quá xa lạ.

FastAPI đã "bắt sóng" ngay lập tức. Mô hình async/await, định tuyến dựa trên decorator và các gợi ý kiểu (type hints) thực sự hoạt động hiệu quả. Nó cảm giác như đang viết Fastify bằng Python. Sự quen thuộc không phải là lý do duy nhất, nhưng tôi sẽ nói dối nếu khẳng định nó không phải là yếu tố tác động.

Những lý do kỹ thuật cũng rất thuyết phục. Hệ thống xử lý các callback webhook đồng thời từ n8n, thăm dò thời gian thực từ bảng điều khiển React và các kết nối asyncpg liên tục với PostgreSQL. Tất cả đều là I/O bất đồng bộ, và FastAPI được xây dựng xung quanh mô hình này. Django hiện tại cũng đã hỗ trợ async, nhưng nó vẫn cảm giác như là một tính năng được thêm vào sau đó chứ không được thiết kế sẵn từ đầu.

Tôi cũng cố tình tránh sử dụng ORM. Mọi truy vấn trong hệ thống đều là SQL viết tay thông qua asyncpg. Với hơn 95 bảng trên 9 lĩnh vực, tôi muốn thấy chính xác những gì đang được gửi tới cơ sở dữ liệu. Không có sự "ma thuật", không có bất ngờ N+1, không có khung migration tạo ra các câu lệnh SQL mà tôi chưa đọc qua.

Giá tôi phải trả khi bỏ qua Django? Không có bảng quản trị (admin panel) miễn phí; tôi đã phải xây dựng bảng điều khiển React từ đầu, điều này mất vài tuần. Không có hệ thống migration tích hợp, tôi quản lý các thay đổi lược đồ bằng các tệp SQL thô được chuyển qua SSH vào Docker, điều này đã gây ra rắc rối cho tôi không chỉ một lần. Và hệ sinh thái plugin mỏng hơn khi tôi cần những thứ mà Django đã có từ 20 năm nay.

Nếu bạn đang xây dựng ứng dụng web với tài khoản người dùng, bảng quản trị và biểu mẫu, hãy dùng Django. FastAPI có ý nghĩa khi backend của bạn là một lớp API điều phối giữa các dịch vụ, và đó chính là trường hợp của tôi.

Cơ sở dữ liệu: PostgreSQL

Đây không phải là một quyết định khó khăn. Dữ liệu của tôi mang tính quan hệ sâu sắc, các giao dịch liên kết với tài khoản ngân hàng, phân loại email tham chiếu đến tin nhắn, kiến thức thực tế được củng cố trên nhiều nguồn, tác vụ lập kế hoạch tham chiếu đến các tác nhân tham chiếu đến mô hình. Cố gắng làm điều này bằng MongoDB sẽ đồng nghĩa với việc chuẩn hóa lại mọi thứ, nhúng tài liệu vào trong tài liệu và xử lý tính nhất quán thủ công.

Tuy nhiên, PostgreSQL mang lại cho tôi những thứ vượt ra ngoài lưu trữ quan hệ thuần túy, điều hóa ra lại quan trọng hơn tôi nghĩ.

Tính năng LISTEN/NOTIFY đã thay thế những gì thường đòi hỏi một hàng đợi tin nhắn (message queue). Khi một email được phân loại, một kích hoạt (trigger) sẽ phát đi thông báo. Dịch vụ não bộ (brain service) bắt được nó trong vài mili-giây qua asyncpg và phản ứng. Không cần Kafka, không cần RabbitMQ; chỉ là một tính năng tích hợp đã có trong Postgres từ nhiều năm nay:

CREATE OR REPLACE FUNCTION notify_email_classified()
RETURNS TRIGGER AS $$
BEGIN
    PERFORM pg_notify('email_classified',
        json_build_object(
            'id', NEW.id,
            'category', NEW.category,
            'urgency', NEW.urgency
        )::text
    );
    RETURN NEW;
END;
$$ LANGUAGE plpgsql;

Ở quy mô của tôi (có lẽ 50-100 sự kiện mỗi giờ), điều này là quá đủ. Việc thêm Kafka sẽ đồng nghĩa với việc thêm một container khác, một cấu hình khác để duy trì và một thứ khác có thể bị lỗi lúc 3 giờ sáng. Tôi sẽ thêm nó khi thực sự cần nó.

Các ràng buộc CHECK (CHECK constraints) hóa ra là một trong những quyết định tốt nhất của cả dự án. Cơ sở dữ liệu thực thi các danh mục mà AI được phép xuất ra:

category VARCHAR(50) CHECK (category IN (
    'billing', 'shipping', 'subscription',
    'employment', 'legal', 'marketing',
    'personal', 'automated' ...
))

LLM đôi khi bỏ qua hướng dẫn của bạn. Một lần, công cụ trích xuất đã bịa ra một danh mục không có trong danh sách cho phép, và câu lệnh INSERT đã thất bại. Đó chính xác là điều nên xảy ra: một sự cố lớn (loud failure) tốt hơn nhiều là âm thầm làm ô nhiễm dữ liệu của bạn với các danh mục không hợp lệ.

Tôi cũng sử dụng các hàm cửa sổ (window functions) và truy vấn khoảng thời gian để giới hạn tốc độ, thời gian làm mạch và cầu chì. Tất cả là những thứ bạn thường nghĩ đến Redis để thực hiện. Bớt đi một container trong stack.

Nơi MongoDB sẽ thắng: dữ liệu có hình dạng tài liệu thực sự với các lược đồ thay đổi. Nội dung CMS, hồ sơ người dùng có các trường không đồng nhất và nhật ký sự kiện với các tải trọng (payload) khác nhau. Dữ liệu của tôi không thuộc loại nào trong số này.

Công cụ quy trình làm việc: n8n

Đây là quyết định mà tôi có cảm xúc phức tạp nhất.

n8n là một trình soạn thảo quy trình làm việc trực quan tự lưu trữ (self-hosted). Bạn kết nối các kích hoạt, yêu cầu HTTP, truy vấn cơ sở dữ liệu và các nút mã hóa (code nodes). Đối với các đường ống email của tôi, khả năng nhìn thấy luồng dưới dạng biểu đồ thực sự có giá trị. Khi có sự cố, tôi có thể nhìn thấy chính xác bước nào thất bại và dữ liệu nó có là gì.

Yếu tố tự lưu trữ ngay lập tức loại trừ Zapier và Make. Các quy trình làm việc của tôi xử lý nội dung email và dữ liệu tài chính. Điều đó không thể đi qua bên thứ ba. Và các nút mã hóa của n8n cho phép tôi thả JavaScript trực tiếp vào một bước quy trình làm việc, đó là cách tôi xây dựng các tải trọng JSON phức tạp cho các cuộc gọi Ollama.

Nhưng n8n đã gây ra nhiều sự cố sản xuất hơn bất kỳ thành phần nào khác trong hệ thống. Các quy trình làm việc theo lịch trình bị chồng chéo vì n8n không ngăn chặn việc thực thi đồng thời theo mặc định. Tôi đã phải xây dựng một lớp bảo vệ ở cấp cơ sở dữ liệu để kiểm tra xem lần chạy trước đó vẫn còn đang tiến hành hay không. API âm thầm cắt ngắn các truy vấn SQL dài mà không có bất kỳ lỗi nào. Các nút mã hóa chạy trong môi trường cô lập V8 nơi process.env không tồn tại (bạn cần $env thay thế), và việc xây dựng JSON trong các biểu thức Yêu cầu HTTP đủ mong manh nên các tải trọng phức tạp luôn nên đi qua Nút mã hóa trước.

Không một cái nào trong số này là "báo động đỏ" (dealbreakers) về mặt riêng lẻ. Nhưng tổng thể, n8n đòi hỏi một mức độ lập trình phòng thủ mà tôi không mong đợi từ một công cụ quy trình làm việc. Mọi quy trình làm việc liên quan đến cuộc gọi LLM giờ đây đều có kiểm tra xếp chồng (stacking check), mọi truy vấn SQL đều được xác minh sau khi triển khai, và tôi đã học cách xây dựng các tải trọng trong Nút mã hóa thay vì các trường biểu thức.

Nếu các quy trình làm việc của bạn chủ yếu là mã với ít lợi ích trực quan, hãy viết các tập lệnh Python với trình lập lịch. Trình soạn thảo trực quan là lợi thế thực sự của n8n. Nếu bạn không cần nó, bạn đang thêm sự phức tạp mà không có lý do.

Phục vụ LLM cục bộ: Ollama

Ollama chiến thắng nhờ sự đơn giản và không có gì khác. Cài đặt nó, ollama pull qwen3:14b, và bạn có một API phục vụ mô hình tại localhost:11434. Không cần cấu hình CUDA, không cần quản lý môi trường Python, không cần đau đầu về việc chuyển GPU qua Docker.

Việc chuyển đổi giữa các mô hình chỉ là thay đổi một chuỗi trong tải trọng yêu cầu. API nhất quán trên mọi mô hình (/api/chat, /api/generate, /api/embed), điều này giúp logic định tuyến trong hệ thống của tôi trở nên tầm thường.

Điều tôi đánh đổi: vLLM cung cấp song song hóa tensor (tensor parallelism), định dạng liên tục (continuous batching) và kiểm soát lượng tử hóa (quantization) mà Ollama ẩn sau sự trừu tượng hóa của nó. Đối với một nền tảng phục vụ nhiều người dùng đồng thời, vLLM là lựa chọn đúng đắn. Đối với hệ thống người dùng duy nhất chạy một mô hình tại một thời điểm trên Mac mini, các mặc định của Ollama rất tốt, và sự khác biệt về thời gian cài đặt được tính bằng giờ.

Giao tiếp: Mattermost (Hiện tại)

Tôi cần phê duyệt có con người (human-in-the-loop) cho mọi hành động quan trọng. Hệ thống đăng lên một cuộc trò chuyện với ngữ cảnh và các nút Phê duyệt/Từ chối. Tôi nhấp, một webhook kích hoạt, và quy trình làm việc tiếp tục.

Tôi chọn Mattermost vì nó là mã nguồn mở, tự lưu trữ và có các tệp đính kèm tin nhắn tương tác. Đó là toàn bộ quá trình đánh giá. Nó không mang tính chiến lược. Nó đơn giản là "cái này chạy trong Docker và có các nút bấm".

Nó hoạt động. Nhưng tôi đang có kế hoạch chuyển sang Rocket.Chat. Tôi muốn tương tác giọng nói với trợ lý cuối cùng, và tính năng gọi điện của Mattermost bị hạn chế. Rocket.Chat cũng có các ứng dụng di động trưởng thành hơn, điều quan trọng vì toàn bộ điểm của HITL là phê duyệt hành động khi tôi không ngồi trước bàn làm việc.

Mạng: Tailscale

Ba máy cần giao tiếp với nhau. Tailscale cung cấp cho mỗi máy một IP ổn định hoạt động bất kể mạng vật lý. Không cần chuyển tiếp cổng, không cần DNS động, không cần mở cổng trên bộ định tuyến. Thiết lập mất khoảng 10 phút.

Tôi có thể đã cấu hình WireGuard thủ công để có cùng mã hóa và hiệu suất, nhưng sau đó tôi sẽ phải tự quản lý việc xoay khóa, cấu hình điểm cuối và duyệt NAT. Đối với mạng ba nút, sự tiện lợi của Tailscale là xứng đáng.

Một điều mọi người hỏi: tại sao không dùng Cloudflare Tunnels? Bởi vì chúng giải quyết một vấn đề khác. Cloudflare Tunnels lộ các dịch vụ ra internet thông qua mạng của Cloudflare. Các dịch vụ của tôi không cần ở trên internet; chúng cần nói chuyện riêng tư với nhau. Là Mesh VPN, không phải reverse proxy.

Tìm kiếm: Elasticsearch (Thêm vào sau)

Tôi không bắt đầu với Elasticsearch. Tôi bắt đầu với ChromaDB vì nó nhẹ hơn, chạy trong Docker, có API Python đơn giản và đủ tốt cho tìm kiếm vectơ cơ bản.

Vấn đề xuất hiện khi cơ sở kiến thức phát triển. Tôi có hàng nghìn sự kiện, thực thể và mô hình, và tôi cần tìm kiếm theo ý nghĩa theo từ khóa chính xác trong cùng một truy vấn. ChromaDB xử lý vectơ. PostgreSQL xử lý từ khóa. Nhưng chạy hai tìm kiếm trên hai hệ thống và hợp nhất kết quả là mong manh và chậm.

Elasticsearch làm cả hai một cách nguyên bản (BM25 để đối sánh từ khóa chính xác, kNN để tìm kiếm vectơ tương đồng) trong một truy vấn duy nhất. Đó là lý do tôi chuyển đổi. Sự đánh đổi là 4GB bộ nhớ heap trên một máy vốn đã thiếu thốn. Đối với các bộ dữ liệu nhỏ hơn hoặc tìm kiếm vectơ thuần túy, ChromaDB hoặc pgvector là những lựa chọn nhẹ nhàng hơn.

Tôi sẽ đề cập đến việc di chuyển trong một bài viết riêng.

Điều tôi sẽ làm khác đi

Triển khai (Deployment). Hiện tại, tôi triển khai bằng SSH vào máy chủ Windows và chạy các lệnh PowerShell. Không CI/CD, không GitHub Actions. Nó hoạt động vì tôi là nhà phát triển duy nhất, nhưng đó là điều đầu tiên sẽ bị phá vỡ nếu người khác cần đóng góp.

Nếu tôi bắt đầu lại, sử dụng Linux ngay từ ngày đầu tiên và một pipeline GitHub Actions cơ bản; đẩy đến nhánh chính, xây dựng container, triển khai. Không phải Kubernetes, không phải Terraform. Chỉ cần tự động hóa đoạn mã 90 giây mà tôi hiện đang chạy thủ công.

Đây là Phần 2 của series "Một nhà phát triển, 22 container". Tiếp theo là: di chuyển từ ChromaDB sang Elasticsearch, và lý do tìm kiếm lai đã thay đổi cách hệ thống AI của tôi tìm kiếm thông tin.

Nếu bạn đã đưa ra những lựa chọn tương tự — hoặc khác biệt — tôi rất muốn nghe điều đó trong phần bình luận. Hãy tìm tôi trên GitHub.

Bài viết được tổng hợp và biên soạn bằng AI từ các nguồn tin tức công nghệ. Nội dung mang tính tham khảo. Xem bài gốc ↗