A Resposta Errada Parece Exatamente a Certa
Você envia um prompt para o GPT-4o. Ele retorna um blob JSON com uma confiança de 0.97. Você envia o mesmo prompt para o Claude 3.5 Sonnet. Ele retorna um blob JSON diferente, também com uma confiança de 0.97. Ambos os modelos soam certos. Ambos estão errados de formas diferentes.
Isso não é hipotético. Se você executar qualquer carga de trabalho não trivial de LLM por mais de alguns milhares de requisições, você encontrará casos em que dois modelos capazes discordam sobre fatos, classificações ou saída estruturada. A própria divergência é um sinal. A maioria das equipes ignora.
Programação de N versões, a ideia antiga de executar múltiplas implementações e votar no resultado, era considerada cara demais para a maioria dos softwares. Com LLMs, as “implementações” são apenas chamadas de API diferentes. O custo é real, mas o custo de enviar a resposta errada frequentemente é pior.
Como a Programação de N Versões Se Parece para LLMs
A versão clássica vem de sistemas críticos de segurança. Três programas escritos independentemente processam a mesma entrada. Um votador compara as saídas. Se dois concordam e um não, a maioria vence e a minoria é sinalizada para inspeção.
Com LLMs, você não precisa de três equipes de engenharia. Você precisa de três chaves de API. A independência é mais fraca, os modelos compartilham dados de treinamento, mas a divergência ainda é mensurável e útil. Dois modelos treinados em corpora sobrepostos ainda podem alucinar fatos diferentes, interpretar ambiguidade de forma diferente ou falhar em casos de borda opostos.
A questão não é se eles discordam. Eles vão discordar. A questão é o que você faz quando discordam.
Um Detector Simples de Divergência que Realmente Funciona
Aqui está um sistema concreto em Python. Ele envia o mesmo prompt para múltiplos modelos, extrai saída estruturada e os compara usando regras conscientes do domínio em vez de igualdade ingênua de strings.
import asyncio
from dataclasses import dataclass
from typing import Callable, List, Any
import json
@dataclass(frozen=True)
class VariantResult:
model: str
output: dict
raw: str
latency_ms: float
class DisagreementResolver:
def __init__(
self,
comparators: List[Callable[[Any, Any], bool]],
tiebreaker: Callable[[List[VariantResult]], VariantResult],
):
# Each comparator returns True if two outputs are "the same"
self.comparators = comparators
# Tiebreaker picks a winner when no clear majority exists
self.tiebreaker = tiebreaker
def cluster(self, results: List[VariantResult]) -> List[List[VariantResult]]:
"""Group results into equivalence classes."""
clusters: List[List[VariantResult]] = []
for r in results:
placed = False
for cluster in clusters:
# A result joins a cluster if it matches the cluster's first member
if all(c(r.output, cluster[0].output) for c in self.comparators):
cluster.append(r)
placed = True
break
if not placed:
clusters.append([r])
return clusters
def resolve(self, results: List[VariantResult]) -> tuple[VariantResult, List[VariantResult]]:
"""Returns (winner, dissenters)."""
clusters = self.cluster(results)
clusters.sort(key=len, reverse=True)
if len(clusters) == 1:
# Unanimous agreement. Pick the fastest as winner.
winner = min(clusters[0], key=lambda r: r.latency_ms)
return winner, []
if len(clusters[0]) > len(clusters[1]):
# Clear majority.
winner = min(clusters[0], key=lambda r: r.latency_ms)
dissenters = [r for c in clusters[1:] for r in c]
return winner, dissenters
# No clear majority. Invoke tiebreaker.
winner = self.tiebreaker([c[0] for c in clusters])
dissenters = [r for c in clusters for r in c if r.model != winner.model]
return winner, dissenters
A escolha chave de design é o comparador. Igualdade de string é inútil para saída de LLM. Dois modelos podem retornar {"category": "refund"} e {"category": "Refund"} e discordar de nada que importe.
def category_comparator(a: dict, b: dict) -> bool:
return a.get("category", "").lower() == b.get("category", "").lower()
def amount_comparator(a: dict, b: dict) -> bool:
# Floats from different models can differ slightly
return abs(a.get("amount", 0) - b.get("amount", 0)) < 0.01
def strict_json_comparator(a: dict, b: dict) -> bool:
return json.dumps(a, sort_keys=True) == json.dumps(b, sort_keys=True)
# Use them in order of domain importance
resolver = DisagreementResolver(
comparators=[category_comparator, amount_comparator],
tiebreaker=lambda candidates: min(candidates, key=lambda r: r.latency_ms),
)
Onde Isso Realmente Captura Bugs
Detecção de divergência não é sobre encontrar erros óbvios. Erros óbvios já são capturados por meios mais simples.
É sobre capturar erros sutis que parecem corretos para qualquer modelo individual. Aqui estão os três padrões que vemos com mais frequência:
Divergência de alucinação confiante. Um modelo inventa um SKU de produto. Outro inventa um SKU diferente. Ambas as saídas validam contra o schema. A divergência é o único sinal de que algo foi fabricado.
Divisões de condição de limite. Um modelo classifica uma transaction de $999.99 como “high value.” Outro a classifica como “standard.” O limiar é ambíguo no prompt. A divergência diz que o prompt está subespecificado.
Deriva temporal. Um modelo retorna uma data em ISO 8601. Outro retorna a mesma data em um fuso horário diferente porque o prompt disse “today” e os modelos têm suposições diferentes de relógio no tempo de inferência.
Em cada caso, nenhuma saída está obviamente quebrada. A divergência é o relatório de bug.
O Modelo de Custo é Surpreendentemente Tolerável
Três chamadas de API custam três vezes mais que uma. Essa é a matemática superficial. A matemática real depende do que você faz com os resultados.
Se você executar três modelos em cada requisição e pegar a saída da maioria, você está queimando dinheiro. A maioria das equipes não deveria fazer isso.
O padrão mais inteligente é em camadas:
- Caminho rápido: Um modelo barato lida com a requisição.
- Caminho de auditoria: Uma amostra de requisições, ou todas as requisições acima de um limiar de confiança, são enviadas para um segundo modelo de forma assíncrona.
- Caminho de escalonamento: Quando o caminho de auditoria detecta divergência, um terceiro modelo desempata. A divergência é registrada para revisão de prompt engineering.
Isso significa que você paga o triplo apenas para o subconjunto de requisições que realmente precisam de escrutínio. Para muitas cargas de trabalho, isso é menos de 5% do tráfego.
async def tiered_resolve(prompt: str, primary_model: str, audit_model: str) -> dict:
primary = await call_model(primary_model, prompt)
# Async audit: do not block the fast path
audit_task = asyncio.create_task(call_model(audit_model, prompt))
try:
audit = await asyncio.wait_for(audit_task, timeout=2.0)
except asyncio.TimeoutError:
# Audit lagged. Ship the primary result.
return primary.output
if outputs_agree(primary.output, audit.output):
return primary.output
# Disagreement detected. Escalate.
tiebreaker = await call_model("gpt-4o", prompt)
log_disagreement(primary, audit, tiebreaker)
return tiebreaker.output
A chamada log_disagreement é onde reside o valor de longo prazo. Cada divergência é um ponto de dados dizendo onde seu prompt é ambíguo, onde seu schema está subconstrito ou onde seus exemplos de treinamento entram em conflito.
O Que Isso Não Corrige
Programação de N versões com LLMs tem limites reais, e fingir o contrário a torna perigosa.
Modos de falha compartilhados. Se ambos os modelos falham porque o prompt contém um erro de digitação, ambos falharão da mesma forma. Os modelos não são independentes da forma como três programas escritos à mão são. Eles compartilham arquitetura, objetivos de treinamento e grandes pedaços da internet.
Viés sistemático. Se seu prompt codifica implicitamente um viés, múltiplos modelos podem reproduzi-lo consistentemente. Detecção de divergência apenas captura divergência, não erros compartilhados.
Custo em escala. Mesmo uma taxa de auditoria de 5% se torna cara se seu volume é de bilhões de requisições por mês. A economia funciona para decisões de alto risco, não para cada autocompletar.
Latência no caminho de escalonamento. Quando dois modelos discordam e você precisa de um terceiro, você adicionou duas idas e voltas ao caminho crítico. Para casos de uso em tempo real, você pode precisar enviar o resultado primário e resolver a divergência de forma assíncrona, aceitando uma breve janela de potencial erro.
Como Começar Sem Superengenharia
Você não precisa de um framework de votação completo no primeiro dia. Comece com comparação par a par no seu prompt de maior risco.
Escolha uma chamada de saída estruturada onde uma resposta errada realmente custa dinheiro ou confiança. Adicione uma segunda chamada de modelo em um job em segundo plano. Compare as saídas com um comparador específico do domínio. Registre divergências. Revise os logs semanalmente.
Depois de duas semanas, você saberá se seu prompt é sólido ou um desastre. A maioria dos prompts é mais frágil do que seus autores pensam. O log de divergências é uma verificação de honestidade.
Uma vez que você tenha dados, pode decidir se constrói um resolvedor, ajusta seu prompt ou aceita que a tarefa é ambígua demais para manipulação totalmente automatizada.
A Verdadeira Vitória É o Ciclo de Feedback
O objetivo da programação de N versões com LLM não é construir um votador perfeito. O objetivo é fechar o ciclo entre divergência em produção e melhoria de prompt.
Toda vez que dois modelos discordam, você encontrou um lugar onde sua especificação está incompleta. Corrija a especificação, não apenas a saída. Com o tempo, a taxa de divergência cai. Quando cai o suficiente, você pode reduzir a amostra de auditoria ou rebaixar o modelo de desempate.
O sistema fica mais barato à medida que melhora. Isso é o oposto da maioria dos mecanismos de segurança, que ficam mais caros à medida que a escala aumenta.
Comece com um prompt, dois modelos e uma linha de log. As divergências dirão exatamente onde olhar.