Ứng dụng AI vào Kỹ thuật Phần mềm: Xây dựng hệ thống RAG với Python & LangChain

Trong quá trình vận hành các hệ thống phần mềm quy mô lớn, tri thức kỹ thuật của tổ chức thường nằm phân tán dưới nhiều định dạng: tài liệu quyết định kiến trúc (Architecture Decision Records — ADRs), kịch bản xử lý sự cố (Incident Runbooks), đặc tả OpenAPI, và biên bản phân tích lỗi sau sự cố (Post-mortem Reports). Khi kỹ sư tích hợp trực tiếp các Mô hình Ngôn ngữ Lớn (Large Language Models — LLMs) thương mại vào quy trình tra cứu nội bộ, rào cản lớn nhất không nằm ở khả năng diễn đạt ngôn ngữ mà ở hiện tượng "ảo giác" (Hallucination) — trạng thái mô hình tự suy diễn ra các tham số cấu hình, tên hàm, hoặc quy trình khắc phục lỗi hoàn toàn không tồn tại trong hệ thống thực tế.

Kiến trúc RAG (Retrieval-Augmented Generation), được chuẩn hóa lần đầu bởi nhóm nghiên cứu của Patrick Lewis và cộng sự (2020), giải quyết triệt để bài toán này bằng cách kết hợp bộ nhớ tham số (Parametric Memory) có sẵn trong trọng số của mô hình ngôn ngữ với bộ nhớ phi tham số (Non-parametric Memory) được truy xuất trực tiếp từ cơ sở dữ liệu vector nội bộ tại thời điểm thực thi truy vấn.

Bài viết kỹ thuật chuyên sâu này hướng dẫn từng bước thiết kế, triển khai và tối ưu hóa một hệ thống RAG chuẩn Production sử dụng Python 3.11+, LangChain v0.3 (LCEL — LangChain Expression Language), Chroma Vector Database, và OpenAI Embeddings. Toàn bộ mã nguồn được xây dựng theo hướng mô-đun hóa, bảo toàn cấu trúc ngữ cảnh Markdown, tích hợp tìm kiếm lai (Hybrid Search kết hợp BM25 và Dense Vector), bắt buộc trích dẫn nguồn (Source Attribution), và kiểm soát chất lượng bằng các chỉ số đánh giá kỹ thuật chuẩn xác.


Mục Lục Nội Dung

1. Tổng quan Kiến trúc Hệ thống RAG trong Kỹ thuật Phần mềm

1.1 Bài toán "Ảo giác" (Hallucination) và giới hạn của Parametric Memory

Mọi mô hình ngôn ngữ lớn tiền huấn luyện (Pre-trained LLMs) đều lưu trữ tri thức dưới dạng các tham số xác suất tĩnh (Parametric Memory) được đóng băng tại thời điểm kết thúc quá trình huấn luyện (Training Knowledge Cutoff). Khi được hỏi về một dịch vụ nội bộ — ví dụ: "Khi gặp mã lỗi ERR_PAYMENT_IDEMPOTENCY_LOCK trên cụm Redis cụm B, kỹ sư trực ca (on-call) cần thực hiện bước xả khóa nào?" — một mô hình LLM thuần túy không hề có dữ liệu về kiến trúc nội bộ của doanh nghiệp. Thay vì từ chối trả lời, cơ chế dự đoán token tiếp theo (Next-token Prediction) có xu hướng sinh ra một câu lệnh CLI trông có vẻ hợp lý nhưng sai lệch hoàn toàn với thiết kế hệ thống.

Theo nghiên cứu nền tảng Retrieval-Augmented Generation for Knowledge-Intensive NLP Tasks (Lewis et al., NeurIPS 2020), việc bổ sung một tầng truy xuất thông tin (Retriever) đứng trước bộ sinh văn bản (Generator) mang lại ba lợi thế kỹ thuật cốt lõi cho hệ thống phần mềm:

  1. Neo giữ sự thật theo ngữ cảnh (Contextual Grounding): Mô hình chỉ được phép tổng hợp câu trả lời dựa trên các đoạn văn bản (chunks) thực tế được tìm thấy trong kho tài liệu kỹ thuật.
  2. Cập nhật tri thức tức thời không cần huấn luyện lại (Zero-Retraining Updates): Khi một Runbook hoặc tài liệu API thay đổi trên Git repository, hệ thống chỉ cần chạy lại tiến trình băm và tạo vector (Re-indexing) cho tệp tin vừa thay đổi trong vài giây, thay vì tốn hàng chục giờ chạy Fine-tuning.
  3. Truy vết nguồn gốc và phân quyền (Verifiability & Access Control): Mỗi câu trả lời sinh ra đều đính kèm định danh tệp nguồn (source), tiêu đề mục (header), và mã đoạn (chunk_id), cho phép kỹ sư kiểm chứng ngay lập tức đoạn tài liệu gốc.

Để hiểu rõ hơn bức tranh tổng thể về cách lựa chọn bộ khung phần mềm khi đưa LLM vào ứng dụng thực tế, bạn có thể tham khảo bài phân tích chuyên sâu về Tích hợp mô hình ngôn ngữ lớn LLM vào sản phẩm với LangChain và LlamaIndex.

1.2 So sánh kỹ thuật: RAG vs. Fine-Tuning vs. Long-Context Prompting

Trong thiết kế kiến trúc AI cho doanh nghiệp, kỹ sư thường phải cân nhắc giữa ba phương pháp tiếp cận: truyền toàn bộ tài liệu vào cửa sổ ngữ cảnh lớn (Long-Context Prompting), tinh chỉnh trọng số mô hình (Fine-tuning), hoặc xây dựng hệ thống RAG. Bảng dưới đây phân tích các đánh đổi kỹ thuật (Engineering Trade-offs) giữa ba phương pháp này:

Tiêu chí Kỹ thuật RAG (Retrieval-Augmented Generation) Fine-Tuning (SFT / LoRA / QLoRA) Long-Context Prompting (Nhồi toàn bộ tài liệu)
Cơ chế lưu trữ tri thức Non-parametric (Vector Index + Lexical Index bên ngoài mô hình) Parametric (Cập nhật trực tiếp vào trọng số mạng nơ-ron hoặc Adapter) Tạm thời trong bộ nhớ KV-Cache của phiên gọi API
Tần suất cập nhật dữ liệu Thời gian thực (Real-time / Near real-time) theo từng commit tài liệu Rất chậm; đòi hỏi chuẩn bị tập dữ liệu huấn luyện và chạy lại chu kỳ GPU Tức thời nhưng giới hạn bởi dung lượng tổng kho tài liệu
Kiểm soát ảo giác & Trích dẫn nguồn Rất cao — Trích xuất chính xác tệp tin, dòng và tiêu đề mục chứa thông tin Thấp — Mô hình không thể trích dẫn chính xác đoạn tài liệu gốc đã học Trung bình – Cao — Có thể suy giảm độ chính xác ở vùng giữa ngữ cảnh (Lost in the Middle)
Chi phí Suy luận (Inference Cost) & Độ trễ Tối ưu và ổn định; chỉ gửi Top-K đoạn văn bản liên quan nhất (thường 1.500 – 3.000 tokens) Thấp khi suy luận, nhưng chi phí hạ tầng huấn luyện và vận hành mô hình riêng cao Rất đắt đỏ và độ trễ (Time-To-First-Token) tăng tuyến tính hoặc bậc hai theo độ dài prompt
Bài toán kỹ thuật phù hợp nhất Tra cứu tài liệu kiến trúc, Runbooks, API Docs, Log lỗi thay đổi liên tục Điều chỉnh văn phong, định dạng đầu ra chuyên biệt (DSL, SQL nội bộ), phân loại tác vụ Phân tích một tệp tài liệu đơn lẻ hoặc một Pull Request cụ thể trong một phiên làm việc

