Cách xây dựng LLM từ đầu (và những bài học rút ra)

Ashley Innocent

Ashley Innocent

7 tháng 4 2026

Cách xây dựng LLM từ đầu (và những bài học rút ra)

Apidog cho doanh nghiệp

Triển khai tại chỗ

SSO & RBAC

Tuân thủ SOC 2

Khám phá Apidog Enterprise

TL;DR (Tóm tắt)

Xây dựng một mô hình ngôn ngữ tối giản từ đầu chỉ tốn dưới 300 dòng mã Python. Quá trình này cho thấy chính xác cách tokenization, cơ chế attention và inference hoạt động, giúp bạn trở thành một người dùng API hiểu biết hơn nhiều khi tích hợp các LLM sản xuất vào ứng dụng của mình.

Giới thiệu

Hầu hết các nhà phát triển coi mô hình ngôn ngữ như những hộp đen. Bạn gửi văn bản vào, token được trả ra, và ở đâu đó giữa chừng, điều kỳ diệu xảy ra. Mô hình tư duy đó hoạt động tốt cho đến khi bạn cần gỡ lỗi một tích hợp API bị hỏng, tinh chỉnh các tham số lấy mẫu, hoặc tìm hiểu lý do tại sao mô hình của bạn liên tục tạo ra dữ liệu có cấu trúc bịa đặt.

GuppyLM, một dự án gần đây đã lọt vào trang chủ HackerNews với 842 điểm, làm cho các cơ chế bên trong trở nên rõ ràng. Đây là một mô hình transformer 8,7 triệu tham số được viết từ đầu bằng Python. Nó có thể được huấn luyện trong vòng chưa đầy một giờ trên một GPU thông thường. Mã nguồn nằm gọn trong một file duy nhất. Mục tiêu không phải là cạnh tranh với GPT-4; mà là để làm sáng tỏ những gì LLM thực sự làm.

Bài viết này sẽ hướng dẫn cách xây dựng một LLM nhỏ, chức năng của từng thành phần, và những hiểu biết về cơ chế bên trong mà bạn có thể học được khi làm việc chuyên nghiệp với các API AI.

💡
Nếu bạn đang kiểm thử tích hợp API AI, Tính năng Kịch bản Kiểm thử (Test Scenarios) của Apidog cho phép bạn xác minh phản hồi theo luồng (streaming responses), xác nhận cấu trúc token và mô phỏng các trường hợp hoàn thành đặc biệt mà không tốn kém tài nguyên sản xuất. Chúng ta sẽ tìm hiểu thêm về điều này sau.
nút

Điều gì làm cho một mô hình ngôn ngữ trở nên "nhỏ gọn"?

Một LLM sản xuất như GPT-4 có hàng trăm tỷ tham số. Một LLM "nhỏ gọn" nằm trong khoảng từ 1 triệu đến 25 triệu tham số. Các dự án như GuppyLM (8,7 triệu), nanoGPT của Karpathy (124 triệu) và MicroLM (1-2 triệu) đều thuộc loại này.

LLM nhỏ gọn có thể: - Huấn luyện trên máy tính xách tay hoặc Google Colab - Hoạt động hoàn toàn trong bộ nhớ CPU - Được kiểm tra, sửa đổi và gỡ lỗi ở cấp độ trọng số

Chúng không thể: - Xử lý suy luận phức tạp - Tạo văn bản dài mạch lạc một cách đáng tin cậy - Sánh bằng chiều sâu kiến thức thực tế của các mô hình sản xuất

Giá trị không nằm ở kết quả đầu ra. Nó nằm ở sự hiểu biết mà bạn có được từ việc xây dựng một mô hình như vậy.

Các thành phần cốt lõi: LLM thực sự hoạt động như thế nào

Trước khi viết bất kỳ đoạn mã nào, bạn cần biết bốn thành phần chính hoạt động ra sao.

Bộ mã hóa token (Tokenizer)

