---
title: "Fine-tuning là gì? Khi nào chọn fine-tune, khi nào chọn RAG"
description: "Fine tuning là gì: khi kiến thức hay đổi thì dùng RAG; muốn giữ format và giọng shop thì xét fine-tune hoặc LoRA."
canonical: "https://200lab.io/blog/fine-tuning-la-gi"
published: "2026-11-11T13:24:37Z"
updated: "2026-11-11T13:24:37Z"
authors: ["Việt Trần"]
tags: ["AI"]
reading_minutes: 17
access: "public"
---

Giả sử bạn đang làm một chatbot cho shop thời trang. Khách hỏi mua nhầm size thì gửi lại được không, hoặc áo size M còn hàng không. Chính sách và tồn kho hay đổi, nên bot cần tra sổ tài liệu hiện tại chứ không thuộc lòng.

Cùng lúc, shop muốn câu tồn kho chỉ ra đúng một trong ba nhãn cố định và giữ giọng ngắn; bước retrieval lấy được tài liệu nhưng không tự ép được format đó. Đây chỉ là ví dụ minh họa giả định, không phải trải nghiệm hay sự cố thật. Bài này nói fine-tuning là gì, khác pre-training chỗ nào, khi nào chọn RAG, khi nào chọn fine-tune, LoRA ở mức nhập môn, rồi chạy thử một bộ FAQ mẫu.

## Fine-tuning là gì, khác pre-training chỗ nào

Foundation model, tức model nền tảng cỡ lớn, bắt đầu bằng pre-training: model được train trên dữ liệu rộng và đa nhiệm để có khả năng ngôn ngữ chung. Lúc này language model đọc hiểu và nối chữ hợp lý, nhưng chưa quen cách làm riêng trong app của bạn.

Fine-tuning là bước tiếp tục train model đã qua pre-training trên một dataset nhỏ hơn, gắn với một task hoặc một lĩnh vực cụ thể. Khi bạn đưa cặp input và output có nhãn để model học theo, bước này còn gọi là supervised fine-tuning. Quá trình này cập nhật các weight, tức các con số bên trong quyết định cách model tính đầu ra, trên toàn bộ hoặc một phần model.

Pre-training giống học đọc viết phổ thông, còn fine-tuning là lúc rèn cách trả lời đúng mẫu phiếu của một quầy tiếp khách. Chúng ta thường fine-tune vì hai lý do. Một là tăng độ đúng và tính nhất quán trên một task, nhờ cho model xem nhiều example làm mẫu. Hai là tiết kiệm khi chạy thật: đạt chất lượng tương đương với ít token trong prompt hơn, nhờ đó giảm latency, tức độ trễ phản hồi, hoặc dùng được một model nhỏ hơn.