Trong thực tế, RAG và Fine-tuning không loại trừ lẫn nhau. Nếu hệ thống của bạn đòi hỏi mô hình mã nguồn mở phải tuân thủ nghiêm ngặt một định dạng ngôn ngữ truy vấn nội bộ hoặc giọng văn chuyên ngành trước khi kết hợp với RAG, hãy xem thêm hướng dẫn Fine-tuning mô hình mã nguồn mở Llama 3 phục vụ nghiệp vụ chuyên biệt.

1.3 Sơ đồ Kiến trúc Luồng Dữ liệu: Offline Indexing và Online Retrieval

Một hệ thống RAG chuẩn kỹ thuật phần mềm luôn được tách biệt thành hai luồng xử lý độc lập nhằm đảm bảo hiệu năng và khả năng mở rộng:

  1. Luồng Lập chỉ mục Ngoại tuyến (Offline Indexing Pipeline): Chịu trách nhiệm đọc tài liệu thô, làm sạch, phân mảnh theo cấu trúc ngữ nghĩa (Structural & Recursive Chunking), chuyển đổi văn bản thành vector nhiều chiều thông qua Embedding Model, và lưu trữ đồng thời vào Vector Index cùng Lexical Index (BM25).
  2. Luồng Truy xuất và Sinh văn bản Trực tuyến (Online Retrieval & Generation Pipeline): Xử lý truy vấn của kỹ sư theo thời gian thực, thực hiện tìm kiếm lai (Hybrid Search), lọc theo ngưỡng tương đồng (Relevance Score Threshold), đóng gói ngữ cảnh kèm siêu dữ liệu (Metadata), và gọi LLM sinh câu trả lời có kiểm soát.
flowchart TD
    subgraph Offline["Luồng 1: Offline Indexing Pipeline (Xử lý Bất đồng bộ / CI-CD)"]
        A["Kho Tài liệu Kỹ thuật: (Markdown ADRs, Runbooks, API Docs)"] --> B["MarkdownHeaderTextSplitter: (Bảo toàn ngữ cảnh H1/H2/H3 vào Metadata)"]
        B --> C["RecursiveCharacterTextSplitter: (Chunk Size: 800, Overlap: 150, SHA-256 ID)"]
        C --> D["OpenAI Embedding Model: (text-embedding-3-small / 1536 dims)"]
        D --> E[("Chroma Vector Store: (Dense HNSW Index)")]
        C --> F[("BM25 Lexical Index: (Sparse Keyword Matching)")]
    end

    subgraph Online["Luồng 2: Online Retrieval & Generation Pipeline (Runtime)"]
        U["Truy vấn Kỹ sư: (Technical Query)"] --> G["Input Guardrails: (PII Scrubbing & Injection Check)"]
        G --> H["EnsembleRetriever: (Hybrid Search: Vector + BM25 RRF)"]
        E -.->|"Cosine Similarity"| H
        F -.->|"Exact Error Codes / Tokens"| H
        H --> I["Score Threshold Filter: (Loại bỏ Chunk dưới ngưỡng 0.65)"]
        I --> J["Prompt Assembly (LCEL): (System Rules + Grounded Context + Query)"]
        J --> K["LLM Generator: (gpt-4o-mini, temperature=0.0)"]
        K --> L["Phản hồi Kỹ thuật: (Câu trả lời + Trích dẫn Source & Chunk ID)"]
    end

Ở cấp độ thực thi từng yêu cầu (Request Lifecycle), trình tự tương tác giữa các thành phần phần mềm trong luồng trực tuyến diễn ra như sơ đồ tuần tự dưới đây:

sequenceDiagram
    autonumber
    actor Eng as Kỹ sư Phần mềm
    participant API as Python RAG Service (LCEL)
    participant Guard as Security Guardrail
    participant Ret as Hybrid Retriever (Chroma + BM25)
    participant Emb as OpenAI Embeddings API
    participant LLM as OpenAI Chat Completion (gpt-4o-mini)

    Eng->>API: Gửi câu hỏi kỹ thuật (VD: Xử lý lỗi ERR_POOL_EXHAUSTED)
    API->>Guard: Kiểm tra Prompt Injection & Ẩn thông tin nhạy cảm (PII/Secret)
    Guard-->>API: Truy vấn đã làm sạch (Sanitized Query)
    API->>Emb: Tạo vector biểu diễn cho câu hỏi (1536 chiều)
    Emb-->>API: Query Vector Float Array
    API->>Ret: Truy vấn kết hợp Dense Vector + BM25 Keyword
    Ret-->>API: Trả về danh sách ứng viên kèm điểm tương đồng (Relevance Score)
    alt Điểm tương đồng cao nhất < Ngưỡng an toàn (0.65)
        API-->>Eng: Cảnh báo: Không tìm thấy tài liệu kỹ thuật khớp với truy vấn
    else Có đoạn tài liệu vượt ngưỡng (>= 0.65)
        API->>LLM: Gửi Prompt gồm [System Instructions] + [Top-K Chunks + Metadata] + [Query]
        LLM-->>API: Câu trả lời bám sát ngữ cảnh + Mã trích dẫn [chunk_id]
        API-->>Eng: Trả về JSON (answer, citations, latency_ms)
    end

2. Thiết lập Môi trường Phát triển & Quản lý Dependencies (Python 3.11+ & LangChain v0.3)

2.1 Kiến trúc phân tách package của LangChain v0.3

Từ phiên bản LangChain v0.3, kiến trúc thư viện đã được tái cấu trúc toàn diện nhằm loại bỏ sự phụ thuộc cồng kềnh (monolithic dependencies) và chuẩn hóa hoàn toàn trên Pydantic v2:

  • langchain-core: Chứa các giao diện trừu tượng nền tảng (Runnable, Document, ChatPromptTemplate, VectorStore) và cú pháp đường ống LCEL (LangChain Expression Language).
  • langchain-text-splitters: Tách riêng các thuật toán băm văn bản theo cấu trúc Markdown, HTML, JSON, và mã nguồn.
  • langchain-community: Chứa các bộ tích hợp bên thứ ba do cộng đồng duy trì (như BM25Retriever hoặc các trình đọc tệp cục bộ).
  • langchain-openai: Gói tích hợp chính thức kết nối với hệ sinh thái mô hình của OpenAI (ChatOpenAI, OpenAIEmbeddings).

2.2 Khởi tạo Virtual Environment và cấu hình biến môi trường an toàn

Tuyệt đối không bao giờ viết cứng (hardcode) khóa OPENAI_API_KEY trực tiếp trong mã nguồn Python. Chúng ta sẽ khởi tạo môi trường ảo độc lập trên Python 3.11+ và ghim cố định phiên bản (version pinning) trong tệp requirements.txt để đảm bảo tính tái lập (reproducibility) trên mọi môi trường CI/CD.

# 1. Kiểm tra phiên bản Python (yêu cầu Python >= 3.11)
python3 --version

# 2. Khởi tạo thư mục dự án và môi trường ảo
mkdir -p rag-engineering-assistant/docs
cd rag-engineering-assistant
python3 -m venv .venv
source .venv/bin/activate  # Trên Windows PowerShell: ..venvScriptsActivate.ps1

# 3. Tạo tệp requirements.txt với các phiên bản đã kiểm chứng
cat << 'EOF' > requirements.txt
langchain==0.3.7
langchain-core==0.3.15
langchain-community==0.3.5
langchain-text-splitters==0.3.2
langchain-openai==0.2.5
langchain-chroma==0.1.4
chromadb==0.5.18
rank-bm25==0.2.2
pydantic==2.9.2
python-dotenv==1.0.1
tiktoken==0.8.0
EOF

