Post

Langfuse em 2026Q3: observabilidade, avaliação e engenharia de sistemas de IA

Se preferir começar por um resumo, peça um TL;DR ao ChatGPT ou ao Claude.

Como combinar tracing, scores, datasets, experiments e revisão humana para transformar execuções de IA em evidência para o próximo release.

Colocar uma aplicação com LLM em produção leva rapidamente a perguntas que um log de entrada e saída não responde bem:

  • qual prompt e qual modelo produziram a resposta;
  • onde ficou a maior parte do custo e da latência;
  • qual ferramenta o agente decidiu chamar;
  • se o retriever trouxe os documentos adequados;
  • qual versão da aplicação estava ativa;
  • se a resposta foi boa e, quando não foi, qual parte falhou.

É nesse espaço que uso o Langfuse. O projeto se apresenta como uma plataforma open source de AI Engineering que reúne tracing, métricas, gestão de prompts, avaliações, datasets e experiments. Essas capacidades estão documentadas no repositório oficial e na visão geral do produto.

O ponto que me interessa não é apenas enxergar uma chamada ao modelo. É acompanhar o comportamento de uma aplicação de IA como um sistema de software e transformar falhas observadas em casos reproduzíveis para o próximo release.

Este texto é um recorte de 17 de agosto de 2026. Os exemplos seguem os conceitos do Langfuse v4 e a API atual do SDK Python v4. Não são uma aplicação completa: funções como search_documents() e generate_answer() pertencem ao sistema que está sendo instrumentado. Como os SDKs mudam, vale conferir o guia de migração do Python v3 para v4 antes de copiar um trecho para outra versão.

O problema aparece quando a aplicação vira um sistema

Uma chamada isolada pode ser representada assim:

1
usuário → prompt → LLM → resposta

Registrar entrada, saída, tokens e latência resolve boa parte da investigação nesse caso. Um agente real tende a acrescentar outras fronteiras:

1
2
3
4
5
6
7
8
9
10
request
└── agent
    ├── retriever
    ├── generation
    ├── tool → API externa
    ├── outro agent
    │   ├── retriever
    │   └── generation
    ├── guardrail
    └── resposta

A resposta final pode estar errada mesmo quando a geração funcionou como esperado. O retriever pode ter escolhido o documento errado, a ferramenta pode ter recebido argumentos incorretos, a API externa pode ter devolvido dados incompletos ou o guardrail pode ter alterado a saída.

Sem a árvore inteira, tudo isso tende a aparecer como “o modelo falhou”. Com a árvore, a investigação começa na etapa que produziu o comportamento.

Observation, trace e session têm papéis diferentes

O modelo de dados do Langfuse organiza a telemetria em três conceitos:

1
2
3
4
5
session
└── trace
    ├── observation
    ├── observation
    └── observation

Uma observation é uma unidade de trabalho: geração, chamada de ferramenta, retrieval, avaliação ou outro passo. Observations podem ser aninhadas.

Um trace agrupa as observations de uma operação lógica, como processar uma mensagem. Uma session pode agrupar vários traces relacionados, por exemplo os turnos de uma conversa.

Os tipos de observation incluem generation, agent, tool, chain, retriever, evaluator, embedding, guardrail, span e event. A tipagem não é decorativa. Uma generation, por exemplo, comporta modelo, uso e custo; uma tool pode ser filtrada separadamente quando quero avaliar seleção e argumentos.

Uma árvore útil preserva o vocabulário da aplicação:

1
2
3
4
5
6
customer-support-agent
├── retriever: search-documentation
├── generation: analyze-question
├── tool: billing-api
├── generation: compose-answer
└── guardrail: validate-output

A instrumentação manual precisa contar a mesma história

O primeiro passo é instalar o SDK e configurar as credenciais. Para Langfuse Cloud, a URL depende da região; em uma instalação própria, aponta para a instância local.

1
2
3
4
5
python -m pip install --upgrade langfuse

export LANGFUSE_SECRET_KEY="sk-lf-..."
export LANGFUSE_PUBLIC_KEY="pk-lf-..."
export LANGFUSE_BASE_URL="https://cloud.langfuse.com"

