Transformando Engenharia de Contexto de LLM em um Loop de Avaliação com DSPy
Notas de dois fins de semana cavando o DSPy. Parei de tratar prompts como a fonte da verdade e comecei a tratá-los como saída compilada de uma assinatura tipada, uma métrica e um otimizador. Aqui está o menor programa end-to-end que mantive, como o MIPROv2 de fato busca, e onde a abordagem cai por terra na prática.
Toda app LLM que construí no último ano eventualmente bate na mesma parede. Um prompt que funcionava numa terça silenciosamente regride numa sexta quando o vendor lança um novo snapshot de modelo. Um pipeline RAG que detona o caminho feliz desmorona em perguntas multi-hop. Um agente perde o rumo no meio de uma cadeia de tools. Eu costumava debugar isso lendo traces e editando o prompt à mão. Parecia escrever CSS sem um browser para recarregar.
DSPy empurra esse trabalho para o código. Passei dois fins de semana com ele — lendo o código-fonte, construindo um classificador de tickets descartável, rodando MIPROv2 contra um punhado de exemplos de hold-out — e o takeaway das minhas notas é simples. O trabalho não é mais "escrever um prompt melhor". O trabalho é declarar o que o modelo deve fazer, definir como medir sucesso, e deixar um otimizador compilar o prompt e as demonstrações para mim.
Por que strings de prompt param de escalar
Um prompt é três coisas emaranhadas: uma interface (o que o modelo aceita e retorna), uma instrução (como ele deve se comportar), e um conjunto de exemplos (como se parece o bom). Quando mudo de provider ou ajusto um schema downstream, os três mexem ao mesmo tempo, e eu não tenho jeito de dizer qual parte regrediu. Não há versão, não há dataset, não há métrica. O prompt também é não portável: trocar de gpt-4o-mini para um Qwen local quase sempre exige uma reescrita, porque a string antiga estava overfitada às manias do modelo antigo.
DSPy desemaranha as três. Uma signature é a interface. Um módulo é o comportamento. Examples são os dados. Uma métrica pontua a saída. Um optimizer busca no espaço de instruções e demonstrações contra essa métrica. Esse é o modelo mental inteiro, e uma vez que o escrevi, a maior parte do meu trabalho passado de prompts começou a parecer um gigantesco otimizador ad-hoc que eu vinha rodando dentro da minha cabeça.
O programa mínimo
Aqui está o menor programa DSPy end-to-end que mantive nas minhas notas. Ele classifica tickets de suporte em três baldes, mede acurácia num dev set minúsculo, e roda uma passada de MIPROv2 para melhorar o prompt e escolher demos de few-shot automaticamente.
# classify.py — a minimal DSPy program with a metric and an optimizer pass.
import dspy
# 1. Configure the LM. Any LiteLLM-supported provider works.
dspy.configure(lm=dspy.LM("openai/gpt-4o-mini"))
# 2. Declare a signature. This replaces the prompt string.
class ClassifyTicket(dspy.Signature):
"""Classify a support ticket as 'billing', 'technical', or 'other'."""
ticket: str = dspy.InputField()
category: str = dspy.OutputField()
# 3. Wire a module. Predict is simplest; ChainOfThought adds reasoning.
classify = dspy.ChainOfThought(ClassifyTicket)
# 4. A labeled dataset — the backbone for eval and optimization.
examples = [
dspy.Example(ticket="My invoice is wrong.", category="billing").with_inputs("ticket"),
dspy.Example(ticket="Why was I charged twice?", category="billing").with_inputs("ticket"),
dspy.Example(ticket="The app crashes on login.", category="technical").with_inputs("ticket"),
dspy.Example(ticket="504 error when I upload a file.", category="technical").with_inputs("ticket"),
dspy.Example(ticket="Do you have a Slack channel?", category="other").with_inputs("ticket"),
dspy.Example(ticket="Can I change my account timezone?", category="other").with_inputs("ticket"),
]
# 5. A metric: exact match on the category field.
def accuracy(example, prediction, trace=None):
return example.category.strip().lower() == prediction.category.strip().lower()
# 6. Baseline eval, before any optimization.
evaluate = dspy.Evaluate(devset=examples, metric=accuracy, display_progress=True)
print("baseline:", evaluate(classify))
# 7. Optimize: MIPROv2 searches instructions and few-shot demos.
optimizer = dspy.MIPROv2(metric=accuracy, auto="light")
optimized = optimizer.compile(classify, trainset=examples)
print("optimized:", evaluate(optimized))
# Save the compiled program so it can be reloaded without re-compiling.
optimized.save("optimized_classifier.json")Rode com:
pip install dspy
OPENAI_API_KEY=sk-... python classify.pyAlgumas linhas merecem atenção. dspy.configure(lm=...) seta o modelo global via LiteLLM, então o mesmo programa roda contra OpenAI, Anthropic, um endpoint local Ollama, ou um modelo SageMaker mudando uma string. A Signature é tipada — ticket: str e category: str — e DSPy compila esses tipos no prompt que de fato envia. A métrica é uma função Python pura; qualquer coisa que eu consiga pontuar, o otimizador consegue melhorar. auto="light" pede ao MIPROv2 para escolher defaults razoáveis de contagem de trials e de candidatos com base no tamanho do trainset, então não estou ajustando o otimizador à mão por cima de ajustar o prompt à mão.
Como MIPROv2 de fato busca
O nome não é decorativo. MIPROv2 é uma busca Bayesiana sobre duas coisas ao mesmo tempo — instruções e demonstrações few-shot — em três fases. O esboço abaixo é o que quero que o diagrama torne visível antes de eu percorrê-lo.