# 4. Cài đặt toàn bộ gói phụ thuộc
pip install --upgrade pip
pip install -r requirements.txt

Tiếp theo, cấu hình khóa API thông qua biến môi trường hoặc tệp .env nằm trong danh sách loại trừ của .gitignore:

# Thêm .env và thư mục dữ liệu vector cục bộ vào .gitignore
cat << 'EOF' > .gitignore
.venv/
.env
chroma_data/
__pycache__/
EOF

# Thiết lập biến môi trường cho phiên làm việc hiện tại (thay bằng khóa thực tế khi chạy)
export OPENAI_API_KEY="your-actual-openai-api-key-from-secret-manager"

3. Xây dựng Pipeline Nạp và Phân mảnh Tài liệu Kỹ thuật (Ingestion & Chunking)

3.1 Tại sao Naive Chunking thất bại với tài liệu Markdown và Mã nguồn?

Sai lầm phổ biến nhất trong các bản thử nghiệm RAG sơ khai là cắt văn bản thuần túy theo độ dài ký tự cố định (ví dụ: cắt cứ mỗi 1.000 ký tự bằng CharacterTextSplitter mà không quan tâm đến cấu trúc tài liệu). Đối với tài liệu kỹ thuật viết bằng Markdown, phương pháp cắt ngây thơ (Naive Chunking) gây ra hai hệ lụy nghiêm trọng:

  1. Đứt gãy ngữ cảnh tiêu đề (Orphaned Paragraphs): Một đoạn hướng dẫn gồm 5 bước xử lý lỗi nằm ở cuối mục ## 2. Khắc phục lỗi đầy kết nối Database bị tách rời hoàn toàn khỏi dòng tiêu đề H2. Khi đoạn văn bản đó được vector hóa độc lập, nó mất đi từ khóa quan trọng nhất cho biết các bước đó dùng để xử lý lỗi gì.
  2. Cắt ngang khối lệnh (Broken Code Blocks): Một khối lệnh SQL hoặc kịch bản Bash bị cắt làm đôi ở ký tự thứ 1.000 sẽ khiến LLM nhận được một đoạn lệnh thiếu tham số hoặc sai cú pháp, dẫn đến hướng dẫn nguy hiểm cho kỹ sư vận hành.

3.2 Chiến lược Phân mảnh 2 Tầng: MarkdownHeaderTextSplitter kết hợp RecursiveCharacterTextSplitter

Để giải quyết triệt để vấn đề trên, chúng ta áp dụng chiến lược Phân mảnh 2 Tầng (Two-Stage Structural Chunking):

  • Tầng 1 (MarkdownHeaderTextSplitter): Phân tách tài liệu dựa trên các cấp tiêu đề #, ##, ### và tự động đính kèm toàn bộ phân cấp tiêu đề đó vào từ điển metadata của từng đối tượng Document.
  • Tầng 2 (RecursiveCharacterTextSplitter): Chia nhỏ các mục dài theo danh sách ký tự phân cách ưu tiên (\n\n, \n, . , ) với độ chồng lấp (chunk_overlap=150) để bảo toàn tính liên tục ngữ nghĩa giữa hai đoạn liền kề, đồng thời tạo mã định danh băm SHA-256 (chunk_id) tất định (deterministic) giúp tránh trùng lặp dữ liệu khi chạy lại pipeline.

Hãy tạo tệp ingest.py dưới đây. Đoạn mã này tự động tạo một tài liệu Runbook mẫu và thực thi quy trình phân mảnh hai tầng:

# File: ingest.py
"""
Module nạp và phân mảnh tài liệu kỹ thuật Markdown theo cấu trúc 2 tầng.
Yêu cầu: Python 3.11+, langchain-text-splitters==0.3.2, langchain-core==0.3.15
"""

import hashlib
from pathlib import Path
from typing import List
from langchain_core.documents import Document
from langchain_text_splitters import (
    MarkdownHeaderTextSplitter,
    RecursiveCharacterTextSplitter,
)

SAMPLE_RUNBOOK_CONTENT = """# Runbook Vận hành: Dịch vụ Thanh toán Lõi (Payment Core Service)

## 1. Kiến trúc Kết nối Cơ sở dữ liệu
Dịch vụ `payment-core-v2` sử dụng PostgreSQL 16 thông qua tầng quản lý kết nối PgBouncer.
Giới hạn `max_client_conn` được thiết lập ở mức 2000 kết nối và `default_pool_size` là 40 kết nối cho mỗi phân mảnh (shard).
Thời gian chờ truy vấn tối đa (`statement_timeout`) được khóa cứng ở mức 3000ms để ngăn chặn hiện tượng nghẽn dây chuyền (cascading failure).

## 2. Xử lý Sự cố Mã lỗi ERR_DB_POOL_EXHAUSTED
### 2.1 Triệu chứng và Cảnh báo
Khi chỉ số `pgbouncer_cl_waiting` vượt ngưỡng 150 trong hơn 60 giây liên tiếp, hệ thống cảnh báo Prometheus sẽ kích hoạt mã sự cố `ERR_DB_POOL_EXHAUSTED`.
Độ trễ API `/v2/transactions/authorize` sẽ tăng vọt lên trên 3000ms và trả về mã trạng thái HTTP 503.

### 2.2 Quy trình Khắc phục Khẩn cấp (Mitigation Steps)
Kỹ sư trực ca (On-call SRE) cần thực hiện tuần tự 3 bước kiểm tra và xử lý sau:
1. Kiểm tra các tiến trình giữ khóa lâu (long-running locks) trên PostgreSQL replica và primary bằng truy vấn `pg_stat_activity`.
2. Ngắt các phiên truy vấn bị treo vượt quá 5000ms bằng hàm quản trị `SELECT pg_terminate_backend(pid)` đối với các tiến trình báo cáo batch không thuộc luồng thanh toán thời gian thực.
3. Thực thi lệnh nạp lại cấu hình PgBouncer không gây gián đoạn: `kubectl exec -n prod-payment deploy/pgbouncer -- pgbouncer -R /etc/pgbouncer/pgbouncer.ini`.

## 3. Cơ chế Khóa Bất biến (Idempotency Lock) trên Redis
Mỗi yêu cầu thanh toán đều đi kèm khóa `X-Idempotency-Key` được lưu trên cụm Redis Cluster với thời gian sống (TTL) là 86400 giây (24 giờ).
Nếu phát hiện khóa bị kẹt ở trạng thái `IN_PROGRESS` quá 120 giây do pod ứng dụng bị OOMKilled, dịch vụ đối soát nền (`reconciliation-worker`) sẽ tự động kiểm tra trạng thái giao dịch tại cổng ngân hàng trước khi giải phóng khóa.
"""

def ensure_sample_runbook(file_path: Path) -> Path:
    """Khởi tạo tệp tài liệu kỹ thuật mẫu nếu chưa tồn tại."""
    file_path.parent.mkdir(parents=True, exist_ok=True)
    if not file_path.exists():
        file_path.write_text(SAMPLE_RUNBOOK_CONTENT.strip(), encoding="utf-8")
    return file_path