Bộ mã hóa token chuyển đổi văn bản thô thành các ID số nguyên. "Hello, world!" sẽ trở thành một cái gì đó như [15496, 11, 995, 0]. Mỗi số nguyên tương ứng với một đơn vị từ con từ một từ vựng cố định.

Tại sao điều này lại quan trọng đối với công việc API: số lượng token ảnh hưởng trực tiếp đến độ trễ và chi phí. Việc hiểu cách bộ mã hóa token chia tách văn bản giúp bạn viết các lời nhắc (prompt) phù hợp với cửa sổ ngữ cảnh và tránh bị cắt bớt ngoài ý muốn.

GuppyLM sử dụng một bộ mã hóa token đơn giản ở cấp độ ký tự. Các mô hình sản xuất như GPT-4 sử dụng BPE (mã hóa cặp byte) với từ vựng 50K-100K token.

Lớp nhúng (Embedding layer)

Lớp nhúng chuyển đổi các ID token thành các vector dày đặc. Mỗi token nhận được một vector được học (ví dụ: 384 chiều trong GuppyLM). Các vector này mang ý nghĩa ngữ nghĩa: các token tương tự sẽ nằm gần nhau trong không gian vector.

Các nhúng vị trí (position embeddings) được thêm vào, để mô hình biết thứ tự token.

Các khối Transformer

Đây là phần tính toán cốt lõi. Mỗi khối có hai phần:

Tự chú ý (Self-attention): cho phép mỗi token nhìn vào tất cả các token khác trong chuỗi và quyết định những token nào quan trọng để dự đoán token tiếp theo. GuppyLM sử dụng 6 head attention trên 6 lớp.

Mạng truyền thẳng (Feed-forward network): một MLP hai lớp được áp dụng cho biểu diễn của mỗi token sau cơ chế attention. GuppyLM sử dụng hàm kích hoạt ReLU, đơn giản hơn SwiGLU được dùng trong các kiến trúc mới hơn.

Đầu ra (Output head)

Sau khối transformer cuối cùng, một lớp tuyến tính chiếu biểu diễn của mỗi token thành một vector có kích thước bằng từ vựng. Áp dụng softmax để có được xác suất, chọn token tiếp theo có khả năng nhất (hoặc lấy mẫu), và lặp lại.

Xây dựng một LLM tối giản bằng Python

Dưới đây là một LLM tối giản đang hoạt động dựa trên phương pháp GuppyLM. Mã này chạy trong PyTorch tiêu chuẩn.

import torch
import torch.nn as nn
import torch.nn.functional as F

# Hyperparameters
VOCAB_SIZE = 256     # character-level: one slot per ASCII char
D_MODEL = 128        # embedding dimension
N_HEADS = 4          # attention heads
N_LAYERS = 3         # transformer blocks
SEQ_LEN = 64         # context window
DROPOUT = 0.1

class SelfAttention(nn.Module):
    def __init__(self, d_model, n_heads):
        super().__init__()
        self.n_heads = n_heads
        self.head_dim = d_model // n_heads
        self.qkv = nn.Linear(d_model, 3 * d_model, bias=False)
        self.proj = nn.Linear(d_model, d_model, bias=False)
        self.dropout = nn.Dropout(DROPOUT)

    def forward(self, x):
        B, T, C = x.shape
        qkv = self.qkv(x).reshape(B, T, 3, self.n_heads, self.head_dim)
        q, k, v = qkv.unbind(dim=2)
        q = q.transpose(1, 2)
        k = k.transpose(1, 2)
        v = v.transpose(1, 2)
        # Causal mask: each token can only attend to previous tokens
        scale = self.head_dim ** -0.5
        attn = (q @ k.transpose(-2, -1)) * scale
        mask = torch.triu(torch.ones(T, T, device=x.device), diagonal=1).bool()
        attn = attn.masked_fill(mask, float('-inf'))
        attn = F.softmax(attn, dim=-1)
        attn = self.dropout(attn)
        out = (attn @ v).transpose(1, 2).reshape(B, T, C)
        return self.proj(out)

