Ejecutar el mismo prompt cinco veces produce cinco copias del mismo error
La programación N-version asume que la diversidad proviene de diferentes autores. Con los LLMs, eso significa diferentes modelos, diferentes proveedores, tal vez diferentes entrenamientos. Pero esa suposición es errónea. Puedes obtener diversidad significativa del mismo modelo exacto cambiando cómo preguntas, no qué preguntas.
El problema: subir la temperature a 1.0 y ejecutar tu prompt cinco veces no es una estrategia. Obtendrás variación superficial. Los nombres de variables cambian. Los comentarios se reordenan. La estructura se mantiene idéntica, y los bugs también.
Si quieres implementaciones que fallen de forma independiente, necesitas solicitar diferentes patrones de pensamiento, no diferentes outputs.
Qué es lo que la programación N-version realmente necesita de los LLMs
La programación N-version es una técnica de tolerancia a fallos donde múltiples implementaciones independientes de la misma especificación se ejecutan en paralelo. Los outputs se comparan, y un majority vote determina el resultado correcto. La idea es que diferentes desarrolladores, trabajando de forma independiente, introducirán diferentes bugs. Los bugs no estarán correlacionados, así que un majority vote los suprime.
Es una idea antigua. También es cara. Estás pagando a N equipos para construir lo mismo.
Los LLMs lo hacen lo suficientemente barato como para intentarlo. En lugar de N equipos, tienes N API calls. El problema es que N API calls al mismo modelo con el mismo prompt producen N implementaciones casi idénticas. Los bugs se correlacionan perfectamente. Tu majority vote es inútil.
La solución es tratar el prompt como el desarrollador, no el modelo. Diferentes prompts producen diferentes desarrolladores.
Por qué la temperature por sí sola produce diversidad cosmética
La temperature controla la distribución de probabilidad sobre los tokens. A alta temperature, el modelo elige tokens siguientes menos probables. Esto crea variación en la formulación, el nombrado de variables y la estructura superficial.
No crea variación en el enfoque algorítmico. Si pides una función para encontrar la subcadena palindrómica más larga, la temperature cambia si usas left y right o l y r. No cambia si recurre a expand-around-center o a dynamic programming.
Para la programación N-version, eso es inútil. Necesitas implementaciones que resuelvan el problema de forma diferente, no implementaciones que se vean diferentes mientras lo resuelven de la misma manera.
Cuatro estrategias de prompting que fuerzan la diversidad algorítmica
Aquí hay cuatro enfoques que cambian cómo el modelo piensa sobre el problema.
Varía el encuadre del problema
La misma tarea encuadrada como “escribe un parser” versus “escribe una state machine que reconozca esta gramática” producirá código diferente. Una podría usar recursive descent. La otra podría usar un enfoque table-driven.
Puedes automatizar esto pidiendo al modelo que adopte un encuadre específico antes de resolver:
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))
Al ejecutar esto con GPT-4o, el encuadre de state machine produce consistentemente un parser carácter por carácter con un enum de estado explícito. El encuadre de recursive descent produce un lexer y funciones de parser separadas. El encuadre de split-and-fix produce una solución más compacta pero frágil.
Cambia de persona
Diferentes personas activan diferentes conocimientos. Un systems programmer escribe código diferente a un data scientist o un 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.",
]
El prompting de persona es sorprendentemente efectivo para la diversidad estructural. El systems programmer recurre a arrays e indexes. El Pythonic developer recurre a itertools y comprehensions. El algorithms researcher podría incorporar una librería o escribir una solución más formal.
Restringe las herramientas disponibles
Restringir o expandir el toolkit disponible fuerza diferentes enfoques.
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.",
]
Esto es particularmente útil cuando sabes que un enfoque tiene un punto ciego. Si tus parsers basados en regex siguen manejando mal las comillas anidadas, fuerza una versión que no use regex.
Chain-of-thought con razonamiento divergente
En lugar de pedir código directamente, pide al modelo que genere múltiples estrategias de solución y elija la menos obvia.
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,
)
El chain-of-thought fuerza al modelo a exponer su razonamiento. La restricción de “la más diferente” lo empuja lejos de la solución por defecto. En la práctica, esto produce la mayor diversidad estructural de cualquier técnica individual.
Dónde esto falla: el techo de diversidad
La diversidad de mismo modelo tiene límites, y los alcanzarás.
Las lagunas de conocimiento fundamentales son compartidas. Si los datos de entrenamiento contienen un malentendido sistemático sobre comparación de punto flotante, cada encuadre y cada persona reproducirán ese malentendido. El modelo tiene un conjunto de pesos. No puedes evitar eso con prompts.
También hay rendimientos decrecientes. Los primeros tres encuadres podrían darte una state machine, un parser recursivo y un enfoque basado en split. El cuarto encuadre podría darte una state machine con diferentes nombres de variables. Después de tres a cinco enfoques genuinamente diferentes, estás raspando el fondo del barril.
Algunas técnicas degradan la calidad. La restricción de “la más diferente” ocasionalmente produce soluciones que son diferentes porque están mal. La divergencia por el mero hecho de divergir no es útil. Necesitas un mecanismo de voting o testing para filtrar las malas ideas.
Una configuración práctica que puedes desplegar hoy
Si estás construyendo esto en un sistema, no aleatorices. Diseña tu diversidad.
Elige de tres a cinco técnicas de la lista anterior. Genera una implementación por técnica. Ejecuta tu test suite o property-based tests contra todas ellas. Quédate con las que pasen. Usa un simple majority vote para el output final.
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]
El paso de filtrado por tests no es negociable. La diversidad sin corrección es solo ruido.
Preguntas frecuentes
¿Funciona esto con modelos más pequeños?
Sí, pero el techo de diversidad es más bajo. Los modelos más pequeños tienen menos estrategias de solución distintas en sus datos de entrenamiento. Podrías obtener dos enfoques genuinamente diferentes en lugar de cuatro. Las técnicas siguen funcionando; simplemente producen menos variación.
¿Cuántas implementaciones realmente necesito?
Tres es el mínimo práctico para majority voting. Cinco te da mejor cobertura pero con un coste que aumenta linealmente. Después de cinco, la diversidad de mismo modelo se degrada en variación cosmética. Si necesitas más de cinco, cambia a diversidad cross-model.
¿Es la diversidad de mismo modelo tan buena como la diversidad cross-model?
No. Los modelos diferentes tienen diferentes datos de entrenamiento, arquitecturas y fine-tuning. Fallan de formas genuinamente diferentes. La diversidad de mismo modelo es un compromiso entre coste y conveniencia operativa. Úsala cuando necesites buena tolerancia a fallos rápido, no cuando necesites tolerancia a fallos perfecta.
¿Puedo combinar estas técnicas?
Absolutamente. Un prompt de persona combinado con una restricción de herramientas y un paso de chain-of-thought producirá más diversidad que cualquier técnica individual por sí sola. El coste es un prompt más largo y más tokens por generación. Para rutas de código críticas, los tokens extra valen la pena.
Qué probar primero
Empieza con la variación de encuadre. Es la más fácil de implementar y produce la diversidad estructural más consistente. Añade el cambio de persona si necesitas más. Guarda la diversidad cross-model para los casos donde la diversidad de mismo modelo alcanza su techo.
Ejecuta tus implementaciones a través del mismo test suite antes de dejar que voten. Una implementación diversa no testeada es solo una implementación con bugs que aún no has conocido.