같은 프롬프트를 5번 돌리면 같은 실수가 5개 나온다

N-version programming은 다양성이 서로 다른 작성자에게서 나온다고 가정한다. LLM을 쓸 때는 다른 모델, 다른 제공자, 아마도 다른 학습 실행본을 의미한다. 하지만 그 가정은 틀렸다. 똑같은 모델에게서도 질문하는 방식을 바꾸면—질문 내용이 아니라—의미 있는 다양성을 얻을 수 있다.

함정은 이것이다: temperature를 1.0으로 올리고 프롬프트를 5번 실행하는 것은 전략이 아니다. 표면적인 변화만 생긴다. 변수 이름이 바뀌고, 주석 위치가 달라진다. 구조는 똑같고, 버그도 똑같이 남아 있다.

독립적으로 실패하는 구현을 원한다면, 다른 출력이 아니라 다른 사고 패턴을 요구해야 한다.

N-Version Programming이 LLM에게 실제로 필요로 하는 것

N-version programming은 동일한 명세에 대한 여러 독립 구현을 병렬로 실행하는 fault tolerance 기법이다. 출력을 비교하고, 다수결로 올바른 결과를 결정한다. 핵심 아이디어는 서로 다른 개발자가 독립적으로 작업하면 서로 다른 버그를 만들어낸다는 것이다. 버그는 상관관계가 없으므로 다수결로 억제할 수 있다.

오래된 아이디어다. 그리고 비싸다. 같은 것을 N개 팀에게 만들게 하며 비용을 지불해야 한다.

LLM은 이를 시도할 만큼 저렴하게 만든다. N개 팀 대신 N번의 API call이면 된다. 문제는 같은 모델에 같은 프롬프트로 N번의 API call을 하면 N개의 거의 동일한 구현이 나온다는 점이다. 버그가 완벽하게 상관된다. 다수결은 무용지물이 된다.

해결책은 프롬프트를 개발자로 대우하는 것이다. 모델이 아니라. 다른 프롬프트가 다른 개발자를 만든다.

Temperature만으로는 표면적 다양성만 나온다

Temperature는 토큰 확률 분포를 조절한다. 높은 temperature에서는 모델이 덜 가능성 높은 다음 토큰을 선택한다. 이는 표현 방식, 변수 이름, 피상적 구조에서 변화를 만든다.

하지만 알고리즘적 접근 방식의 변화는 만들지 않는다. 가장 긴 회문 부분 문자열을 찾는 함수를 요청하면, temperature는 leftright를 쓸지 lr을 쓸지를 바꿀 뿐이다. expand-around-center를 쓸지 dynamic programming을 쓸지는 바꾸지 않는다.

N-version programming 관점에서는 이는 쓸모가 없다. 문제를 다르게 푸는 구현이 필요지, 같은 방식으로 풀되 겉모습만 다른 구현은 필요 없다.

알고리즘적 다양성을 강제하는 4가지 프롬프팅 전략

문제에 대해 모델이 생각하는 방식을 바꾸는 4가지 접근법이다.

문제 프레이밍을 달리하기

“파서를 작성하라”고 framing한 것과 “이 문법을 인식하는 state machine을 작성하라”고 framing한 것은 다른 코드를 낳는다. 하나는 recursive descent를 쓸 수도 있고, 다른 하나는 table-driven 접근을 쓸 수도 있다.

모델에게 특정 framing을 채택하도록 요청하면 이를 자동화할 수 있다:

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 machine framing은 명시적 state enum을 가진 문자 단위 파서를 일관되게 낳는다. recursive descent framing은 lexer와 별도 parser 함수를 낳는다. split-and-fix framing은 더 compact하지만 brittle한 해결책을 낳는다.

페르소나 교체하기

다른 페르소나는 다른 지식을 활성화한다. systems programmer가 작성하는 코드는 data scientist나 competitive programmer가 작성하는 코드와 다르다.

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.",
]

페르소나 프롬프팅은 구조적 다양성을 위해서 놀랍도록 효과적이다. systems programmer는 배열과 인덱스를 찾는다. Pythonic developer는 itertools와 comprehension을 찾는다. algorithms researcher는 라이브러리를 가져오거나 더 formal한 해결책을 작성할 수도 있다.