def load_and_chunk_markdown(
    file_path: Path,
    chunk_size: int = 800,
    chunk_overlap: int = 150,
) -> List[Document]:
    """
    Thực hiện phân mảnh 2 tầng cho tài liệu Markdown kỹ thuật:
    1. Tách theo tiêu đề H1, H2, H3 để giữ nguyên cấu trúc phân cấp vào metadata.
    2. Tách đệ quy theo kích thước ký tự và sinh mã định danh SHA-256 cố định.
    """
    raw_markdown = file_path.read_text(encoding="utf-8")

    # Tầng 1: Tách theo cấu trúc tiêu đề Markdown
    headers_to_split_on = [
        ("#", "header_h1"),
        ("##", "header_h2"),
        ("###", "header_h3"),
    ]
    markdown_splitter = MarkdownHeaderTextSplitter(
        headers_to_split_on=headers_to_split_on,
        strip_headers=False,
    )
    header_splits = markdown_splitter.split_text(raw_markdown)

    # Tầng 2: Tách đệ quy bảo toàn ngữ nghĩa đoạn văn
    recursive_splitter = RecursiveCharacterTextSplitter(
        chunk_size=chunk_size,
        chunk_overlap=chunk_overlap,
        add_start_index=True,
        separators=["nn", "n", ". ", " ", ""],
    )
    final_chunks = recursive_splitter.split_documents(header_splits)

    # Bổ sung metadata truy vết nguồn và mã định danh tất định (Idempotent ID)
    enriched_chunks: List[Document] = []
    for idx, chunk in enumerate(final_chunks):
        h1 = chunk.metadata.get("header_h1", "N/A")
        h2 = chunk.metadata.get("header_h2", "N/A")
        h3 = chunk.metadata.get("header_h3", "N/A")
        header_path = " > ".join(
            [h for h in (h1, h2, h3) if h != "N/A"]
        )

        # Tạo SHA-256 hash từ đường dẫn tệp + nội dung để chống trùng lặp khi re-index
        content_signature = f"{file_path.as_posix()}::{header_path}::{chunk.page_content}"
        chunk_id = hashlib.sha256(content_signature.encode("utf-8")).hexdigest()[:16]

        chunk.metadata.update(
            {
                "source": file_path.as_posix(),
                "header_path": header_path,
                "chunk_id": f"chk_{chunk_id}",
                "chunk_index": idx,
            }
        )
        enriched_chunks.append(chunk)

    return enriched_chunks

if __name__ == "__main__":
    doc_path = ensure_sample_runbook(Path("docs/payment_service_runbook.md"))
    chunks = load_and_chunk_markdown(doc_path, chunk_size=500, chunk_overlap=80)
    print(f"[INGEST] Đã phân tách '{doc_path}' thành {len(chunks)} chunks chuẩn hóa:")
    for c in chunks:
        print(
            f"  - ID: {c.metadata['chunk_id']} | "
            f"Header: {c.metadata['header_path']} | "
            f"Độ dài: {len(c.page_content)} ký tự"
        )

4. Vector hóa và Lưu trữ Tri thức với OpenAI Embeddings & Chroma

4.1 Cơ chế biểu diễn ngữ nghĩa của mô hình text-embedding-3-small

Sau khi tài liệu đã được chia thành các đơn vị ngữ nghĩa độc lập (chunks), bước tiếp theo là ánh xạ mỗi đoạn văn bản thành một vector số thực nhiều chiều thông qua mô hình Embedding. Theo tài liệu chính thức của OpenAI (OpenAI Embeddings Guide), mô hình text-embedding-3-small chuyển đổi mỗi đoạn văn bản thành một vector có chiều mặc định d = 1536 (và hỗ trợ rút gọn số chiều linh hoạt nhờ kỹ thuật Matryoshka Representation Learning thông qua tham số dimensions).

Độ tương đồng ngữ nghĩa giữa vector câu hỏi của kỹ sư q và vector đoạn tài liệu d trong không gian 1536 chiều được tính toán thông qua độ đo Cosine Similarity:

CosineSimilarity(q, d) = (q · d) / (||q||_2 * ||d||_2) = sum(q_i * d_i, i=1..1536) / (sqrt(sum(q_i^2)) * sqrt(sum(d_i^2)))

Vì các vector đầu ra của text-embedding-3-small đã được chuẩn hóa độ dài đơn vị (||v||_2 = 1), phép tính Cosine Similarity thực chất rút gọn thành phép nhân vô hướng (Inner Product / Dot Product), giúp tăng tốc độ tìm kiếm láng giềng gần nhất xấp xỉ (Approximate Nearest Neighbor — ANN) trên chỉ mục đồ thị HNSW (Hierarchical Navigable Small World).

4.2 Khởi tạo Vector Store bền vững và Truy vấn theo Ngưỡng điểm tương đồng (Score Threshold)

Trong giai đoạn phát triển dịch vụ đơn nút (Single-node Service) hoặc chạy kiểm thử tích hợp trên CI/CD, Chroma là lựa chọn phù hợp nhờ khả năng lưu trữ bền vững trực tiếp xuống ổ đĩa cục bộ (persist_directory) mà không cần vận hành cụm máy chủ riêng biệt. Tuy nhiên, khi kho tài liệu kỹ thuật của doanh nghiệp mở rộng lên hàng chục triệu vector đòi hỏi phân mảnh ngang (Horizontal Sharding), sao lưu đa vùng (Replication), và lọc metadata tốc độ cao ở quy mô lớn, bạn nên chuyển đổi tầng lưu trữ theo hướng dẫn tại bài viết Xây dựng Vector Database với Milvus và Qdrant cho bài toán tìm kiếm ngữ nghĩa.

Một kỹ thuật quan trọng thường bị bỏ qua trong các bài hướng dẫn cơ bản là: truyền danh sách ids tất định khi lưu vào Vector Store và lọc theo ngưỡng điểm tương đồng (score_threshold) khi truy vấn. Nếu bạn chỉ gọi Chroma.from_documents(chunks) nhiều lần mà không truyền ids, mỗi lần chạy lại script sẽ nhân bản các đoạn văn bản giống hệt nhau trong cơ sở dữ liệu, làm sai lệch hoàn toàn kết quả Top-K.

Hãy tạo tệp vector_store.py thực thi việc lập chỉ mục bất biến (Idempotent Indexing) và kiểm tra điểm tương đồng:

# File: vector_store.py
"""
Module khởi tạo Chroma Vector Store, lập chỉ mục bất biến (Idempotent Upsert)
và truy xuất có kiểm soát ngưỡng tương đồng (Relevance Score Threshold).
"""

import os
from pathlib import Path
from typing import List, Tuple
from dotenv import load_dotenv
from langchain_chroma import Chroma
from langchain_core.documents import Document
from langchain_openai import OpenAIEmbeddings

from ingest import ensure_sample_runbook, load_and_chunk_markdown

load_dotenv()

CHROMA_PERSIST_DIR = "./chroma_data"
COLLECTION_NAME = "infoteck_engineering_runbooks"

def build_or_update_vectorstore(chunks: List[Document]) -> Chroma:
    """
    Khởi tạo hoặc cập nhật Chroma Vector Store sử dụng chunk_id tất định
    để ngăn chặn tình trạng nhân bản dữ liệu (Duplicate Chunks) khi chạy lại.
    """
    if not os.environ.get("OPENAI_API_KEY"):
        raise EnvironmentError(
            "Thiếu biến môi trường OPENAI_API_KEY. Vui lòng thiết lập trước khi chạy."
        )

    embeddings = OpenAIEmbeddings(
        model="text-embedding-3-small",
        dimensions=1536,
    )

    # Sử dụng danh sách chunk_id cố định làm khóa chính trong Chroma
    deterministic_ids = [chunk.metadata["chunk_id"] for chunk in chunks]

    vectorstore = Chroma.from_documents(
        documents=chunks,
        embedding=embeddings,
        ids=deterministic_ids,
        collection_name=COLLECTION_NAME,
        persist_directory=CHROMA_PERSIST_DIR,
        collection_metadata={"hnsw:space": "cosine"},
    )
    return vectorstore

