Pular para o conteúdo

Observabilidade

Um formato de fio (OpenTelemetry + OpenInference), três destinos de exportação (Langfuse Cloud Hobby, Phoenix auto-hospedado, OTLP genérico) e um acumulador de custo + latência por turno que chega em cada resposta de /chat e em cada relatório de avaliação. Veja a ADR-0006 para a decisão; este arquivo é o manual do operador.

O agente emite spans OpenTelemetry anotados com convenções semânticas OpenInference. Os spans cobrem os nós do LangGraph (agent.intake, agent.guardrail_pre, agent.retrieve_context, agent.generate_response, agent.guardrail_post, agent.closing, mais agent.review_response quando o nó HITL opcional está habilitado) mais a chamada de LLM (llm.complete). Contagens de tokens, latência, nome do modelo e contagens de decisões viajam nos spans como atributos. O texto da mensagem do usuário nunca viaja.

Três backends consomem o mesmo formato de fio:

  • Langfuse Cloud Hobby para a demonstração ao vivo no Google Cloud Run. Camada gratuita: 50K observações / mês, retenção de 30 dias, interface hospedada com compartilhamento por link público.
  • Phoenix auto-hospedado para o CI de avaliação. Levantado via a pilha Docker Compose opcional; sem cota, sem rede externa.
  • OTLP/HTTP genérico para operadores que já enviam traces ao Datadog, Grafana, Honeycomb ou qualquer outra pilha compatível com OTLP. Configurado via OTLP_ENDPOINT.

Arize AX é suportado como painel gerenciado secundário sobre o mesmo fio OTLP, para um operador que queira uma visão de observabilidade de GenAI hospedada ao lado do Langfuse; ele viaja pelo caminho OTLP genérico e não requer mudança de código.

Os três backends estão desligados por padrão. O agente ainda produz um TracerProvider com um exportador em memória que descarta spans, então tracer.start_as_current_span(...) é sempre seguro de chamar.

O agente usa o OpenInference, as convenções semânticas da Arize para GenAI. O OpenInference roda sobre o OpenTelemetry e adiciona atributos específicos de LLM que o OTel puro não cobre (modelo, provedor, uso de tokens, contextos de recuperação, chamadas de ferramenta).

A auto-instrumentação para LangChain (cobre o LangGraph), OpenAI e Anthropic é instalada quando o extra opcional obs está presente. O código do agente também emite spans explícitos para cada nó + cada chamada de LLM, então a árvore de trace é legível mesmo quando a auto-instrumentação está ausente (por exemplo, dentro de um processo de teste de unidade).

Padrão para a demonstração ao vivo do Google Cloud Run.

Cadastre-se em https://cloud.langfuse.com, crie um projeto, copie as chaves pública + secreta.

Terminal window
export LANGFUSE_PUBLIC_KEY=pk-lf-...
export LANGFUSE_SECRET_KEY=sk-lf-...
# Opcional; o padrão é https://cloud.langfuse.com
export LANGFUSE_HOST=https://cloud.langfuse.com

Reinicie a API; o lifespan do FastAPI instala um processador de span OTLP vinculado ao Langfuse sobre o processador padrão. Abra o dashboard do Langfuse em ${LANGFUSE_HOST}/project/... para inspecionar os traces.

Padrão para as execuções de avaliação offline.

Terminal window
make obs-up
export PHOENIX_OTLP_ENDPOINT=http://localhost:6006/v1/traces
uv run python -m ai_agent_eval.evals run --locale all --with-phoenix

O Phoenix escuta em :6006 (UI + OTLP/HTTP) e :4317 (OTLP/gRPC). O armazenamento de traces persiste no volume Docker phoenix_data; derrube a pilha com make obs-down quando terminar.

Para operadores que já têm um endpoint OTLP.

Terminal window
export OTLP_ENDPOINT=https://otlp.example.com/v1/traces

O lifespan instala um OTLPSpanExporter apontado para essa URL.

O Logfire entrega um SDK Python com uma camada gratuita de 10M spans por mês em vigor a partir de 2026-01-01. O formato de fio OpenInference significa que trocar para o Logfire é uma mudança de configuração, não uma mudança de código: instale logfire, configure seu exportador OTLP contra https://logfire-api.pydantic.dev/v1/traces e desabilite o processador do Langfuse. A distribuição não entrega um módulo de backend Logfire de primeira classe - isso é deixado ao operador que o escolher.

sequenceDiagram
  participant User
  participant API as FastAPI /chat
  participant Graph as LangGraph
  participant LLM as LLM Adapter

  User->>API: POST /chat
  API->>Graph: agent.invoke(state)
  Graph->>Graph: agent.intake span
  Graph->>Graph: agent.guardrail_pre span
  Graph->>Graph: agent.retrieve_context span (when RAG)
  Graph->>LLM: llm.complete span (graph-level)
  LLM->>LLM: llm.complete span (adapter-level)
  LLM-->>Graph: CompletionResult
  Graph->>Graph: agent.generate_response span
  Graph->>Graph: agent.guardrail_post span
  Graph->>Graph: agent.closing span
  Graph-->>API: final state (incl. CostAccumulator)
  API-->>User: ChatResponse {tokens_in, tokens_out, latency_ms, cost}

O agente sempre emite um span llm.complete na camada do grafo. Os adaptadores reais (OpenAI, Anthropic, Groq) emitem um segundo span llm.complete na camada do adaptador para a chamada HTTP real. Os fakes de teste não possuem um tracer, então o span da camada do grafo mantém a topologia consistente.

