Stack AI 0 đồng: giải thích chi tiết kèm ví dụ

Bài này có bản video ngắn:YouTubeTikTok

1. Tổng quan

Nhiều người nghĩ xây một hệ thống AI — agent, RAG, chatbot nội bộ — tốn vài nghìn đô mỗi tháng vì phải trả tiền API cho OpenAI hay Anthropic. Thực tế không phải vậy: có một stack gồm 8 tầng, mỗi tầng đều có phiên bản miễn phí hoặc self-hosted, đủ để bạn build và chạy một hệ thống AI hoàn chỉnh với 0 đồng. Bài này đi qua từng tầng của stack đó, giải thích vai trò, kèm ví dụ code cụ thể để bạn hình dung cách ghép chúng lại. Đọc xong, bạn sẽ biết chọn công cụ nào cho từng tầng và biết giới hạn thật của “miễn phí” nằm ở đâu.

2. Vì sao “0 đồng” không có nghĩa là mãi mãi miễn phí

Trước khi đi vào chi tiết, cần nói rõ một điều: “0 đồng” ở đây là chi phí phần mềm/license, không phải chi phí hạ tầng vô hạn. Vercel free tier có giới hạn băng thông, Supabase free tier có giới hạn storage, chạy Ollama local tốn CPU/RAM máy bạn. Stack này phù hợp để prototype, học, demo cho khách hàng nhỏ, không phải để scale cho triệu user.

Lưu ý: Nếu traffic thật của bạn vượt free tier, bạn vẫn phải trả tiền — chỉ là trả cho hạ tầng (compute, storage) thay vì trả cho vendor LLM theo token. Đây là sự khác biệt quan trọng cần hiểu trước khi commit vào stack này cho production thật.

3. Tầng Frontend: Next.js hoặc Streamlit

Tầng giao diện không cần phức tạp. Nếu bạn muốn UI đẹp, dùng Next.js deploy trên Vercel free tier. Nếu chỉ cần demo nhanh cho một pipeline AI, Streamlit là lựa chọn nhanh hơn nhiều vì không cần viết frontend riêng.

# app.py
import streamlit as st
import requests

st.title("Trợ lý hỏi đáp tài liệu nội bộ")

question = st.text_input("Nhập câu hỏi của bạn")

if st.button("Hỏi") and question:
    response = requests.post(
        "http://localhost:8000/ask",
        json={"question": question},
        timeout=30,
    )
    st.write(response.json()["answer"])

Đoạn code này tạo một ô input và một nút bấm. Khi bạn bấm nút, nó gọi API backend (chạy local) và hiển thị câu trả lời. Chạy bằng streamlit run app.py, Streamlit tự host trên localhost:8501. Không cần viết React, không cần build pipeline — phù hợp cho giai đoạn validate ý tưởng trước khi đầu tư vào Next.js.

4. Tầng Agent Orchestration: LangGraph

Khi hệ thống của bạn không chỉ là “gọi LLM một lần rồi trả kết quả” mà cần nhiều bước (tìm kiếm → tổng hợp → kiểm tra → trả lời), bạn cần một orchestrator. LangGraph mô hình hóa agent như một graph có state, cho phép rẽ nhánh và lặp.

<!-- pom.xml không áp dụng ở đây vì LangGraph là thư viện Python -->
pip install langgraph langchain-ollama
from langgraph.graph import StateGraph, END
from typing import TypedDict

class AgentState(TypedDict):
    question: str
    answer: str

def retrieve_step(state: AgentState) -> AgentState:
    state["answer"] = f"Đang tìm tài liệu cho: {state['question']}"
    return state

def generate_step(state: AgentState) -> AgentState:
    state["answer"] += " -> đã tổng hợp câu trả lời"
    return state

graph = StateGraph(AgentState)
graph.add_node("retrieve", retrieve_step)
graph.add_node("generate", generate_step)
graph.set_entry_point("retrieve")
graph.add_edge("retrieve", "generate")
graph.add_edge("generate", END)