def retrieve_with_threshold(
    vectorstore: Chroma,
    query: str,
    top_k: int = 3,
    score_threshold: float = 0.65,
) -> List[Tuple[Document, float]]:
    """
    Truy vấn các đoạn tài liệu liên quan kèm điểm tương đồng chuẩn hóa [0.0, 1.0].
    Loại bỏ mọi đoạn tài liệu có độ tương đồng thấp hơn ngưỡng an toàn.
    """
    results_with_scores = vectorstore.similarity_search_with_relevance_scores(
        query=query,
        k=top_k,
    )

    filtered_results = [
        (doc, round(score, 4))
        for doc, score in results_with_scores
        if score >= score_threshold
    ]
    return filtered_results

if __name__ == "__main__":
    runbook_file = ensure_sample_runbook(Path("docs/payment_service_runbook.md"))
    doc_chunks = load_and_chunk_markdown(runbook_file, chunk_size=500, chunk_overlap=80)
    vs = build_or_update_vectorstore(doc_chunks)

    test_query = "Làm thế nào để khắc phục lỗi ERR_DB_POOL_EXHAUSTED trên PgBouncer?"
    matched_docs = retrieve_with_threshold(vs, query=test_query, top_k=3, score_threshold=0.60)

    print(f"n[VECTOR SEARCH] Truy vấn: '{test_query}'")
    print(f"[VECTOR SEARCH] Số đoạn vượt ngưỡng (>= 0.60): {len(matched_docs)}")
    for doc, score in matched_docs:
        print(
            f"  -> Score: {score} | ID: {doc.metadata['chunk_id']} | "
            f"Mục: {doc.metadata['header_path']}"
        )

5. Xây dựng Chuỗi Truy vấn LCEL (LangChain Expression Language) và Chống Ảo giác

5.1 Thiết kế System Prompt ràng buộc ngữ cảnh nghiêm ngặt (Strict Grounding)

Chỉ tìm đúng tài liệu là chưa đủ để ngăn chặn ảo giác; bạn phải thiết kế một System Prompt có tính phòng thủ cao (Defensive Prompting) kết hợp với thiết lập tham số sinh văn bản temperature=0.0 (chọn token có xác suất cao nhất một cách tất định).

System Prompt chuẩn kỹ thuật cho hệ thống RAG nội bộ phải tuân thủ 3 nguyên tắc:

  1. Ràng buộc biên giới tri thức (Closed-World Assumption): Bắt buộc mô hình chỉ sử dụng thông tin nằm trong thẻ <context>...</context>. Nghiêm cấm sử dụng kiến thức bên ngoài để tự suy đoán tên lệnh hoặc tham số.
  2. Cơ chế từ chối minh bạch (Explicit Abstention): Nếu ngữ cảnh được cung cấp không chứa đủ thông tin để trả lời câu hỏi, mô hình phải trả lời rõ ràng: "Tài liệu kỹ thuật hiện tại không chứa thông tin về vấn đề này" thay vì cố gắng bịa câu trả lời.
  3. Trích dẫn bắt buộc ở cấp độ câu/đoạn (Mandatory Inline Citations): Mỗi bước hướng dẫn hoặc thông số kỹ thuật đưa ra phải đính kèm mã [chunk_id] ngay cuối câu để kỹ sư đối chiếu.

5.2 Triển khai RAG Chain hoàn chỉnh với trích xuất nguồn (Source Attribution)

Trong LangChain v0.3, thay vì sử dụng lớp RetrievalQA cũ đã bị ngừng khuyến nghị (deprecated), chúng ta sử dụng các hàm khởi tạo dựa trên LCEL là create_stuff_documents_chain và create_retrieval_chain. Đặc biệt, chúng ta định nghĩa tham số document_prompt tùy chỉnh để tiêm trực tiếp chunk_id, source, và header_path của từng đoạn tài liệu vào bên trong cửa sổ ngữ cảnh mà LLM đọc được.

Hãy tạo tệp rag_chain.py như sau:

# File: rag_chain.py
"""
Triển khai chuỗi RAG chuẩn Production sử dụng LangChain v0.3 (LCEL),
ràng buộc ngữ cảnh nghiêm ngặt (Strict Grounding) và trích xuất nguồn trích dẫn.
"""

from pathlib import Path
from typing import Any, Dict, List
from dotenv import load_dotenv
from langchain.chains import create_retrieval_chain
from langchain.chains.combine_documents import create_stuff_documents_chain
from langchain_core.prompts import ChatPromptTemplate, PromptTemplate
from langchain_openai import ChatOpenAI

from ingest import ensure_sample_runbook, load_and_chunk_markdown
from vector_store import build_or_update_vectorstore

load_dotenv()

SYSTEM_PROMPT_TEMPLATE = """Bạn là Trợ lý Kỹ thuật Hệ thống (Staff Site Reliability Engineer Assistant) nội bộ.
Nhiệm vụ của bạn là trả lời câu hỏi kỹ thuật của kỹ sư DỰA HOÀN TOÀN vào nội dung trong khối <context> dưới đây.

QUY TẮC BẮT BUỘC (STRICT GROUNDING RULES):
1. Chỉ sử dụng thông tin có mặt trực tiếp trong <context>. Tuyệt đối không tự suy diễn tham số, câu lệnh CLI hoặc cấu hình không được nêu rõ.
2. Với mỗi thông số kỹ thuật hoặc bước xử lý bạn nêu ra, BẮT BUỘC trích dẫn mã định danh đoạn tài liệu theo định dạng [chunk_id] (ví dụ: [chk_a1b2c3d4]).
3. Nếu thông tin trong <context> không đủ để trả lời chính xác câu hỏi, hãy phản hồi đúng câu sau: "Tài liệu kỹ thuật nội bộ hiện tại không có thông tin đủ căn cứ để trả lời câu hỏi này."

<context>
{context}
</context>"""

def build_grounded_rag_chain(vectorstore):
    """
    Khởi tạo chuỗi LCEL RAG kết hợp định dạng tài liệu giàu siêu dữ liệu (Metadata-Rich Formatting).
    """
    # 1. Khởi tạo LLM với temperature=0.0 để đảm bảo tính nhất quán tối đa
    llm = ChatOpenAI(
        model="gpt-4o-mini",
        temperature=0.0,
        max_tokens=1024,
        timeout=30,
        max_retries=2,
    )

    # 2. Định dạng từng Document trước khi ghép vào biến {context}
    document_prompt = PromptTemplate.from_template(
        "--- ĐOẠN TÀI LIỆU [{chunk_id}] ---n"
        "Nguồn tệp: {source}n"
        "Mục tài liệu: {header_path}n"
        "Nội dung:n{page_content}n"
    )

    # 3. Khởi tạo ChatPromptTemplate kết hợp System Prompt và câu hỏi người dùng
    qa_prompt = ChatPromptTemplate.from_messages(
        [
            ("system", SYSTEM_PROMPT_TEMPLATE),
            ("human", "Câu hỏi kỹ thuật: {input}"),
        ]
    )

    # 4. Tạo chuỗi tổng hợp tài liệu (Stuff Documents Chain)
    question_answer_chain = create_stuff_documents_chain(
        llm=llm,
        prompt=qa_prompt,
        document_prompt=document_prompt,
        document_variable_name="context",
    )

    # 5. Khởi tạo Retriever áp dụng ngưỡng điểm tương đồng
    retriever = vectorstore.as_retriever(
        search_type="similarity_score_threshold",
        search_kwargs={
            "k": 4,
            "score_threshold": 0.55,
        },
    )

    # 6. Kết nối Retriever và Generator thành chuỗi RAG hoàn chỉnh
    rag_chain = create_retrieval_chain(retriever, question_answer_chain)
    return rag_chain