class TransformerBlock(nn.Module):
    def __init__(self, d_model, n_heads):
        super().__init__()
        self.attn = SelfAttention(d_model, n_heads)
        self.ff = nn.Sequential(
            nn.Linear(d_model, 4 * d_model),
            nn.ReLU(),
            nn.Linear(4 * d_model, d_model),
            nn.Dropout(DROPOUT),
        )
        self.ln1 = nn.LayerNorm(d_model)
        self.ln2 = nn.LayerNorm(d_model)

    def forward(self, x):
        x = x + self.attn(self.ln1(x))
        x = x + self.ff(self.ln2(x))
        return x

class TinyLLM(nn.Module):
    def __init__(self):
        super().__init__()
        self.embed = nn.Embedding(VOCAB_SIZE, D_MODEL)
        self.pos_embed = nn.Embedding(SEQ_LEN, D_MODEL)
        self.blocks = nn.ModuleList([
            TransformerBlock(D_MODEL, N_HEADS) for _ in range(N_LAYERS)
        ])
        self.ln_f = nn.LayerNorm(D_MODEL)
        self.head = nn.Linear(D_MODEL, VOCAB_SIZE, bias=False)

    def forward(self, idx):
        B, T = idx.shape
        tok_emb = self.embed(idx)
        pos = torch.arange(T, device=idx.device)
        pos_emb = self.pos_embed(pos)
        x = tok_emb + pos_emb
        for block in self.blocks:
            x = block(x)
        x = self.ln_f(x)
        logits = self.head(x)
        return logits

# Initialize and count parameters
model = TinyLLM()
total_params = sum(p.numel() for p in model.parameters())
print(f"Model size: {total_params:,} parameters")  # ~1.2M

Vòng lặp huấn luyện

import torch.optim as optim

def train(model, data, epochs=100, lr=3e-4):
    optimizer = optim.AdamW(model.parameters(), lr=lr)
    model.train()
    for epoch in range(epochs):
        # data: tensor of token IDs, shape [batch, seq_len+1]
        x = data[:, :-1]   # input: all tokens except last
        y = data[:, 1:]    # target: all tokens shifted by 1
        logits = model(x)
        loss = F.cross_entropy(logits.reshape(-1, VOCAB_SIZE), y.reshape(-1))
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()
        if epoch % 10 == 0:
            print(f"Epoch {epoch}, loss: {loss.item():.4f}")

Suy luận (tạo văn bản)

@torch.no_grad()
def generate(model, prompt_ids, max_new_tokens=50, temperature=1.0, top_k=10):
    model.eval()
    ids = torch.tensor([prompt_ids])
    for _ in range(max_new_tokens):
        idx_cond = ids[:, -SEQ_LEN:]  # crop to context window
        logits = model(idx_cond)
        logits = logits[:, -1, :] / temperature  # last token only
        # top-k sampling
        v, _ = torch.topk(logits, min(top_k, logits.size(-1)))
        logits[logits < v[:, [-1]]] = float('-inf')
        probs = F.softmax(logits, dim=-1)
        next_id = torch.multinomial(probs, num_samples=1)
        ids = torch.cat([ids, next_id], dim=1)
    return ids[0].tolist()

Điều này dạy bạn điều gì về hành vi của API AI

Việc xây dựng mô hình này sẽ tiết lộ một số điều giúp bạn trở thành một người dùng API hiểu biết hơn.

Temperature và sampling là cơ chế, không phải phép màu

Temperature chia các logit trước khi áp dụng softmax. Temperature cao hơn = phân phối phẳng hơn = đầu ra ngẫu nhiên hơn. Temperature thấp hơn = phân phối sắc nét hơn = đầu ra xác định hơn. Khi API sản xuất của bạn trả về kết quả không nhất quán với temperature=0.0, đó không phải là lỗi. Temperature bằng 0 thực sự là một argmax tham lam, và nhiều API thường đặt nó hơi cao hơn một chút để tránh các kết quả suy biến.

