Instrumentar uma aplicação de IA com OpenTelemetry pode parecer simples. A requisição entra, a aplicação chama um modelo, recebe uma resposta e encerra o trace. No dashboard, vemos que uma chamada levou três segundos e terminou sem erro. Tecnicamente, o sistema está instrumentado. Na prática, ainda sabemos muito pouco sobre o que aconteceu.
Uma resposta ruim pode ter sido causada pelo modelo, pelo contexto enviado, por um documento desatualizado, por uma ferramenta que retornou dados incompletos ou por um fallback acionado silenciosamente. Também é possível que a chamada tenha funcionado perfeitamente do ponto de vista técnico, mas a resposta tenha violado uma regra do negócio.
Um trace tradicional costuma responder perguntas como: quanto tempo a requisição levou, quais serviços foram chamados e onde ocorreu uma exceção. Em aplicações de IA, precisamos responder algumas perguntas adicionais: qual modelo foi utilizado, qual versão do prompt estava ativa, que contexto foi recuperado, quais ferramentas participaram e se a resposta passou pelos critérios de qualidade esperados.
Isso não significa registrar tudo. O trace não deveria virar uma cópia completa da conversa, dos documentos internos e dos argumentos enviados às ferramentas. O objetivo é registrar informações suficientes para explicar a execução sem transformar a plataforma de observabilidade em um novo risco de segurança.
OpenTelemetry continua sendo OpenTelemetry
OpenTelemetry não é uma ferramenta exclusiva para IA. Ele continua oferecendo os mesmos conceitos usados em sistemas distribuídos tradicionais: traces, spans, métricas e logs. O que muda é o tipo de operação que queremos representar.
Em uma aplicação comum, um trace pode mostrar uma chamada HTTP, uma consulta ao banco e uma publicação em fila. Em uma aplicação de IA, o mesmo trace também pode mostrar:
- a execução de um agente ou workflow;
- a recuperação de contexto;
- uma ou mais chamadas ao modelo;
- o uso de ferramentas;
- a aplicação de guardrails;
- a avaliação da resposta;
- um fallback para outro modelo ou para atendimento humano.
A instrumentação automática ainda é importante. Ela mostra chamadas HTTP, acesso a banco, filas e dependências externas. Porém, essas operações não explicam sozinhas a intenção do sistema. Para isso, normalmente precisamos adicionar alguns spans de negócio.
Um trace precisa contar a história da execução
Imagine um assistente de atendimento que responde dúvidas sobre reembolso. Para produzir uma resposta, ele consulta uma política interna, chama um modelo, busca os dados do pedido e verifica se a resposta respeita as regras da empresa.
Um trace útil poderia ser representado assim:
POST /support/answer
└── responder dúvida sobre reembolso
├── recuperar política de reembolso
├── chamar modelo
├── consultar pedido
├── chamar modelo novamente
└── validar resposta
Essa árvore é mais útil do que enxergar apenas:
POST /support/answer
└── POST /model
No segundo caso, sabemos que o modelo foi chamado. No primeiro, conseguimos entender o caminho percorrido pela aplicação.
O trace não precisa representar cada detalhe interno do algoritmo. Ele precisa destacar as etapas que ajudam alguém a investigar latência, custo, comportamento ou qualidade.
O span principal deve representar a tarefa
O span principal da execução deveria representar o que a aplicação está tentando fazer. Nomes genéricos como agent, llm-call ou ai-operation fornecem pouca informação.
Alguns nomes mais úteis seriam:
responder dúvida sobre reembolso
analisar incidente de checkout
revisar pull request
extrair informações de contrato
classificar solicitação de suporte
Nesse span principal, podemos registrar informações estáveis sobre a tarefa:
workflow.name: customer-support
workflow.version: 3.2
task.type: refund-question
result: answered
fallback.used: false
human.review.required: false
Esses atributos permitem comparar execuções sem registrar a pergunta completa do usuário. Também facilitam métricas como taxa de fallback por workflow, latência por tipo de tarefa e percentual de casos enviados para revisão humana.
O que registrar na chamada ao modelo
A chamada ao modelo é uma parte importante do trace, mas não deveria ser a única. Nesse span, o objetivo é registrar informações que ajudem a identificar mudanças de comportamento, custo e performance.
As informações mais úteis normalmente são:
| Informação | Por que ajuda |
|---|---|
| Operação | Diferencia chat, geração, embedding ou classificação |
| Provider | Mostra qual plataforma executou a chamada |
| Modelo solicitado | Indica o modelo configurado pela aplicação |
| Modelo utilizado | Mostra a versão que realmente respondeu |
| Versão do prompt | Permite relacionar regressões a uma mudança |
| Tokens de entrada | Ajuda a entender contexto e custo |
| Tokens de saída | Ajuda a entender custo e tamanho da resposta |
| Duração | Mostra o tempo total da chamada |
| Motivo de encerramento | Indica conclusão, limite de tokens, bloqueio ou erro |
| Número da tentativa | Mostra retries |
| Fallback | Indica se o modelo era a primeira opção |
Um trace poderia mostrar:
chamar modelo
provider: provider-a
requested.model: model-standard
response.model: model-standard-2026-06
prompt.name: answer-refund-policy
prompt.version: 7
input.tokens: 1840
output.tokens: 220
finish.reason: stop
retry.count: 0
Esse conjunto de metadados costuma ser suficiente para muitas investigações. O texto completo do prompt e da resposta não precisa estar presente em todas as execuções.
Não confunda trace com histórico de conversa
Um dos erros mais comuns em observabilidade de IA é armazenar o prompt completo, a pergunta do usuário, a resposta do modelo e todos os documentos recuperados dentro dos spans.
Isso parece útil porque facilita o debugging. Porém, em produção, esse conteúdo pode incluir:
- dados pessoais;
- informações financeiras;
- documentos internos;
- código proprietário;
- credenciais;
- contratos;
- mensagens de clientes;
- argumentos sensíveis enviados a ferramentas.
Além do risco de segurança, armazenar esse conteúdo aumenta o volume de telemetria e torna a consulta mais difícil.
Uma alternativa mais segura é registrar referências e versões:
prompt.name: answer-refund-policy
prompt.version: 7
context.source: refund-policies
context.index.version: 2026-07
context.documents.selected: 3
Caso seja necessário investigar o conteúdo exato, ele pode estar disponível em um armazenamento separado, com acesso controlado e retenção específica. O trace aponta para a execução, mas não precisa carregar todo o conteúdo.
Como observar o contexto recuperado
Em aplicações com RAG, parte da resposta depende dos documentos encontrados. Quando a resposta está errada, o problema pode não estar no modelo. O sistema pode ter recuperado documentos irrelevantes, desatualizados ou sem permissão adequada.
Um span de recuperação de contexto deveria mostrar informações como:
recuperar política de reembolso
source: customer-policies
index.version: 2026-07-10
documents.found: 8
documents.selected: 3
top.score: 0.91
permission.filter: applied
reranking.used: true
O objetivo é permitir que alguém investigue se o contexto correto chegou ao modelo.
Não é necessário colocar o conteúdo integral dos documentos no trace. Em muitos casos, quantidade, origem, versão do índice e scores já ajudam bastante. Identificadores de documentos podem ser registrados apenas quando forem realmente necessários e quando o acesso estiver protegido.
Como observar ferramentas
Quando um agente chama uma ferramenta, ele deixa de apenas produzir texto e passa a consultar ou alterar outros sistemas. Por isso, tool calls merecem spans próprios.
Uma ferramenta pode consultar um pedido, buscar um documento, abrir um ticket ou executar uma alteração. O trace deveria deixar claro:
- qual ferramenta foi chamada;
- se a operação era de leitura ou escrita;
- quanto tempo levou;
- se a autorização foi aceita;
- se houve retry;
- se houve timeout;
- se a operação exigia aprovação;
- qual foi o resultado.
Exemplo:
consultar pedido
tool.name: get-order
tool.version: 2
tool.access: read
authorization.result: allowed
approval.required: false
result: success
duration.ms: 120
Para uma ferramenta de escrita:
cancelar pedido
tool.name: cancel-order
tool.access: write
authorization.result: allowed
approval.required: true
approval.result: approved
result: success
Essa distinção ajuda a separar uma simples consulta de uma operação capaz de produzir efeitos reais.
Os argumentos completos da ferramenta também não deveriam ser registrados por padrão. Em vez de armazenar todo o payload, podemos registrar o tipo da operação, a quantidade de itens, o tamanho do payload ou um identificador protegido.
Avaliação também faz parte do trace
Em aplicações tradicionais, uma resposta HTTP 200 geralmente indica que a operação funcionou. Em aplicações de IA, uma chamada pode terminar sem erro técnico e ainda produzir uma resposta ruim.
Por isso, a avaliação da resposta pode aparecer como uma etapa do trace:
validar resposta
evaluation.name: refund-policy-adherence
evaluator.version: 2.1
score: 0.94
result: pass
final.action: answer
Em um caso reprovado:
validar resposta
evaluation.name: refund-policy-adherence
score: 0.42
result: fail
final.action: human-review
Esse detalhe ajuda a separar três resultados diferentes:
- A infraestrutura falhou.
- A chamada ao modelo funcionou, mas a qualidade foi insuficiente.
- A resposta foi produzida e aprovada.
Sem essa separação, dashboards podem indicar 100% de sucesso enquanto usuários recebem respostas inadequadas.
Fallbacks precisam estar visíveis
Uma aplicação pode tentar um modelo principal e, em caso de timeout, usar um modelo alternativo. Também pode reduzir o contexto, usar uma resposta em cache ou encaminhar a tarefa para uma pessoa.
Se o fallback não aparecer no trace, a resposta será analisada como se tivesse seguido o caminho normal.
Um exemplo:
chamar modelo principal
result: timeout
chamar modelo alternativo
fallback.reason: primary-model-timeout
fallback.position: 1
result: success
Isso permite responder perguntas importantes:
- Qual modelo está falhando mais?
- Quantas execuções usam fallback?
- O fallback aumenta a latência?
- A qualidade cai quando o modelo alternativo é utilizado?
- O custo está sendo afetado por tentativas duplicadas?
Fallback silencioso pode preservar a disponibilidade, mas esconder um problema operacional. Observabilidade deve tornar esse comportamento visível.
Um exemplo simples em .NET
Os conceitos são independentes de linguagem. Java, Python, Go, JavaScript e outras plataformas possuem bibliotecas para criar spans e adicionar atributos. Em .NET, esse trabalho normalmente é feito com ActivitySource.
O exemplo abaixo cria um span para uma chamada ao modelo e registra apenas metadados:
private static readonly ActivitySource Source =
new("MyCompany.AI");
public async Task<ModelResponse> GenerateAsync(
ModelRequest request,
CancellationToken cancellationToken)
{
using var activity = Source.StartActivity(
"chamar modelo",
ActivityKind.Client);
activity?.SetTag("ai.provider", request.Provider);
activity?.SetTag("ai.request.model", request.Model);
activity?.SetTag("ai.prompt.name", request.PromptName);
activity?.SetTag("ai.prompt.version", request.PromptVersion);
try
{
var response = await _client.GenerateAsync(
request,
cancellationToken);
activity?.SetTag("ai.response.model", response.Model);
activity?.SetTag("ai.input.tokens", response.InputTokens);
activity?.SetTag("ai.output.tokens", response.OutputTokens);
activity?.SetTag("ai.finish.reason", response.FinishReason);
return response;
}
catch (Exception exception)
{
activity?.SetStatus(
ActivityStatusCode.Error,
exception.Message);
throw;
}
}
Em outra linguagem, a estrutura seria praticamente a mesma:
iniciar span "chamar modelo"
adicionar provider
adicionar modelo solicitado
adicionar versão do prompt
executar chamada
adicionar modelo respondente
adicionar tokens
adicionar motivo de encerramento
se houver erro:
marcar span como erro
encerrar span
O ponto importante não é a API específica do .NET. É a decisão sobre quais informações pertencem ao span.
Use instrumentação automática e manual juntas
OpenTelemetry consegue instrumentar automaticamente várias operações, como:
- chamadas HTTP;
- acesso a banco;
- filas;
- caches;
- frameworks web;
- clientes de serviços externos.
Essa instrumentação deve continuar existindo. Porém, ela não sabe necessariamente que uma consulta ao banco fazia parte de uma recuperação de contexto ou que uma chamada HTTP era uma ferramenta usada por um agente.
Por isso, a combinação ideal costuma ser:
span manual: recuperar contexto
└── span automático: consulta ao vector database
span manual: executar ferramenta
└── span automático: HTTP GET /orders/123
O span manual explica a intenção. O span automático explica a infraestrutura.
Também é importante evitar duplicação. Se o framework do agente, o cliente MCP e a biblioteca HTTP criarem spans equivalentes para a mesma operação, o trace pode ficar difícil de ler. Cada span deveria representar uma etapa lógica ou uma fronteira técnica relevante.
Traces, métricas e logs têm papéis diferentes
Nem toda informação precisa estar em todos os traces.
Traces são melhores para investigar uma execução específica. Eles mostram a sequência, as dependências e o ponto em que algo mudou.
Métricas são melhores para acompanhar comportamento agregado, como:
- latência por modelo;
- tokens por workflow;
- taxa de fallback;
- erros por ferramenta;
- avaliações reprovadas;
- custo médio por tarefa.
Logs são úteis para eventos operacionais, auditoria e mudanças de configuração, como:
- publicação de uma nova versão do prompt;
- ativação de um modelo;
- mudança em uma política;
- alteração de permissões de uma ferramenta.
Um trace pode indicar que o prompt v7 foi usado. Uma métrica mostra a taxa de reprovação do v7. Um log de auditoria mostra quando o v7 foi publicado e por quem.
Cuidado com cardinalidade
Alguns atributos possuem poucos valores possíveis e são bons para dashboards:
workflow.name
model.name
provider.name
prompt.version
tool.name
evaluation.result
Outros possuem milhões de valores diferentes:
user.question
document.content
customer.id
conversation.id
tool.arguments
model.response
Atributos de alta cardinalidade aumentam custo e dificultam agregações. Eles não deveriam ser usados indiscriminadamente como dimensões de métricas.
Uma boa regra é usar atributos estáveis para dashboards e deixar detalhes específicos apenas nos traces, quando realmente forem necessários. Mesmo no trace, conteúdo sensível e identificadores pessoais precisam de tratamento cuidadoso.
Uma política simples de captura
Uma estratégia prática é trabalhar com três níveis.
Produção padrão
Registra metadados sobre workflow, modelo, prompt, tokens, retrieval, ferramentas, avaliações e fallbacks. Não armazena conteúdo completo.
Diagnóstico controlado
Permite capturar uma pequena amostra com conteúdo mascarado, por tempo limitado, com acesso restrito e retenção curta.
Desenvolvimento
Pode registrar conteúdo mais detalhado, desde que sejam usados dados sintéticos ou anonimizados.
Essa divisão evita que uma configuração temporária de debugging se transforme em captura permanente de informações sensíveis.
Como começar sem instrumentar tudo
Uma aplicação não precisa nascer com dezenas de atributos. Um caminho simples pode ser dividido em três etapas.
Na primeira, crie spans para as principais partes da execução:
workflow
modelo
retrieval
ferramentas
avaliação
Na segunda, adicione os metadados mais úteis:
modelo
provider
versão do prompt
tokens
resultado
fallback
Na terceira, evolua para informações de qualidade e governança:
scores de avaliação
versões de políticas
controle de conteúdo
sampling
redaction
retenção
Essa evolução gradual reduz a chance de o time criar uma instrumentação enorme antes de saber quais perguntas realmente precisa responder.
Como seria um trace útil
Um trace de um assistente de atendimento poderia terminar assim:
POST /support/answer
└── responder dúvida sobre reembolso
workflow.version: 3.2
task.type: refund-question
├── recuperar política
│ source: customer-policies
│ index.version: 2026-07
│ documents.selected: 3
│
├── chamar modelo
│ provider: provider-a
│ request.model: model-standard
│ prompt.version: 7
│ input.tokens: 1840
│ output.tokens: 96
│ finish.reason: tool-call
│
├── consultar pedido
│ tool.name: get-order
│ tool.access: read
│ authorization.result: allowed
│
├── chamar modelo
│ input.tokens: 2130
│ output.tokens: 248
│ finish.reason: stop
│
└── validar resposta
evaluation.name: refund-policy-adherence
score: 0.94
result: pass
final.action: answer
Esse trace não contém a pergunta do cliente, o conteúdo da política, os dados completos do pedido nem a resposta final. Mesmo assim, ele explica como o sistema chegou ao resultado.
Caso a resposta esteja incorreta, o time pode começar a investigação sabendo:
- qual workflow foi executado;
- qual versão estava ativa;
- qual contexto foi recuperado;
- qual modelo respondeu;
- qual ferramenta foi chamada;
- quanto contexto foi consumido;
- qual avaliação aprovou a resposta.
Esse é o equilíbrio desejado entre utilidade e exposição.
Fechando a ideia
Observabilidade de IA não deveria começar pela captura de prompts e respostas. Ela deveria começar pela estrutura da execução.
Um bom trace mostra qual tarefa estava sendo realizada, que modelo participou, qual versão do prompt estava ativa, que contexto foi recuperado, quais ferramentas foram chamadas, se houve fallback e como a resposta foi avaliada.
OpenTelemetry oferece uma base comum para representar essas operações em diferentes linguagens e plataformas. O exemplo em .NET muda em Java, Python, Go ou JavaScript, mas o desenho permanece o mesmo: cada etapa importante vira um span, e cada span recebe apenas os metadados necessários para explicar seu comportamento.
A maturidade não está em registrar tudo. Está em conseguir responder perguntas importantes sem transformar observabilidade em um depósito de conteúdo sensível.
Porque um trace útil não precisa reproduzir toda a conversa. Ele precisa explicar o caminho que levou até a resposta.