O exemplo abaixo instrumenta um fluxo pequeno, mas coerente. O contexto é propagado antes de abrir a observation raiz; retrieval e generation aparecem como filhos. O código registra a quantidade de documentos, não seu conteúdo integral — uma escolha que precisa ser revista conforme a política de dados e a necessidade de depuração.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
from langfuse import get_client, propagate_attributes

langfuse = get_client()

with propagate_attributes(
    user_id=user_id,
    session_id=session_id,
    environment="production",
    version=RELEASE,
    tags=["customer-support"],
):
    with langfuse.start_as_current_observation(
        as_type="agent",
        name="answer-user",
        input={"question": question},
    ) as agent:
        with langfuse.start_as_current_observation(
            as_type="retriever",
            name="search-knowledge-base",
            input={"query": question},
        ) as retrieval:
            documents = search_documents(question)
            retrieval.update(
                output={"documents_found": len(documents)}
            )

        with langfuse.start_as_current_observation(
            as_type="generation",
            name="compose-answer",
            model=MODEL,
            input={
                "question": question,
                "document_count": len(documents),
            },
        ) as generation:
            answer = generate_answer(question, documents)
            generation.update(output={"answer": answer})

        agent.update(output={"answer": answer})

langfuse.flush()

O uso de start_as_current_observation() mantém o contexto OpenTelemetry ativo e faz os filhos herdarem o pai. Essa é a forma principal descrita no guia oficial de instrumentação. Em processos curtos, flush() evita encerrar antes de enviar os eventos pendentes; aplicações servidoras normalmente deixam o SDK agrupar e enviar os eventos em segundo plano.

Integrações automáticas e OpenTelemetry podem conviver

Quando a aplicação usa diretamente o SDK Python da OpenAI, o wrapper do Langfuse preserva uma interface familiar e cria a generation:

1
2
3
4
5
6
7
8
9
10
11
12
13
from langfuse.openai import openai

completion = openai.chat.completions.create(
    name="support-answer",
    model=MODEL,
    messages=[
        {
            "role": "system",
            "content": "Answer using the provided context.",
        },
        {"role": "user", "content": question},
    ],
)

A integração e os parâmetros suportados estão no cookbook oficial para OpenAI. Se a chamada ocorrer dentro de uma observation ativa, ela entra naquela árvore em vez de ficar isolada.

No LangChain, o CallbackHandler transforma eventos do framework em observations. A instrumentação manual continua útil para acrescentar a unidade de domínio que o framework não conhece:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from langfuse import propagate_attributes
from langfuse.langchain import CallbackHandler

handler = CallbackHandler()

with propagate_attributes(
    user_id=user_id,
    session_id=session_id,
    environment="production",
    version=RELEASE,
):
    response = chain.invoke(
        {"question": question},
        config={"callbacks": [handler]},
    )

O guia inicial de observabilidade mantém exemplos do handler. O cuidado é não assumir que instrumentação automática produzirá, sozinha, a melhor árvore para o domínio.

Os SDKs atuais são baseados em OpenTelemetry. Isso permite manter o Langfuse na camada especializada de IA e outra stack para HTTP, banco, CPU, filas e infraestrutura:

1
2
3
4
5
6
7
8
9
                    aplicação
                        │
              contexto OpenTelemetry
                 ┌──────┴──────┐
                 ↓             ↓
          observabilidade    Langfuse
            tradicional
          HTTP · DB · CPU    LLM · agent
          filas · infra      tools · evals

Não significa que todos os spans devam ir para todos os destinos. O SDK possui filtros por escopo de instrumentação, e a seleção precisa equilibrar capacidade de diagnóstico, ruído e custo. A documentação de recursos avançados explica filtragem, sampling e isolamento de providers OpenTelemetry.

Contexto transforma volume em diagnóstico

Um trace isolado já ajuda a depurar. Milhões de traces só permitem comparação quando carregam dimensões consistentes, como usuário, sessão, ambiente, release, feature e versão do agente ou do prompt.

Esses atributos permitem perguntas que importam para a operação:

  • qual release começou a produzir mais erros;
  • qual feature concentra custo ou latência;
  • quais traces pertencem à mesma sessão;
  • como um agente se comporta por ambiente;
  • quanto uma conversa ou um tenant consome.