Cửa sổ ngữ cảnh là giới hạn cứng, không phải gợi ý mềm

Dòng mã idx_cond = ids[:, -SEQ_LEN:] trong vòng lặp suy luận cho thấy chính xác điều gì xảy ra tại giới hạn ngữ cảnh. Mô hình tự động bỏ đi các token cũ hơn. Nếu tích hợp API của bạn giả định rằng mô hình ghi nhớ toàn bộ lịch sử hội thoại, thì sau một điểm nhất định, nó sẽ không nhớ nữa. Xem [internal: how-ai-agent-memory-works] để biết cách các tác nhân (agents) xử lý vấn đề này.

Các token truyền trực tuyến chỉ là các bước suy luận được hiển thị

Các API truyền trực tuyến không làm điều gì khác biệt về kiến trúc. Chúng chạy vòng lặp suy luận và gửi từng token đến luồng phản hồi ngay khi nó được tạo ra. Hiểu điều này giúp ích khi bạn viết logic thử lại: một luồng bị ngắt giữa chừng trong quá trình tạo không thể tiếp tục, mà phải bắt đầu lại.

Logit giải thích tại sao đầu ra có cấu trúc lại khó

Mô hình gán xác suất cho mỗi token trong từ vựng ở mỗi bước. Để tạo ra JSON hợp lệ, token phù hợp phải thắng ở mọi vị trí. Các thư viện như Outlines và Guidance hạn chế phân phối logit để áp đặt ngữ pháp trong quá trình suy luận. Khi bạn thấy các API AI cung cấp chế độ "đầu ra có cấu trúc", đây là những gì chúng đang làm bên trong.

Cách kiểm thử tích hợp API AI với Apidog

Khi bạn đã hiểu cách thức hoạt động của suy luận LLM, bạn có thể viết các bài kiểm thử API tốt hơn nhiều. Tính năng Kịch bản Kiểm thử (Test Scenarios) của Apidog cho phép bạn chuỗi các lệnh gọi API và xác nhận cấu trúc của các phản hồi AI.

Ví dụ, khi kiểm thử một API chat truyền trực tuyến:

  1. Tạo một Kịch bản Kiểm thử trong Apidog với điểm cuối /v1/chat/completions của bạn
  2. Đặt các xác nhận để xác minh cấu trúc phản hồi: response.choices[0].finish_reason == "stop", response.usage.total_tokens < 4096
  3. Thêm một bước tiếp theo để gửi phản hồi làm ngữ cảnh cho lượt tiếp theo, mô phỏng một cuộc hội thoại đa lượt
  4. Sử dụng Smart Mock của Apidog để giả lập điểm cuối AI và kiểm thử khả năng xử lý lỗi của ứng dụng của bạn: mô phỏng finish_reason: "length" (đầu ra bị cắt bớt), finish_reason: "content_filter", và thời gian chờ mạng giữa luồng

Đây là cách bạn kiểm thử tích hợp AI mà không tốn API credit cho mỗi lần chạy CI. Xem [internal: api-testing-tutorial] để có cái nhìn tổng quan hơn về các phương pháp kiểm thử API.

Kiểm thử xác nhận số lượng token

{
  "assertions": [
    {
      "field": "response.usage.completion_tokens",
      "operator": "less_than",
      "value": 512
    },
    {
      "field": "response.choices[0].finish_reason",
      "operator": "equals",
      "value": "stop"
    },
    {
      "field": "response.choices[0].message.content",
      "operator": "not_empty"
    }
  ]
}

Chạy thử nghiệm này trên nhiều mô hình (GPT-4o, Claude 3.5 Sonnet, Gemini 1.5 Pro) trong một Kịch bản Kiểm thử duy nhất để phát hiện sự khác biệt về lược đồ API trước khi chúng được đưa vào sản xuất.

Nâng cao: Lượng tử hóa và tối ưu hóa suy luận

