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 user dibungkus pasangan penanda seperti [INST] ... [/INST], jawaban assistant mengikutinya 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 #

KomponenFungsiContoh penanda
Peran (role)memisahkan instruksi, pertanyaan, jawabansystem, user, assistant
Token pembuka/penutup giliranbatas eksplisit antar isi[INST], [/INST], `<
Token akhir sekuens (EOS)menghentikan decoding</s>, `<
System promptaturan main sepanjang percakapangiliran system pertama
Prompt generasikepala assistant tanpa isi`<
Riwayat multi-gilirankonteks percakapan sebelumnyaseluruh 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:

python
"""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:

text
== 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:

  1. Baris 14-22 (KHUSUS dan tokenisasi): token khusus diberi id cadangan 0-5 agar tidak bentrok dengan id kata. Regex ber-grup-tangkapan pada POLA.split memecah teks sambil mempertahankan penanda khusus sebagai potongan utuh — tokenisasi nyata bekerja dengan prinsip yang sama sebelum berurai ke subword.
  2. 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 .
  3. Baris 70-77 (template_peran): gaya penanda peran memperlakukan semua peran setara; parameter generasi menentukan apakah render berhenti setelah kepala assistant tanpa isi.
  4. Baris 80-85 (PESAN): percakapan empat giliran. Pasangan user-assistant pertama berfungsi ganda: riwayat percakapan sekaligus contoh few-shot — model melihat pola tanya-jawab satu kata sebelum menjawab pertanyaan berikutnya.
  5. 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.
  6. 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 .