Kiến trúc15/04/20258 phút đọc

Xây dựng RAG Pipeline Thực tế với pgvector

Cách chúng tôi xây dựng retrieval-augmented generation pipeline xử lý PDF, DOCX, URL web với chunking, embedding và cosine search — đạt 91% accuracy so với 34% khi dùng LLM thuần.

AC

Assistant Core Team

Engineering

Khi doanh nghiệp muốn AI assistant trả lời chính xác dựa trên tài liệu nội bộ — hợp đồng, quy trình, chính sách sản phẩm — câu trả lời là RAG (Retrieval-Augmented Generation). Bài viết này giải thích cách chúng tôi xây dựng pipeline RAG trong Assistant Core, từ ingest đến query production.

Vấn đề với LLM thông thường

LLM được huấn luyện trên dữ liệu công khai — chúng không biết sản phẩm của bạn, quy trình nội bộ, hay trạng thái đơn hàng mới nhất. Khi hỏi trực tiếp về dữ liệu không có trong training, LLM sẽ "hallucinate" — tạo ra câu trả lời trông hợp lý nhưng hoàn toàn sai.

Fine-tuning là một lựa chọn nhưng tốn kém, chậm cập nhật và không phù hợp với dữ liệu thay đổi thường xuyên. RAG giải quyết vấn đề này theo cách khác: tìm kiếm trước, trả lời sau.

Kiến trúc 2 giai đoạn của RAG

RAG tách làm 2 pipeline độc lập:

  • Ingest Pipeline — chạy offline khi có tài liệu mới: Tài liệu → Parse → Chunk → Embed → Lưu vào pgvector
  • Query Pipeline — chạy real-time mỗi request: Câu hỏi → Embed → Cosine Search → Top-K chunks → Inject vào prompt → LLM trả lời

Ingest Pipeline — chi tiết implementation

Chúng tôi hỗ trợ 7 định dạng đầu vào: PDF, DOCX, XLSX, CSV, TXT, URL web (qua Firecrawl), và plain text. Mỗi loại có parser riêng để extract text thuần trước khi chunking.

# Chunking strategy trong Assistant Core
CHUNK_SIZE = 4000       # characters
CHUNK_OVERLAP = 500     # overlap để không mất context

# Embedding
EMBEDDING_MODEL = "text-embedding-3-large"
EMBEDDING_DIMENSIONS = 2048  # reduced từ 3072

async def ingest_document(file_path: str, kb_id: str):
    # 1. Parse
    text = await parse_document(file_path)

    # 2. Chunk with overlap
    chunks = chunk_text(text, CHUNK_SIZE, CHUNK_OVERLAP)

    # 3. Embed batch (128 chunks/request)
    embeddings = await embed_batch(chunks)

    # 4. Store in pgvector
    await store_chunks(chunks, embeddings, kb_id)

Batch embedding quan trọng để tiết kiệm chi phí API. Với 128 chunks/request, một tài liệu 200 trang chỉ cần ~20 API calls thay vì ~500.

pgvector — vector search ngay trong PostgreSQL

Thay vì dùng Pinecone hay Weaviate, chúng tôi chọn pgvector — extension PostgreSQL cho vector search. Ưu điểm lớn: không cần quản lý thêm một service riêng, transaction ACID vẫn đảm bảo, và metadata filtering dùng SQL thuần.

-- Schema cho document chunks
CREATE TABLE document_chunks (
    id          UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    kb_id       UUID NOT NULL REFERENCES knowledge_bases(id),
    content     TEXT NOT NULL,
    embedding   VECTOR(2048),
    metadata    JSONB DEFAULT '{}'
);

-- IVFFlat index cho tốc độ query
CREATE INDEX ON document_chunks
USING ivfflat (embedding vector_cosine_ops)
WITH (lists = 100);

-- Query: semantic search với metadata filter
SELECT content, 1 - (embedding <=> $query_vec) AS score
FROM document_chunks
WHERE kb_id = $kb_id
  AND (metadata->>'doc_type') = 'policy'  -- filter
ORDER BY embedding <=> $query_vec
LIMIT 5;

Query Pipeline — real-time flow

Khi người dùng gửi câu hỏi, pipeline thực hiện 3 bước trong vòng ~150ms:

  1. Embed query — dùng cùng model với ingest (text-embedding-3-large)
  2. Cosine search — tìm 5–8 chunks gần nhất trong pgvector
  3. Inject context — chèn chunks vào system prompt, LLM trả lời với grounding
# Query pipeline
async def query_knowledge_base(question: str, kb_id: str) -> str:
    # Embed question
    q_embedding = await embed_text(question)

    # Semantic search
    chunks = await pgvector_search(
        embedding=q_embedding,
        kb_id=kb_id,
        top_k=5,
        min_score=0.75
    )

    # Build grounded prompt
    context = "\n---\n".join([c.content for c in chunks])
    system_prompt = f"""Trả lời dựa trên context sau:

{context}

Nếu thông tin không có trong context, hãy nói rõ."""

    return await llm.chat(system_prompt, question)

Production tips từ thực tế

  • Chunk size 3000–4000 ký tự hoạt động tốt nhất cho tài liệu doanh nghiệp. Quá nhỏ mất context, quá lớn gây noise.
  • Overlap 400–600 ký tự đảm bảo không bị mất thông tin ở ranh giới chunks.
  • Minimum score threshold 0.70–0.75 để loại bỏ chunks không liên quan.
  • Cache embeddings của các câu hỏi phổ biến trong Redis (TTL 1 giờ) giảm ~40% latency.
  • Metadata tagging khi ingest cho phép filter theo loại tài liệu, phòng ban, ngày cập nhật.

Kết quả đo được

Với pipeline này, assistant trả lời chính xác dựa trên knowledge base nội bộ, trích dẫn nguồn cụ thể, và hầu như không hallucinate. Trong thử nghiệm với 500 câu hỏi về chính sách sản phẩm, độ chính xác đạt 91% so với 34% khi dùng LLM thuần không có RAG.

Sẵn sàng thử Assistant Core?

Tạo AI assistant trong vài phút — RAG, voice, MCP tools và edge device.

Tìm hiểu thêm