Langfuse WorkshopClickHouse Workshops

02 Tracing

Este é o ponto inicial da etapa de tracing — o mesmo código de checkpoint/01-base-app, ainda sem integração com o Langfuse. Os pacotes já estão em package.json — execute npm inst...

O material do workshop é mantido no repositório público langfuse/langfuse-workshop. Use o repositório para executar a aplicação, acessar os branches de checkpoint e fazer a configuração local.

Ver este arquivo Markdown

Ponto de partida

git checkout checkpoint/02-tracing

Este é o ponto inicial da etapa de tracing — o mesmo código de checkpoint/01-base-app, ainda sem integração com o Langfuse. Os pacotes já estão em package.json; execute npm install se ainda não fez isso. Confirme que .env contém sua OPENAI_API_KEY e as chaves do Langfuse.

Por que fazemos tracing

Tracing registra cada etapa do agente — chamadas ao modelo, ferramentas invocadas, entradas e saídas — na ordem em que ocorreram. Ele transforma o agente de uma caixa-preta em algo que pode ser examinado depois; quando uma resposta estiver errada, você identifica a etapa exata em vez de adivinhar.

Para entender a motivação geral, consulte a lição da Langfuse Academy sobre tracing. Para detalhes técnicos — opções do SDK, internals do OpenTelemetry e atributos de spans — consulte a documentação de tracing.

Objetivo

Quando o pai pergunta "Como ligo o Bluetooth?", o agente não chama a OpenAI apenas uma vez. Ele pergunta o que fazer, chama get_support_context para buscar a configuração do iPhone, pergunta novamente, chama search_help_library para obter as etapas do Bluetooth e faz mais uma chamada à OpenAI para produzir a resposta numerada. Nada disso está visível hoje.

O objetivo é tornar todas essas etapas visíveis no Langfuse: um turno de chat vira um trace aninhado, com a execução do agente, as generations da OpenAI e as duas chamadas de ferramentas registradas em ordem.

Processo passo a passo de Specs

Construiremos o trace em três etapas que refletem a estrutura do agente:

  1. Primeiro trace — registrar as generations da OpenAI.
  2. Traces aninhados — agrupar as generations em uma execução do agente por turno.
  3. Registro das ferramentas — transformar cada invocação em uma observação.

Etapa 1 — Primeiro trace

Queremos observar as chamadas à OpenAI para ver entradas, saídas, custo, tokens e tempo. Duas mudanças bastam.

src/server/index.ts

Inicie o processador de spans do Langfuse perto do início do arquivo:

import { NodeSDK } from "@opentelemetry/sdk-node";
import { LangfuseSpanProcessor } from "@langfuse/otel";

new NodeSDK({ spanProcessors: [new LangfuseSpanProcessor()] }).start();

O processador lê LANGFUSE_PUBLIC_KEY, LANGFUSE_SECRET_KEY e LANGFUSE_BASE_URL do ambiente do processo Node. O servidor carrega o .env do repositório antes do SDK; edite .env em vez de depender de valores exportados no shell.

Observação: se o último trace às vezes aparecer atrasado ao parar ou reiniciar npm run dev, volte a index.ts e transforme a linha acima nas variáveis langfuseSpanProcessor e sdk, permitindo que shutdown() faça flush:

const langfuseSpanProcessor = new LangfuseSpanProcessor();
const sdk = new NodeSDK({ spanProcessors: [langfuseSpanProcessor] });
sdk.start();

async function shutdown() {
  server.close();
  await langfuseSpanProcessor.forceFlush();
  await sdk.shutdown();
}

Isso não é necessário para compreender tracing, mas evita a confusão de perder o último trace no desenvolvimento local.

src/server/support-agent.ts

Adicione o import:

import { observeOpenAI } from "@langfuse/openai";

Depois, envolva o cliente OpenAI no ponto em que ele é criado. Encontre esta linha em runSupportConversation:

const openai = new OpenAI({ apiKey: env.openaiApiKey });

e altere para:

const openai = observeOpenAI(new OpenAI({ apiKey: env.openaiApiKey }));

Essa é toda a mudança. Sem factory ou cliente bruto separado: observeOpenAI envolve o cliente inline, e openai.chat.completions.create(...) passa a emitir um trace em cada chamada.

Verifique: execute npm run dev, faça uma pergunta e atualize o Langfuse. Você verá uma generation por chamada, com prompt, resposta, tokens e latência. Cada generation ainda é um trace de nível superior; corrigiremos isso em seguida.

Visualização Traces depois da Etapa 1 — cada turno aparece como generations openai-chat-completion independentes.

Etapa 2 — Traces aninhados

Para contextualizar as generations, vamos agrupá-las em uma execução de agente por turno. São três edições em src/server/support-agent.ts, sem mudar o corpo da função.

1. Adicione o import:

import { observe } from "@langfuse/tracing";

2. Rebaixe a função existente. Encontre:

