Intisari
Membangun model bahasa minimal dari awal hanya membutuhkan kurang dari 300 baris kode Python. Proses ini mengungkap dengan tepat bagaimana tokenisasi, perhatian, dan inferensi bekerja, yang membuat Anda menjadi konsumen API yang jauh lebih baik saat mengintegrasikan LLM produksi ke dalam aplikasi Anda.
Pendahuluan
Sebagian besar pengembang memperlakukan model bahasa sebagai kotak hitam. Anda mengirim teks masuk, token keluar, dan di antara itu semua, keajaiban terjadi. Model mental itu berfungsi dengan baik sampai Anda perlu men-debug integrasi API yang rusak, menyetel parameter pengambilan sampel, atau mencari tahu mengapa model Anda terus berhalusinasi data terstruktur.
GuppyLM, sebuah proyek yang baru-baru ini mencapai halaman depan HackerNews dengan 842 poin, membuat internalnya terlihat. Ini adalah transformer berparameter 8.7M yang ditulis dari awal dalam Python. Model ini dilatih dalam waktu kurang dari satu jam pada GPU konsumen. Kodenya muat dalam satu file. Tujuannya bukan untuk bersaing dengan GPT-4; tujuannya adalah untuk mengungkap apa sebenarnya yang dilakukan LLM.
Artikel ini menjelaskan cara membangun LLM kecil, apa saja komponennya, dan apa yang diajarkan oleh pemahaman internal ketika Anda bekerja dengan API AI secara profesional.
Apa yang membuat model bahasa "kecil"?
LLM produksi seperti GPT-4 memiliki ratusan miliar parameter. LLM "kecil" berada dalam kisaran 1M hingga 25M parameter. Proyek seperti GuppyLM (8.7M), nanoGPT Karpathy (124M), dan MicroLM (1-2M) semuanya termasuk dalam kategori ini.
LLM kecil dapat: - Dilatih di laptop atau Google Colab - Muat sepenuhnya di memori CPU - Diinspeksi, dimodifikasi, dan di-debug pada tingkat bobot
Mereka tidak dapat: - Menangani penalaran yang kompleks - Menghasilkan teks panjang yang koheren dengan andal - Menyamai kedalaman faktual model produksi
Nilainya bukan pada outputnya. Nilainya adalah pemahaman yang Anda dapatkan dari membangunnya.
Komponen inti: bagaimana LLM sebenarnya bekerja
Sebelum menulis kode apa pun, Anda perlu tahu apa yang dilakukan keempat bagian utama ini.
Tokenisasi
Tokenizer mengubah teks mentah menjadi ID bilangan bulat. "Hello, world!" menjadi sesuatu seperti [15496, 11, 995, 0]. Setiap bilangan bulat memetakan ke unit subkata dari kosakata tetap.
Mengapa ini penting untuk pekerjaan API: jumlah token secara langsung memengaruhi latensi dan biaya. Memahami bagaimana tokenizer membagi teks membantu Anda menulis prompt yang sesuai dengan jendela konteks dan menghindari pemotongan yang tidak terduga.
GuppyLM menggunakan tokenizer tingkat karakter sederhana. Model produksi seperti GPT-4 menggunakan BPE (byte-pair encoding) dengan kosakata 50K-100K token.
Lapisan embedding
Lapisan embedding mengubah ID token menjadi vektor padat. Setiap token mendapatkan vektor yang dipelajari (misalnya 384 dimensi di GuppyLM). Vektor-vektor ini membawa makna semantik: token yang serupa akhirnya berdekatan di ruang vektor.
Embedding posisi ditambahkan di atasnya, sehingga model mengetahui urutan token.
Blok Transformer
Ini adalah komputasi inti. Setiap blok memiliki dua bagian:
Self-attention: memungkinkan setiap token melihat semua token lain dalam urutan dan memutuskan mana yang penting untuk memprediksi token berikutnya. GuppyLM menggunakan 6 kepala perhatian di 6 lapisan.
Jaringan feed-forward: MLP dua lapis diterapkan pada representasi setiap token setelah perhatian. GuppyLM menggunakan aktivasi ReLU, yang lebih sederhana daripada SwiGLU yang digunakan dalam arsitektur yang lebih baru.
Kepala output
Setelah blok transformer terakhir, lapisan linear memproyeksikan representasi setiap token ke vektor dengan ukuran yang sama dengan kosakata. Terapkan softmax untuk mendapatkan probabilitas, pilih token berikutnya yang paling mungkin (atau sampel), dan ulangi.
Membangun LLM minimal dalam Python
Berikut adalah LLM minimal yang berfungsi berdasarkan pendekatan GuppyLM. Ini berjalan di PyTorch standar.
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
Loop pelatihan
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}")
Inferensi (pembuatan teks)
@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()
Apa yang diajarkan ini tentang perilaku API AI
Membangun ini mengungkapkan beberapa hal yang membuat Anda menjadi konsumen API yang lebih baik.
Suhu dan pengambilan sampel bersifat mekanis, bukan magis
Suhu membagi logit sebelum softmax. Suhu yang lebih tinggi = distribusi yang lebih datar = output yang lebih acak. Suhu yang lebih rendah = distribusi yang lebih tajam = output yang lebih deterministik. Ketika API produksi Anda mengembalikan hasil yang tidak konsisten dengan temperature=0.0, itu bukan bug. Suhu nol yang sebenarnya adalah argmax serakah, dan banyak API sedikit menurunkannya untuk menghindari output yang merosot.
Jendela konteks adalah batasan keras, bukan saran lunak
Baris idx_cond = ids[:, -SEQ_LEN:] dalam loop inferensi menunjukkan dengan tepat apa yang terjadi pada batas konteks. Model secara diam-diam menghapus token yang lebih lama. Jika integrasi API Anda mengasumsikan model mengingat riwayat percakapan lengkap, itu tidak berlaku setelah titik tertentu. Lihat [internal: how-ai-agent-memory-works] untuk bagaimana agen menangani masalah ini.
Token streaming hanyalah langkah inferensi yang terlihat
API streaming tidak melakukan hal yang berbeda secara arsitektur. Mereka menjalankan loop inferensi dan mengirim setiap token ke aliran respons saat dihasilkan. Memahami ini membantu ketika Anda menulis logika percobaan ulang: aliran yang terputus di tengah generasi tidak dapat dilanjutkan, harus dimulai ulang.
Logit menjelaskan mengapa output terstruktur sulit
Model menetapkan probabilitas untuk setiap token dalam kosakata pada setiap langkah. Menghasilkan JSON yang valid membutuhkan token yang tepat untuk menang di setiap posisi. Pustaka seperti Outlines dan Guidance membatasi distribusi logit untuk menerapkan tata bahasa pada waktu inferensi. Ketika Anda melihat API AI menawarkan mode "output terstruktur", inilah yang mereka lakukan secara internal.
Cara menguji integrasi API AI dengan Apidog
Setelah Anda memahami cara kerja inferensi LLM, Anda dapat menulis pengujian API yang jauh lebih baik. Skenario Uji Apidog memungkinkan Anda merangkai panggilan API dan menegaskan struktur respons AI.
Misalnya, saat menguji API obrolan streaming:
- Buat Skenario Uji di Apidog dengan endpoint
/v1/chat/completionsAnda - Atur pernyataan untuk memverifikasi struktur respons:
response.choices[0].finish_reason == "stop",response.usage.total_tokens < 4096 - Tambahkan langkah lanjutan yang mengirim respons sebagai konteks ke giliran berikutnya, mensimulasikan percakapan multi-giliran
- Gunakan Smart Mock Apidog untuk menguji endpoint AI dan menguji penanganan kesalahan aplikasi Anda: simulasikan
finish_reason: "length"(output terpotong),finish_reason: "content_filter", dan batas waktu jaringan di tengah aliran
Beginilah cara Anda menguji integrasi AI tanpa membakar kredit API pada setiap CI run. Lihat [internal: api-testing-tutorial] untuk melihat gambaran yang lebih luas tentang pendekatan pengujian API.
Menguji pernyataan jumlah 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"
}
]
}
Jalankan ini di beberapa model (GPT-4o, Claude 3.5 Sonnet, Gemini 1.5 Pro) dalam satu Skenario Uji untuk menangkap perbedaan skema API sebelum mencapai produksi.
Lanjutan: kuantisasi dan optimisasi inferensi
Setelah Anda memiliki LLM kecil yang berfungsi, dua teknik patut dipahami karena secara langsung berlaku untuk cara model produksi dilayani.
Kuantisasi
Bobot dalam model kita adalah float 32-bit secara default. Kuantisasi menguranginya menjadi bilangan bulat 8-bit (INT8) atau bahkan 4-bit (INT4). Ini mengurangi penggunaan memori sebanyak 4-8x dengan kehilangan akurasi yang moderat.
# Example: dynamic INT8 quantization in PyTorch
import torch.quantization
quantized_model = torch.quantization.quantize_dynamic(
model, {nn.Linear}, dtype=torch.qint8
)
API produksi menjalankan model terkuantisasi. Ketika Anda melihat kualitas output yang berbeda pada "versi" yang berbeda dari model yang sama, kuantisasi sering kali terlibat.
Cache KV
Dalam loop inferensi kita, kita menghitung ulang perhatian di seluruh urutan setiap langkah. Sistem produksi menyimpan pasangan kunci-nilai dari token sebelumnya (cache KV) sehingga setiap token baru hanya membutuhkan satu komputasi perhatian baru. Inilah sebabnya mengapa token pertama dalam respons streaming membutuhkan waktu lebih lama daripada token berikutnya.
LLM kecil vs. API produksi: kapan menggunakan masing-masing
| Kasus Penggunaan | LLM Kecil | API Produksi |
|---|---|---|
| Mempelajari internal model | Terbaik untuk | Berlebihan |
| Membuat prototipe aplikasi baru | Kualitas tidak memadai | Terbaik untuk |
| Data pribadi/sensitif | Pilihan bagus | Tergantung penyedia |
| Penyebaran offline/edge | Layak | Tidak mungkin |
| Sensitif biaya, volume tinggi | Mungkin dengan kompromi | Mahal pada skala besar |
| Tugas yang membutuhkan penalaran berat | Tidak layak | Diperlukan |
Jawaban sebenarnya bagi sebagian besar pengembang: gunakan API produksi untuk aplikasi Anda, tetapi jalankan model kecil untuk memahami apa yang terjadi di balik layar. Keduanya tidak bersaing. Artikel [internal: open-source-coding-assistants-2026] membahas alat yang mengaburkan batas ini dengan pengaturan "bring-your-own-model".
Kesimpulan
Membangun LLM kecil dari awal membutuhkan waktu akhir pekan. Yang Anda dapatkan bukanlah sistem produksi; ini adalah model mental yang berfungsi tentang bagaimana setiap model bahasa, dari GuppyLM hingga GPT-4o, sebenarnya bekerja. Pemahaman itu terbayar setiap kali Anda men-debug integrasi streaming, menyetel parameter pengambilan sampel, atau merancang pernyataan untuk pengujian API AI Anda.
Proyek GuppyLM adalah titik awal yang baik. Kloning, latih pada dataset teks apa pun, dan luangkan sore untuk membaca loop inferensi. Kemudian kembali ke integrasi API produksi Anda dan Anda akan melihatnya secara berbeda.
Coba Skenario Uji Apidog untuk membawa ketelitian yang sama ke pengujian API AI Anda seperti yang akan Anda terapkan pada sistem backend lainnya.
FAQ
Berapa banyak parameter yang dibutuhkan LLM "kecil" untuk menghasilkan teks yang koheren?Sekitar 10M-50M parameter dengan dataset pelatihan yang layak dapat menghasilkan kalimat yang koheren secara lokal. Di bawah 1M, Anda akan mendapatkan omong kosong pada sebagian besar tugas. GuppyLM pada 8.7M berfungsi untuk percakapan singkat di domain pelatihannya (60 topik).
Bisakah saya menjalankan LLM kecil tanpa GPU?Ya. Model di bawah 100M parameter berjalan dengan baik di CPU, meskipun inferensi lebih lambat. Model di atas (1.2M parameter) menghasilkan token dalam milidetik di CPU laptop.
Dataset apa yang harus saya latih?Model tingkat karakter bekerja dengan baik dengan teks Project Gutenberg, subset Wikipedia, atau korpus teks biasa apa pun. GuppyLM menggunakan dataset percakapan 60K entri di HuggingFace (arman-bd/guppylm-60k-generic). Untuk pembuatan kode, gunakan The Stack atau CodeParrot.
Apa perbedaan antara suhu dan pengambilan sampel top-k?Suhu menskalakan distribusi logit (mengontrol keacakan keseluruhan). Top-k membatasi kumpulan pengambilan sampel ke k token yang paling mungkin sebelum menerapkan suhu. Keduanya diterapkan bersama: pertama top-k memfilter kandidat, kemudian suhu membentuk probabilitas dalam kumpulan itu.
Mengapa LLM saya terkadang mengulang dirinya sendiri?Pengulangan adalah mode kegagalan di mana model menetapkan probabilitas tinggi pada token yang baru saja dibuat karena muncul dalam konteks. API produksi menggunakan penalti pengulangan (penyesuaian logit yang mendiskon token yang baru saja dibuat). Tambahkan repetition_penalty=1.1 dalam panggilan API Anda untuk mengurangi ini.
Berapa lama waktu yang dibutuhkan untuk melatih LLM kecil?Model di atas dilatih hingga output koheren dalam waktu kurang dari 2 jam pada satu GPU (RTX 3060 atau setara). GuppyLM dilatih di Colab dalam waktu yang kira-kira sama. Model yang lebih besar (100M+) membutuhkan pengaturan multi-GPU dan berhari-hari pelatihan.
Apa cara tercepat untuk beralih dari LLM kecil ke endpoint API nyata?Ekspor ke format GGUF menggunakan skrip konversi llama.cpp, lalu sajikan dengan llama-server. Ini memberi Anda endpoint API yang kompatibel dengan OpenAI yang berjalan secara lokal. Anda kemudian dapat mengarahkan Apidog ke sana untuk pengujian, lihat [internal: rest-api-best-practices].
Bagaimana LLM produksi menangani konteks yang lebih panjang dari jendela pelatihannya?Teknik seperti RoPE (Rotary Position Embedding) dengan penskalaan yang diperluas, sliding window attention, dan retrieval-augmented generation semuanya memperluas konteks yang efektif. Arsitektur transformer inti tidak berubah; ini adalah modifikasi pada cara informasi posisi dikodekan dan bagaimana jendela perhatian diterapkan.
