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.
Um ano rodando agentes "autônomos" em produção encerrou um debate que parecia filosófico em 2024 e parece operacional em 2026: se o seu fluxo de controle vive dentro da janela de contexto de um modelo, você não tem fluxo de controle. Você tem uma sugestão.
O padrão que venceu, de forma silenciosa e em bases de código bastante diferentes, não é "agente maior, prompts melhores". É o oposto. Uma espinha dorsal de workflow determinística — uma máquina de estados chata, uma saga ou um workflow engine — conduz a execução, e a inteligência é invocada em etapas específicas e estreitas que devolvem o controle à espinha dorsal no momento em que terminam. O guia de effective-agents da Anthropic, o Koog da JetBrains, o Agent Harness do Spring AI, o LangGraph e as primitivas de IA do Temporal apontam todos para a mesma direção.
Este post defende que essa arquitetura híbrida deve ser o seu padrão para qualquer feature de IA que vá para produção, e percorre o que ela traz em testes, observabilidade, custo e resposta a incidentes. É escrito para engenheiros de backend que já subiram um agente e se arrependeram, ou que estão prestes a fazer isso.
Onde o Loop Quebra
O padrão de 2024 era assim: dê ao modelo um system prompt, um conjunto de ferramentas e um loop. O modelo decide o que chamar, em qual ordem, com quais argumentos e quando terminou. É sedutor. Também é quase impossível de operar acima de um certo nível de tráfego.
Três coisas tendem a quebrar.
O fluxo de controle é não-local. Quando um agente de suporte acaba reembolsando o pedido errado, o bug não está em nenhuma ferramenta específica. Está na sequência que o modelo escolheu, dado um contexto que não existe mais. Você não pode colocar um breakpoint em um prompt. Você não pode fazer diff de uma cadeia de raciocínio contra a cadeia de raciocínio da semana passada. Seu "stack trace" é uma transcrição, e transcrições não reproduzem.
O custo é ilimitado. Um loop aberto que pode chamar ferramentas até decidir parar não tem um orçamento de tokens a priori. Um único input patológico — um documento que confunde o planner, uma ferramenta que retorna erros ambíguos — pode queimar 40x o seu custo p50. O financeiro vai notar antes de você.
Os testes degradam em vibes. Testes unitários assumem determinismo. Um loop que pode seguir quatro caminhos diferentes no mesmo input não pode ser testado unitariamente em nenhum sentido significativo. Os times acabam com suítes de "transcrições douradas" que passam em 98% das vezes e nunca pegam a regressão que importa, porque a regressão é uma cauda de 2%.
Nenhum desses é um problema de prompt. São problemas de arquitetura. O sistema não tem costuras.
Inverta a Pergunta
O padrão de espinha dorsal determinística inverte a pergunta. Em vez de perguntar "qual é o menor conjunto de instruções que permite ao modelo realizar esta tarefa?", você pergunta "qual é a maior fração desta tarefa que não precisa de um modelo, de forma alguma?"
O modelo mental é simples. Seu workflow é um grafo direcionado com estado tipado. A maioria dos nós é código comum — leituras de banco, chamadas de API, validações, roteamento. Um pequeno número de nós invoca um LLM, e cada uma dessas invocações é estreita: um único prompt, um único formato de saída esperado, uma política de retry limitada e um teto rígido de tokens. Quando o nó retorna, a espinha dorsal toma posse do estado novamente.
Três propriedades surgem disso, e são a razão inteira pela qual o padrão vence:
- O grafo é inspecionável. Você pode desenhá-lo. Você pode logar cada transição. Você pode reproduzi-lo a partir do estado.
- Cada etapa do agente é uma função pura de suas entradas. Não é "o agente que roda a tarefa inteira". É "o classificador que transforma um e-mail de suporte em uma das sete intenções" ou "o extrator que puxa itens de linha de um PDF". Esses são testáveis.
- As partes caras são locais. Custo de tokens, latência e falhas estão concentrados em nós específicos. Você pode aplicar cache, fallback ou circuit-break em cada um deles independentemente.
Essa não é uma ideia nova. É como todo sistema distribuído maduro lida com o problema de chamar algo pouco confiável há vinte anos. Sagas, outbox patterns, workflow engines — o caso de IA tem o mesmo formato com um novo tipo de dependência não confiável.
Um Pipeline de Triagem em Kotlin
Vou tornar isso concreto em Kotlin com Spring. O mesmo padrão traduz-se para Temporal, Flowable, LangGraph ou uma máquina de estados feita à mão — o que importa é o formato, não o framework.
Imagine um pipeline de triagem de suporte: um e-mail chega, classifico, extraio dados estruturados se relevante, roteio para uma fila e rascunho uma resposta apenas quando a confiança passa de um limiar. A versão agêntica ingênua entrega essa tarefa inteira a um LLM com cinco ferramentas. A versão com espinha dorsal fica assim:
sealed interface TriageState {
data class Received(val email: Email) : TriageState
data class Classified(val email: Email, val intent: Intent, val confidence: Double) : TriageState
data class Extracted(val email: Email, val intent: Intent, val fields: Map<String, Any>) : TriageState
data class Routed(val ticketId: TicketId, val queue: Queue) : TriageState
data class Drafted(val ticketId: TicketId, val draft: String) : TriageState
data class Escalated(val ticketId: TicketId, val reason: String) : TriageState
}
@Component
class TriageWorkflow(
private val classifier: IntentClassifier, // etapa estreita de LLM
private val extractor: FieldExtractor, // etapa estreita de LLM
private val router: Router, // código comum
private val drafter: ReplyDrafter, // etapa estreita de LLM
private val tickets: TicketRepository,
) {
fun run(email: Email): TriageState {
val classified = classifier.classify(email) // limitado: 1 chamada, <800 tokens
if (classified.confidence < 0.6) {
return escalate(email, "low_confidence_classification")
}
val extracted = if (classified.intent.needsExtraction) {
extractor.extract(email, classified.intent) // limitado: 1 chamada, schema estrito
} else {
TriageState.Extracted(email, classified.intent, emptyMap())
}
val routed = router.route(extracted) // código comum, determinístico
return if (classified.intent.autoReplyEligible) {
drafter.draft(routed) // limitado: 1 chamada, ancorado em template
} else {
routed
}
}
}Cada chamada de LLM aqui é uma função. Cada uma tem uma entrada tipada, uma saída tipada, um timeout, um teto de tokens e sua própria política de retry. O fluxo de controle — os ifs, a escalação, o roteamento — é Kotlin comum. Você pode colocar um breakpoint em qualquer linha.
O classificador é onde os trade-offs interessantes vivem:
@Component
class IntentClassifier(private val chat: ChatClient) {
fun classify(email: Email): TriageState.Classified {
val response = chat.prompt()
.system(CLASSIFY_SYSTEM_PROMPT)
.user(email.normalizedBody())
.options { it.maxTokens(200).temperature(0.0) }
.call()
.entity(ClassificationResult::class.java)
return TriageState.Classified(email, response.intent, response.confidence)
}
}
data class ClassificationResult(val intent: Intent, val confidence: Double)Três coisas vale a pena destacar. A temperatura é zero, porque esta etapa está fazendo classificação estruturada e não-determinismo é um bug, não uma feature. maxTokens é apertado, porque o formato de saída é pequeno e um orçamento estourado significa que o prompt derivou. A resposta é parseada em um objeto tipado — se o modelo retornar algo fora do schema, você recebe uma exceção na fronteira do nó, não uma corrupção a jusante.
Para o próprio workflow, normalmente recorro ao Spring StateMachine ou a um workflow engine assim que o grafo tem mais de cinco ou seis nós, ramificações ou qualquer etapa de longa duração. O Temporal é particularmente bom aqui porque retries, timeouts e replay de histórico vêm de graça, e etapas de LLM são exatamente o tipo de atividade instável, cara e propensa a timeout para a qual o Temporal foi projetado. Em Kotlin:
class TriageWorkflowImpl : TriageWorkflow {
override fun run(email: Email): TriageState {
val classified = Workflow.newActivityStub(
ClassifierActivity::class.java,
ActivityOptions.newBuilder()
.setStartToCloseTimeout(Duration.ofSeconds(10))
.setRetryOptions(RetryOptions.newBuilder().setMaximumAttempts(3).build())
.build()
).classify(email)
// ...
}
}Cada atividade de LLM tem um timeout limitado, uma contagem de retry limitada e um histórico replayable. Se uma execução falhar pela metade, o Temporal retoma a partir da última atividade completada, não do início. Essa única propriedade — replay determinístico em caso de falha parcial — é a razão pela qual parei de escrever orquestração ad-hoc para qualquer coisa que converse com um modelo em produção.
Os trade-offs que valem ser nomeados:
- Você abre mão de comportamento emergente. Um agente que pode escolher sua própria ordem de ferramentas às vezes encontra um caminho que você não tinha pensado. A espinha dorsal não vai. Para workflows bem compreendidos, é isso que você quer. Para tarefas exploratórias, é uma perda real.
- Você move complexidade para o grafo. Ramificações, guards e caminhos de escalação voltam a ser seu problema. Isso é uma feature — é complexidade que você pode ver e revisar — mas não é de graça.
- Você fica mais acoplado ao seu domínio. Um único mega-agente é tentadoramente genérico. Uma espinha dorsal tem o formato do seu negócio. Isso é bom em escala e ruim quando os requisitos mudam semanalmente.
Modos de Falha Que Continuo Vendo
Alguns modos de falha que vi times baterem, geralmente no primeiro trimestre após adotar o padrão.
Determinismo falso. Os times envolvem chamadas de LLM em um workflow engine e declaram vitória, depois deixam a temperatura em 0.7 e configuram maxTokens como infinito. A espinha dorsal é determinística; as etapas não são; nada é reproduzível. Se a etapa está fazendo trabalho estruturado, fixe a temperatura em 0 e restrinja a saída com um schema. Se está fazendo trabalho generativo, aceite o não-determinismo e isole-o — não finja que uma etapa criativa é uma função pura.
Proliferação de grafo. O padrão recompensa quebrar coisas em pequenos nós, então os times quebram coisas em pequenos nós demais. Grafos de trinta nós ficam ilegíveis pela mesma razão que classes de trinta métodos ficam. Agrupe etapas relacionadas em sub-workflows e resista ao impulso de modelar cada ramificação como um nó.
Contrabandeando um agente de volta. Um nó de LLM começa precisando "só chamar uma ferramenta para checar uma coisa", depois duas, depois entra em loop. Parabéns, você tem um agente de novo, agora escondido dentro de um nó que finge ser atômico. Se uma etapa precisa chamar ferramentas, transforme as chamadas de ferramenta em nós do grafo. Se isso for impraticável, reconheça que você tem uma etapa-agente e dê a ela um orçamento e um limite de passos explícitos.
Ignorar o caminho de escalação. O valor todo da espinha dorsal é que ela sabe quando parar de estar confiante. Se toda ramificação acaba alimentando "deixa o agente descobrir", você construiu um wrapper caro em torno do mesmo loop autônomo que estava tentando substituir. Handoff humano, heurísticas de fallback e paradas rígidas são features de primeira classe, não pensadas depois.
Confiar demais no replay. O replay é poderoso e sedutor. Também é enganoso quando o modelo subjacente derivou. Uma transcrição que replayou limpa em janeiro pode classificar a mesma entrada de forma diferente em abril porque o provedor subiu um novo checkpoint. Fixe versões de modelo nas suas activity options e inclua o identificador do modelo no histórico do seu workflow.
Conclusões Práticas
- Trate toda chamada de LLM como uma atividade não confiável, cara e não-determinística — porque é. Projete em torno dela da mesma forma que projetaria em torno de uma API externa instável.
- Faça do fluxo de controle código comum. Ramificações, retries e escalação devem ser legíveis na sua IDE, não inferidas de uma transcrição.
- Fixe temperatura, max tokens e versão do modelo em cada etapa. Uma etapa estreita com botões folgados é uma etapa larga disfarçada.
- Coloque etapas de LLM atrás de interfaces tipadas com saída estruturada. Violações de schema devem falhar rápido na fronteira do nó.
- Recorra a um workflow engine (Temporal, Flowable, LangGraph, Spring StateMachine) assim que seu grafo ramificar. Orquestração ad-hoc envelhece mal.
- Reserve loops totalmente autônomos para tarefas onde você aceita custo e latência ilimitados em troca de exploração — pesquisa, agentes de código, análise de longo horizonte. Não para workflows transacionais voltados ao cliente.
- Migre incrementalmente. Envolva seu agente existente como um único nó em uma nova espinha dorsal, depois puxe comportamento dele uma etapa de cada vez. Você não precisa de uma reescrita.
Comece Pelo Grafo
Agentes totalmente autônomos não estão errados. Eles são uma ferramenta específica para um problema específico: tarefas exploratórias, de baixo SLA, de alta tolerância a custo, onde o valor de encontrar um caminho novo supera o custo de não conseguir limitá-lo. Essa é uma categoria real. Não é a maioria das features de produção.
Para todo o resto — triagem, extração, roteamento, moderação, sumarização, a longa cauda de "LLM em algum lugar do pipeline" — a espinha dorsal determinística é o padrão certo em 2026. Não porque está na moda, mas porque é a única arquitetura que devolve a você as três coisas sem as quais sistemas de produção não podem funcionar: reprodutibilidade, custo limitado e um lugar para colocar um breakpoint.
Se você está começando uma nova feature de IA neste trimestre, comece pelo grafo. Adicione inteligência nos nós que precisam dela. Se você está rodando um agente autônomo em produção hoje e está cansado, a migração é menos assustadora do que parece: envolva, recorte, substitua.
Curtindo? Talvez goste disso aqui.
Nada parecido — quer tentar outro ângulo?
Posts Relacionados
Expondo Agentes Spring AI via o Protocolo A2A: O Que a Interoperabilidade Realmente Te Dá
A integração A2A do lado servidor no Spring AI já está estável o suficiente para colocar em produção, mas o protocolo é mais útil em fronteiras organizacionais, não como substituto de RPC interno. Este post percorre o que de fato muda em uma codebase Spring AI, onde ainda existem arestas afiadas, e um framework prático de decisão entre A2A, MCP e REST puro.
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.
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.