O propagate_attributes() usado no exemplo é o mecanismo atual para propagar user_id, session_id, version, tags, metadata e environment. A documentação de instrumentação ressalta que environment é um atributo de primeira classe no SDK Python, não apenas uma chave arbitrária em metadata.

Custo também precisa de proveniência. O Langfuse pode receber uso e custo do provedor ou inferi-los a partir da definição do modelo; valores enviados têm prioridade. A documentação de tokens e custos também alerta que buckets sobrepostos podem contar tokens duas vezes.

Latência precisa ser decomposta pelo mesmo motivo. Os números abaixo são ilustrativos, não uma medição deste projeto:

Etapa Latência ilustrativa
retrieval 400 ms
generation 1 1.800 ms
tool HTTP 4.700 ms
generation 2 900 ms
validation 200 ms

“O agente demorou oito segundos” descreve o sintoma. A árvore mostra qual fronteira merece investigação.

Observar ainda não diz se a resposta foi boa

Uma execução pode ter baixa latência, custo pequeno e nenhum erro técnico, mas entregar uma resposta incorreta. Por isso, tracing e avaliação precisam se encontrar.

No Langfuse, scores armazenam resultados de avaliação. Eles podem vir de feedback do usuário, revisão humana, código, LLM-as-a-Judge ou outro pipeline e podem ser ligados a trace, observation, session ou dataset run. A documentação de scores distingue valores numéricos, categóricos, booleanos e textuais.

A granularidade muda o diagnóstico. Em vez de um único “bom” ou “ruim” para a execução inteira, posso separar:

Alvo Exemplo de sinal Pergunta respondida
retriever retrieval_relevance Os documentos sustentavam a resposta?
tool tool_selection O agente escolheu a operação correta?
generation answer_correctness A saída respondeu ao pedido?
trace user_satisfaction O resultado final serviu ao usuário?

Uma resposta ruim pode vir de uma geração boa apoiada no documento errado. Um score único esconderia essa diferença.

LLM-as-a-Judge é um sinal, não uma sentença

Um Judge recebe entrada, saída, contexto e rubrica, e devolve um score com uma justificativa. A documentação de LLM-as-a-Judge permite aplicar avaliadores a observations de produção ou a experiments com datasets.

Isso ajuda a avaliar volume, mas eu evitaria transformar o Judge em árbitro absoluto. Uso três sinais com autoridades diferentes:

Sinal Onde ajuda Limitação principal
regra determinística schema, regex, campos e contratos objetivos só prova a regra implementada
LLM-as-a-Judge critérios semânticos e revisão em escala pode reproduzir vieses e discordar de especialistas
avaliação humana decisão contextual e casos de alto impacto custa tempo e não escala da mesma forma

As Annotation Queues organizam revisão manual de traces, observations ou sessions e permitem que especialistas acrescentem scores, comentários e saídas corrigidas.

Em outro post, mostrei como uso Langfuse com LLM-as-a-Judge sem tratar score como verdade. Ali há uma execução observada, números reais, diferenças entre dois projetos e os motivos pelos quais o Judge ainda não funciona como gate. Este texto tem outro papel: organizar as peças da plataforma e o loop que elas podem formar.

Dataset, experiment e prompt fecham o loop

Um caso interessante encontrado em produção pode deixar de ser apenas um incidente e entrar no conjunto permanente de avaliação:

1
2
3
4
5
6
7
8
9
falha em produção
      ↓
trace + análise
      ↓
dataset item
      ↓
experiment
      ↓
proteção contra regressão

O guia de datasets define cada dataset como uma coleção de entradas e, opcionalmente, saídas esperadas. O modelo de avaliação combina esses itens com uma task, métodos de avaliação e uma execução do experiment.

O mesmo dataset permite comparar prompts, modelos, retrievers ou releases do agente. Os números abaixo também são hipotéticos; mostram o formato da decisão, não o resultado de um benchmark:

Métrica Versão A Versão B
correctness 0,82 0,89
latência 2,1 s 2,4 s
custo por caso US$ 0,011 US$ 0,008

Não existe vencedor universal nessa tabela. A versão B melhora dois sinais e piora outro; a escolha depende do limite de latência, da relevância do ganho de qualidade e da confiabilidade do método que produziu o score.

Prompt Management acrescenta proveniência. Versões preservam o conteúdo exato; labels como staging e production apontam para a versão usada por cada ambiente. A documentação de controle de versão mostra que mover o label também permite rollback.