Khi bạn đã có một LLM nhỏ gọn hoạt động, hai kỹ thuật đáng để tìm hiểu vì chúng áp dụng trực tiếp vào cách các mô hình sản xuất được triển khai.

Lượng tử hóa (Quantization)

Theo mặc định, các trọng số trong mô hình của chúng ta là số thực 32-bit. Lượng tử hóa giảm chúng xuống số nguyên 8-bit (INT8) hoặc thậm chí 4-bit (INT4). Điều này giúp giảm 4-8 lần mức sử dụng bộ nhớ với tổn thất độ chính xác vừa phải.

# Example: dynamic INT8 quantization in PyTorch
import torch.quantization
quantized_model = torch.quantization.quantize_dynamic(
    model, {nn.Linear}, dtype=torch.qint8
)

Các API sản xuất chạy các mô hình đã được lượng tử hóa. Khi bạn thấy chất lượng đầu ra khác nhau ở các "phiên bản" khác nhau của cùng một mô hình, thì việc lượng tử hóa thường liên quan đến điều đó.

Bộ nhớ đệm KV (KV cache)

Trong vòng lặp suy luận của chúng ta, chúng ta tính toán lại cơ chế attention trên toàn bộ chuỗi ở mỗi bước. Các hệ thống sản xuất lưu trữ các cặp khóa-giá trị (key-value pairs) từ các token trước đó (bộ nhớ đệm KV) để mỗi token mới chỉ cần một phép tính attention mới. Đây là lý do tại sao token đầu tiên trong phản hồi truyền trực tuyến mất nhiều thời gian hơn các token tiếp theo.

LLM nhỏ gọn so với API sản xuất: Khi nào sử dụng loại nào

Trường hợp sử dụng LLM nhỏ gọn API sản xuất
Tìm hiểu cơ chế bên trong mô hình Tốt nhất Quá mức cần thiết
Tạo mẫu ứng dụng mới Chất lượng không đủ Tốt nhất
Dữ liệu riêng tư/nhạy cảm Lựa chọn tốt Tùy thuộc nhà cung cấp
Triển khai ngoại tuyến/tại biên Khả thi Không thể
Chi phí nhạy cảm, khối lượng lớn Có thể với sự đánh đổi Đắt đỏ ở quy mô lớn
Các tác vụ đòi hỏi suy luận cao Không khả thi Bắt buộc

Câu trả lời thực sự cho hầu hết các nhà phát triển: sử dụng API sản xuất cho ứng dụng của bạn, nhưng chạy một mô hình nhỏ để hiểu điều gì đang diễn ra bên dưới. Hai thứ này không cạnh tranh với nhau. Bài viết [internal: open-source-coding-assistants-2026] đề cập đến các công cụ làm mờ ranh giới này với các thiết lập "mang mô hình của riêng bạn".

Kết luận

Xây dựng một LLM nhỏ gọn từ đầu mất một cuối tuần. Những gì bạn nhận được không phải là một hệ thống sản xuất; đó là một mô hình tư duy hoạt động về cách mọi mô hình ngôn ngữ, từ GuppyLM đến GPT-4o, thực sự hoạt động. Sự hiểu biết đó sẽ mang lại lợi ích mỗi khi bạn gỡ lỗi tích hợp luồng, tinh chỉnh các tham số lấy mẫu hoặc thiết kế các xác nhận cho các bài kiểm thử API AI của mình.

Dự án GuppyLM là một điểm khởi đầu tốt. Sao chép nó, huấn luyện nó trên bất kỳ bộ dữ liệu văn bản nào, và dành một buổi chiều để đọc vòng lặp suy luận. Sau đó quay lại với các tích hợp API sản xuất của bạn và bạn sẽ nhìn chúng theo một cách khác.

Hãy thử Tính năng Kịch bản Kiểm thử (Test Scenarios) của Apidog để áp dụng sự chặt chẽ tương tự vào việc kiểm thử API AI của bạn như cách bạn áp dụng cho bất kỳ hệ thống backend nào khác.

nút

Câu hỏi thường gặp