def format_rag_response(raw_response: Dict[str, Any]) -> Dict[str, Any]:
    """Chuẩn hóa đầu ra kèm danh sách nguồn trích dẫn minh bạch."""
    context_docs = raw_response.get("context", [])
    citations: List[Dict[str, str]] = []
    seen_ids = set()

    for doc in context_docs:
        cid = doc.metadata.get("chunk_id", "unknown")
        if cid not in seen_ids:
            seen_ids.add(cid)
            citations.append(
                {
                    "chunk_id": cid,
                    "source": doc.metadata.get("source", "unknown"),
                    "header_path": doc.metadata.get("header_path", "unknown"),
                }
            )

    return {
        "query": raw_response.get("input", ""),
        "answer": raw_response.get("answer", ""),
        "citations": citations,
    }

if __name__ == "__main__":
    doc_path = ensure_sample_runbook(Path("docs/payment_service_runbook.md"))
    chunks = load_and_chunk_markdown(doc_path, chunk_size=500, chunk_overlap=80)
    vs = build_or_update_vectorstore(chunks)

    chain = build_grounded_rag_chain(vs)
    raw_result = chain.invoke(
        {
            "input": "Khi gặp lỗi ERR_DB_POOL_EXHAUSTED, giới hạn statement_timeout hiện tại là bao nhiêu và cần chạy lệnh kubectl nào để nạp lại PgBouncer?"
        }
    )
    structured_output = format_rag_response(raw_result)

    print("n=== KẾT QUẢ PHẢN HỒI TỪ HỆ THỐNG RAG ===")
    print(f"Câu hỏi : {structured_output['query']}")
    print(f"nCâu trả lời:n{structured_output['answer']}")
    print("nNguồn trích dẫn đã kiểm chứng (Verified Citations):")
    for cite in structured_output["citations"]:
        print(f"  - [{cite['chunk_id']}] {cite['source']} ({cite['header_path']})")

6. Tối ưu hóa Hệ thống RAG cho Môi trường Production

Khi chuyển dịch hệ thống RAG từ bản thử nghiệm (Prototype) sang môi trường vận hành thực tế phục vụ hàng trăm kỹ sư đồng thời, một pipeline chỉ dùng tìm kiếm vector đơn thuần sẽ bộc lộ các điểm nghẽn về độ chính xác từ khóa chính xác, hiệu năng I/O đồng bộ, và rủi ro bảo mật dữ liệu. Dưới đây là 3 kỹ thuật nâng cấp cốt lõi cho môi trường Production.

6.1 Tìm kiếm Lai (Hybrid Search): Kết hợp BM25 Lexical Search và Dense Vector

Tìm kiếm vector dày đặc (Dense Vector Search) rất mạnh trong việc nắm bắt ý nghĩa khái quát (ví dụ: hiểu rằng "nghẽn kết nối cơ sở dữ liệu" liên quan đến "connection pool saturation"). Tuy nhiên, các mô hình Embedding lại thường hoạt động kém hiệu quả khi kỹ sư truy vấn các chuỗi ký tự định danh chính xác như mã lỗi (ERR_DB_POOL_EXHAUSTED), tên biến cấu hình (max_client_conn), mã lỗ hổng CVE, hoặc địa chỉ hàm.

Giải pháp chuẩn công nghiệp là sử dụng Hybrid Search thông qua bộ EnsembleRetriever của LangChain, kết hợp:

  • BM25 Retriever (Sparse Lexical Search): Thuật toán xếp hạng dựa trên tần suất từ khóa (TF-IDF cải tiến), cực kỳ nhạy bén với các mã lỗi, tên hàm và thông số kỹ thuật chính xác.
  • Dense Vector Retriever (Chroma): Tìm kiếm ngữ nghĩa theo vector 1536 chiều.
  • Reciprocal Rank Fusion (RRF): Thuật toán hợp nhất thứ hạng từ hai bộ truy xuất độc lập mà không phụ thuộc vào sự khác biệt thang điểm giữa BM25 và Cosine Similarity:

RRF_Score(d in D) = sum_{m in {BM25, Dense}} [ w_m / (c + rank_m(d)) ]

Trong đó w_m là trọng số cấu hình cho từng bộ truy xuất (ví dụ: weights=[0.4, 0.6]), rank_m(d) là vị trí xếp hạng của đoạn tài liệu d trong danh sách kết quả của bộ truy xuất m, và c là hằng số làm mượt (thường mặc định c = 60). Với các hệ thống đòi hỏi độ chính xác cao hơn nữa ở bước hậu kiểm, bạn có thể đặt thêm một tầng Cross-Encoder Reranker (như BAAI/bge-reranker-v2-m3 hoặc CohereRerank) ngay sau EnsembleRetriever để chấm điểm lại Top-20 ứng viên xuống còn Top-4 đoạn văn bản chất lượng nhất trước khi gửi vào LLM.

6.2 Xử lý Bất đồng bộ (Async I/O) và Bảo mật dữ liệu (PII Scrubbing & Guardrails)

Trong một dịch vụ API thực tế (ví dụ xây dựng bằng FastAPI), toàn bộ thời gian chờ phản hồi từ Vector Database và OpenAI API đều là các tác vụ I/O-bound. Nếu gọi phương thức đồng bộ .invoke(), tiến trình worker sẽ bị chặn (block) hoàn toàn trong suốt 1 – 3 giây chờ mạng. Do đó, ở môi trường Production, chúng ta bắt buộc phải sử dụng giao diện bất đồng bộ .ainvoke() hoặc .astream() của LCEL. Nếu bạn muốn nắm vững cách tối ưu hóa Event Loop và quản lý luồng thực thi không chặn trong Python cho các dịch vụ tải cao, hãy đọc bài viết chuyên sâu Lập trình Bất đồng bộ trong Python: Asyncio, Threads và Multiprocessing.

Bên cạnh hiệu năng, một yêu cầu bắt buộc về bảo mật (Security Hygiene) khi kỹ sư dán log lỗi vào hệ thống RAG là lọc bỏ thông tin nhạy cảm (PII & Secret Scrubbing) và chặn tấn công tiêm nhiễm câu lệnh (Prompt Injection Defense) trước khi dữ liệu rời khỏi mạng nội bộ để gửi tới API của nhà cung cấp LLM.

Đoạn mã production_rag.py dưới đây tích hợp đầy đủ Hybrid Search (BM25 + Chroma), tầng lọc bảo mật đầu vào, và thực thi bất đồng bộ với asyncio:

# File: production_rag.py
"""
Pipeline RAG chuẩn Production tích hợp:
1. Hybrid Search (BM25 + Chroma Dense Vector qua Reciprocal Rank Fusion).
2. Security Guardrails (Ẩn khóa bí mật/email trong log & Chặn Prompt Injection).
3. Thực thi bất đồng bộ (Asyncio .ainvoke) không chặn luồng I/O.
"""

import asyncio
import re
import time
from pathlib import Path
from typing import Any, Dict, List
from dotenv import load_dotenv
from langchain.chains import create_retrieval_chain
from langchain.chains.combine_documents import create_stuff_documents_chain
from langchain.retrievers import EnsembleRetriever
from langchain_community.retrievers import BM25Retriever
from langchain_core.documents import Document
from langchain_core.prompts import ChatPromptTemplate, PromptTemplate
from langchain_openai import ChatOpenAI

