Anda perlu menguji sebuah function di mana output yang benar tidak mungkin diketahui sebelumnya. Sebuah route optimizer. Sebuah sentiment classifier. Sebuah simulasi fisika. Anda sudah membaca tentang metamorphic testing: temukan hubungan antara input dan output yang harus berlaku, lalu uji hubungan-hubungan tersebut alih-alih nilai yang tepat.

Masalahnya adalah menemukan relation-relation tersebut. Anda menatap signature function Anda dan otak kosong.

Jadi Anda bertanya kepada LLM. LLM membalas dengan sepuluh metamorphic relations dalam hitungan detik. Kebanyakan terlihat masuk akal. Beberapa salah jelas. Satu atau dua benar-benar cerdas. LLM adalah mitra brainstorming yang berguna, tetapi bukan pengganti pengetahuan domain. Tempelkan relation-relation tersebut ke test suite Anda tanpa diperiksa, dan Anda akan mengirimkan pengujian yang lulus pada kode yang rusak atau gagal pada kode yang benar.

Apa yang Sebenarnya Diberikan LLM kepada Anda

Ketika Anda meminta LLM untuk mengusulkan metamorphic relations untuk sebuah function, LLM melakukan pattern matching tingkat lanjut melalui data pelatihan. LLM telah melihat ribuan implementasi algoritma pengurutan, library geometri, dan pipeline machine learning. LLM mengenali bahwa “menambahkan konstanta harus menggeser mean dengan konstanta yang sama” adalah properti yang umum, dan LLM menyarankannya.

Ini bukan pemahaman. Ini adalah asosiasi statistik. LLM tidak mengetahui apakah function Anda menangani daftar kosong, nilai NaN, atau integer overflow. LLM mengusulkan relation karena LLM pernah melihat relation yang serupa sebelumnya.

Berikut tampilan prompt dan respons khas. Anda memberi LLM sebuah function dan meminta relation:

# Your function under test
def percentile(values: list[float], p: float) -> float:
    """Return the p-th percentile using linear interpolation."""
    if not values:
        raise ValueError("empty list")
    if not 0.0 <= p <= 1.0:
        raise ValueError("p must be in [0, 1]")
    sorted_vals = sorted(values)
    n = len(sorted_vals)
    idx = p * (n - 1)
    lower = int(idx)
    upper = lower + 1
    if upper >= n:
        return sorted_vals[-1]
    frac = idx - lower
    return sorted_vals[lower] * (1 - frac) + sorted_vals[upper] * frac
Prompt: Propose 5 metamorphic relations for this percentile function.

Response:
1. Monotonicity: if p1 < p2, then percentile(values, p1) <= percentile(values, p2)
2. Scaling: percentile([v * k for v in values], p) == percentile(values, p) * k
3. Translation: percentile([v + c for v in values], p) == percentile(values, p) + c
4. Permutation invariance: percentile(values, p) == percentile(shuffled(values), p)
5. Boundary: percentile(values, 0.0) == min(values), percentile(values, 1.0) == max(values)

Tiga dari ini benar dan berguna. Satu salah secara halus. Satu secara trivial benar tetapi terlalu longgar sehingga hampir tidak menangkap bug.

Relation monotonicity benar, meskipun Anda perlu menangani nilai duplikat. Relation scaling gagal untuk k <= 0 karena urutan pengurutan terbalik. Relation translation solid. Permutation invariance sebagian besar menguji apakah Anda ingat untuk mengurutkan. Relation boundary benar hanya jika interpolasi Anda memperlakukan persentil ke-0 dan ke-100 sebagai min dan max.

LLM tidak memperingatkan Anda tentang apa pun dari ini. LLM menyajikan kelima relation dengan keyakinan yang sama.

Cara Memfilter Relation yang Dihasilkan LLM

Workflow yang berguna bukan “tanyakan LLM, salin output, makan siang.” Workflow yang berguna adalah “tanyakan LLM, perlakukan output sebagai daftar kandidat, lalu verifikasi setiap kandidat dengan penalaran dan pengujian.”

Langkah pertama adalah mengklasifikasikan setiap relation yang diusulkan berdasarkan tipe. Relation struktural, seperti permutation invariance atau idempotence, cenderung lebih aman karena mereka bergantung lebih sedikit pada semantik domain. Relation aritmatika, seperti scaling atau translation, kuat ketika berlaku, tetapi sering gagal pada edge case yang tidak LLM pertimbangkan: pengali negatif, koleksi kosong, pembulatan floating-point.

Langkah kedua adalah berburu counterexample. Untuk setiap relation yang diusulkan, coba temukan input di mana relation tersebut gagal. Ini adalah cara tercepat untuk menemukan halusinasi LLM.

def test_percentile_scaling_counterexample():
    """The LLM proposed scaling. It fails for negative k."""
    values = [1.0, 2.0, 3.0, 4.0]
    original = percentile(values, 0.5)  # 2.5
    
    k = -1.0
    scaled_values = [v * k for v in values]
    scaled_result = percentile(scaled_values, 0.5)  # -2.5
    
    # The relation holds here by accident. For nearest-rank,
    # multiplying by a negative flips sort order and breaks it.
    assert abs(scaled_result - original * k) < 1e-9

Relation scaling secara kebetulan berlaku untuk interpolasi linear, tetapi Anda hanya mengetahuinya karena Anda mengujinya. LLM tidak tahu. LLM menebak berdasarkan pattern matching. Algoritma persentil yang berbeda akan merusak scaling dengan cara yang jelas.