A fase um inicializa demonstrações candidatas. O DSPy amostra do trainset, roda o programa atual, e mantém apenas os traces cujas saídas passam na métrica. Defaults são quatro demos bootstrapadas mais quatro rotuladas, a partir da página da API do MIPROv2.
A fase dois propõe instruções candidatas. Um prompt_model (o mesmo LM por default) lê o código do programa, um resumo curto do dataset, e as demos bootstrapadas, e rascunha várias instruções que valem a pena tentar. É aqui que o DSPy explora informação que o hand-prompting joga fora — o formato real dos dados, e o formato do programa em torno do prompt.
A fase três combina instruções e demos e as avalia em um minibatch do trainset (tamanho default 35). A otimização Bayesiana escolhe quais combinações tentar em seguida. No final, MIPROv2 retorna o programa com melhor pontuação — um módulo com um prompt compilado diferente anexado.
O resultado interessante do paper do MIPROv2 não é que busca Bayesiana seja mágica. É que eu paro de precisar escolher qual parte do prompt editar. Mude o dataset, re-rode compile, ganhe um prompt que combina com os novos dados.
Os trade-offs que enfrentei
O tempo de compile é real. Meu classificador de seis exemplos termina auto="light" em cerca de um minuto no gpt-4o-mini. Escale para 200 exemplos e um programa mais pesado, e a estimativa aproximada que continuo vendo citada — cinco minutos a uma hora — bate com o que medi localmente. Cada trial é mais chamadas de LM; não existe almoço grátis.
O custo se move na mesma direção. Eu pago pelos bootstraps, pelas propostas de instrução e pelas avaliações de minibatch. Em troca, posso cair para um modelo mais barato em tempo de serving. O write-up do Dropbox sobre o juiz de relevância do Dash é o exemplo público mais limpo: eles compilaram qualidade em um modelo menor em vez de pagar por um maior para sempre. Não consigo reproduzir os números deles, mas o formato da vitória bate com o que vi no meu classificador de brinquedo.
A abstração é desconhecida. Na primeira vez que vi Predict("question -> answer: float") li como teatro de DSL. Dois dias depois, percebi que a string é uma signature, a signature é um tipo, e o tipo é o que permite o otimizador raciocinar sobre o que mudou entre dois candidatos. A fricção é adiantada; o retorno é que um prompt para de ser um beco sem saída de debug.
A avaliação ainda é a parte difícil. DSPy não vai me salvar de uma métrica ruim. Uma métrica que recompensa a coisa errada será overfitada com entusiasmo. Isso não é bug do DSPy — é a lição clássica de ML que a engenharia de contexto herda. A disciplina que o framework força é: escreva a métrica primeiro, discuta sobre ela, depois otimize.
Quando uso, e quando pulo
Uso DSPy quando o programa tem mais de uma chamada de LM, ou quando preciso do mesmo comportamento em dois ou mais modelos, ou quando tenho pelo menos uma dúzia de exemplos rotulados e uma métrica na qual confio. RAG multi-hop, classificação com casos de borda frágeis, uso de tools agêntico — são onde o otimizador paga aluguel.
Pulo para prompts one-shot que toco uma vez por mês. O boilerplate não vale a pena. Também pulo quando não consigo definir uma métrica que importa. Sem métrica, sem otimização; nesse ponto estou de volta à engenharia de prompt, e tudo bem.
- Trate um prompt como a saída compilada de um programa, não como fonte da verdade.
- Comece declarando a signature e escrevendo a métrica. O otimizador vem por último.
- Use
auto="light"antes de ajustar parâmetros de otimizador à mão. Defaults geralmente estão bons. - Reserve tempo real e tokens reais para o compile. Cacheie o programa compilado em disco com
.save()e recarregue com.load(). - Portabilidade entre modelos é uma propriedade da disciplina, não da ferramenta. DSPy só torna barato exercitá-la.
Use DSPy quando a tarefa é mensurável, o programa é multi-step, ou trocas de modelo estão no roadmap. Evite para prompts pontuais, tarefas sem uma métrica utilizável, ou workflows onde mesmo cinco minutos de tempo de compile são inviáveis.
Curtindo? Talvez goste disso aqui.
Nada parecido — quer tentar outro ângulo?
Posts Relacionados
JetBrains Tracy: Observabilidade Pragmática de IA para Kotlin
JetBrains Tracy é uma biblioteca Kotlin que conecta tracing ciente de LLM na sua aplicação em cima do OpenTelemetry. Este post percorre como eu integrei no serviço Spring Boot, as decisões de design que importam, e os modos de falha que times encontram quando chamadas de LLM se tornam o caminho mais quente do sistema.
A Espinha Dorsal Determinística: Por Que Sistemas de IA em Produção Estão Se Afastando de Agentes Totalmente Autônomos
Agentes totalmente autônomos são difíceis de limitar, difíceis de testar e caros de operar. Uma espinha dorsal determinística com etapas de agente estreitas devolve o controle de fluxo a você enquanto mantém a inteligência onde ela importa. Veja como projetar, testar e migrar nessa direção.
Avaliação de Memória: Medindo Como a Memória de IA se Degrada ao Longo da Vida de um Projeto
A maioria dos benchmarks de memória de IA avalia recall e para por aí. Isso esconde o modo de falha real: fatos desatualizados envenenando silenciosamente a janela de contexto. Aqui está um framework de avaliação baseado em ciclo de vida que testa recall, revisão e esquecimento controlado em todos os pontos de mudança pelos quais um projeto de longa duração passa.