app = graph.compile()
result = app.invoke({"question": "Chính sách bảo hành là gì?", "answer": ""})
print(result["answer"])
Đang tìm tài liệu cho: Chính sách bảo hành là gì? -> đã tổng hợp câu trả lời

StateGraph định nghĩa luồng xử lý gồm hai node: retrieve và generate, mỗi node nhận state và trả state mới. set_entry_point chỉ định node bắt đầu, add_edge nối các bước. Khi gọi app.invoke(...), LangGraph chạy tuần tự qua graph và trả state cuối cùng. Toàn bộ chạy local, không gọi API trả phí nào.

5. Tầng RAG Pipeline: Chroma và Qdrant

RAG (Retrieval-Augmented Generation) cần một vector store để lưu embedding và tìm kiếm theo độ tương tự. Chroma chạy embedded, không cần server riêng — phù hợp cho prototype.

pip install chromadb
import chromadb

client = chromadb.PersistentClient(path="./chroma_data")
collection = client.get_or_create_collection(name="policies")

collection.add(
    documents=["Bảo hành sản phẩm 12 tháng kể từ ngày mua.",
               "Đổi trả trong 7 ngày nếu còn hộp nguyên vẹn."],
    ids=["doc1", "doc2"],
)

results = collection.query(query_texts=["chính sách bảo hành"], n_results=1)
print(results["documents"])
[['Bảo hành sản phẩm 12 tháng kể từ ngày mua.']]

PersistentClient lưu dữ liệu xuống ổ đĩa thay vì chỉ trong RAM, nên data sống qua các lần restart. collection.add tự động tính embedding cho documents (Chroma có embedding function mặc định). collection.query tìm document gần nhất về mặt vector với câu hỏi. Nếu dataset lớn hơn, bạn chuyển sang Qdrant chạy local qua Docker — API tương tự nhưng tối ưu hơn cho scale vừa.

6. Tầng LLM: Ollama chạy model local

Đây là tầng cốt lõi giúp bạn thoát khỏi hóa đơn API. Ollama cho phép chạy Llama, Mistral, Gemma ngay trên máy, không giới hạn số request.

ollama pull llama3.1
ollama run llama3.1 "Tóm tắt chính sách bảo hành trong một câu."
Sản phẩm được bảo hành 12 tháng kể từ ngày mua.

Lệnh ollama pull tải model về máy (model được quantize để chạy trên CPU/GPU phổ thông). Lệnh ollama run khởi động model và gửi prompt, trả kết quả trực tiếp trong terminal. Ollama cũng mở một REST API ở localhost:11434, nên bạn tích hợp vào code Python hoặc Java đều được.

Nếu backend của bạn là Spring Boot, gọi Ollama qua RestClient rất đơn giản:

import org.springframework.web.client.RestClient;

public class OllamaClient {

    private final RestClient restClient = RestClient.create("http://localhost:11434");

    public String generate(String model, String prompt) {
        var response = restClient.post()
                .uri("/api/generate")
                .body(new OllamaRequest(model, prompt, false))
                .retrieve()
                .body(OllamaResponse.class);
        return response.response();
    }

    record OllamaRequest(String model, String prompt, boolean stream) {}
    record OllamaResponse(String response) {}
}

OllamaClient gói gọn lời gọi HTTP tới Ollama. RestClient của Spring 6 thay cho RestTemplate cũ, viết ngắn hơn. OllamaRequest set stream = false để nhận toàn bộ response một lần thay vì streaming từng token — dễ xử lý hơn cho ví dụ demo.

7. Tool Use qua MCP (Model Context Protocol)

Agent chỉ nói chuyện với LLM thì không làm được gì thực tế. MCP là chuẩn giao tiếp giúp agent gọi tool bên ngoài — đọc file, query database, gọi GitHub API — theo một interface thống nhất, không cần viết integration riêng cho mỗi tool.

