RLM 2027 · Capítulo 12 de 24 · 13 min
Montar o protótipo com dspy.RLM e auditar o rastro de cada resposta
Resposta tipada com evidências, limites de chamadas e o rastro de cada passo transformam um protótipo que acerta num protótipo que Helena consegue assinar.
Este capítulo faz parte do curso gratuito RLM 2027. Para marcar como concluído e salvar o progresso, abra este capítulo na página do curso.
Com a biblioteca oficial rodando sobre o acervo, faltavam a Caio duas coisas para o primeiro protótipo completo. A resposta precisava sair em campos separados, texto de um lado e lista de evidências do outro, e cada execução precisava deixar um rastro que Helena lesse sem ajuda do engenheiro.
Sem isso, o protótipo acerta e ninguém assina. Helena responde ao cliente e ao órgão ambiental; uma condicionante declarada vencida sem o laudo que prova o vencimento vira retrabalho, e uma declarada em dia por engano vira multa. O acerto que não se audita vale pouco num parecer técnico.
Ao terminar, você configura o dspy.RLM com os parâmetros atuais, entende por que o nome do limite de iterações mudou em agosto de 2026 e lê a trajetória de uma execução para achar onde o tempo, o dinheiro e o erro apareceram.
Por que o rastro vale tanto quanto a resposta
Os autores do artigo RLM listam fragilidades que só aparecem olhando o caminho. O modelo às vezes entrega o plano como se fosse a resposta final. O Qwen3-Coder, sem uma linha extra no prompt, fazia milhares de subchamadas numa tarefa básica. Modelos de raciocínio estouravam o limite de saída com tokens de pensamento. Nenhum desses defeitos aparece na nota final; todos aparecem na trajetória.
DSPy é uma biblioteca de Stanford para montar programas com modelos de linguagem a partir de assinaturas, declarações curtas de entrada e saída como “pergunta -> resposta”. O módulo dspy.RLM entrou na versão 3.1.1, de 19 de janeiro de 2026, e a documentação ainda o marca como experimental, com o aviso de que a API pode mudar. A evolução desde então foi rápida, e ela importa para quem copia código de tutorial.
O dspy.RLM de janeiro a setembro de 2026
- 19 de janeiro de 2026Concluído
DSPy 3.1.1
Entra o módulo RLM e um PythonInterpreter revisto
- 21 de abril de 2026Concluído
DSPy 3.2.0
RLM e intérprete endurecidos; prévia de variável passa a 1.000 caracteres
- 3 de agosto de 2026Concluído
DSPy 3.3.0
max_iterations vira max_iters; nomes e ferramentas validados via dspy.Tool
- 21 de agosto de 2026Concluído
DSPy 3.3.1
CodeAct e ProgramOfThought depreciados em favor do RLM; remoção prevista no 3.5
- 11 de setembro de 2026Agora
DSPy 3.4.0b1 (beta)
LocalInterpreter passa a servir ao RLM e ao novo dspy.Flex
A troca de agosto é a armadilha comum mais barata de evitar. Tutoriais escritos até julho de 2026 usam max_iterations, e o parâmetro atual é max_iters. A depreciação do CodeAct e do ProgramOfThought, no mesmo mês, sinaliza que o DSPy passou a tratar o RLM como o caminho padrão para programas que escrevem e executam código.
Parâmetros do construtor dspy.RLM na documentação atual
| Parâmetro | Padrão | O que controla |
|---|---|---|
| max_iters | 20 | Laços do REPL antes da extração de reserva da resposta |
| max_llm_calls | 50 | Teto de llm_query e llm_query_batched por execução |
| max_output_chars | 10_000 | Quanto da saída do REPL volta ao histórico do modelo raiz |
| sub_lm | dspy.settings.lm | Modelo das subchamadas; a documentação recomenda um mais barato |
| tools | nenhuma | Funções extras que o código do sandbox pode chamar |
| interpreter_factory | PythonInterpreter | Cria um intérprete novo por invocação (Deno com Pyodide) |
| verbose | False | Imprime os passos durante a execução |
Fonte: DSPy, documentação do módulo RLM (branch main, setembro de 2026)
O intérprete padrão roda Python compilado para WebAssembly, o Pyodide, dentro do Deno. WebAssembly: um formato que executa código num compartimento fechado, como uma calculadora que não enxerga o resto do computador. A alternativa LocalInterpreter vem com aviso explícito da documentação: não é um sandbox de segurança, e o código gerado herda arquivos, credenciais, rede e processos do usuário.
A fábrica de intérpretes exige cuidado em execução paralela. A documentação pede que ela devolva um intérprete novo a cada vez e que o PythonInterpreter fique na mesma linha de execução em que foi usado primeiro. Há uma versão assíncrona, chamada com await rlm.acall(...). Caio rodou as 120 perguntas em sequência no primeiro protótipo; pela mediana, isso leva perto de 7 horas e 20 minutos (120 vezes 3 minutos e 40 segundos), e a cauda acrescenta mais.
Dentro do sandbox, o modelo raiz encontra llm_query, llm_query_batched, print e SUBMIT, com as bibliotecas re, json, collections e math. SUBMIT encerra a execução com os campos da assinatura preenchidos. Cada implementação escolheu um nome diferente para esse gesto, e confundir os nomes leva a execuções que não reconhecem o próprio fim. Quem migra código entre as três implementações precisa trocar o gesto de encerramento junto com o import, porque o resto do código costuma funcionar sem aviso de erro.
O parâmetro max_output_chars reproduz a escolha central do artigo. Só 10 mil caracteres da saída de cada passo voltam ao histórico do modelo raiz. Se ele imprimir um laudo inteiro, verá só o começo; para ler o resto, precisa guardar o texto numa variável e passá-lo a uma subchamada. O limite força o trabalho por variáveis, que é o que mantém a janela do modelo raiz pequena mesmo com o acervo grande.
Como cada implementação recebe o contexto e encerra a execução
| Implementação | Onde fica o contexto | Subchamada | Como encerra |
|---|---|---|---|
| Artigo RLM (v3) | Variável context | llm_query | FINAL(resposta) ou FINAL_VAR(variável) |
| Pacote rlms 0.1.3 | Variável context | llm_query, llm_query_batched, rlm_query | answer["content"] e answer["ready"] = True |
| dspy.RLM | Um campo por entrada da assinatura | llm_query, llm_query_batched | SUBMIT(...) com os campos de saída |
| Prime Intellect (verifiers) | Dados extras só pelo REPL | llm_batch() | answer["content"] e answer["ready"] |
Fonte: Artigo RLM v3 (maio de 2026); código do pacote rlms 0.1.3; documentação do DSPy; Prime Intellect (janeiro de 2026)
Como a Veredas leu o rastro das 120 respostas do primeiro protótipo
Caio tomou uma decisão de desenho antes de escrever o protótipo. A fábrica de intérpretes cria um sandbox novo a cada pergunta, e copiar 208 milhões de caracteres para dentro dele 120 vezes gastaria tempo antes de qualquer raciocínio. Ele passou ao sandbox só o índice, uma linha de cabeçalho por documento, e uma ferramenta que devolve o texto de um documento pelo identificador. O modelo raiz filtra o índice com código e busca só o que vai delegar.
A assinatura carrega a outra metade do desenho. Em “indice, pergunta -> resposta, evidencias: list[str]”, a saída tem dois campos, e o segundo é uma lista de textos, como no exemplo da documentação que devolve uma contagem e uma lista de erros críticos. O SUBMIT só encerra a execução com os dois campos preenchidos, e a lista de evidências pode ser conferida por programa antes de chegar a Helena.
# Protótipo da Veredas com dspy.RLM (DSPy 3.3.0 ou superior; módulo experimental)
# pip install "dspy[deno]" -> intérprete Deno com Pyodide, sandbox WASM local
import json
import re
from pathlib import Path
import dspy
dspy.configure(lm=dspy.LM("openai/gpt-5")) # modelo raiz
barato = dspy.LM("openai/gpt-5-mini") # modelo das subchamadas
def ler_documento(doc_id: str) -> str:
"""Devolve o texto de um documento do acervo, com o cabeçalho."""
if not re.fullmatch(r"VRD-[0-9]{4}-[0-9]{4}", doc_id):
raise ValueError("id fora do padrão") # impede ler fora da pasta do acervo
return Path("acervo_texto", doc_id + ".txt").read_text(encoding="utf-8")
# 6.400 linhas curtas: [DOC id] empreendimento=... tipo=... data=... ocr=...
indice = Path("indice.txt").read_text(encoding="utf-8").splitlines()
auditor = dspy.RLM(
"indice, pergunta -> resposta, evidencias: list[str]",
max_iters=20, # nome atual; até o DSPy 3.2 era max_iterations
max_llm_calls=50, # teto de subchamadas por pergunta
max_output_chars=10_000,
sub_lm=barato,
tools=[ler_documento],
)
perguntas = json.loads(Path("perguntas_120.json").read_text(encoding="utf-8"))
with open("rastros.jsonl", "w", encoding="utf-8") as saida:
for item in perguntas:
r = auditor(indice=indice, pergunta=item["pergunta"])
saida.write(json.dumps({
"id": item["id"],
"resposta": r.resposta,
"evidencias": r.evidencias,
"passos": r.trajectory, # reasoning, code e output de cada passo
"raciocinio_final": r.final_reasoning,
}, ensure_ascii=False) + "\n")A ferramenta ler_documento roda fora do sandbox, e por isso Caio a fez recusar qualquer identificador fora do padrão. Sem essa linha, um texto malicioso escondido num documento poderia induzir o modelo a pedir um caminho como “../../credenciais”. A defesa completa contra instruções embutidas no acervo fica para a trilha de segurança; a validação do identificador custou uma linha e entrou já no protótipo.
O sub_lm aponta para o GPT-5-mini pela mesma razão do artigo, que usa GPT-5 na raiz e GPT-5-mini nas subchamadas: a documentação do DSPy recomenda um modelo mais barato ali. O modelo raiz decide o que ler e como juntar; as subchamadas leem trechos e extraem datas e números. Trocar o raiz por um modelo menor é outra decisão, com outro risco, e fica para a trilha de custo.
No cenário da Veredas, o protótipo acertou 94 das 120 perguntas: 37 de 40 em localização, 38 de 50 em agregação e 19 de 30 em cruzamento. O RAG fazia 64. O salto veio onde a trilha anterior previa: agregação foi de 21 para 38 e cruzamento de 7 para 19, enquanto localização ganhou uma pergunta. O custo médio ficou em US$ 0,86 por pergunta, a mediana de tempo em 3 minutos e 40 segundos, e a pior execução levou 19 minutos.
O primeiro protótipo da Veredas contra os limiares do protocolo
94 de 120
perguntas certas
37 de 40, 38 de 50 e 19 de 30; o RAG fazia 64
+29
acertos em agregação e cruzamento
De 28 para 57 somados sobre 80; o limiar pedia 15
US$ 0,86
custo médio por pergunta
Limiar de US$ 1,50; rodada completa perto de US$ 103
19 min
pior execução
Mediana de 3 min 40 s; limiar de 20 minutos
Fonte: Veredas Ambiental, cenário composto deste curso; não é resultado de benchmark
Os US$ 0,86 têm conta. Numa execução média, o modelo raiz fez 12 turnos de cerca de 15 mil tokens de entrada, 180 mil no total, a US$ 1,25 por milhão: US$ 0,225. Escreveu 24 mil tokens a US$ 10 por milhão: US$ 0,24. As 30 subchamadas leram 1,2 milhão de tokens a US$ 0,25 por milhão, US$ 0,30, e escreveram 45 mil a US$ 2, US$ 0,09. A soma dá US$ 0,855. Pouco mais da metade do custo estava no modelo raiz (US$ 0,465 de 0,855), e isso orienta a trilha de custo.
Todas as 94 respostas certas citavam documento, e o protocolo passou nos cinco limiares. Helena foi além e abriu a evidência de cada acerto. Em 88, o documento citado sustentava a resposta. Em 6, a resposta estava certa e a citação apontava para um documento vizinho, um laudo do mesmo poço de outro ano. Helena registrou esses 6 como acerto sem rastro, ressalva que entrou no relatório.
A primeira conferência foi automática. Um script de dez linhas cruzou cada identificador da lista de evidências com o índice, e nenhum identificador citado nas 94 respostas certas era inventado. Essa checagem barata elimina o erro mais grosseiro, a citação de documento que não existe, e deixa para Helena só a pergunta que exige leitura técnica: o documento citado prova o que a resposta diz?
Os 26 erros foram lidos um a um no rastro, e quatro causas explicaram todos. Em 11, o filtro por expressão regular perdeu documentos porque a mesma condicionante aparecia como “condicionante nº 14” e “cond. 14”. Em 8, uma subchamada extraiu a data errada de uma tabela mal convertida. Em 6, o modelo chamou SUBMIT com o plano no lugar da resposta, a fragilidade que o artigo descreve. Em 1, o teto de chamadas foi atingido.
As 26 perguntas erradas, pela causa encontrada no rastro
| Causa | Perguntas | Sinal no rastro | Correção planejada |
|---|---|---|---|
| Filtro por regex incompleto | 11 | Poucos documentos após o filtro; variantes de grafia ausentes | Normalizar grafias de condicionante no cabeçalho |
| Extração errada na subchamada | 8 | Saída da subchamada com data que não existe no documento | Converter tabelas de laboratório antes da leitura |
| SUBMIT antecipado com o plano | 6 | SUBMIT nos dois primeiros passos; resposta em forma de lista de etapas | Exigir impressão da resposta candidata antes do SUBMIT |
| Teto de 50 chamadas atingido | 1 | Laço com llm_query um documento por vez; 20 passos | Trocar por llm_query_batched e rever o limite |
Fonte: Veredas Ambiental, cenário composto deste curso
A pior execução, de 19 minutos, era a do teto de chamadas. No sexto passo, o modelo raiz escreveu um laço que chamava llm_query para um documento de cada vez, em sequência. Chegou às 50 chamadas no décimo quarto passo, gastou os seguintes tentando contornar o limite e terminou pela extração de reserva, com resposta errada. O artigo aponta a mesma origem para a cauda de latência: subchamadas sequenciais.
O rastro também mostrou o que funcionou, e isso pesa tanto quanto os erros. Em 71 das 94 respostas certas, o modelo raiz filtrou o índice por código antes da primeira subchamada, delegou a leitura com llm_query_batched e fez as contas em Python. Esse caminho é o padrão que o blog de Alex Zhang descreve, e virou a referência contra a qual Helena compara qualquer trajetória nova: quando um rastro foge dele, alguém abre o arquivo.
Para achar esses casos sem ler 120 trajetórias inteiras, Caio escreveu uma triagem curta. Ela marca as perguntas com muitos passos, com SUBMIT logo no começo, sem evidência ou com llm_query dentro de laço. Helena lê primeiro as marcadas.
# Triagem do rastro: quais perguntas Helena deve ler primeiro
import json
for linha in open("rastros.jsonl", encoding="utf-8"):
reg = json.loads(linha)
passos = reg["passos"] # lista de dicionários com reasoning, code e output
sinais = []
if len(passos) >= 18:
sinais.append(f"{len(passos)} passos, perto do teto de 20")
if any("SUBMIT(" in p["code"] for p in passos[:2]):
sinais.append("SUBMIT nos dois primeiros passos")
if any("for " in p["code"] and "llm_query(" in p["code"] for p in passos):
sinais.append("llm_query dentro de laço: chamadas em sequência")
if not reg["evidencias"]:
sinais.append("sem evidência citada")
if sinais:
print(reg["id"], "|", "; ".join(sinais))A biblioteca oficial oferece o mesmo tipo de leitura por outro formato. O RLMLogger grava um arquivo JSONL por execução: a primeira linha traz os metadados da configuração, e cada linha seguinte é uma iteração, com a resposta do modelo, os blocos de código, a saída e os erros de cada bloco, as subchamadas feitas e o tempo. O repositório traz um visualizador em Node.js que abre esses arquivos no navegador, por padrão na porta 3001 da própria máquina (localhost:3001).
Caio manteve as duas ferramentas no laboratório por razões diferentes. O dspy.RLM deu a assinatura tipada, o teto de chamadas por pergunta e o sandbox WebAssembly local. O pacote rlms deu os ambientes em contêiner e em nuvem, o registro em JSONL e o visualizador. O protótipo das 120 perguntas rodou no DSPy; a comparação de ambientes para produção vai usar o rlms, e o formato do registro dele virou o modelo do arquivo de rastros da Veredas.
- Etapa 1 de 6: Triagem
O script marca passos demais, SUBMIT cedo, laço sequencial e falta de evidência
- Etapa 2 de 6: Primeiro passo
O modelo espiou o índice antes de filtrar?
- Etapa 3 de 6: Filtro
Quantos documentos sobraram e com quais termos
- Etapa 4 de 6: Subchamadas
Agrupadas ou em sequência; saída coerente com o documento
- Etapa 5 de 6: Agregação
Contas feitas em Python, não pedidas a um modelo
- Etapa 6 de 6: Entrega
Evidência citada abre o documento que sustenta a resposta
O protótipo passou nos cinco limiares, com uma ressalva registrada sobre as 6 citações vizinhas e uma lista de quatro correções com dono. Some agora, no rastro de cada pergunta, as subchamadas e os tokens do modelo raiz, e separe as 20 perguntas mais caras. Se elas responderem por mais de um terço do custo total da rodada, a cauda é o primeiro alvo da trilha de custo.
Seu caderno neste capítulo
Abrir o caderno completoSelecione um trecho do capítulo para destacar ou anotar. No teclado, selecione com Shift e as setas e use Alt+Shift+D para destacar ou Alt+Shift+N para anotar.
Salvo neste navegador. Entre na sua conta para levar o caderno a outros aparelhos.
Entre na sua conta para compartilhar o que aprendeu e convidar alguém para estudar com você.
Voltar ao capítulo anterior: Preparar o acervo e rodar a biblioteca oficial rlm
Todos os capítulos de RLM 2027
- 01Descobrir onde o modelo encontra cada informação que usa
- 02Medir o limite real do contexto e dividir o problema
- 03Guardar o acervo numa variável e deixar o modelo programar a leitura
- 04Desenhar a árvore de chamadas e decidir a profundidade
- 05Explorar o acervo antes de decidir onde cortar
- 06Processar as partes e juntar as respostas sem perder a evidência
- 07Comparar contexto longo, RAG e compactação antes de escolher o RLM
- 08Situar agentes, híbridos e RLM numa matriz de decisão
- 09Julgar um benchmark antes de acreditar no número dele
- 10Comparar métodos nas mesmas condições e ler os resultados contrários
- 11Preparar o acervo e rodar a biblioteca oficial rlm
- 12Montar o protótipo com dspy.RLM e auditar o rastro de cada resposta
- 13Decompor o custo de uma execução e pôr teto em cada parte
- 14Baixar o custo por pergunta e vigiar o que a média esconde
- 15Isolar o código gerado e tratar cada documento como dado
- 16Impedir que o erro de uma folha chegue à resposta final
- 17Treinar um modelo pequeno para decidir quando delegar
- 18Provar uma auto-melhoria antes de acreditar nela
- 19Julgar as apostas de recursão aprendida e execução previsível
- 20Mapear híbridos, imagem e trabalho prolongado com evidência e lacuna
- 21Ler 2027 por sinais observáveis em vez de apostar numa previsão
- 22Escolher onde o RLM entra primeiro e decidir o piloto
- 23Definir o problema e construir quatro alternativas comparáveis
- 24Avaliar as alternativas e dizer em quais condições o RLM compensa