RAG Cho Người Bận Rộn: Từ Zero Đến Có ChatGPT Riêng Cho Codebase

Karify98 & Amy 🌸·
Cover Image for RAG Cho Người Bận Rộn: Từ Zero Đến Có ChatGPT Riêng Cho Codebase

Tại Sao ChatGPT Không Hiểu Codebase Của Bạn?

TL;DR: LLM chỉ biết những gì nó đã học trong training data. Codebase của bạn — logic nghiệp vụ, config nội bộ, convention team — hoàn toàn vô hình với nó.

Bạn paste 500 dòng code vào ChatGPT và hỏi "hàm này dùng để làm gì?". Nó trả lời khá OK. Nhưng hỏi "khi user submit order, flow xử lý từ controller đến database trông như thế nào?" — nó bó tay. Tại sao?

Vì LLM hoạt động như một cuốn sách đã in xong. Nó biết rất nhiều về thế giới nói chung, nhưng hoàn toàn mù tịt về codebase riêng của bạn. Những gì bạn cần là cho nó khả năng "tra cứu" tài liệu của bạn trước khi trả lời.

Đó chính là RAG.

RAG Là Gì? Giải Thích Đơn Giản

TL;DR: RAG = tìm tài liệu liên quan trong kho dữ liệu của bạn → nhét vào prompt → cho LLM trả lời dựa trên tài liệu đó.

Không academic, không phức tạp. RAG chỉ là 3 bước:

  1. Nhét tài liệu của bạn vào một cái "kho" thông minh — code, docs, wiki, Slack messages, tùy bạn
  2. Khi có câu hỏi, tìm những đoạn liên quan nhất trong kho đó
  3. Đưa những đoạn tìm được + câu hỏi cho LLM — nó sẽ trả lời dựa trên context thực tế

Giống như bạn cho một junior dev quyền search codebase trước khi trả lời câu hỏi. Không phải memorize toàn bộ — chỉ cần biết tìm ở đâu.

Tại sao không fine-tune? Fine-tune = dạy lại model từ đầu (tốn tiền, tốn thời gian, phải retrain khi code thay đổi). RAG = cho model mượn tài liệu khi cần (nhanh, rẻ, cập nhật realtime). Với hầu hết use case, RAG thắng fine-tune cả về chi phí lẫn tính thực tế.

Architecture: Embeddings → Vector DB → Retrieval → LLM

TL;DR: Biến text thành vector số → lưu vào DB → tìm vector gần nhất khi query → đưa cho LLM sinh câu trả lời.

┌─────────────┐     ┌──────────────┐     ┌─────────────┐     ┌──────────┐
│  Documents   │────▶│  Embeddings  │────▶│  Vector DB  │────▶│  Store   │
│ (code, docs) │     │   (OpenAI)   │     │ (pgvector)  │     │          │
└─────────────┘     └──────────────┘     └─────────────┘     └──────────┘

User Query ──▶ Embedding ──▶ Vector Search ──▶ Top-K Results ──▶ LLM + Context ──▶ Answer

Từng bước:

1. Embeddings — Biến text thành số

Embedding model chuyển mỗi đoạn text thành một vector (mảng số) có hàng nghìn chiều. Hai đoạn text có nghĩa tương tự sẽ có vector "gần nhau" trong không gian nhiều chiều.

// "Hàm xử lý thanh toán" → [0.023, -0.156, 0.891, ...] (1536 chiều)
const embedding = await openai.embeddings.create({
  model: "text-embedding-3-small",
  input: "Hàm xử lý thanh toán đơn hàng",
});

2. Vector DB — Lưu và tìm kiếm

Vector database lưu các embedding và cho phép tìm kiếm theo độ tương tự (cosine similarity). Thay vì SELECT * WHERE name = 'foo', bạn tìm "đoạn nào có nghĩa gần nhất với câu hỏi?".

3. Retrieval — Tìm tài liệu liên quan

Khi user hỏi, câu hỏi cũng được embed → so sánh với tất cả vectors trong DB → lấy top-K kết quả gần nhất.

4. Generation — LLM trả lời

Nối top-K kết giấuvào prompt, kèm câu hỏi gốc. LLM giờ có context thực tế để trả lời chính xác.

Hands-on: Build Mini RAG Với NodeJS/TypeScript

TL;DR: Dùng OpenAI cho embeddings, PostgreSQL + pgvector cho storage, 50 dòng code là chạy được.

Yêu cầu

  • Node.js 18+
  • PostgreSQL 15+ với extension pgvector
  • OpenAI API key

Bước 1: Thiết Lập Database

-- Bật extension pgvector
CREATE EXTENSION IF NOT EXISTS vector;

-- Tabela lưu documents + embeddings
CREATE TABLE documents (
  id SERIAL PRIMARY KEY,
  content TEXT NOT NULL,
  metadata JSONB DEFAULT '{}',
  embedding vector(1536)  -- text-embedding-3-small output dimension
);

-- Index cho cosine similarity search
CREATE INDEX ON documents USING ivfflat (embedding vector_cosine_ops)
  WITH (lists = 100);