O tempo por nó também é exposto fora do pipeline OTel. O modo de streaming SSE em /chat e /chat/resume (veja a ADR-0009) emite um evento node_started e um node_completed por nó executado, com o node_completed carregando um duration_ms medido pelo emissor - o intervalo de relógio de parede entre os eventos de início e fim do nó para o run id correspondente. Esse número é medido independentemente dos spans OTel acima (não é uma duração de span de tracing) e alimenta o Agent Execution Graph da demonstração; os spans OTel permanecem o registro de observabilidade autoritativo exportado ao Langfuse e ao Phoenix.

Cada span carrega apenas METADADOS:

  • service.name, service.namespace=healthtech-demo, service.version, deployment.environment
  • agent.node (um de intake / guardrail_pre / retrieve_context / generate_response / guardrail_post / closing)
  • agent.tokens_in, agent.tokens_out, agent.latency_ms
  • agent.guardrail_decisions_count, agent.citations_count
  • llm.provider (openai / anthropic / groq), llm.model, llm.tokens_in, llm.tokens_out, llm.latency_ms, llm.finish_reason

Nenhum atributo de span carrega o texto da mensagem do usuário, o texto da resposta do assistente ou qualquer PHI. Isto é imposto por um teste de unidade dedicado que afirma o invariante de privacidade. Violar este invariante significa um gate de CI reprovado. A motivação é a postura regulatória: os traces deixam o processo local; as mensagens do usuário não devem.

Se você precisar inspecionar uma transcrição, faça isso a partir dos logs do FastAPI no ambiente confiável, NÃO a partir do armazenamento de traces. Um trabalho futuro poderia adicionar um botão opt-in trace.include_content=True com reconhecimento explícito do operador; hoje a resposta é “não”.

Os orçamentos por turno vivem nas configurações da aplicação:

ConfiguraçãoPadrãoVariável de ambiente
cost_budget_tokens_in_per_turn4000COST_BUDGET_TOKENS_IN_PER_TURN
cost_budget_tokens_out_per_turn1000COST_BUDGET_TOKENS_OUT_PER_TURN
cost_budget_latency_ms_per_turn8000COST_BUDGET_LATENCY_MS_PER_TURN

O executor de avaliação compara os números médios por turno do corpus contra esses orçamentos. O gate de custo é estrito e bloqueia o PR por padrão: a CLI de avaliação encerra com código não-zero quando a média do corpus por turno viola qualquer orçamento, e o relatório legível por humanos carrega uma tabela de status por dimensão e uma linha resolvida [cost-gate=PASS|WARN|FAIL|off] sob a seção “Cost & latency”. Passe --cost-gate warn para comportamento apenas-de-aviso ou --cost-gate off para suprimir a renderização de custo inteiramente. Como uma válvula de escape sem chave, o gate estrito se degrada automaticamente para apenas-de-aviso quando nenhuma chave de provedor capaz de juiz está definida, de modo que um PR sem chave não pode falhar por custo.

Para sobrescrever os padrões de orçamento, defina as variáveis de ambiente (um arquivo .env funciona da mesma forma que as chaves de LLM).

Terminal window
# 1. Suba a pilha de observabilidade opcional (Phoenix).
make obs-up
# 2. Conecte a CLI de avaliação para enviar traces ao Phoenix.
export PHOENIX_OTLP_ENDPOINT=http://localhost:6006/v1/traces
uv run python -m ai_agent_eval.evals run \
--locale all \
--with-phoenix \
--report-dir evals/reports
# 3. Abra a UI do Phoenix.
# http://localhost:6006

Para a API ao vivo + Langfuse:

Terminal window
export LANGFUSE_PUBLIC_KEY=pk-lf-...
export LANGFUSE_SECRET_KEY=sk-lf-...
uv run uvicorn ai_agent_eval.api.main:app --reload
# Abra https://cloud.langfuse.com/project/<id>/traces

O Langfuse Cloud Hobby limita em 50K observações / mês sem cobrança de excedente - o tráfego de pico acima do limite é descartado silenciosamente. Isto é intencional: mantém o sink de observabilidade da demonstração dentro da sua camada gratuita a $0 / mês.

Quando o teto é atingido, as opções são:

  1. Parar de enviar traces ao Langfuse: remova LANGFUSE_PUBLIC_KEY / LANGFUSE_SECRET_KEY e reimplante.
  2. Trocar para o Logfire (10M spans/mês grátis): veja a §3.4.
  3. Fazer upgrade do Langfuse para Pro (camada paga).
  4. Auto-hospedar o Langfuse via o serviço comentado na pilha Docker Compose opcional.

Para o caminho de CI de avaliação, o backend Phoenix auto-hospedado não tem teto de cota no armazenamento de traces; a restrição operativa ali é o gate de custo estrito que bloqueia o PR (§6), não uma cota de observações.

SintomaCausa provávelCorreção
Sem spans no LangfuseChaves não definidas ou host erradoReverifique as variáveis de ambiente
make obs-up dá erroDaemon do Docker não está rodandoInicie o Docker
UI do Phoenix vaziaPHOENIX_OTLP_ENDPOINT não definido no produtorExporte a variável de ambiente antes de rodar a avaliação
make eval trava > 10 sExportador OTLP bloqueado em um endpoint ausenteRemova os endpoints OTLP / reinicie a avaliação
Relatório de custo ausenteUnidades de custo nunca registradasO grafo acrescenta unidades de custo a partir do nó de geração; confirme contra o caminho de teste de relatório de custo