你需要測試一個無法事先知道正確輸出的函式。一個路徑最佳化器。一個情緒分類器。一個物理模擬。你讀過 metamorphic testing:找出輸入與輸出之間必須成立的關係,然後測試這些關係,而不是精確值。

問題在於想出這些關係。你盯著你的函式簽章,腦袋一片空白。

於是你去問 LLM。它在幾秒內丟回十個 metamorphic relations。大多數看似合理。少數明顯錯誤。一兩個真正聰明。LLM 是有用的腦力激盪夥伴,但無法取代領域知識。未經檢查就把那些關係貼進測試套件,你會上線在錯誤程式碼上通過、或在正確程式碼上失敗的測試。

LLM 實際上給了你什麼

當你請 LLM 為一個函式提出 metamorphic relations 時,它做的是對訓練資料進行進階模式匹配。它看過數千種排序演算法、幾何函式庫與機器學習管線的實作。它認知到「加上一個常數應該讓平均值偏移相同常數」是一個常見性質,然後提出建議。

這不是理解。這是統計關聯。LLM 不知道你的函式是否處理空列表、NaN 值或整數溢位。它提出這個關係只是因為它看過類似的。

以下是一個典型的提示與回應長什麼樣子。你把一個函式餵給 LLM,然後請它給關係:

# 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)

這五個裡面有三個正確且有用。一個微妙地錯誤。一個顯然為真但過於鬆散,幾乎抓不到任何缺陷。

單調性關係是正確的,雖然你需要處理重複值。縮放關係在 k <= 0 時會失敗,因為排序順序反轉了。平移關係是穩健的。排列不變性主要測試你是否記得排序。邊界關係只有當你的插值將第 0 與第 100 百分位數視為最小值與最大值時才正確。

LLM 不會警告你任何一項。它以同樣的自信呈現全部五個。

如何過濾 LLM 產生的關係

有用的工作流程不是「問 LLM、複製輸出、去午餐」。而是「問 LLM、將輸出視為候選清單,然後用推理與測試驗證每個候選」。

第一步是依類型分類每個提出的關係。結構性關係,例如排列不變性或idempotency,往往更安全,因為它們較不依賴領域語意。算術關係,例如縮放或平移,成立時很強大,但經常在 LLM 沒考慮到的邊界情況下失敗:負數乘數、空集合、浮點數捨入。

第二步是尋找反例。對每個提出的關係,試著找一個它失敗的輸入。這是發現 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

縮放關係對線性插值來說偶然成立,但你只有測試過才知道。LLM 不知道。它只是根據模式匹配猜測。不同的百分位數演算法會以顯而易見的方式破壞縮放。

第三步是 mutation testing。一旦你把一個關係寫成測試,就對它執行 mutation testing 工具。如果 mutants 存活,你的關係太弱。如果正確程式碼被殺掉,你的關係是錯的。

LLM 發光與摔跤的地方

LLM 在已有大量足跡的領域中,確實有助於發現關係。它們知道影像分類器應該對水平翻轉不變,排序演算法應該是idempotent的,以及矩陣乘法應該對加法具有分配性。LLM 能立即回想起這些經典關係。

它們在訓練資料中沒出現的隱含限制領域中較沒用。如果你在測試一個帶有區域折扣與促銷碼互動商業規則的自訂定價引擎,LLM 毫無頭緒。它會提出忽略商業邏輯的通用算術關係,或者更糟,提出與之矛盾的關係。

失敗模式是可預測的:

過於籠統的關係。 LLM 建議「輸出應該為正數」給一個回傳機率的函式。那是弱的合理性檢查,不是 metamorphic relation。它只抓到崩潰,抓不到邏輯缺陷。

假設連續性的關係。 LLM 提出輸入的微小改變會產生輸出的微小改變。這對閾值函式與離散分類器會失敗。

忽略型別限制的關係。 LLM 建議依欄位排序一個 dataclass 列表,然後檢查第一個元素的欄位是否是最小值。它忘了有些欄位可能是 optional,或者該型別的比較運算子可能未定義。

數學上錯誤的關係。 LLM 曾經建議串接列表的中位數等於子列表中位數的平均。這不是真的。LLM 以與排列不變性同樣的自信呈現了它。

實務工作流程

不要請 LLM 取代你的大腦。請它加速你盯著空白頁發呆的階段。

先撰寫一段關於你函式的描述,包含型別、限制與已知的邊界情況。你給的脈絡越多,LLM 幻覺越少。

請它依類型分類關係:invariance relations、monotonicity relations、additive relations 與 structural relations。這個框架幫助 LLM 組織它的模式匹配,並產生更一致的輸出。

對每個提出的關係,跑過這份檢查清單:

  1. 它對空輸入成立嗎?
  2. 它對單元素輸入成立嗎?
  3. 它對負數、零、NaN 或無限大成立嗎?
  4. 它對已經排序的輸入成立嗎?反向順序呢?
  5. 我能寫一個只有當這個關係成立時才會殺死 mutants 的 mutation test 嗎?

讓通過全部五項的關係留下。丟棄其他的,並記錄原因。

以下是我們內部使用的提示範本:

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)

在 0.3 溫度下,LLM 比較不會天馬行空,但比較一致。對於 metamorphic relations 來說,一致性勝過創意。你要的是無聊但正確的關係,不是聰明但錯誤的關係。

真正的瓶頸仍然是你

LLM 能加速發現,但無法取代驗證。一個你沒有親自驗證過的 metamorphic relation 不是測試。它是穿上斷言外衣的猜測。

「LLM 能幫我發現 test oracles 嗎?」這個問題的誠實答案是:部分的。它能發現候選、喚醒你的記憶、建議邊界情況。它無法告訴你哪些關係對你的特定實作、你的特定限制、你的特定領域是正確的。

那部分仍然需要一個懂程式碼的人。LLM 是腦力激盪夥伴,而不是 oracles 的 oracle。

如果你從零開始,挑選一個帶有薄弱 oracle 的函式,向 LLM 要五個關係,然後花二十分鐘試著打破每一個。存活的關係是你的種子集合。被打破的關係教會你更多關於你的函式的事情,比 LLM 所能教的還多。