사용 가능한 도구를 제약하기

사용 가능한 도구를 제한하거나 확장하면 다른 접근 방식을 강제한다.

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.",
]

이는 특정 접근 방식에 blind spot이 있을 때 특히 유용하다. regex 기반 파서가 중첩된 인용부호를 계속 잘못 처리한다면, regex를 사용하지 않는 버전을 강제하라.

Divergent Reasoning이 결합된 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는 모델이 추론 과정을 표면에 드러내게 한다. “가장 다른” 제약은 기본 해결책에서 멀어지게 밀어낸다. 실제로, 이는 단일 기법 중 가장 높은 구조적 다양성을 만든다.

한계점: 다양성 상한선

같은 모델 내 다양성에는 한계가 있으며, 언젠가는 닿는다.

근본적인 지식 격차는 공유된다. 학습 데이터에 floating-point 비교에 대한 systematic misunderstanding이 들어 있다면, 모든 framing과 모든 페르소나가 그 misunderstanding을 재생산할 것이다. 모델은 하나의 가중치 세트를 가진다. 프롬프트로는 이를 우회할 수 없다.

또한 수익递减도 있다. 처음 세 가지 framing은 state machine, recursive parser, split 기반 접근을 줄 수 있다. 네 번째 framing은 변수 이름만 다른 state machine을 줄 수 있다. 3~5개의 진정으로 다른 접근 뒤에는 바닥을 긁게 된다.

어떤 기법은 품질을 저하시킨다. “가장 다른” 제약은 때로 틀려서 다른 해결책을 만들어낸다. 다름을 위한 다름은 쓸모 없다. 나쁜 아이디어를 거를 voting이나 testing 메커니즘이 필요하다.

오늘 바로 배포할 수 있는 실용적 설정

시스템에 이를 내장한다면 무작위화하지 마라. 다양성을 설계하라.

위 목록에서 3~5가지 기법을 고른다. 기법당 하나의 구현을 생성한다. 모든 구현에 대해 test suite나 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]

Test filtering 단계는 절대 협상 대상이 아니다. 정확성 없는 다양성은 그저 노이즈일 뿐이다.

FAQ

더 작은 모델에서도 작동하나요?

예, 하지만 다양성 상한선이 더 낮다. 더 작은 모델은 학습 데이터에 더 적은 수의 구별되는 해결 전략을 가지고 있다. 네 가지 대신 두 가지의 진정으로 다른 접근을 얻을 수 있다. 기법 자체는 여전히 작동한다; 단지 변화가 덜 생길 뿐이다.

실제로 몇 개의 구현이 필요한가요?

다수결을 위해서 3개가 실질적인 최소치다. 5개는 더 나은 커버리지를 주지만 비용은 선형적으로 증가한다. 5개 이후에는 같은 모델 내 다양성이 표면적 변화로 퇴화한다. 5개보다 더 필요하다면 cross-model diversity로 전환하라.

같은 모델 내 다양성이 cross-model diversity만큼 좋나요?

아니다. 다른 모델은 다른 학습 데이터, 아키텍처, fine-tuning을 가진다. 그들은 진정으로 다른 방식으로 실패한다. 같은 모델 내 다양성은 비용과 운영 편의성 사이의 trade-off다. 완벽한 fault tolerance가 아니라 빠르게 좋은 fault tolerance가 필요할 때 사용하라.

이 기법들을 조합할 수 있나요?

물론이다. 페르소나 프롬프트에 도구 제약과 chain-of-thought 단계를 결합하면 단일 기법보다 더 많은 다양성을 만든다. 비용은 더 긴 프롬프트와 generation당 더 많은 토큰이다. 중요한 코드 경로에서는 추가 토큰이 worth it하다.

먼저 시도할 것

Framing variation부터 시작하라. 구현하기 가장 쉽고 가장 일관된 구조적 다양성을 만든다. 더 필요하면 페르소나 교체를 추가하라. Cross-model diversity는 같은 모델 내 다양성이 상한선에 닿은 경우를 위해 남겨두라.

투표에 부치기 전에 모든 구현을 같은 test suite에 통과시켜라. 테스트되지 않은 다양한 구현은 아직 만나지 못한 버그투성이 구현일 뿐이다.