Một LLM "nhỏ gọn" cần bao nhiêu tham số để tạo ra văn bản mạch lạc?Khoảng 10 triệu đến 50 triệu tham số với một bộ dữ liệu huấn luyện tốt có thể tạo ra các câu văn mạch lạc cục bộ. Dưới 1 triệu, bạn sẽ nhận được những từ vô nghĩa trong hầu hết các tác vụ. GuppyLM với 8,7 triệu tham số hoạt động tốt cho các cuộc trò chuyện ngắn trong lĩnh vực huấn luyện của nó (60 chủ đề).

Tôi có thể chạy một LLM nhỏ gọn mà không cần GPU không?Có. Các mô hình dưới 100 triệu tham số vẫn chạy tốt trên CPU, mặc dù quá trình suy luận sẽ chậm hơn. Mô hình trên (1,2 triệu tham số) tạo ra token trong vài mili giây trên CPU máy tính xách tay.

Tôi nên huấn luyện trên bộ dữ liệu nào?Các mô hình cấp độ ký tự hoạt động tốt với các văn bản từ Project Gutenberg, các tập con của Wikipedia, hoặc bất kỳ kho văn bản thuần túy nào. GuppyLM sử dụng bộ dữ liệu hội thoại 60K mục trên HuggingFace (arman-bd/guppylm-60k-generic). Để tạo mã, hãy sử dụng The Stack hoặc CodeParrot.

Sự khác biệt giữa temperature và top-k sampling là gì?Temperature điều chỉnh phân phối logit (kiểm soát mức độ ngẫu nhiên tổng thể). Top-k hạn chế nhóm lấy mẫu chỉ với k token có khả năng nhất trước khi áp dụng temperature. Chúng được áp dụng cùng nhau: đầu tiên top-k lọc các ứng cử viên, sau đó temperature định hình các xác suất trong tập hợp đó.

Tại sao LLM của tôi đôi khi lặp lại chính nó?Lặp lại là một chế độ thất bại khi mô hình gán xác suất cao cho các token mà nó vừa tạo ra vì chúng xuất hiện trong ngữ cảnh. Các API sản xuất sử dụng hình phạt lặp lại (một điều chỉnh logit làm giảm giá trị của các token được tạo gần đây). Thêm repetition_penalty=1.1 vào lệnh gọi API của bạn để giảm điều này.

Mất bao lâu để huấn luyện một LLM nhỏ gọn?Mô hình trên huấn luyện để tạo ra đầu ra mạch lạc trong vòng chưa đầy 2 giờ trên một GPU duy nhất (RTX 3060 hoặc tương đương). GuppyLM huấn luyện trong Colab với thời gian tương tự. Các mô hình lớn hơn (100M+) cần thiết lập đa GPU và mất nhiều ngày huấn luyện.

Cách nhanh nhất để chuyển từ LLM nhỏ gọn sang điểm cuối API thực tế là gì?Xuất sang định dạng GGUF bằng cách sử dụng script chuyển đổi của llama.cpp, sau đó phục vụ bằng llama-server. Điều này cung cấp cho bạn một điểm cuối API tương thích OpenAI chạy cục bộ. Sau đó, bạn có thể trỏ Apidog đến đó để kiểm thử, xem [internal: rest-api-best-practices].

Các LLM sản xuất xử lý ngữ cảnh dài hơn cửa sổ huấn luyện của chúng như thế nào?Các kỹ thuật như RoPE (Rotary Position Embedding) với mở rộng tỷ lệ, cửa sổ chú ý trượt (sliding window attention), và tạo sinh tăng cường truy xuất (retrieval-augmented generation) đều mở rộng ngữ cảnh hiệu quả. Kiến trúc transformer cốt lõi không thay đổi; đây là những sửa đổi về cách thông tin vị trí được mã hóa và cách cửa sổ chú ý được áp dụng.

Thực hành thiết kế API trong Apidog

Khám phá cách dễ dàng hơn để xây dựng và sử dụng API