← Voltar aos artigosTRILHA 05 · Construindo um assistente conversacional: LangGraph, DSPy e Redis · PARTE 7 DE 10
Arquitetura & agentesObservabilidadeEngenharia

Observabilidade num grafo LangGraph: o que o Langfuse vê que o log não vê

Nos posts anteriores mostrei como o DSPy classifica a intenção e como o roteador customizado por embeddings decide a macro-intent. Os dois têm uma coisa em comum: podem degradar silenciosamente.

O DSPy degrada quando o provider atualiza o modelo. O semantic router degrada quando o vocabulário dos usuários muda. Nenhum dos dois lança exceção quando isso acontece. Você descobre pela qualidade da resposta (ou pela reclamação do usuário).

Observabilidade é o que muda esse cenário. Esse post é sobre como instrumentar um grafo LangGraph com Langfuse para rastrear exatamente onde cada decisão aconteceu e como detectar degradação antes que chegue ao usuário.


O que o log estruturado não resolve#

O logger estruturado cobre o que aconteceu dentro de cada nó. Ele não responde perguntas como:

  • Para qual scope o DSPy roteou nas últimas 500 queries? Qual a distribuição?

  • A taxa de fallback para geral subiu nos últimos 30 minutos?

  • O critique_node interveio em quantas respostas hoje? Por qual tipo de violação?

  • O dspy.Refine precisou de mais de uma tentativa em quantas requisições?

Essas perguntas exigem uma visão agregada sobre múltiplas execuções, não o log de uma execução individual. É aí que o Langfuse entra.


A instrumentação base: @observe nos nós#

O ponto de entrada mais simples é o decorator @observe nos nós do grafo:

Ampliar ↗

capture_input=False e capture_output=False não são paranoia. Em qualquer sistema com dados de usuário, capturar o estado completo do grafo no Langfuse significa capturar histórico de conversa, identificadores de sessão e potencialmente dados pessoais. O que você quer no trace não é o estado, são os metadados da decisão.


update_trace_metadata: injetando contexto na trace ativa#

Dentro de qualquer nó, update_trace_metadata acumula metadados na trace da requisição atual:

Ampliar ↗

Cada nó contribui com seus metadados para o mesma trace. No Langfuse, você vê o trace completo de uma requisição com todos esses campos agregados, sem precisar cruzar logs de diferentes workers.


Tags e user_id: segmentação sem dados pessoais#

No supervisor, ao montar a configuração do grafo:

Ampliar ↗

O langfuse_user_id é um identificador interno, nunca um CPF, e-mail ou número de telefone. As tags permitem filtrar traces por canal, ambiente e modo degradado no dashboard sem expor dados pessoais.


RouterMetricsCollector: métricas in-process com backend Redis#

O Langfuse rastreia execuções individuais. Para métricas agregadas em tempo real, taxa de fallback, distribuição de scopes, hits de anáfora, o sistema tem um coletor próprio:

Ampliar ↗

O backend Redis usa HINCRBY, compatível com deploys multi-worker onde cada processo tem seu próprio in-process counter, mas todos escrevem na mesma HASH:

Ampliar ↗

O increment é fire-and-forget: não bloqueia o hot path do router_node. Se o Redis estiver indisponível, o contador in-process ainda funciona, e o alerta de fallback ainda dispara.


O que monitorar para detectar degradação do DSPy#

No router_node, após cada inferência:

Ampliar ↗

Os três indicadores que mais importam:

  • Taxa de fallback para geral: quando o DSPy não consegue classificar a query, retorna scope="geral". Uma taxa acima de 25% é sinal de que os demos compilados estão desatualizados em relação ao vocabulário dos usuários. Correção: expandir o dataset e recompilar.

  • Taxa de anáfora resolvida: se anaphora_hit_count/total cair abruptamente, os padrões regex do resolver de anáforas pararam de casar com as mensagens dos usuários. Correção: revisar os patterns.

  • Taxa de coerção forçada: se coercion_fallback_count subir, o LLM começou a retornar formatos de output que a camada de coerção não consegue parsear. Correção: inspecionar dspy.inspect_history() e revisar a Signature.


O que o Langfuse mostra que o log não mostra#

Com update_trace_metadata em todos os nós, o Langfuse agrega num único trace:

Ampliar ↗

Em 5 segundos de execução você sabe: a anáfora foi resolvida, o DSPy roteou para busca de catálogo, o Refine precisou de 2 tentativas, e o critique injetou um disclaimer ausente.

Sem esse trace, você tem 5 logs em arquivos diferentes, sem correlação direta, sem timeline.


Itens importantes:#

  • capture_input=False em todos os spans que tocam mensagens do usuário. capture_output=False em todos os spans que retornam estado do grafo.

  • O que vai para o Langfuse são os metadados da decisão, scope, tool selecionada, latência, flags de compliance, não os dados em si.

  • Dados de usuário pertencem ao arquivo legal. Metadados de decisão pertencem ao sistema de observabilidade. Misturar os dois cria problemas de privacidade e infla o custo do Langfuse sem adicionar valor para debugar.


Na semana que vem: como o módulo de geração usa autocorreção em runtime com função de recompensa, e o trade-off de latência que isso cria.

Publicado originalmente no LinkedIn. Ver publicação ↗