1
2
3
4
5
6
7
8
9
prompt version
      ↓
experiment
      ↓
evaluation
      ↓
production label
      ↓
traces e scores

O trace precisa continuar ligado à versão efetivamente usada. Caso contrário, o prompt fica versionado na plataforma, mas a resposta em produção perde a proveniência.

Self-hosting controla o destino, não o conteúdo capturado

O Langfuse pode ser executado na própria infraestrutura. A arquitetura atual é formada por Web, Worker, PostgreSQL, ClickHouse, Redis ou Valkey e object storage. Um desenho simplificado é:

1
2
3
4
5
6
7
8
9
10
SDKs e usuários
       ↓
   Langfuse Web
   ├── PostgreSQL
   ├── ClickHouse
   ├── object storage
   └── Redis / Valkey → Worker
                        ├── PostgreSQL
                        ├── ClickHouse
                        └── object storage

A arquitetura mantida no repositório oficial explica o papel desses componentes. O Docker Compose é o caminho mais simples para uma máquina ou ambiente de teste, mas a própria documentação registra que esse arranjo não oferece alta disponibilidade, escalabilidade horizontal ou backup. Para cargas maiores, existe o deployment com Kubernetes e Helm, inclusive com serviços de dados externos.

Eu já descrevi um upgrade real do Langfuse no meu homelab, com backup, restauração, Nomad e uma pendência de compatibilidade que permaneceu depois do deploy.

Self-hosting ajuda a controlar onde os traces ficam. Não decide quais dados deveriam entrar neles. Prompt, documentos recuperados, parâmetros de tools, respostas, dados de usuário e metadata podem carregar informações pessoais, segredos ou conteúdo interno.

A política precisa responder, antes da captura:

  • quais campos são necessários para investigar e avaliar;
  • o que não pode sair da aplicação;
  • o que precisa ser mascarado;
  • quem pode acessar cada ambiente;
  • quanto tempo os dados serão mantidos;
  • quando basta registrar ids, tamanhos ou contagens.

O Langfuse oferece masking no SDK, inclusive no estágio de exportação OpenTelemetry. Masking é uma barreira adicional; não substitui reduzir a coleta na origem. Observabilidade não deve criar um novo vazamento em nome de investigar o anterior.

Eu começaria pelo caminho crítico

Em uma aplicação nova, eu não instrumentaria tudo nem criaria dez scores de uma vez. Começaria pelo fluxo cuja falha possui consequência identificável:

1
2
3
4
5
6
7
8
9
10
11
request
   ↓
agent
   ↓
retrieval
   ↓
tools
   ↓
generations
   ↓
response

Depois acrescentaria:

  1. user_id, session_id, ambiente, release e versões de agente e prompt;
  2. entrada e saída estritamente necessárias para revisar o caso;
  3. latência, uso, custo e feedback do usuário;
  4. um ou dois checks determinísticos ligados ao contrato;
  5. uma rubrica semântica curta, quando houver razão para usar um Judge;
  6. revisão humana associada exatamente à saída avaliada.

O próximo hábito é perguntar, depois de cada falha relevante: este caso deve entrar no dataset? Se a resposta for sim, o incidente passa a proteger uma comparação futura. A aplicação não aprende sozinha; o processo de engenharia acumula casos reais e os usa para avaliar releases.

O dashboard não é o objetivo

É fácil instalar observabilidade, abrir uma tela cheia de traces e sentir que o problema foi resolvido. Milhares de traces sem processo continuam sendo apenas milhares de traces.

O valor aparece quando existe um loop operacional:

1
2
3
4
5
6
7
8
9
observar
   ↓
entender e medir
   ↓
transformar em caso reproduzível
   ↓
experimentar e comparar
   ↓
mudar e medir novamente ↺

Tracing é a porta de entrada. O ganho maior aparece quando uma execução fica ligada a usuário, sessão, release, prompt, custo, latência, score, feedback, dataset e experiment.

Nesse ponto, a pergunta deixa de ser apenas “o que o modelo respondeu?” e passa a orientar a evolução do sistema:

Como sabemos se este sistema está ficando melhor?

Para mim, essa é uma das perguntas centrais de AI Engineering em 2026.