{
  "mcpServers": {
    "filesystem": {
      "command": "npx",
      "args": ["-y", "@modelcontextprotocol/server-filesystem", "/data/docs"]
    },
    "github": {
      "command": "npx",
      "args": ["-y", "@modelcontextprotocol/server-github"]
    }
  }
}

File config này khai báo hai MCP server: một cho filesystem (đọc/viết file trong /data/docs), một cho GitHub (tạo issue, đọc PR). Agent của bạn (LangGraph, Claude Code, hay bất kỳ client hỗ trợ MCP) chỉ cần kết nối tới các server này và gọi tool theo schema chuẩn, không phải viết wrapper API riêng cho từng dịch vụ.

Lưu ý: MCP server chạy local qua npx không tốn phí license, nhưng một số server đòi access token (ví dụ GitHub token) — token đó vẫn phải xin từ dịch vụ gốc.

8. Code Agent: Aider

Khi muốn agent tự viết và sửa code trong repo, Aider là công cụ CLI mã nguồn mở, kết hợp tốt với model local qua Ollama.

pip install aider-chat
aider --model ollama/llama3.1 --file src/main/java/OrderService.java

Lệnh trên mở Aider, chỉ định dùng model llama3.1 qua Ollama (không qua API trả phí), và nạp sẵn file OrderService.java vào context. Từ đây bạn gõ yêu cầu bằng tiếng Việt hoặc tiếng Anh (“thêm validate số lượng âm”), Aider sẽ generate diff và tự commit nếu bạn đồng ý.

9. Tầng Data: SQLite, DuckDB, Supabase

Không phải hệ thống AI nào cũng cần Postgres cluster. SQLite đủ cho structured storage nhỏ, DuckDB mạnh cho phân tích dữ liệu dạng file (CSV, Parquet) mà không cần server.

import duckdb

con = duckdb.connect()
result = con.execute("""
    SELECT category, COUNT(*) AS total
    FROM read_csv_auto('orders.csv')
    GROUP BY category
    ORDER BY total DESC
""").fetchall()
print(result)
[('electronics', 120), ('books', 45), ('clothing', 30)]

DuckDB đọc trực tiếp file CSV bằng read_csv_auto, không cần ETL hay import vào database riêng. Câu SQL chạy gần như tức thì với dữ liệu vài trăm nghìn dòng — đủ cho hầu hết use case phân tích nội bộ. Khi cần một backend có REST API và auth sẵn, Supabase free tier cấp Postgres managed kèm realtime và storage.

10. Deployment: Docker, Cloudflare Workers, Hugging Face

Container hóa bằng Docker giúp bạn deploy nhất quán giữa máy dev và máy host miễn phí.

# docker-compose.yml
services:
  ollama:
    image: ollama/ollama
    ports:
      - "11434:11434"
    volumes:
      - ollama_data:/root/.ollama

  app:
    build: .
    ports:
      - "8000:8000"
    depends_on:
      - ollama

volumes:
  ollama_data:

File này định nghĩa hai service: ollama chạy model LLM, app là backend của bạn, phụ thuộc vào ollama để gọi inference. docker compose up khởi động cả hai cùng lúc, volume ollama_data giữ model đã pull qua các lần restart container. Khi cần public demo, bạn đẩy phần frontend lên Cloudflare Workers (edge, free tier generous) và model nhỏ lên Hugging Face Spaces để người khác test mà không cần cấu hình máy họ.

11. Ghép toàn bộ stack lại

Luồng chạy thực tế: người dùng gõ câu hỏi trên Streamlit → LangGraph nhận request, gọi Chroma để retrieve context liên quan → ghép context vào prompt, gửi cho Ollama → nếu cần tool (đọc file, query DB), agent gọi qua MCP → kết quả trả về UI. Tất cả chạy trên một máy hoặc container, không request nào đi ra ngoài tới API trả phí.

def answer_question(question: str) -> str:
    docs = collection.query(query_texts=[question], n_results=2)
    context = "\n".join(docs["documents"][0])
    prompt = f"Dựa vào thông tin sau:\n{context}\n\nTrả lời: {question}"
    import ollama
    response = ollama.generate(model="llama3.1", prompt=prompt)
    return response["response"]

