Framework ou feito à mão?
LangGraph · StateGraph · Teste de paridade · 173 testes
A pergunta de negócio
Um time vai construir um sistema agêntico. Alguém pergunta se adota um framework de agentes ou escreve a orquestração à mão. A resposta honesta é depende — o que não ajuda ninguém.
Então: pegar um sistema multiagente que já funciona sem framework, reconstruir a orquestração em LangGraph, e medir a diferença em vez de discuti-la.
O arranjo
O objeto é o analista de commodities multiagente do estudo de caso 01 — quatro agentes sobre um núcleo econométrico determinístico em R, com laço de crítica e trava anti-alucinação.
A regra que dá sentido à comparação: só a orquestração muda. Os quatro agentes e todo o núcleo em agro/ são importados, não reescritos. A versão à mão é um laço for com break; a versão LangGraph é um StateGraph com cinco nós e duas arestas condicionais.
Trocar entre os dois é uma flag:
python run.py --commodity milho --pergunta "..." --fake-llm --engine manual
python run.py --commodity milho --pergunta "..." --fake-llm --engine langgraphO método, e por que esse
Paridade primeiro, opinião depois. Seis testes rodam os dois motores com a mesma entrada e comparam campo a campo: família escolhida, número de tentativas, histórico de reprovações, diagnósticos de resíduo, backtest, o conjunto de números que a trava autoriza — e o markdown final.
Sem isso, “reimplementei em LangGraph” é uma afirmação. Com isso, é uma propriedade. E uma reimplementação que se comporta quase igual é pior que nenhuma: parece uma alternativa e é uma armadilha.
Os agentes não viraram tools. O LangGraph oferece ToolNode e o padrão ReAct, em que o modelo decide qual ferramenta chamar. Não é o que este sistema faz. A ordem — coletar, ajustar, criticar, redigir — é fixa de propósito, e o LLM escolhe parâmetros, nunca a próxima etapa. Transformar fit_model numa tool seria uma mudança de semântica disfarçada de mudança de framework, e a comparação só vale se os dois lados fizerem a mesma coisa.
O resultado
Os relatórios são idênticos byte a byte. Mesmo SHA-256, ponta a ponta, do cache congelado:
manual : 94488AF71916321F4CE51258547E01F8BB4F9E43D43FBCE8238C20BF0543DCF0
langgraph: 94488AF71916321F4CE51258547E01F8BB4F9E43D43FBCE8238C20BF0543DCF0
O que o framework cobrou. O MemorySaver — o checkpointer, e a principal razão para adotar LangGraph — serializa o estado a cada nó com msgpack. Um pandas.Series não é serializável, então a primeira versão do grafo simplesmente quebrou:
TypeError: Type is not msgpack serializable: Series
O laço escrito à mão nunca encontra isso, porque lá a série é uma variável local lida uma vez. Para manter o checkpointing, o estado passou a carregar só o bundle de dados — um dataclass de primitivos com o caminho do arquivo — e cada nó que precisa da série a relê do disco.
Medido em execução: 1 leitura de parquet no motor manual, 3 no grafo. O tempo total quase não mudou, porque o gargalo é o subprocess do R, não o disco. Nessa escala o custo é real e irrelevante. Numa série grande deixaria de ser irrelevante, e a escolha passaria a ser abrir mão do checkpointer ou escrever um serializador próprio.
O que o framework comprou. O estado do pipeline deixa de ser implícito em variáveis locais e vira um TypedDict declarado. O laço de reprovação vira uma aresta condicional que se lê sem rastrear o fluxo de cabeça. E o checkpointer permite inspecionar e retomar uma execução no meio — coisa que o laço à mão não oferece.
O preço em código: 54 linhas de código à mão contra 120 no grafo, para comportamento idêntico.
Então qual? Para este sistema, o laço à mão ganha: o pipeline é curto, o fluxo de controle cabe numa tela, e nada aqui precisa de retomada. O framework começaria a se pagar com mais ramificações, pausas com humano no meio, ou execuções longas o bastante para retomar importar. Isso é conclusão tirada de medição, não de gosto — e, de qualquer forma, os testes de paridade fazem a troca custar uma flag.
O código
github.com/rhozon/commodity-agents
Os dois motores vivem no mesmo repositório: agents/orchestrator.py e agents_langgraph/graph.py, com tests/test_paridade.py prendendo os dois ao mesmo resultado. A suíte tem 173 testes, e roda sem chave de API e sem rede.
langgraph e langchain-core estão no requirements.txt, mas só o motor do grafo e o teste de paridade os importam — o import é adiado, então o motor padrão não precisa deles.