同じプロンプトを5回実行しても、5つの同じバグが出てくるだけ
N-version programmingでは、多様性は異なる作成者から生まれるものだと想定されている。LLMの場合、それは異なるモデル、異なるプロバイダー、あるいは異なる学習実行を意味する。だがこの前提は間違っている。同じモデルから、問う内容ではなく問い方を変えることで、意味のある多様性を得られる。
問題は、temperatureを1.0に上げてプロンプトを5回実行するのは戦略にならないということだ。表面的な違いは出る。変数名が変わる。コメントの配置が入れ替わる。だが構造は同一であり、バグも同一のままだ。
独立して失敗する実装が必要なら、異なる出力ではなく、異なる思考パターンをプロンプトで引き出す必要がある。
LLMにN-Version Programmingが本当に求めるもの
N-version programmingとは、同じ仕様の複数の独立した実装を並列実行するフォールト・トレランス技法だ。出力を比較し、多数決で正しい結果を決定する。異なる開発者が独立して作業すれば、異なるバグを埋め込むという発想である。バグは相関しないため、多数決で抑制できる。
古いアイデアだ。かつ高価だ。同じものをNのチームに作らせるのだから。
LLMはそれを安価に試せるようにする。Nのチームの代わりに、N回のAPI呼び出しで済む。問題は、同じモデルに同じプロンプトをN回投げても、N個のほぼ同一の実装が返ってくることだ。バグは完全に相関する。多数決は意味を失う。
解決策は、モデルではなくプロンプトを開発者として扱うことだ。異なるプロンプトが異なる開発者を生み出す。
なぜTemperatureだけでは見た目だけの多様性しか生まれないのか
Temperatureはトークン上の確率分布を制御する。高いtemperatureでは、モデルは次のトークンとして可能性の低いものを選ぶ。これにより表現、変数名、表面的な構造に変化が生まれる。
しかしアルゴリズム的アプローチの変化は生まれない。最長回文部分文字列を求める関数を依頼した場合、temperatureが変わるのはleftとrightを使うかlとrを使うかだ。expand-around-centerを選ぶかdynamic programmingを選ぶかは変わらない。
N-version programmingにとって、それは無価値だ。問題を異なる方法で解く実装が必要であり、同じ方法で解きながら見た目だけ違う実装は必要ない。
アルゴリズム的な多様性を強制する4つのプロンプト戦略
以下は、モデルの問題に対する思考方法を変える4つのアプローチだ。
問題のフレーミングを変える
「パーサーを書け」と依頼するのと、「この文法を認識するステートマシンを書け」と依頼するのでは、異なるコードが出てくる。一方はrecursive descentになるかもしれない。もう一方はtable-drivenアプローチになるかもしれない。
これを自動化するには、モデルに特定のフレーミングを適用させてから解かせればよい:
import os
from openai import OpenAI
client = OpenAI(api_key=os.environ["OPENAI_API_KEY"])
def generate_with_framing(task: str, framing: str) -> str:
prompt = f"""{framing}
Task: {task}
Write a complete, correct implementation. Do not explain your approach."""
response = client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": prompt}],
temperature=0.7,
)
return response.choices[0].message.content
task = "Parse a CSV string into a list of dictionaries, handling quoted fields and newlines within quotes."
framings = [
"Approach this as a finite state machine with explicit state transitions.",
"Approach this using recursive descent parsing with a lexer and parser.",
"Approach this by splitting on delimiters and post-processing edge cases.",
]
for framing in framings:
print(f"=== {framing} ===")
print(generate_with_framing(task, framing))
これをGPT-4oに実行すると、ステートマシンのフレーミングは一貫して明示的なstate enumを持つ文字単位のパーサーを生成する。recursive descentのフレーミングはlexerと独立したparser関数を生成する。split-and-fixのフレーミングはよりコンパクトだが壊れやすい解決策を生成する。
ペルソナを切り替える
異なるペルソナは異なる知識を呼び起こす。システムプログラマーはデータサイエンティストや競技プログラマーとは異なるコードを書く。
personas = [
"You are a systems programmer who prioritizes memory efficiency and avoids unnecessary allocations.",
"You are a Pythonic developer who prefers concise, idiomatic code using standard library features.",
"You are an algorithms researcher who reaches for theoretically optimal solutions even if the code is longer.",
]
ペルソナ・プロンプトは構造的な多様性において驚くほど効果的だ。システムプログラマーは配列とインデックスを選ぶ。Pythonicな開発者はitertoolsやcomprehensionを選ぶ。アルゴリズム研究者はライブラリを取り込んだり、より形式的な解決策を書いたりするかもしれない。
利用可能なツールを制約する
利用可能なツールセットを制限したり拡張したりすることで、異なるアプローチが強制される。
constraints = [
"You may only use the Python standard library. No external dependencies.",
"You may use numpy and pandas. Optimize for vectorized operations.",
"You must implement this without using regular expressions.",
]
これは、あるアプローチに盲点があることを知っているときに特に有用だ。regexベースのパーサーがネストされたクォートを繰り返し誤処理するなら、regexを使わない実装を強制すればよい。
発散的推論を伴うChain-of-Thought
コードを直接依頼するのではなく、モデルに複数の解決戦略を生成させ、最も自明でないものを選ばせる。
cot_prompt = f"""Task: {task}
First, list three different algorithms or approaches to solve this problem.
Then, pick the one that is most different from the others and implement it.
Do not pick the most obvious approach."""
response = client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": cot_prompt}],
temperature=0.7,
)
chain-of-thoughtはモデルに推論を表出させる。「最も異なる」という制約は、デフォルトの解決策から離れるように押しやる。実際、これは単一の技法の中で最も高い構造的多様性を生み出す。
どこで機能しなくなるか:多様性の天井
同じモデルからの多様性には限界があり、それにぶつかる。
根本的な知識の欠如は共有されている。学習データに浮動小数点比較に関する体系的な誤解が含まれているなら、どんなフレーミングやペルソナでもその誤解を再現する。モデルは1セットの重みを持つ。プロンプトではそれを回避できない。
また、収益逓減もある。最初の3つのフレーミングはステートマシン、recursive parser、splitベースのアプローチを与えるかもしれない。4つ目のフレーミングは変数名が異なるステートマシンを与えるかもしれない。3〜5つの本当に異なるアプローチの後、底を浚うことになる。
一部の技法は品質を低下させる。「最も異なる」制約は、時に異なる理由が間違っているから異なる解決策を生み出す。多様性そのもののための divergence は有用ではない。悪いアイデアをフィルタリングするための投票やテストの仕組みが必要だ。
今日から導入できる実用的な構成
システムに組み込むなら、ランダム化するな。多様性を設計する。
上記のリストから3〜5の技法を選ぶ。技法ごとに1つの実装を生成する。すべてに対してテストスイートやproperty-based testを実行する。合格したものを残す。最終出力には単純な多数決を使う。
from collections import Counter
def majority_vote(outputs: list[str], test_fn) -> str:
passing = [o for o in outputs if test_fn(o)]
if not passing:
raise RuntimeError("No implementation passed tests")
# Exact match voting; swap for AST comparison if needed
return Counter(passing).most_common(1)[0][0]
テストによるフィルタリングは譲れない。正確性なき多様性は単なるノイズだ。
FAQ
これは小さなモデルでも機能するか?
はい。ただし多様性の天井は低い。小さなモデルは学習データに少ない種類の解決戦略しか持っていない。4つではなく2つの本当に異なるアプローチが得られるかもしれない。技法自体は機能するが、変化の量は少ない。
実際に必要な実装はいくつか?
多数決の実用的な最小値は3つだ。5つはより良いカバレッジを与えるが、コストは線形に増加する。5つを超えると、同じモデルからの多様性は見た目だけの変化に劣化する。5つ以上必要なら、cross-model diversityに切り替える。
同じモデルからの多様性は、異なるモデルからの多様性と同じくらい良いか?
いいえ。異なるモデルは異なる学習データ、アーキテクチャ、ファインチューニングを持つ。本当に異なる方法で失敗する。同じモデルからの多様性は、コストと運用の利便性のトレードオフだ。完璧なフォールト・トレランスではなく、迅速に良好なフォールト・トレランスが必要なときに使う。
これらの技法を組み合わせられるか?
もちろん。ペルソナ・プロンプトにツール制約とchain-of-thoughtステップを組み合わせれば、単一の技法よりも多くの多様性が生まれる。代償はより長いプロンプトと生成あたりのより多くのトークンだ。クリティカルなコードパスでは、追加のトークンは価値がある。
まず試すべきこと
フレーミングの変化から始める。実装が最も簡単で、最も一貫した構造的多様性を生み出す。さらに多様性が必要ならペルソナの切り替えを追加する。cross-model diversityは、同じモデルからの多様性が天井にぶつかったケースのために取っておく。
投票させる前に、すべての実装を同じテストスイートに通す。テストされていない多様な実装は、まだ出会っていないだけのバグのある実装に過ぎない。