Bước 2: Install Dependencies

npm init -y
npm install openai pg
npm install -D typescript @types/node @types/pg
// tsconfig.json
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "strict": true,
    "outDir": "dist"
  }
}

Bước 3: Core RAG Code

// src/rag.ts
import OpenAI from "openai";
import pg from "pg";

const openai = new OpenAI();
const pool = new pg.Pool({ connectionString: process.env.DATABASE_URL });

// --- INDEX: Chunk + Embed + Store ---

function chunkText(text: string, chunkSize = 1000, overlap = 200): string[] {
  const chunks: string[] = [];
  let start = 0;
  while (start < text.length) {
    chunks.push(text.slice(start, start + chunkSize));
    start += chunkSize - overlap;
  }
  return chunks;
}

async function indexDocument(content: string, metadata: Record<string, unknown> = {}) {
  const chunks = chunkText(content);
  const client = await pool.connect();

  try {
    for (const chunk of chunks) {
      // 1. Tạo embedding cho mỗi chunk
      const { data } = await openai.embeddings.create({
        model: "text-embedding-3-small",
        input: chunk,
      });

      // 2. Lưu vào pgvector
      await client.query(
        `INSERT INTO documents (content, metadata, embedding)
         VALUES ($1, $2, $3::vector)`,
        [chunk, JSON.stringify(metadata), JSON.stringify(data[0].embedding)]
      );
    }
    console.log(`✅ Indexed ${chunks.length} chunks`);
  } finally {
    client.release();
  }
}

// --- QUERY: Retrieve + Generate ---

async function query(question: string, topK = 5): Promise<string> {
  // 1. Embed câu hỏi
  const { data } = await openai.embeddings.create({
    model: "text-embedding-3-small",
    input: question,
  });

  const queryEmbedding = JSON.stringify(data[0].embedding);

  // 2. Tìm top-K documents gần nhất (cosine distance)
  const client = await pool.connect();
  try {
    const { rows } = await client.query(
      `SELECT content, metadata,
              1 - (embedding <=> $1::vector) AS similarity
       FROM documents
       ORDER BY embedding <=> $1::vector
       LIMIT $2`,
      [queryEmbedding, topK]
    );

    // 3. Ghép context + generate answer
    const context = rows
      .map((r, i) => `[${i + 1}] (similarity: ${r.similarity.toFixed(3)})\n${r.content}`)
      .join("\n\n---\n\n");

    const completion = await openai.chat.completions.create({
      model: "gpt-4o-mini",
      messages: [
        {
          role: "system",
          content: `Bạn là trợ lý code assistant. Trả lời câu hỏi dựa trên context được cung cấp. Nếu context không đủ thông tin, nói rõ rằng bạn không tìm thấy. Luôn cite source bằng số [1], [2], etc.`,
        },
        {
          role: "user",
          content: `Context:\n${context}\n\n---\n\nCâu hỏi: ${question}`,
        },
      ],
    });

    return completion.choices[0].message.content ?? "Không có câu trả lời.";
  } finally {
    client.release();
  }
}

// --- USAGE ---

async function main() {
  // Index một đoạn code
  await indexDocument(
    `
    // src/services/payment.service.ts
    export class PaymentService {
      async processOrder(orderId: string) {
        const order = await this.orderRepo.findById(orderId);
        if (!order) throw new Error("Order not found");

        const payment = await this.stripe.charges.create({
          amount: order.total,
          currency: "vnd",
          customer: order.customerId,
        });

        await this.orderRepo.update(orderId, { status: "paid", paymentId: payment.id });
        return payment;
      }
    }
    `,
    { file: "src/services/payment.service.ts", type: "code" }
  );

  // Hỏi
  const answer = await query("Flow xử lý thanh toán đơn hàng trông như thế nào?");
  console.log(answer);
}

main().catch(console.error);

Chạy:

export DATABASE_URL="postgresql://localhost:5432/your_db"
export OPENAI_API_KEY="sk-..."

npx tsx src/rag.ts

Bước 4: Index Toàn Bộ Codebase

// src/index-codebase.ts
import { readdir, readFile } from "fs/promises";
import { join, extname } from "path";
import { indexDocument } from "./rag";

const SKIP_DIRS = ["node_modules", ".git", "dist", "build", ".next"];
const CODE_EXTS = [".ts", ".tsx", ".js", ".jsx", ".md", ".sql", ".yaml", ".yml", ".json"];

async function walkDir(dir: string): Promise<string[]> {
  const files: string[] = [];
  const entries = await readdir(dir, { withFileTypes: true });

  for (const entry of entries) {
    const fullPath = join(dir, entry.name);
    if (entry.isDirectory()) {
      if (!SKIP_DIRS.includes(entry.name)) {
        files.push(...(await walkDir(fullPath)));
      }
    } else if (CODE_EXTS.includes(extname(entry.name))) {
      files.push(fullPath);
    }
  }
  return files;
}

