Strategi decoding pada bab sebelumnya mengasumsikan satu hal yang tak terlihat: teks yang masuk ke model sudah berbentuk benar. Model chat tidak dilatih pada teks polos, melainkan pada jutaan percakapan terformat — peran system/user/assistant ditandai token khusus, giliran diakhiri penanda akhir, dan seluruh riwayat dirangkai menjadi satu string panjang. Mengirim prompt tanpa format itu sama dengan memasukkan data dari distribusi asing: model bisa menjawab, tetapi keyakinannya menurun dan perilakunya tak terduga. Bab ini membedah anatomi template, membangun mesin template mini beserta tokenisasinya, lalu menutup dengan risiko keamanan yang melekat pada cara system prompt bekerja.
Mengapa Format Menentukan Hasil #
LLM adalah model bahasa pada teks apa pun — tetapi model yang telah melalui instruction tuning
dan preference optimization
dikondisikan pada format percakapan spesifik. Token seperti [INST] atau <|im_start|> bukan hiasan: selama fine-tuning, token-token itu menjadi sinyal bersyarat yang memicu perilaku “mode asisten” — menjawab ringkas, menolak permintaan di luar batas, mengakhiri giliran dengan benar. Prompt yang salah format membuat sinyal itu tidak pernah menyala.
Konsekuensi praktisnya: format antar model berbeda dan tidak bisa dicampur. Dua pola yang paling sering dijumpai:
- Gaya pembungkus: giliran
userdibungkus pasangan penanda seperti[INST] ... [/INST], jawabanassistantmengikutinya dan diakhiri</s>. - Gaya penanda peran: tiap giliran dibuka token peran seperti
<|im_start|>user, isi di baris berikutnya, ditutup<|im_end|>.
Kedua pola berikut masing-masing disederhanakan dari keluarga model nyata; versi produksinya menambahkan detail sendiri (penanda sistem terpisah, spasi, token bos). Aturan selalu sama: gunakan format milik model yang dipakai, persis.
Anatomi Chat Template #
| Komponen | Fungsi | Contoh penanda |
|---|---|---|
| Peran (role) | memisahkan instruksi, pertanyaan, jawaban | system, user, assistant |
| Token pembuka/penutup giliran | batas eksplisit antar isi | [INST], [/INST], `< |
| Token akhir sekuens (EOS) | menghentikan decoding | </s>, `< |
| System prompt | aturan main sepanjang percakapan | giliran system pertama |
| Prompt generasi | kepala assistant tanpa isi | `< |
| Riwayat multi-giliran | konteks percakapan sebelumnya | seluruh giliran dirangkai urut |
Demo: Merakit Template Sendiri #
Cara tercepat memahami template adalah membangun satu. Berkas chat_template.py mendefinisikan dua fungsi render, satu tokenisasi mini dengan enam token khusus ber-id cadangan, lalu merender percakapan kuis yang sama pada kedua gaya. Percakapan sengaja memuat satu pasang tanya-jawab lama — giliran itu sekaligus berperan sebagai contoh few-shot di dalam konteks:
"""Chat template: merakit percakapan menjadi satu string siap model.
Bagian 1: dua gaya template (pembungkus INST vs penanda peran per baris)
merender percakapan yang sama menjadi string yang berbeda.
Bagian 2: tokenisasi mini — token khusus mendapat id cadangan 0-5,
kata mendapat id dari kosakata urut; jumlah token kedua gaya
dibandingkan.
Bagian 3: prompt generasi — render dihentikan tepat setelah kepala
assistant sehingga model tinggal melanjutkan jawaban.
"""
import re
KHUSUS = {
"[INST]": 0,
"[/INST]": 1,
"</s>": 2,
"<|im_start|>": 3,
"<|im_end|>": 4,
"\n": 5,
}
POLA = re.compile(r"(\[INST\]|\[/INST\]|</s>|<\|im_start\|>|<\|im_end\|>|\n)")
def tokenisasi(teks):
"""Pecah teks menjadi token khusus dan kata."""
potongan = []
for b in POLA.split(teks):
if b == "":
continue
if b in KHUSUS:
potongan.append(b)
else:
potongan.extend(b.split())
return potongan
def jadi_ids(potongan, kosakata):
ids = []
for p in potongan:
ids.append(KHUSUS[p] if p in KHUSUS else len(KHUSUS) + kosakata[p])
return ids
def template_inst(pesan):
"""Gaya pembungkus: tiap giliran user dibungkus pasangan [INST]."""
sistem = next(isi for peran, isi in pesan if peran == "system")
teks = []
pertama = True
for peran, isi in pesan:
if peran == "system":
continue
if peran == "user":
if pertama:
teks.append(f"[INST] {sistem}\n\n{isi} [/INST]")
pertama = False
else:
teks.append(f"[INST] {isi} [/INST]")
else:
teks.append(f" {isi} </s>")
return "".join(teks)
def template_peran(pesan, generasi=False):
"""Gaya penanda peran: tiap giliran diapit token awal dan akhir."""
teks = []
for peran, isi in pesan:
teks.append(f"<|im_start|>{peran}\n{isi}<|im_end|>\n")
if generasi:
teks.append("<|im_start|>assistant\n")
return "".join(teks)
PESAN = [
("system", "Kamu asisten kuis. Jawab satu kata saja."),
("user", "Ibukota Jepang?"),
("assistant", "Tokyo"),
("user", "Ibukota Indonesia?"),
]
# ---------- 1. Dua gaya template ----------
s_inst = template_inst(PESAN)
s_peran = template_peran(PESAN)
print("== 1. dua template untuk percakapan yang sama ==")
print("--- gaya pembungkus INST ---")
print(s_inst)
print("--- gaya penanda peran ---")
print(s_peran)
# ---------- 2. Tokenisasi mini ----------
semua_kata = sorted({
kata
for s in (s_inst, s_peran)
for potongan in tokenisasi(s)
for kata in [potongan] if potongan not in KHUSUS
})
kosakata = {kata: i for i, kata in enumerate(semua_kata)}
print()
print("== 2. tokenisasi mini ==")
pot_inst = tokenisasi(s_inst)
pot_peran = tokenisasi(s_peran)
print(f"kata unik: {len(kosakata)} | id kata mulai dari {len(KHUSUS)}")
print(f"ukuran kosakata total: {len(KHUSUS) + len(kosakata)} id")
print(f"gaya pembungkus INST : {len(pot_inst)} token")
print(f"gaya penanda peran : {len(pot_peran)} token")
print("id token gaya peran :", jadi_ids(pot_peran, kosakata))
# ---------- 3. Prompt generasi ----------
print()
print("== 3. prompt generasi (render berhenti di kepala assistant) ==")
print(template_peran(PESAN, generasi=True) + "<jawaban model di sini>")Output yang Dihasilkan:
== 1. dua template untuk percakapan yang sama ==
--- gaya pembungkus INST ---
[INST] Kamu asisten kuis. Jawab satu kata saja.
Ibukota Jepang? [/INST] Tokyo </s>[INST] Ibukota Indonesia? [/INST]
--- gaya penanda peran ---
<|im_start|>system
Kamu asisten kuis. Jawab satu kata saja.<|im_end|>
<|im_start|>user
Ibukota Jepang?<|im_end|>
<|im_start|>assistant
Tokyo<|im_end|>
<|im_start|>user
Ibukota Indonesia?<|im_end|>
== 2. tokenisasi mini ==
kata unik: 14 | id kata mulai dari 6
ukuran kosakata total: 20 id
gaya pembungkus INST : 19 token
gaya penanda peran : 32 token
id token gaya peran : [3, 18, 5, 10, 12, 15, 8, 17, 14, 16, 4, 5, 3, 19, 5, 6, 9, 4, 5, 3, 13, 5, 11, 4, 5, 3, 19, 5, 6, 7, 4, 5]
== 3. prompt generasi (render berhenti di kepala assistant) ==
<|im_start|>system
Kamu asisten kuis. Jawab satu kata saja.<|im_end|>
<|im_start|>user
Ibukota Jepang?<|im_end|>
<|im_start|>assistant
Tokyo<|im_end|>
<|im_start|>user
Ibukota Indonesia?<|im_end|>
<|im_start|>assistant
<jawaban model di sini>Mari kita telaah kode di atas baris per baris:
- Baris 14-22 (
KHUSUSdantokenisasi): token khusus diberi id cadangan 0-5 agar tidak bentrok dengan id kata. Regex ber-grup-tangkapan padaPOLA.splitmemecah teks sambil mempertahankan penanda khusus sebagai potongan utuh — tokenisasi nyata bekerja dengan prinsip yang sama sebelum berurai ke subword. - Baris 51-67 (
template_inst): gaya pembungkus menempelkan system prompt ke dalam giliran user pertama dan membungkus tiap pertanyaan dengan[INST] ... [/INST]; jawaban assistant ditulis polos dan diakhiri</s>— penanda yang juga menjadi stop condition pada decoding . - Baris 70-77 (
template_peran): gaya penanda peran memperlakukan semua peran setara; parametergenerasimenentukan apakah render berhenti setelah kepalaassistanttanpa isi. - Baris 80-85 (
PESAN): percakapan empat giliran. Pasanganuser-assistantpertama berfungsi ganda: riwayat percakapan sekaligus contoh few-shot — model melihat pola tanya-jawab satu kata sebelum menjawab pertanyaan berikutnya. - Baris 99-106 (perbandingan): isi percakapan sama, tetapi gaya penanda peran menghabiskan 32 token sementara gaya pembungkus hanya 19. Overhead format adalah bagian dari anggaran konteks — pada percakapan panjang, perbedaan seperti ini nyata terasa.
- Baris 109-111 (prompt generasi): string berakhir di
<|im_start|>assistant\n— model melanjutkan dari kepala itu, dan seluruh aturan pada system prompt (“jawab satu kata saja”) berlaku pada kelanjutannya.
Zero-Shot, Few-Shot, dan System Prompt #
Tiga knob penyusun prompt bekerja pada tingkat berbeda. System prompt menetapkan aturan main yang berlaku sepanjang sesi — persona, batasan, format keluaran — dan menempati posisi pertama dalam string final. Zero-shot meminta model mengerjakan tugas tanpa contoh, mengandalkan pengetahuan pra-latih. Few-shot menyisipkan contoh tuntas di dalam konteks, seperti giliran lama pada demo di atas; model meniru pola contoh itu secara in-context tanpa satu bobot pun berubah. Semakin jarang tugasnya di data pra-latih, semakin besar manfaat few-shot — dengan taruhan yang setara: contoh yang bias akan diteruskan ke seluruh jawaban.
Format dan prompt kini lengkap: model menerima masukan terstruktur, menjawab dalam format yang benar, dan berhenti pada waktunya. Pertanyaan yang tersisa sebelum menyandang model ke produksi adalah pertanyaan paling tua di pembelajaran mesin: seberapa baik sebenarnya kinerjanya? Metrik, benchmark, dan jebakan-jebakan pengukurannya menanti di Evaluasi LLM .