from ingest import ensure_sample_runbook, load_and_chunk_markdown
from rag_chain import SYSTEM_PROMPT_TEMPLATE, format_rag_response
from vector_store import build_or_update_vectorstore

load_dotenv()

# Biểu thức chính quy phát hiện thông tin nhạy cảm thường bị kỹ sư dán kèm trong log
SECRET_PATTERNS = [
    (re.compile(r"(Bearers+)[A-Za-z0-9-._~+/]+=*", re.IGNORECASE), r"1[REDACTED_TOKEN]"),
    (re.compile(r"(sk-[A-Za-z0-9]{20,})"), "[REDACTED_API_KEY]"),
    (re.compile(r"[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+.[a-zA-Z0-9-.]+"), "[REDACTED_EMAIL]"),
]

INJECTION_SIGNATURES = [
    "ignore previous instructions",
    "bỏ qua các hướng dẫn trước",
    "system prompt",
    "reveal your instructions",
]

def sanitize_and_validate_query(raw_query: str) -> str:
    """
    Làm sạch thông tin nhạy cảm (Tokens, API Keys, Emails) và kiểm tra dấu hiệu
    tấn công Prompt Injection trước khi chuyển tiếp truy vấn tới LLM.
    """
    lowered = raw_query.lower()
    for signature in INJECTION_SIGNATURES:
        if signature in lowered:
            raise ValueError(
                f"Truy vấn bị từ chối bởi Security Guardrail: Phát hiện mẫu câu lệnh bất thường ('{signature}')."
            )

    sanitized = raw_query
    for pattern, replacement in SECRET_PATTERNS:
        sanitized = pattern.sub(replacement, sanitized)
    return sanitized

def build_hybrid_retriever(
    chunks: List[Document],
    vectorstore,
    top_k: int = 3,
) -> EnsembleRetriever:
    """
    Khởi tạo bộ truy xuất lai (Hybrid Retriever) kết hợp:
    - BM25Retriever (trọng số 0.4): Bắt chính xác mã lỗi, tên hàm, tham số cấu hình.
    - Chroma VectorStoreRetriever (trọng số 0.6): Bắt tương đồng ngữ nghĩa sâu.
    """
    bm25_retriever = BM25Retriever.from_documents(chunks)
    bm25_retriever.k = top_k

    dense_retriever = vectorstore.as_retriever(
        search_type="similarity",
        search_kwargs={"k": top_k},
    )

    ensemble_retriever = EnsembleRetriever(
        retrievers=[bm25_retriever, dense_retriever],
        weights=[0.4, 0.6],
    )
    return ensemble_retriever

async def execute_production_query(
    rag_chain,
    raw_query: str,
) -> Dict[str, Any]:
    """Thực thi truy vấn bất đồng bộ kèm đo lường độ trễ và bảo vệ đầu vào."""
    start_time = time.perf_counter()
    clean_query = sanitize_and_validate_query(raw_query)

    raw_response = await rag_chain.ainvoke({"input": clean_query})
    formatted = format_rag_response(raw_response)
    formatted["sanitized_query"] = clean_query
    formatted["latency_ms"] = round((time.perf_counter() - start_time) * 1000, 2)
    return formatted

async def main() -> None:
    doc_path = ensure_sample_runbook(Path("docs/payment_service_runbook.md"))
    chunks = load_and_chunk_markdown(doc_path, chunk_size=500, chunk_overlap=80)
    vs = build_or_update_vectorstore(chunks)

    hybrid_retriever = build_hybrid_retriever(chunks, vs, top_k=3)

    llm = ChatOpenAI(model="gpt-4o-mini", temperature=0.0, max_tokens=1024)
    document_prompt = PromptTemplate.from_template(
        "--- ĐOẠN TÀI LIỆU [{chunk_id}] ---n"
        "Nguồn tệp: {source}n"
        "Mục tài liệu: {header_path}n"
        "Nội dung:n{page_content}n"
    )
    qa_prompt = ChatPromptTemplate.from_messages(
        [
            ("system", SYSTEM_PROMPT_TEMPLATE),
            ("human", "Câu hỏi kỹ thuật: {input}"),
        ]
    )
    stuff_chain = create_stuff_documents_chain(
        llm=llm,
        prompt=qa_prompt,
        document_prompt=document_prompt,
    )
    production_rag_chain = create_retrieval_chain(hybrid_retriever, stuff_chain)

    # Giả lập truy vấn thực tế của kỹ sư có chứa kèm Bearer Token và Email trong log
    incident_query = (
        "Log báo lỗi từ user [email protected] với header Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 "
        "khi gặp mã ERR_DB_POOL_EXHAUSTED. Tôi cần kiểm tra bảng nào trong PostgreSQL và xử lý các phiên treo quá bao nhiêu ms?"
    )

    result = await execute_production_query(production_rag_chain, incident_query)
    print("n=== KẾT QUẢ PRODUCTION HYBRID RAG (ASYNC) ===")
    print(f"Truy vấn đã lọc PII : {result['sanitized_query']}")
    print(f"Thời gian thực thi  : {result['latency_ms']} ms")
    print(f"nCâu trả lời:n{result['answer']}")
    print("nDanh sách Citations:")
    for c in result["citations"]:
        print(f"  * [{c['chunk_id']}] {c['source']} -> {c['header_path']}")

if __name__ == "__main__":
    asyncio.run(main())

Để kiểm chứng toàn bộ quy trình từ khâu băm tài liệu đến truy xuất lai bất đồng bộ trên dòng lệnh (CLI Verification), bạn thực thi tuần tự các lệnh sau:

# Bước 1: Kiểm tra cú pháp toàn bộ các module Python (Static Syntax Verification)
python3 -m py_compile ingest.py vector_store.py rag_chain.py production_rag.py

# Bước 2: Chạy kiểm thử bộ phân mảnh 2 tầng (không cần gọi API ngoài)
python3 ingest.py

# Bước 3: Chạy toàn bộ pipeline Production Hybrid RAG (yêu cầu đã export OPENAI_API_KEY)
python3 production_rag.py

Kết quả đầu ra tiêu chuẩn trên cửa sổ dòng lệnh khi thực thi:

[INGEST] Đã phân tách 'docs/payment_service_runbook.md' thành 4 chunks chuẩn hóa:
  - ID: chk_8f92a1c4e7b03d12 | Header: Runbook Vận hành: Dịch vụ Thanh toán Lõi (Payment Core Service) > 1. Kiến trúc Kết nối Cơ sở dữ liệu | Độ dài: 378 ký tự
  - ID: chk_3c41d9e0a2158b76 | Header: Runbook Vận hành: Dịch vụ Thanh toán Lõi (Payment Core Service) > 2. Xử lý Sự cố Mã lỗi ERR_DB_POOL_EXHAUSTED > 2.1 Triệu chứng và Cảnh báo | Độ dài: 294 ký tự
  - ID: chk_7a19b5f3d8204e61 | Header: Runbook Vận hành: Dịch vụ Thanh toán Lõi (Payment Core Service) > 2. Xử lý Sự cố Mã lỗi ERR_DB_POOL_EXHAUSTED > 2.2 Quy trình Khắc phục Khẩn cấp (Mitigation Steps) | Độ dài: 486 ký tự
  - ID: chk_5e08c2d7f1946a33 | Header: Runbook Vận hành: Dịch vụ Thanh toán Lõi (Payment Core Service) > 3. Cơ chế Khóa Bất biến (Idempotency Lock) trên Redis | Độ dài: 352 ký tự