async function main() {
  const rootDir = process.argv[2] || ".";
  const files = await walkDir(rootDir);
  console.log(`📁 Found ${files.length} files`);

  for (const file of files) {
    const content = await readFile(file, "utf-8");
    if (content.trim().length === 0) continue;

    await indexDocument(content, { file, type: "codebase" });
    console.log(`  ✅ ${file}`);
  }

  console.log(`\n🎉 Done! Indexed ${files.length} files.`);
}

main().catch(console.error);
npx tsx src/index-codebase.ts ./my-project

So Sánh Nhanh: pgvector vs Pinecone vs Weaviate

TL;DR: Dùng PostgreSQL rồi? pgvector. Muốn managed service? Pinecone. Cần hybrid search? Weaviate.

  • pgvector — Extension cho PostgreSQL, zero infrastructure thêm. Developer đã quen PostgreSQL thì đây là lựa chọn mặc định. Performance ổn với index IVFFlat/HNSW, miễn phí (tự host), nhưng cần tune index khi data lớn (>1M vectors).

  • Pinecone — Managed vector DB, không cần lo infrastructure. Thiết lập 5 phút, auto-scaling, metadata filtering tốt. Nhưng là vendor lock-in, giá tăng nhanh khi scale, và data nằm trên server người khác.

  • WeaviateOpen-source, hỗ trợ hybrid search (vector + keyword). Module built-in cho nhiều embedding model. Nhưng learning curve cao hơn, tự host thì phải maintain cluster.

Khuyến nghị cho developer bận rộn: Bắt đầu với pgvector. Bạn đã có PostgreSQL, đã biết SQL, không cần thêm infrastructure. Khi nào scale lên hàng triệu documents rồi hãy nghĩ đến Pinecone hay Weaviate.

Tips & Pitfalls Thực Tế

TL;DR: RAG trông đơn giản nhưng có nhiều chi tiết nhỏ giết chết chất lượng nếu bỏ qua.

🔹 Chunking strategy quyết định 80% chất lượng

Chunk quá nhỏ → mất context. Chunk quá lớn → noise át signal. Bắt đầu với 500-1000 tokens, overlap 10-20%. Với code, chunk theo function/class thay vì theo số ký tự cố định.

🔹 Metadata filtering là vũ khí bí mật

Thay vì search toàn bộ, filter theo metadata trước:

SELECT * FROM documents
WHERE metadata->>'type' = 'code'
  AND metadata->>'file' LIKE '%payment%'
ORDER BY embedding <=> $1::vector
LIMIT 5;

Nhanh hơn, chính xác hơn.

🔹 Không phải câu hỏi nào cũng cần RAG

"What is a REST API?" → LLM trả lời được, không cần RAG. "How does our payment service handle refunds?" → Cần RAG.

Dùng RAG khi câu hỏi chỉ có thể trả lời bằng dữ liệu nội bộ của bạn.

🔹 Chunk overlap prevents context bleeding

Nếu cắt text tại ranh giới câu, bạn sẽ mất nghĩa. Overlap 100-200 tokens giữa các chunk giúp giữ context liền mạch.

🔹 Test retrieval trước generation

Nếu search không ra đúng document liên quan, LLM giỏi mấy cũng trả lời sai. Luôn log top-K results để debug:

console.log("Top results:", rows.map(r => ({
  similarity: r.similarity,
  preview: r.content.slice(0, 100),
})));

🔹 Rate limits & cost

Embedding 1000 file × 10 chunks/file = 10,000 API calls. Với text-embedding-3-small, giá ~$0.02/1M tokens — rẻ. Nhưng nếu dùng text-embedding-3-large (3072 chiều), giá gấp 6.5x. Chọn model phù hợp túi tiền.

🔹 Stale embeddings là kẻ giết người thầm lặng

Code thay đổi mỗi ngày nhưng embeddings thì không. Cần pipeline re-index định kỳ (cron job, git hook, hoặc on-demand). Nếu không, AI sẽ trả lời dựa trên code version cũ — nguy hiểm hơn là không trả lời.

Kết Luận + Next Steps

RAG không phải giải pháp vạn năng, nhưng nó là cách tiếp cận thực tế nhất để cho AI hiểu codebase của bạn. Không cần fine-tune đắt đỏ. Không cần upload code lên cloud service lạ (nếu dùng pgvector tự host). Thiết lập trong 1 giờ, chạy ngay.

Next steps nếu bạn muốn đi sâu hơn:

  • Agentic RAG — Cho AI tự quyết định khi nào cần search, search gì, và search几次
  • Multi-modal RAG — Index cả hình ảnh, PDF, diagram (dùng CLIP embeddings)
  • Reranking — Dùng cross-encoder model để sắp xếp lại kết quả retrieval cho chính xác hơn
  • Hybrid search — Kết hợp vector search + keyword search (BM25) cho kết quả tốt hơn

Bài tiếp theo trong series "AI For Developers" sẽ đi sâu vào Agentic RAG — khi nào AI nên tự search, khi nào nên hỏi lại, và làm sao để nó tự cải thiện theo thời gian.


Bài viết này là phần 1 của series AI For Developers — hướng dẫn thực tế cho developer muốn dùng AI trong công việc hàng ngày.