export async function runSupportConversation(request: ChatRequest): Promise<ChatResponse> {

Remova o export e renomeie:

async function runSupportConversationInner(request: ChatRequest): Promise<ChatResponse> {

O corpo permanece igual.

3. Adicione o export envolvido ao fim do arquivo:

export const runSupportConversation = observe(runSupportConversationInner, {
  name: "dad-it-support-chat-turn",
  asType: "agent"
});

index.ts continua importando runSupportConversation da mesma forma. observe(...) captura automaticamente o argumento como entrada do trace e o retorno como saída.

Verifique: um turno agora deve aparecer como uma única observação dad-it-support-chat-turn, com a generation da OpenAI aninhada.

Árvore de trace após a Etapa 2 — uma raiz de agente dad-it-support-chat-turn com a generation da OpenAI como filha.

Etapa 3 — Registrar chamadas de ferramentas

A generation já menciona as ferramentas na saída tool_calls, mas não há uma observação da execução real — não vemos a entrada nem a saída. O mesmo padrão observe(...) pode envolver cada ferramenta.

src/server/tools.ts

Adicione o import e os dois helpers observados acima de executeTool. Depois, substitua o executeTool existente pela versão abaixo, para que o switch chame os helpers. Não altere TOOL_DEFINITIONS.

import { observe } from "@langfuse/tracing";

const getSupportContextTool = observe(
  async () => {
    const context = getSupportContext();

    return {
      ok: true,
      context: {
        id: context.id,
        label: context.label,
        devices: context.devices,
        deviceSummary: context.deviceSummary,
        responseStyle: context.responseStyle,
        scopeHighlights: context.scopeHighlights,
        notableApps: context.notableApps
      }
    };
  },
  { name: "get_support_context", asType: "tool" }
);

const searchHelpLibraryTool = observe(
  async (input: { question: string }) => {
    const guides = searchGuides(input.question);

    return {
      ok: true,
      results: guides.map((guide) => ({
        id: guide.id,
        title: guide.title,
        summary: guide.summary,
        steps: guide.steps,
        caution: guide.caution ?? null
      }))
    };
  },
  { name: "search_help_library", asType: "tool" }
);

export async function executeTool(name: string, input: Record<string, unknown>): Promise<ToolResult> {
  switch (name) {
    case "get_support_context":
      return getSupportContextTool();

    case "search_help_library":
      return searchHelpLibraryTool({ question: String(input.question ?? "") });

    default:
      return { ok: false, error: `Unsupported tool: ${name}` };
  }
}

Se npm run dev parar com Multiple exports with the same name "executeTool", o executeTool original ainda está mais abaixo no arquivo. Exclua-o e mantenha apenas a versão acima.

Trace completo após a Etapa 3 — dad-it-support-chat-turn com a generation da OpenAI e as observações get_support_context + search_help_library abaixo.

Como verificar a conclusão

  • Um turno cria um trace no Langfuse.
  • Observação raiz: dad-it-support-chat-turn (tipo agent).
  • Generation filha de observeOpenAI(...) com prompt, resposta, tokens e latência.
  • Observações filhas de ferramentas: get_support_context, search_help_library.
  • A entrada raiz é a requisição do chat; a saída raiz é a resposta.

Encerramento

Mesmo padrão, tipos de observação diferentes: observe(fn, { asType }) envolve uma função e emite um span com o nome e tipo informados. observeOpenAI(client) é uma versão especializada para o SDK da OpenAI.

Uma forma mais direta de adicionar tracing rico seguindo as práticas recomendadas é a skill do Langfuse (/langfuse). Ela aplica os padrões ao seu código sem que você implemente cada wrapper manualmente. Este percurso mostra o que acontece por baixo dos panos.

observeOpenAI envolve o SDK oficial da OpenAI — por baixo, é equivalente à instrumentação automática para OpenAI JS. Se você usa outro SDK — Anthropic, Vercel AI SDK ou seu próprio cliente HTTP — o catálogo de integrações oferece o wrapper ou guia equivalente.

Apêndice/Bônus — IDs de usuário e sessão

O percurso acima produz um trace limpo no formato pai → generation → ferramenta. Em seguida, muitas equipes querem segmentar por usuário e por sessão — para ver "todos os turnos deste usuário" ou "a sessão completa de ontem". Para simplificar a apresentação, pulamos essa etapa ao vivo, mas os checkpoints posteriores a incluem para que os capítulos seguintes usem as visualizações sem mais código. Consulte Sessions e Users na documentação.

Em resumo:

import { propagateAttributes } from "@langfuse/tracing";

return propagateAttributes(
  {
    userId: request.userId ?? `workshop-${context.id}`,
    sessionId: request.sessionId,
    tags: ["langfuse-workshop", "dad-it-support"]
  },
  async () => {
    // ...the same tool-calling loop...
  }
);

Tudo dentro de propagateAttributes(...) — inclusive os spans filhos emitidos por observeOpenAI — recebe automaticamente userId, sessionId e tags. As visualizações Users, Sessions e os filtros de tags do Langfuse passam a funcionar assim que esses atributos existem.

Estado final

Esta aplicação com tracing concluído é o ponto de partida para 03-prompt-management e 04-monitoring.

Nesta página

Acompanhar seu progresso?

Opcional. Enviaremos um link por e-mail para confirmar seu endereço; o progresso será registrado depois que você o abrir.

Use seu e-mail corporativo, não um endereço pessoal.

O acompanhamento do progresso também exige a aceitação dos Termos de Serviço atuais nas Configurações de privacidade.

PT