=== KẾT QUẢ PRODUCTION HYBRID RAG (ASYNC) ===
Truy vấn đã lọc PII : Log báo lỗi từ user [REDACTED_EMAIL] với header Authorization: Bearer [REDACTED_TOKEN] khi gặp mã ERR_DB_POOL_EXHAUSTED. Tôi cần kiểm tra bảng nào trong PostgreSQL và xử lý các phiên treo quá bao nhiêu ms?
Thời gian thực thi  : 1420.65 ms

Câu trả lời:
Khi xảy ra mã lỗi `ERR_DB_POOL_EXHAUSTED`, bạn cần thực hiện các bước kiểm tra và xử lý trên PostgreSQL như sau:
1. Kiểm tra các tiến trình giữ khóa lâu (long-running locks) trên cả PostgreSQL replica và primary thông qua bảng/khung nhìn hệ thống `pg_stat_activity` [chk_7a19b5f3d8204e61].
2. Ngắt các phiên truy vấn bị treo vượt quá **5000ms** bằng hàm quản trị `SELECT pg_terminate_backend(pid)` đối với các tiến trình báo cáo batch không thuộc luồng thanh toán thời gian thực [chk_7a19b5f3d8204e61].

Danh sách Citations:
  * [chk_7a19b5f3d8204e61] docs/payment_service_runbook.md -> Runbook Vận hành: Dịch vụ Thanh toán Lõi (Payment Core Service) > 2. Xử lý Sự cố Mã lỗi ERR_DB_POOL_EXHAUSTED > 2.2 Quy trình Khắc phục Khẩn cấp (Mitigation Steps)
  * [chk_3c41d9e0a2158b76] docs/payment_service_runbook.md -> Runbook Vận hành: Dịch vụ Thanh toán Lõi (Payment Core Service) > 2. Xử lý Sự cố Mã lỗi ERR_DB_POOL_EXHAUSTED > 2.1 Triệu chứng và Cảnh báo

6.3 Đánh giá Chất lượng Hệ thống RAG bằng Bộ chỉ số Kỹ thuật (RAG Evaluation Metrics)

Trong kỹ thuật phần mềm chuyên nghiệp, chất lượng của một hệ thống RAG không bao giờ được đo lường bằng những con số cảm tính hay tuyên bố chung chung. Thay vào đó, nhóm kỹ sư AI cần xây dựng một bộ dữ liệu kiểm thử chuẩn (Golden Evaluation Dataset — gồm các cặp question, ground_truth_answer, và expected_context_ids) và đo lường định lượng theo khung đánh giá RAGAS (Automated Evaluation of Retrieval Augmented Generation — Es et al., 2023) dựa trên 4 chỉ số độc lập:

  1. Context Precision (Độ chuẩn xác của Ngữ cảnh truy xuất): Đo lường tỷ lệ các đoạn tài liệu thực sự liên quan trong tập Top-K được Retriever trả về và liệu các đoạn quan trọng nhất có được xếp ở vị trí đầu tiên hay không. Nếu Context Precision thấp, bạn cần giảm chunk_size, nâng ngưỡng score_threshold, hoặc bổ sung bộ Cross-Encoder Reranker.
  2. Context Recall (Độ phủ của Ngữ cảnh truy xuất): Đo lường xem tập tài liệu được Retriever lấy ra có chứa đầy đủ mọi thông tin cần thiết để trả lời câu hỏi chuẩn (ground_truth) hay không. Nếu Context Recall thấp, nguyên nhân thường do cách cắt văn bản làm mất tiêu đề hoặc chỉ tìm kiếm Dense Vector mà thiếu BM25 Hybrid Search.
  3. Faithfulness (Độ trung thực / Chống ảo giác của Generator): Đo lường tỷ lệ các mệnh đề khẳng định trong câu trả lời của LLM có thể suy ra trực tiếp từ <context> đã truy xuất. Chỉ số Faithfulness tiến sát tới 1.0 chứng minh System Prompt và thiết lập temperature=0.0 đã khóa chặt hiện tượng mô hình tự bịa thông tin ngoài tài liệu.
  4. Answer Relevance (Độ sát trọng tâm của Câu trả lời): Đánh giá xem câu trả lời cuối cùng có giải quyết trực tiếp và đầy đủ câu hỏi của kỹ sư hay không, tránh tình trạng trả lời lan man hoặc bỏ sót ý phụ trong truy vấn đa vế.

Việc tách bạch rõ ràng hai nhóm chỉ số Retrieval (Context Precision, Context Recall) và Generation (Faithfulness, Answer Relevance) giúp đội ngũ kỹ thuật khoanh vùng chính xác nguyên nhân gốc rễ mỗi khi chất lượng câu trả lời suy giảm, đồng thời thiết lập được cổng kiểm soát chất lượng tự động (Quality Gate) ngay trong pipeline CI/CD trước khi triển khai phiên bản mới lên Production.


7. Tổng kết & Tài liệu tham khảo (References)

7.1 Tóm tắt Checklist Triển khai RAG Production

Để đảm bảo hệ thống RAG vận hành ổn định, chính xác và an toàn trong môi trường kỹ thuật phần mềm doanh nghiệp, hãy rà soát danh sách kiểm tra (Production Readiness Checklist) sau trước khi đưa dịch vụ vào sử dụng:

  • Bảo toàn cấu trúc tài liệu: Luôn kết hợp MarkdownHeaderTextSplitter với RecursiveCharacterTextSplitter để không làm đứt gãy ngữ cảnh tiêu đề H1/H2/H3 khỏi nội dung chi tiết và các khối mã lệnh.
  • Lập chỉ mục bất biến (Idempotent Indexing): Sinh mã băm tất định (SHA-256 chunk_id) cho từng đoạn tài liệu và truyền vào tham số ids của Vector Store để ngăn chặn trùng lặp dữ liệu khi cập nhật tài liệu.
  • Truy xuất lai và lọc ngưỡng: Kết hợp BM25Retriever (từ khóa chính xác, mã lỗi) và VectorStoreRetriever (tương đồng ngữ nghĩa) qua EnsembleRetriever, đồng thời loại bỏ các đoạn có điểm tương đồng thấp.
  • Ràng buộc chống ảo giác: Thiết lập temperature=0.0, khóa biên giới tri thức trong thẻ <context>, cho phép mô hình từ chối trả lời khi thiếu căn cứ, và bắt buộc trích dẫn [chunk_id] trên từng thông số kỹ thuật.
  • Bảo mật & Hiệu năng: Tích hợp lớp làm sạch thông tin nhạy cảm (API Keys, Bearer Tokens, PII) trước khi gọi API ngoài và sử dụng phương thức bất đồng bộ .ainvoke() để tối ưu thông lượng phục vụ.

7.2 Tài liệu tham khảo & Nguồn đối chiếu

  1. Lewis, P., Perez, E., Piktus, A., Petroni, F., Karpukhin, V., Goyal, N., ... & Kiela, D. (2020). Retrieval-Augmented Generation for Knowledge-Intensive NLP Tasks. Advances in Neural Information Processing Systems (NeurIPS 2020), 33, 9459–9474. Truy cập tại: https://arxiv.org/abs/2005.11401
  2. Es, S., James, J., Espinosa-Anke, L., & Schockaert, S. (2023). RAGAS: Automated Evaluation of Retrieval Augmented Generation. Truy cập tại: https://arxiv.org/abs/2309.15217
  3. LangChain Documentation (v0.3). Build a Retrieval Augmented Generation (RAG) App & Text Splitters Guide. Truy cập tại: https://python.langchain.com/docs/tutorials/rag/
  4. OpenAI Platform Documentation. Vector Embeddings Guide (text-embedding-3-small & text-embedding-3-large). Truy cập tại: https://platform.openai.com/docs/guides/embeddings