Bước quyết định nằm ở việc chuẩn bị dataset. Example phải đại diện đúng cho câu hỏi, định dạng và context mà model sẽ gặp lúc chạy thật. Trong phần [giới thiệu về model tuning của Google Cloud](https://docs.cloud.google.com/gemini-enterprise-agent-platform/models/tuning), hướng dẫn chỉ rõ supervised fine-tuning rất hợp với bài toán ép format đầu ra, classification để phân loại dữ liệu, hoặc khi cần chỉnh văn phong riêng cho một domain.

## RAG giải quyết bài nào, fine-tune giải quyết bài nào

Khi làm app với language model, anh em rất dễ phân vân giữa nhồi thêm dữ liệu vào prompt hay đem model đi train tiếp. Nếu bạn mới tiếp cận khái niệm [RAG là gì và cách truy xuất tài liệu](https://200lab.io/blog/rag-la-gi), có thể hiểu ngắn gọn RAG là kỹ thuật tìm các đoạn văn bản liên quan rồi gắn vào prompt trước khi model viết câu trả lời.

Theo tài liệu [hướng dẫn tối ưu độ chính xác LLM của OpenAI](https://developers.openai.com/api/docs/guides/optimizing-llm-accuracy), bài toán nâng chất lượng được chia thành hai trục: context optimization và LLM optimization. Trục context optimization xử lý chuyện model thiếu thông tin: kiến thức không có lúc pre-training, dữ liệu đã cũ, hoặc thông tin nội bộ. RAG nằm ở trục này vì nó bơm đúng thông tin vào context khi người dùng hỏi.

Trục LLM optimization xử lý hành vi của model. Bạn nhìn sang đây khi model đã có đủ thông tin trước mặt nhưng vẫn lệch format, sai giọng, hoặc làm theo hướng dẫn một cách bấp bênh. Fine-tuning nằm ở trục này, giúp tạo thói quen xử lý ổn định qua nhiều example.

Gom lại thành bảng cho dễ nhìn:

| Tiêu chí | RAG (context optimization) | Fine-tune (LLM optimization) |
| --- | --- | --- |
| Bài toán chính | Thiếu kiến thức mới hoặc dữ liệu nội bộ | Sai format, lệch tone, hành vi thiếu ổn định |
| Tác động vào đâu | Thay đổi nội dung trong prompt mỗi lần gọi | Cập nhật weight của model qua bước train |
| Khi dữ liệu đổi | Cập nhật file tài liệu và đánh chỉ mục lại | Không hợp để cập nhật thông tin đổi theo tuần |
| Khả năng kết hợp | Cấp đoạn văn bản gốc làm căn cứ | Train model cách đọc context và xuất đúng mẫu |

**RAG đưa dữ kiện vào prompt, còn fine-tuning rèn thói quen xử lý và định dạng câu trả lời.** Hai kỹ thuật này xếp chồng lên nhau được, không loại trừ nhau.

![Tùy vào nguyên nhân model trả lời lệch để chọn nạp thêm context qua RAG hay can thiệp vào weight.](https://assets.200lab.io/sha256/8f/8fc5cdf3b3a244831544c993d38e3d6dc99355b28275821fba67891a17e8f449/generations/519933685f483b89ce2ba62e92ed988c732190ca5c4360d29e5ef6ee8a62f095)

## Khi nào nên bắt đầu bằng RAG

Quay lại ví dụ giả định về shop thời trang. Quy định đổi trả trong bảy ngày hay số áo size M trong kho là thông tin hay đổi. Nếu đem những câu hỏi này đi fine-tune, mỗi lần kho hết hàng hoặc shop đổi phí gửi hàng, thông tin trong weight sẽ cũ ngay.

Với dữ liệu hay biến động và cần đối chiếu đoạn gốc, bạn nên bắt đầu bằng RAG. Khi chính sách thay đổi, chỉ cần sửa kho tài liệu rồi cập nhật chỉ mục, không phải train lại weight. Lúc kiểm tra log, bạn cũng nhìn thấy chunk, tức mảnh văn bản nào đã được đưa vào prompt.

Trong một luồng RAG tiêu chuẩn, các đoạn văn bản đi qua bước [tạo embedding để tìm kiếm theo ý nghĩa](https://200lab.io/blog/embedding-la-gi), thay vì chỉ khớp từng chữ rời rạc.

Sau đó các vector được đưa vào một [vector database để lưu các chunk đã mã hóa](https://200lab.io/blog/vector-database-la-gi), rồi truy xuất những đoạn gần với câu hỏi của khách nhất.

Trước khi dựng thêm hệ thống phức tạp, hãy làm kỹ prompt engineering và tạo một bộ eval, tức tập câu hỏi kèm câu trả lời chuẩn để chấm đầu ra. Khi prompt cơ bản không giải quyết được chuyện thiếu dữ liệu nội bộ, lúc đó mới lắp thêm RAG. Đi từng bước giúp bạn biết lỗi nằm ở thiếu thông tin hay ở cách ra lệnh cho model.

## Khi nào fine-tune hợp hơn

Giả sử RAG đã tìm ra đúng câu báo áo sơ mi trắng size M còn hàng. Vấn đề mới là code backend cần nhận đúng một trong ba nhãn `IN_STOCK`, `OUT_OF_STOCK` hoặc `NEED_OTHER_SIZE`, hoặc shop cần giữ cách xưng hô ngắn gọn đặc trưng. Retrieval chỉ đặt câu thông báo vào prompt, không ép được model xuất đúng nhãn đó.

Khi bạn đã thử hướng dẫn trong prompt và đưa vài example theo kiểu few-shot mà model vẫn lúc nhớ lúc quên format, đó là lúc tính tới fine-tune. Những bài toán rất hợp gồm classification lặp lại, ép kết quả vào định dạng cố định, giữ văn phong riêng của domain, hoặc sửa lúc model không tuân thủ chỉ dẫn phức tạp. Model quen format qua nhiều example lúc train thì bạn cũng rút được hướng dẫn dài và few-shot trong mỗi request, nhờ vậy tiết token.

Nếu app vừa cần tra tài liệu nội bộ vừa cần giữ format, bạn có thể kết hợp RAG và fine-tune. Dataset cho app có RAG phải kèm sẵn đoạn context đã truy xuất, để model học cách đọc tài liệu đính kèm rồi điền vào đúng template.

Fine-tune không thay thế việc cung cấp dữ kiện. Nếu câu hỏi cần quy định riêng của shop mà prompt không có tài liệu, model dù đã fine-tune vẫn có thể rơi vào tình trạng [ảo giác AI khi thiếu dữ kiện để trả lời](https://200lab.io/blog/ao-giac-ai-la-gi-vi-sao-ai-noi-sai) và tự đoán quy định. Với riêng bài toán ép định dạng, structured output kết hợp prompt rõ ràng đôi khi đã đủ một phần nhu cầu mà chưa cần train lại, nên hãy đo trên bộ eval trước khi quyết định.

## LoRA ở mức nhập môn

Khi fine-tune một model lớn, trở ngại đầu tiên là kích thước. Full fine-tuning cập nhật toàn bộ parameter, tức toàn bộ tham số tạo nên weight. Model càng lớn thì bộ nhớ càng nặng, và lưu từng bản riêng cho mỗi task cũng tốn kém.

Để giảm gánh này, nhóm phương pháp parameter-efficient tuning ra đời, trong đó phổ biến nhất là LoRA. Theo [bài báo gốc giới thiệu LoRA trên arXiv](https://arxiv.org/abs/2106.09685), phương pháp này đóng băng weight đã pre-train và chỉ chèn thêm các ma trận phân rã kích thước nhỏ, thường gọi là adapter, vào các tầng để train. Trên ví dụ GPT-3 175B mà nhóm tác giả nêu, LoRA giảm số parameter cần train xuống 10.000 lần và giảm bộ nhớ GPU xuống 3 lần so với fine-tune toàn bộ bằng Adam. Khi gộp adapter vào weight gốc lúc chạy, paper cho biết cách này không thêm inference latency.

Trong hệ sinh thái mã nguồn mở, bạn hiếm khi phải tự viết lại phần toán bên dưới. Developer thường dùng [thư viện PEFT của Hugging Face cho LoRA](https://huggingface.co/docs/peft/en/quicktour) để bọc model gốc và chọn cấu hình cơ bản trước khi train. Ở mức nhập môn, ý cốt lõi là LoRA giữ khối kiến thức nền và chỉ học thêm một lớp điều chỉnh nhỏ cho task mới.

## Chạy thử cùng câu hỏi mẫu: có và không truy xuất tài liệu

Để thấy rõ ranh giới giữa thiếu context và lệch format, mình đã viết và chạy thật một chương trình nhỏ bằng Go. Chương trình dùng bốn mẩu FAQ tiếng Anh tự soạn trong bộ nhớ, gồm `return-policy`, `stock-shirt-m`, `stock-shirt-l` và `shipping`, xoay quanh chiếc áo `white oxford shirt`. Script chỉ đếm số từ trùng để lấy tài liệu, không gọi language model và không train hay fine-tune model.

Chúng ta thử xem đoạn code dưới đây nha:

```go
// Compare in-memory FAQ retrieval vs no retrieval, and show that
// retrieved context does not enforce a fixed reply format.
//
// Illustrative sample: a shop chatbot answering return policy and stock.
// This program does not call an LLM and does not train or fine-tune a model.
package main

import (
	"fmt"
	"strings"
)

type doc struct{ id, title, text string }

type question struct {
	id, text, requiredFormat string
}

var corpus = []doc{
	{"return-policy", "Return policy", "Customers who bought the wrong size may exchange the item within 7 days if the tag is intact and the item is unused. The shop covers the return shipping fee once."},
	{"stock-shirt-m", "White oxford shirt size M", "White oxford shirt size M is in stock."},
	{"stock-shirt-l", "White oxford shirt size L", "White oxford shirt size L is out of stock and must be restocked."},
	{"shipping", "Shipping", "Standard shipping takes 3 to 5 business days after the order is packed."},
}

var questions = []question{
	{id: "policy", text: "I bought the wrong size. Can I send it back?"},
	{id: "stock", text: "Is the white oxford shirt size M in stock?", requiredFormat: "IN_STOCK | OUT_OF_STOCK | NEED_OTHER_SIZE"},
}

var stop = map[string]struct{}{
	"the": {}, "and": {}, "is": {}, "in": {}, "to": {}, "of": {},
	"a": {}, "an": {}, "or": {}, "for": {}, "on": {},
}

func tokenize(s string) []string {
	fields := strings.FieldsFunc(strings.ToLower(s), func(r rune) bool {
		return !((r >= 'a' && r <= 'z') || (r >= '0' && r <= '9'))
	})
	var out []string
	for _, w := range fields {
		if _, skip := stop[w]; skip || w == "" {
			continue
		}
		out = append(out, w)
	}
	return out
}

func score(q string, d doc) int {
	qset := map[string]struct{}{}
	for _, w := range tokenize(q) {
		qset[w] = struct{}{}
	}
	n := 0
	for _, w := range tokenize(d.title + " " + d.text) {
		if _, ok := qset[w]; ok {
			n++
			delete(qset, w)
		}
	}
	return n
}

func retrieve(q string, k int) []doc {
	type hit struct {
		d doc
		s int
	}
	var hits []hit
	for _, d := range corpus {
		if s := score(q, d); s >= 2 {
			hits = append(hits, hit{d, s})
		}
	}
	for i := 0; i < len(hits); i++ {
		for j := i + 1; j < len(hits); j++ {
			if hits[j].s > hits[i].s || (hits[j].s == hits[i].s && hits[j].d.id < hits[i].d.id) {
				hits[i], hits[j] = hits[j], hits[i]
			}
		}
	}
	if len(hits) > k {
		hits = hits[:k]
	}
	out := make([]doc, len(hits))
	for i, h := range hits {
		out[i] = h.d
		fmt.Printf("  retrieved: %s (overlap=%d)\n", h.d.id, h.s)
	}
	if len(hits) == 0 {
		fmt.Printf("  retrieved: (none)\n")
	}
	return out
}

func buildPrompt(q string, docs []doc) string {
	var b strings.Builder
	b.WriteString("Answer the customer using only the shop documents below.\nDocuments:\n")
	if len(docs) == 0 {
		b.WriteString("(none)\n")
	} else {
		for i, d := range docs {
			fmt.Fprintf(&b, "%d. %s: %s\n", i+1, d.title, d.text)
		}
	}
	fmt.Fprintf(&b, "Question: %s\n", q)
	return b.String()
}

func runBranch(name string, q question, withRetrieval bool) {
	fmt.Printf("--- %s ---\n", name)
	var docs []doc
	if withRetrieval {
		docs = retrieve(q.text, 2)
	} else {
		fmt.Printf("  retrieved: (disabled)\n")
	}
	prompt := buildPrompt(q.text, docs)
	fmt.Printf("  prompt:\n")
	for _, line := range strings.Split(strings.TrimRight(prompt, "\n"), "\n") {
		fmt.Printf("    %s\n", line)
	}
	if len(docs) == 0 {
		fmt.Printf("  grounded: false\n")
		fmt.Printf("  draft_reply: NO_CONTEXT\n")
		fmt.Printf("  note: without documents, a model would have to guess the shop rule.\n")
		return
	}
	draft := docs[0].text // extractive stand-in, not an LLM answer
	fmt.Printf("  grounded: true\n")
	fmt.Printf("  extractive_draft: %s\n", draft)
	if q.requiredFormat == "" {
		return
	}
	ok := draft == "IN_STOCK" || draft == "OUT_OF_STOCK" || draft == "NEED_OTHER_SIZE"
	fmt.Printf("  required_format: %s\n", q.requiredFormat)
	fmt.Printf("  format_match: %v\n", ok)
	fmt.Printf("  note: retrieval puts the fact in the prompt; it does not force the fixed label format.\n")
}

func main() {
	fmt.Println("Sample shop FAQ demo: retrieval vs no retrieval")
	fmt.Println("Illustrative data only; no LLM call and no model training.")
	fmt.Println()
	fmt.Println("=== Question 1: return policy ===")
	fmt.Printf("question: %s\n", questions[0].text)
	runBranch("Branch A (retrieve)", questions[0], true)
	runBranch("Branch B (no retrieval)", questions[0], false)
	fmt.Println()
	fmt.Println("=== Question 2: stock, fixed format ===")
	fmt.Printf("question: %s\n", questions[1].text)
	runBranch("Branch A (retrieve)", questions[1], true)
	runBranch("Branch B (no retrieval)", questions[1], false)
}
```

Khi chạy chương trình, mình thu được output như sau:

```text
Sample shop FAQ demo: retrieval vs no retrieval
Illustrative data only; no LLM call and no model training.

=== Question 1: return policy ===
question: I bought the wrong size. Can I send it back?
--- Branch A (retrieve) ---
  retrieved: return-policy (overlap=3)
  prompt:
    Answer the customer using only the shop documents below.
    Documents:
    1. Return policy: Customers who bought the wrong size may exchange the item within 7 days if the tag is intact and the item is unused. The shop covers the return shipping fee once.
    Question: I bought the wrong size. Can I send it back?
  grounded: true
  extractive_draft: Customers who bought the wrong size may exchange the item within 7 days if the tag is intact and the item is unused. The shop covers the return shipping fee once.
--- Branch B (no retrieval) ---
  retrieved: (disabled)
  prompt:
    Answer the customer using only the shop documents below.
    Documents:
    (none)
    Question: I bought the wrong size. Can I send it back?
  grounded: false
  draft_reply: NO_CONTEXT
  note: without documents, a model would have to guess the shop rule.

=== Question 2: stock, fixed format ===
question: Is the white oxford shirt size M in stock?
--- Branch A (retrieve) ---
  retrieved: stock-shirt-m (overlap=6)
  retrieved: stock-shirt-l (overlap=5)
  prompt:
    Answer the customer using only the shop documents below.
    Documents:
    1. White oxford shirt size M: White oxford shirt size M is in stock.
    2. White oxford shirt size L: White oxford shirt size L is out of stock and must be restocked.
    Question: Is the white oxford shirt size M in stock?
  grounded: true
  extractive_draft: White oxford shirt size M is in stock.
  required_format: IN_STOCK | OUT_OF_STOCK | NEED_OTHER_SIZE
  format_match: false
  note: retrieval puts the fact in the prompt; it does not force the fixed label format.
--- Branch B (no retrieval) ---
  retrieved: (disabled)
  prompt:
    Answer the customer using only the shop documents below.
    Documents:
    (none)
    Question: Is the white oxford shirt size M in stock?
  grounded: false
  draft_reply: NO_CONTEXT
  note: without documents, a model would have to guess the shop rule.
```

Trong ví dụ trên, câu hỏi đổi trả ở nhánh A bật retrieval nên hàm `retrieve` tìm ra `return-policy` với `overlap=3`. Biến `grounded` là `true` vì prompt đã có quy định đổi hàng trong 7 ngày và shop hỗ trợ phí gửi lại một lần. Nhánh B tắt truy xuất, không có tài liệu, và `draft_reply` trả về `NO_CONTEXT`. Như dòng ghi chú trong output, khi thiếu tài liệu thì một language model sẽ phải đoán quy định của shop.

Sang câu hỏi tồn kho áo size M, nhánh A lấy được `stock-shirt-m` với `overlap=6` và `grounded: true`. Cờ `format_match` lại là `false` vì `required_format` chỉ cho phép một nhãn như `IN_STOCK`, trong khi `extractive_draft` giữ nguyên câu văn xuôi `"White oxford shirt size M is in stock."`. `extractive_draft` chỉ sao chép chunk có điểm trùng khớp cao nhất để minh họa, không phải câu trả lời của language model. Điều nó cho thấy vẫn rõ: retrieval chỉ đem fact vào prompt, còn biến fact đó thành đúng nhãn `IN_STOCK` là việc của prompt hoặc của bước fine-tune.

## Sai lầm hay gặp khi chọn fine-tune và RAG

Từ hai trục tối ưu context và tối ưu hành vi, mình suy ra bốn cái bẫy dễ gặp khi mới thiết kế hệ thống.

- **Đem thông tin đổi theo tuần đi fine-tune:** Khi bạn nhét bảng tồn kho hay chính sách ngắn hạn vào tập train, mỗi lần shop đổi số liệu thì weight lại chứa thông tin cũ, trong khi phần này chỉ cần cập nhật ở kho tài liệu của RAG.
- **Trông chờ RAG tự ép được format và giọng điệu:** Như cờ `format_match: false` trong bài chạy thử, tìm đúng đoạn văn đưa vào prompt không tự biến câu văn xuôi thành nhãn `IN_STOCK` hay giữ đúng giọng tư vấn ngắn của shop.
- **Chuẩn bị example lệch với lúc chạy thật:** Nếu app thật dùng RAG để đính tài liệu vào prompt, nhưng lúc gom dataset bạn chỉ đưa câu hỏi trống không có context, model sẽ bỡ ngỡ khi chạy thật.
- **Nhảy cóc qua bước làm prompt và bộ eval:** Khi chưa có prompt rõ và chưa có tập câu hỏi để đo model đang sai vì thiếu thông tin hay vì không theo chỉ dẫn, bạn rất dễ chọn nhầm công cụ và phải làm lại.

> 💡 Trước khi đụng tới weight của model hay dựng thêm kho tìm kiếm, hãy tách xem lỗi hiện tại do model không có cuốn sổ tay dữ kiện trước mặt, hay do model đã nhìn thấy dữ kiện nhưng chưa quen viết đúng biểu mẫu.

Nếu bạn muốn hiểu kỹ hơn cách chọn model, lúc nào nên fine-tune và cách đọc hóa đơn token, thì có thể tham khảo khóa học bên dưới tại 200Lab nhé.

Khóa này miễn phí, bạn chỉ cần đăng ký và bấm vào học miễn phí để tham gia.

![Hiểu model AI đủ để dùng và chọn đúng model - token, prompt caching, model card, reasoning model và fine-tune](https://assets.200lab.io/sha256/16/1686a8d85e2323887f566090f87cba32cba6f91366d26614d7b9f6d27571d347/generations/dad26be3b0dd1a4e42d407b61bb8002a2f873f5bb186a684933b069d6eddecc7)

- [Hiểu model AI đủ để dùng và chọn đúng model - token, prompt caching, model card, reasoning model và fine-tune](https://200lab.io/courses/understanding-ai-models-to-choose-the-right-one.md)

Chúc các bạn chọn đúng giữa cuốn sổ tay RAG và bước fine-tune, để bot vừa tra được chính sách mới vừa trả lời đúng format nhé 🚀

## Đọc thêm

- [Claude Fable 5.2 có gì mới và ưu đãi ra mắt cần biết](https://200lab.io/blog/claude-fable-5-2-whats-new.md): Claude Fable 5.2 được dùng trên app Claude với một số gói, hãy lưu ý hạn ưu đãi 16/11 và lưu dữ liệu 30 ngày; việc hằng ngày vẫn có thể bắt đầu với Opus 5.5.
- [Grok Bot gom phản hồi khách một tuần thành bảng việc cần sửa](https://200lab.io/blog/grok-bot-weekly-customer-feedback.md): Gom phản hồi khách bằng Grok Bot từ tin nhắn cả tuần: bạn có bảng khen chê kèm mã tin, ba việc cần sửa và tự đếm lại nhóm lớn nhất trước khi sửa shop.
- [Function calling là gì? Phân biệt với Structured Outputs và MCP](https://200lab.io/blog/function-calling-la-gi.md): Function calling nối model với dữ liệu thật qua host app: vòng tool call, các bước app phải kiểm tra, và điểm khác với Structured Outputs cùng MCP.
- [Multi-agent là gì? Khi nào một agent không đủ và 3 cách chia việc](https://200lab.io/blog/multi-agent-la-gi.md): Multi-agent là gì: mạnh ở bài toán rộng chạy được song song, nhưng chỉ nên tách khi phần việc làm độc lập được và bù nổi chi phí token.
- [Cách nhờ Grok Bot lọc khách quen và soạn nháp tin hỏi thăm](https://200lab.io/blog/grok-bot-repeat-customer-check-in.md): Soạn tin hỏi thăm khách quen bằng Grok Bot: bot lọc khách theo thời gian cháy, ghi rõ cách tính và chỉ viết nháp để bạn đọc lại rồi tự gửi.
- [Mọi bài viết chủ đề AI](https://200lab.io/blog/tags/ai.md)

Toàn bộ nội dung: https://200lab.io/llms.txt