Langkah ketiga adalah mutation testing. Setelah Anda memiliki relation sebagai pengujian, jalankan alat mutation testing terhadapnya. Jika mutant bertahan, relation Anda terlalu lemah. Jika kode yang benar terbunuh, relation Anda salah.

Di Mana LLM Bersinar dan Di Mana Mereka Gagal

LLM benar-benar berguna untuk menemukan relation di domain yang sudah sering dilalui. LLM tahu bahwa image classifier harus invarian terhadap pembalikan horizontal, bahwa algoritma pengurutan harus idempotent, dan bahwa perkalian matriks harus terdistribusi atas penjumlahan. LLM mengingat relation kanonik ini secara instan.

LLM kurang berguna di domain dengan constraint implisit yang tidak muncul dalam data pelatihan. Jika Anda menguji mesin pricing kustom dengan aturan bisnis tentang diskon regional dan interaksi kode promosi, LLM tidak tahu. LLM akan mengusulkan relation aritmatika generik yang mengabaikan logika bisnis, atau yang lebih buruk, menyarankan relation yang bertentangan dengannya.

Mode kegagalannya dapat diprediksi:

Relation yang terlalu umum. LLM menyarankan “output harus positif” untuk function yang mengembalikan probabilitas. Itu adalah sanity check yang lemah, bukan metamorphic relation. Relation tersebut menangkap crash tetapi tidak bug logika.

Relation yang mengasumsikan kontinuitas. LLM mengusulkan bahwa perubahan input kecil menghasilkan perubahan output kecil. Itu gagal untuk function threshold dan classifier diskrit.

Relation yang mengabaikan constraint tipe. LLM menyarankan mengurutkan daftar dataclass berdasarkan field, lalu memeriksa bahwa field elemen pertama adalah minimum. LLM lupa bahwa beberapa field mungkin opsional, atau bahwa operator perbandingan mungkin tidak didefinisikan untuk tipe tersebut.

Relation yang secara matematis salah. LLM pernah menyarankan bahwa median dari daftar gabungan sama dengan rata-rata median dari sub-daftar. Itu tidak benar. LLM menyajikannya dengan keyakinan yang sama seperti permutation invariance.

Workflow Praktis

Jangan minta LLM untuk menggantikan otak Anda. Minta LLM untuk mempercepat bagian di mana Anda menatap halaman kosong.

Mulailah dengan menulis deskripsi satu paragraf tentang function Anda, termasuk tipe, constraint, dan edge case yang diketahui. Semakin banyak konteks yang Anda berikan, semakin sedikit LLM menghasilkan halusinasi.

Minta relation yang dikategorikan berdasarkan tipe: invariance relations, monotonicity relations, additive relations, dan structural relations. Pembingkai ini membantu LLM mengatur pattern matching-nya dan menghasilkan output yang lebih konsisten.

Untuk setiap relation yang diusulkan, jalankan melalui checklist ini:

  1. Apakah relation berlaku untuk input kosong?
  2. Apakah relation berlaku untuk input satu elemen?
  3. Apakah relation berlaku untuk nilai negatif, nol, NaN, atau infinity?
  4. Apakah relation berlaku ketika input sudah dalam urutan yang diurutkan? Urutan terbalik?
  5. Bisakah saya menulis mutation test yang membunuh mutant hanya jika relation ini berlaku?

Simpan relation yang bertahan kelima pemeriksaan. Buang sisanya, dan dokumentasikan alasannya.

Berikut template untuk prompt yang kami gunakan secara internal:

SYSTEM_PROMPT = """
You are a testing assistant. Given a Python function, propose metamorphic relations.
For each relation:
1. State the relation clearly
2. Identify the type: invariance, monotonicity, additive, or structural
3. List edge cases where it might fail
4. Rate confidence as HIGH, MEDIUM, or LOW
"""

def generate_relations(source_code: str) -> list[dict]:
    response = openai.chat.completions.create(
        model="gpt-4o",
        messages=[
            {"role": "system", "content": SYSTEM_PROMPT},
            {"role": "user", "content": f"Propose relations:\n\n{source_code}"}
        ],
        temperature=0.3,
    )
    return parse_relation_candidates(response.choices[0].message.content)

Pada 0,3, LLM kurang kreatif tetapi lebih konsisten. Untuk metamorphic relations, konsistensi mengalahkan kreativitas. Anda menginginkan relation yang membosankan dan benar, bukan relation yang cerdas dan salah.

Bottleneck Nyata Masih Anda

LLM dapat mempercepat penemuan, tetapi tidak bisa menggantikan verifikasi. Sebuah metamorphic relation yang belum Anda validasi secara pribadi bukanlah pengujian. Itu adalah tebakan yang dandani dengan assertion.

Jawaban jujur untuk “bisakah LLM menemukan test oracles untuk saya?” adalah sebagian. LLM dapat menemukan kandidat, menggugah ingatan Anda, dan menyarankan edge case. LLM tidak bisa memberi tahu Anda relation mana yang benar untuk implementasi spesifik Anda, dengan constraint spesifik Anda, di domain spesifik Anda.

Bagian itu masih membutuhkan manusia yang memahami kode. LLM adalah mitra brainstorming, bukan oracle untuk oracle-oracle.

Jika Anda mulai dari nol, pilih satu function dengan oracle yang lemah, minta LLM untuk lima relation, lalu habiskan dua puluh menit mencoba merusak masing-masing. Relation yang bertahan adalah set seed Anda. Relation yang rusak mengajari Anda lebih banyak tentang function Anda daripada yang pernah LLM lakukan.