Hàm này là “trái tim” của RAG: lấy context từ Chroma, nhúng vào prompt, gọi Ollama để generate câu trả lời cuối cùng. Đây chính là pattern retrieve-then-generate cơ bản nhất, không cần framework phức tạp nếu use case của bạn đơn giản.

12. Viết test cho phần xử lý dữ liệu

Phần dễ test nhất trong pipeline RAG thường là bước chia nhỏ văn bản (chunking) trước khi nhúng vector. Nếu backend Java của bạn xử lý bước này, một unit test đơn giản giúp tránh lỗi off-by-one khi split.

import java.util.List;
import java.util.ArrayList;

public class DocumentChunker {

    public List<String> chunk(String text, int maxLength) {
        List<String> chunks = new ArrayList<>();
        for (int i = 0; i < text.length(); i += maxLength) {
            chunks.add(text.substring(i, Math.min(i + maxLength, text.length())));
        }
        return chunks;
    }
}
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;

class DocumentChunkerTest {

    @Test
    void shouldSplitTextIntoFixedSizeChunks() {
        DocumentChunker chunker = new DocumentChunker();

        List<String> chunks = chunker.chunk("Bảo hành 12 tháng cho toàn bộ sản phẩm", 10);

        assertThat(chunks).hasSizeGreaterThan(1);
        assertThat(chunks.get(0)).hasSize(10);
        assertThat(String.join("", chunks)).isEqualTo("Bảo hành 12 tháng cho toàn bộ sản phẩm");
    }
}

Test này khẳng định ba điều: văn bản dài được chia thành nhiều chunk, chunk đầu đúng độ dài yêu cầu, và nối lại tất cả chunk phải cho đúng văn bản gốc — không mất ký tự nào. Đây là loại bug âm thầm dễ xảy ra khi bạn tự viết chunking logic mà không test kỹ.

13. Lỗi hay gặp và khi nào không nên dùng stack này

  • Model local chậm hơn API cloud nhiều lần nếu máy không có GPU. Với CPU thường, một request llama3.1 8B có thể mất 5-15 giây — chấp nhận được cho demo, không chấp nhận được cho sản phẩm cần phản hồi tức thì.
  • Free tier có rate limit âm thầm. Vercel, Supabase, Cloudflare đều giới hạn request/tháng; vượt ngưỡng app có thể bị chặn mà bạn không biết trước nếu không theo dõi dashboard.
  • Chroma embedded không phù hợp multi-instance. Nếu bạn scale backend ra nhiều pod, mỗi pod có file Chroma riêng sẽ gây inconsistency — lúc đó cần chuyển sang Qdrant server hoặc vector store managed.
  • MCP server local tăng attack surface. Một filesystem MCP server cho phép agent đọc/viết file — nếu expose nhầm ra ngoài internet, đây là lỗ hổng bảo mật nghiêm trọng.

Lưu ý: Nếu bạn đang build sản phẩm có SLA, cần độ trễ thấp ổn định, hoặc traffic lớn ngay từ đầu, stack 0 đồng này chỉ nên dùng ở giai đoạn prototype. Khi go-live thật, cân nhắc trả tiền cho API LLM managed hoặc GPU instance riêng để đảm bảo chất lượng dịch vụ.

14. Kết luận

Stack 0 đồng không phải chiêu marketing — nó là tổ hợp công cụ open source và free tier thật, đủ để bạn dựng một hệ thống AI có RAG, agent, tool use và deployment hoàn chỉnh mà không cần thẻ tín dụng. Rào cản thật sự không nằm ở tiền, mà ở việc bạn có hiểu rõ cách ghép từng tầng, biết giới hạn của free tier, và biết khi nào cần chuyển sang hạ tầng trả phí để đảm bảo chất lượng. Bắt đầu với stack này để học và validate ý tưởng, rồi nâng cấp từng tầng khi sản phẩm thật sự cần scale.

llmragollamalanggraphmcp