Microsoft Foundry: o log da resposta não explica o percurso

Microsoft Foundry: o log da resposta não explica o percurso

Read in English: Microsoft Foundry: the response log does not explain the path

Imagine um agente de suporte que responde sobre contestações de cartão. O cliente pergunta pela contestação DSP-1001 e recebe “foi aprovada”. Mais tarde, pergunta de novo e recebe “está em análise”. As duas respostas são fluentes e se contradizem, e o log do aplicativo guarda só as duas frases. A segunda veio de um cache com dado de 17/10, depois de duas chamadas ao backend que estouraram o tempo, e nada na frase conta isso. O caso é ilustrativo, mas o padrão é real: o log da resposta mostra o que o agente disse, não o caminho que ele percorreu.

Resposta não é percurso

O que é um trace de agente?

É o registro de uma execução como uma árvore de spans: cada passo que o agente deu vira um span, com início, fim, status e atributos. No exemplo medido mais adiante, o Agent Framework gera três tipos: invoke_agent, a raiz, uma por execução; chat, uma por chamada ao modelo; e execute_tool, uma por ferramenta executada. A documentação do Foundry resume a utilidade em duas perguntas: de onde veio uma resposta e qual passo introduziu um erro ou um pico de latência.

O artigo sobre guardrails e avaliação de agentes tratou de instrumentos, como evaluators e red teaming, que rodam antes do deploy e medem em agregado. Aqui o recorte é a investigação de um caso depois do incidente, não a avaliação agregada antes do deploy.

Peça Estado em 19/10/2026 O que isso muda
Tracing de agentes de prompt e hospedados GA A cobertura é estável para esses dois tipos
Tracing de agentes de workflow e externos Preview A cobertura pode mudar
Tracing no projeto Foundry Desligado por padrão Nada é coletado nem guardado até Foundry Account Owner ou Foundry Owner ligar
Retenção e amostragem do trace Seguem o Application Insights O trace do caso pode não existir quando você for procurar
Convenções OpenTelemetry GenAI Status Development Nomes e atributos podem mudar entre versões
Conteúdo das mensagens e das ferramentas Opt-in na instrumentação No Agent Framework, ENABLE_SENSITIVE_DATA vem desligada
<table_name>_steps do evaluate_data_agent, no Fabric Preview É gerada por uma rodada de avaliação, não por tráfego de produção

Leitura das fontes: 19/10/2026. O quadro resume a documentação lida nessa data. O exemplo da seção prática foi executado localmente; nada aqui foi medido em um projeto Foundry nem em um tenant.

O problema que resolve

Quando o único registro é a resposta, a investigação começa na frase e termina em suposição. O mapa abaixo evita três erros: procurar a causa na frase devolvida, ligar o conteúdo em produção para ver um percurso que a estrutura já mostra e ligar o trace sem proteger o conteúdo.

Quadro de seis faixas e três colunas: log da resposta, trace e conteúdo. As faixas mostram o caso, a camada, o padrão, o que cada camada mostra no caso, o erro comum e como corrigir. Verde marca o que mostra o percurso ou protege; âmbar, o que só aparece com uma decisão; rosa, o que não mostra ou expõe.
Uma pergunta, três camadas. Verde mostra o percurso ou protege, âmbar só aparece com uma decisão, rosa não mostra ou expõe. O caso da contestação é ilustrativo. Clique para ampliar.

Como funciona — passo a passo

  1. Ligue o tracing no projeto. Ele vem desligado, e nada é coletado nem guardado até que Foundry Account Owner ou Foundry Owner o ative. Depois de ligado, o projeto oferece traces prontos dos últimos 90 dias.
  2. Ache a execução do caso. Na aba Traces dá para buscar, filtrar e ordenar os traces ingeridos, e a busca por Response ID ou por Trace ID leva à conversa.
  3. Leia a estrutura antes do conteúdo: a ordem dos spans, o status, o error.type e os tokens de entrada de cada volta respondem à maior parte das perguntas.
  4. Conte as tentativas pelos spans execute_tool repetidos. O artigo sobre tool calling mostra o laço em que o modelo pede a ferramenta e o código a executa; cada volta desse laço deixa um span.
  5. Decida o conteúdo. No Agent Framework, a variável ENABLE_SENSITIVE_DATA vem desligada, e a documentação manda ligá-la só em desenvolvimento ou teste.
  6. Proteja a tabela AppGenAIContent com o nível Protected, para que só o papel Privileged Monitoring Data Reader leia o conteúdo.
  7. Teste o acesso antes do incidente: quem só tem Log Analytics Reader não deve ver o conteúdo sensível, e quem tem o papel privilegiado deve.

Um caso de uso: a contestação de cartão

O agente de suporte tem duas ferramentas: get_dispute_status, que consulta o backend, e get_dispute_status_cached, que lê um cache. No exemplo, o roteiro do cliente de chat falso decide qual chamar; em um agente real quem decide é o modelo, e é por isso que o percurso importa. Quem investiga o caso faz seis perguntas, e só três precisam do conteúdo:

Pergunta de quem investiga Onde está Precisa do conteúdo?
Quais ferramentas rodaram, e em que ordem? Spans execute_tool, na ordem da execução Não
Qual chamada falhou ou se repetiu? Status ERROR e error.type do span Não
Quanto contexto cada volta carregou? gen_ai.usage.input_tokens dos spans chat Não
Com quais argumentos a ferramenta rodou? gen_ai.tool.call.arguments Sim
O que ela devolveu, e de que data era o dado? gen_ai.tool.call.result Sim
Qual foi a mensagem do erro? Evento exception, atributo exception.message Sim; o Agent Framework só a inclui com a flag ligada

Na prática: o mesmo agente, duas execuções

O exemplo roda o mesmo agente em duas situações: com o backend saudável e com o backend fora do ar. Ele não rodou em um projeto Foundry, em um Application Insights nem com um modelo real: um cliente de chat falso repete um roteiro, e exportadores em memória guardam spans, métricas e logs. O que ele mostra é o que o Agent Framework grava, não o que o Foundry exibe. O código e as respostas ficam em inglês nos dois idiomas, para que as duas versões do artigo publiquem o mesmo programa.

Medi com agent-framework-core 1.22.0 e OpenTelemetry SDK 1.45.1, em Python 3.12.10. O espelho de pacotes do meu ambiente só tinha até a 1.21.0, então instalei a 1.22.0 da tag python-1.22.0; a 1.21.0 entra só na comparação entre versões, mais adiante. Os tokens de entrada do cliente de chat são uma estimativa (chars // 4), não a contagem de um tokenizador. Os três blocos abaixo formam um programa só: salve-os em um arquivo, na ordem, e rode com python.

O primeiro bloco liga a instrumentação com exportadores em memória e define o roteiro: a pergunta, o e-mail do cliente (dado pessoal, de propósito) e as duas execuções, ok e failed. O conjunto FLAG_KEYS guarda os cinco atributos sensíveis que a flag de conteúdo controla.

import asyncio
import json
import logging
from collections.abc import Mapping, Sequence
from typing import Any, ClassVar

from agent_framework import (
    Agent,
    BaseChatClient,
    ChatMiddlewareLayer,
    ChatResponse,
    Content,
    FunctionInvocationLayer,
    Message,
    tool,
)
from agent_framework.observability import ChatTelemetryLayer, enable_instrumentation
from opentelemetry import metrics, trace
from opentelemetry.sdk.metrics import MeterProvider
from opentelemetry.sdk.metrics.export import InMemoryMetricReader
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import SimpleSpanProcessor
from opentelemetry.sdk.trace.export.in_memory_span_exporter import InMemorySpanExporter

exporter = InMemorySpanExporter()
provider = TracerProvider()
provider.add_span_processor(SimpleSpanProcessor(exporter))
trace.set_tracer_provider(provider)
reader = InMemoryMetricReader()
metrics.set_meter_provider(MeterProvider(metric_readers=[reader]))


class Lines(logging.Handler):
    def __init__(self) -> None:
        super().__init__()
        self.lines: list[str] = []

    def emit(self, record: logging.LogRecord) -> None:
        self.lines.append(record.getMessage())


log = Lines()
logging.getLogger().addHandler(log)
logging.getLogger().setLevel(logging.DEBUG)

QUESTION = "What is the status of dispute DSP-1001?"
EMAIL = "maria@example.com"
CALL = {"dispute_id": "DSP-1001", "customer_email": EMAIL}
SCRIPTS = {
    "ok": [("get_dispute_status", CALL), "Dispute DSP-1001 was approved."],
    "failed": [
        ("get_dispute_status", CALL),
        ("get_dispute_status", CALL),
        ("get_dispute_status_cached", {"dispute_id": "DSP-1001"}),
        "Dispute DSP-1001 is under review.",
    ],
}
FLAG_KEYS = {
    "gen_ai.input.messages", "gen_ai.output.messages", "gen_ai.system_instructions",
    "gen_ai.tool.call.arguments", "gen_ai.tool.call.result",
}

O segundo bloco traz o cliente de chat falso, que devolve o próximo passo do roteiro, as duas ferramentas e a função que roda o agente com a flag de conteúdo ligada ou desligada. A classe Client empilha sobre ele as camadas de ferramenta, de middleware e de telemetria do Agent Framework; é a de telemetria que cria os spans chat.

class Scripted(BaseChatClient):
    def __init__(self, script: list, **kwargs: Any) -> None:
        super().__init__(**kwargs)
        self.script, self.step = script, 0

    def _inner_get_response(self, *, messages: Sequence[Message], stream: bool,
                            options: Mapping[str, Any], **kwargs: Any):
        move = self.script[self.step]
        self.step += 1
        if isinstance(move, str):
            content = Content.from_text(move)
        else:
            content = Content.from_function_call(call_id=f"call_{self.step}", name=move[0], arguments=move[1])
        chars = sum(len(str(getattr(c, a, None) or "")) for m in messages for c in m.contents
                    for a in ("text", "arguments", "result", "exception"))
        response = ChatResponse(
            messages=[Message(role="assistant", contents=[content])], model="script-1",
            usage_details={"input_token_count": chars // 4, "output_token_count": 10})

        async def reply() -> ChatResponse:
            return response

        return reply()


class Client(FunctionInvocationLayer, ChatMiddlewareLayer, ChatTelemetryLayer, Scripted):
    OTEL_PROVIDER_NAME: ClassVar[str] = "script"


def make_tools(backend_up: bool) -> list:
    @tool(approval_mode="never_require")
    async def get_dispute_status(dispute_id: str, customer_email: str) -> dict:
        """Look up the dispute status in the backend."""
        if not backend_up:
            raise TimeoutError("backend timed out after 2 s")
        return {"dispute_id": dispute_id, "status": "approved", "updated_at": "2026-10-18"}

    @tool(approval_mode="never_require")
    async def get_dispute_status_cached(dispute_id: str) -> dict:
        """Look up the dispute status in the cache."""
        return {"dispute_id": dispute_id, "status": "under_review", "updated_at": "2026-10-17"}

    return [get_dispute_status, get_dispute_status_cached]


async def run_agent(scenario: str, sensitive: bool) -> tuple[str, list]:
    exporter.clear()
    log.lines.clear()
    enable_instrumentation(enable_sensitive_data=sensitive)
    agent = Agent(client=Client(script=SCRIPTS[scenario]), name="support",
                  instructions="Answer questions about card disputes.",
                  tools=make_tools(backend_up=scenario == "ok"))
    answer = await agent.run(QUESTION)
    spans = list(exporter.get_finished_spans())
    return answer.text, [s for s in spans if s.parent is None] + [s for s in spans if s.parent]

O terceiro bloco mede o que a flag expõe, abre o conteúdo dos spans de ferramenta e imprime o relatório em JSON.

def signals(spans: list) -> dict:
    keys = {k for s in spans for k in s.attributes}
    exceptions = [e for s in spans for e in s.events if e.name == "exception"]
    points = [set(p.attributes) for rm in reader.get_metrics_data().resource_metrics
              for sm in rm.scope_metrics for m in sm.metrics for p in m.data.data_points]
    return {
        "span_content_keys": len(keys & FLAG_KEYS),
        "tool_definitions_spans": sum("gen_ai.tool.definitions" in s.attributes for s in spans),
        "exception_messages": sum("exception.message" in e.attributes for e in exceptions),
        "log_lines_with_email": sum(EMAIL in line for line in log.lines),
        "metric_tool_content_keys": len({k for p in points for k in p if "arguments" in k or "result" in k}),
    }


def opened(spans: list) -> list:
    rows = []
    for s in spans:
        if s.name.startswith("execute_tool"):
            result = s.attributes.get("gen_ai.tool.call.result")
            message = [e.attributes["exception.message"] for e in s.events if e.name == "exception"]
            rows.append({"tool": s.name.split()[1],
                         "arguments": json.loads(s.attributes["gen_ai.tool.call.arguments"]),
                         "result": json.loads(result) if result else None,
                         "exception": message[0] if message else None})
    return rows


async def main() -> None:
    ok_answer, _ = await run_agent("ok", sensitive=False)
    failed_answer, off = await run_agent("failed", sensitive=False)
    flag_off = signals(off)
    _, on = await run_agent("failed", sensitive=True)
    flag_on = signals(on)
    names = [s.name.split()[1] for s in off if s.name.startswith("execute_tool")]
    report = {
        "response_log": [{"scenario": "ok", "answer": ok_answer}, {"scenario": "failed", "answer": failed_answer}],
        "path": [{"span": s.name, "status": s.status.status_code.name, "error": s.attributes.get("error.type"),
                  "input_tokens": s.attributes.get("gen_ai.usage.input_tokens")} for s in off],
        "calls": [{"tool": t, "calls": names.count(t),
                   "errors": sum(s.name == f"execute_tool {t}" and s.status.status_code.name == "ERROR" for s in off)}
                  for t in dict.fromkeys(names)],
        "exposure": [{"signal": k, "flag_off": flag_off[k], "flag_on": flag_on[k]} for k in flag_off],
        "opened": opened(on),
        "metrics": sorted({m.name for rm in reader.get_metrics_data().resource_metrics
                           for sm in rm.scope_metrics for m in sm.metrics}),
    }
    body = [f'  {json.dumps(key)}: [\n' + ",\n".join(f"    {json.dumps(row)}" for row in rows) + "\n  ]"
            for key, rows in report.items()]
    print("{\n" + ",\n".join(body) + "\n}")


asyncio.run(main())

A saída, copiada da execução:

{
  "response_log": [
    {"scenario": "ok", "answer": "Dispute DSP-1001 was approved."},
    {"scenario": "failed", "answer": "Dispute DSP-1001 is under review."}
  ],
  "path": [
    {"span": "invoke_agent support", "status": "UNSET", "error": null, "input_tokens": 207},
    {"span": "chat script-1", "status": "UNSET", "error": null, "input_tokens": 9},
    {"span": "execute_tool get_dispute_status", "status": "ERROR", "error": "TimeoutError", "input_tokens": null},
    {"span": "chat script-1", "status": "UNSET", "error": null, "input_tokens": 38},
    {"span": "execute_tool get_dispute_status", "status": "ERROR", "error": "TimeoutError", "input_tokens": null},
    {"span": "chat script-1", "status": "UNSET", "error": null, "input_tokens": 67},
    {"span": "execute_tool get_dispute_status_cached", "status": "UNSET", "error": null, "input_tokens": null},
    {"span": "chat script-1", "status": "UNSET", "error": null, "input_tokens": 93}
  ],
  "calls": [
    {"tool": "get_dispute_status", "calls": 2, "errors": 2},
    {"tool": "get_dispute_status_cached", "calls": 1, "errors": 0}
  ],
  "exposure": [
    {"signal": "span_content_keys", "flag_off": 0, "flag_on": 5},
    {"signal": "tool_definitions_spans", "flag_off": 1, "flag_on": 1},
    {"signal": "exception_messages", "flag_off": 0, "flag_on": 2},
    {"signal": "log_lines_with_email", "flag_off": 0, "flag_on": 2},
    {"signal": "metric_tool_content_keys", "flag_off": 0, "flag_on": 0}
  ],
  "opened": [
    {"tool": "get_dispute_status", "arguments": {"dispute_id": "DSP-1001", "customer_email": "maria@example.com"}, "result": null, "exception": "backend timed out after 2 s"},
    {"tool": "get_dispute_status", "arguments": {"dispute_id": "DSP-1001", "customer_email": "maria@example.com"}, "result": null, "exception": "backend timed out after 2 s"},
    {"tool": "get_dispute_status_cached", "arguments": {"dispute_id": "DSP-1001"}, "result": {"dispute_id": "DSP-1001", "status": "under_review", "updated_at": "2026-10-17"}, "exception": null}
  ],
  "metrics": [
    "agent_framework.function.invocation.duration",
    "gen_ai.client.operation.duration",
    "gen_ai.client.token.usage"
  ]
}

Em ordem de leitura: response_log traz as respostas das duas execuções, tudo o que o log do aplicativo guarda; path abre o percurso da execução que falhou, com a flag desligada; calls conta as chamadas por ferramenta; exposure compara o que a flag desligada e a ligada expõem; opened é o conteúdo dos spans de ferramenta com a flag ligada; metrics lista as métricas que o framework criou.

Span Status Erro Tokens de entrada
invoke_agent support UNSET — 207
chat script-1 UNSET — 9
execute_tool get_dispute_status ERROR TimeoutError —
chat script-1 UNSET — 38
execute_tool get_dispute_status ERROR TimeoutError —
chat script-1 UNSET — 67
execute_tool get_dispute_status_cached UNSET — —
chat script-1 UNSET — 93
Ferramenta Chamadas Com erro
get_dispute_status 2 2
get_dispute_status_cached 1 0

O que o trace conta sem abrir o conteúdo:

  • A raiz esconde as falhas. O span invoke_agent termina com status UNSET, apesar de duas falhas: o agente absorveu os erros e respondeu. Quem olha só a raiz vê uma execução saudável.
  • A falha mora nos spans de ferramenta. Os dois execute_tool get_dispute_status terminam em ERROR com error.type igual a TimeoutError, sem precisar do conteúdo.
  • A repetição é um padrão de spans. Dois spans iguais em sequência e um terceiro com outra ferramenta: o agente tentou o backend duas vezes e passou para o cache.
  • O contexto cresce a cada volta. Os tokens de entrada vão de 9 para 38, 67 e 93; a soma, 207, é o valor da raiz.
  • As respostas não carregam nada disso. Em response_log, nenhuma das duas frases contém timeout, cache, retry nem o nome de uma ferramenta.

O que o trace mostra e o que ele não diz

O que mostra, sem abrir o conteúdo: a ordem das ferramentas, qual chamada falhou e com que tipo de erro, a repetição e o crescimento do contexto. Para a pergunta “por que a segunda resposta contradiz a primeira?”, isso já aponta o caminho: o backend falhou duas vezes e a ferramenta de cache rodou em seguida.

O que não diz:

  • O motivo da escolha. Por que o agente foi ao cache depois de duas falhas? No exemplo foi o roteiro; em um agente real seria o modelo, e a estrutura não registra o porquê. Leitura minha: no melhor caso, só as mensagens mostram o que o modelo pediu e o que recebeu, e elas exigem o conteúdo ligado.
  • A idade do dado. A estrutura mostra que get_dispute_status_cached rodou; o updated_at de 17/10 só aparece em gen_ai.tool.call.result, que é conteúdo.
  • A contagem de tentativas. A convenção OpenTelemetry define a métrica gen_ai.invoke_agent.tool_calls, com o número de chamadas de ferramenta por invocação do agente, falhas incluídas. O Agent Framework 1.22.0 não a emitiu: as três métricas medidas são de duração e de tokens. A repetição só aparece como spans repetidos.
  • A estabilidade dos nomes. As convenções GenAI têm status Development; um atributo que hoje sustenta uma consulta pode mudar.

Quem vê o conteúdo: as camadas de acesso

O Foundry guarda o conteúdo sensível dos traces em uma tabela dedicada, que o RBAC do Azure protege. A documentação lista sete atributos nessa categoria: as mensagens de entrada e de saída, as instruções do sistema, as definições de ferramentas, os argumentos e o resultado de cada chamada de ferramenta e a explicação de uma avaliação. No exemplo medido, a flag do Agent Framework cobre cinco dos sete. O gen_ai.tool.definitions sai nos dois casos, porque a definição das ferramentas, com o parâmetro customer_email, vai no span raiz mesmo com a flag desligada; e o gen_ai.evaluation.explanation não aparece, porque o exemplo não roda avaliação.

Desde 30/09/2026, para dado novo, os valores desses sete atributos deixam de ir para AppDependencies, AppTraces e AppEvents. As chaves ficam, com um ponteiro para AppGenAIContent, e quem lia o valor nas tabelas antigas precisa lê-lo na nova. Consultas, alertas, dashboards, workbooks e relatórios que dependiam das tabelas antigas pedem atualização; as experiências internas do Application Insights e do Foundry seguem funcionando. Duas flags de preview do Azure, no namespace Microsoft.Insights, controlam a transição: protectGenAISensitiveData adianta o comportamento novo, e optOutProtectGenAISensitiveData mantém o antigo por um tempo, até ser descontinuada em 30/09/2027. Registrá-las exige o papel Log Analytics Contributor e uma permissão de Owner na assinatura, ou um papel com Microsoft.Features/*.

Depois de proteger a tabela, só o papel Privileged Monitoring Data Reader lê o conteúdo; ele é atribuído no recurso Application Insights ligado ao projeto Foundry. Com PIM, o acesso pode ser temporário. O que a documentação descreve nesta seção não foi executado:

Camada O que decide Padrão
Tracing no projeto Foundry Se o trace existe Desligado; só Foundry Account Owner ou Foundry Owner liga
ENABLE_SENSITIVE_DATA no código Se o conteúdo entra nos spans Desligada
Tabela AppGenAIContent Onde o conteúdo fica guardado Tabela dedicada, separada do resto da telemetria
Nível de proteção Protected Quem é barrado sem permissão explícita Negação por padrão
Papel Privileged Monitoring Data Reader, com PIM Quem lê, e por quanto tempo Só quem recebe o papel

A consulta abaixo (trecho não executado; a página da tabela traz as colunas, não uma consulta de exemplo) lê o conteúdo de um trace:

AppGenAIContent
| where TraceId == "<trace-id>"
| project TimeGenerated, SpanId, ParentSpanId, ToolCallArguments, ToolCallResult
| order by TimeGenerated asc

Sem o papel, a consulta não dá erro. A documentação de tabelas protegidas descreve o teste de quem só tem Log Analytics Reader como uma consulta que tem sucesso e devolve zero linhas. Por isso o teste de acesso precisa vir antes do incidente: zero linhas se lê, sem querer, como “nada foi gravado”.

O que o conteúdo expõe: a medição em duas versões

Os mesmos números, lidos como exposição. Com a flag desligada, o trace guarda a estrutura e nenhum conteúdo; com a flag ligada, ele abre tudo o que a tabela de perguntas chama de “Sim”:

Sinal Flag desligada Flag ligada
Atributos de conteúdo nos spans, de 5 possíveis 0 5
Spans com gen_ai.tool.definitions 1 1
Eventos de exceção com a mensagem 0 2
Linhas de log com o e-mail do cliente 0 2
Chaves de argumento ou resultado nas métricas 0 0

A chave opened do JSON mostra o que a flag ligada abre: os argumentos das duas chamadas ao backend, com customer_email igual a maria@example.com, a mensagem backend timed out after 2 s e o resultado do cache, com updated_at igual a 2026-10-17. É o dado de que a investigação precisava. Com o nível de log em DEBUG, como no exemplo, os argumentos também vão para o log, em duas linhas com o e-mail. Com a flag desligada, o log só diz que a função falhou e o tipo do erro.

O que mudou entre duas versões. Rodei o mesmo código na 1.21.0, de 08/10/2026, e na 1.22.0, de 15/10/2026. Duas linhas mudaram:

Sinal Flag 1.21.0 1.22.0
Eventos de exceção com a mensagem Desligada 2 0
Chaves de argumento ou resultado nas métricas Ligada 1 0

Os outros oito valores da tabela anterior não mudaram. A release 1.22.0 lista, entre as mudanças, redigir telemetria sensível e tirar os argumentos de ferramenta das métricas (pull requests 9288 e 9361, fundidos em 14/10/2026). Na 1.21.0, com a flag ligada, a métrica agent_framework.function.invocation.duration carregava a chave gen_ai.tool.call.arguments, com os argumentos e o e-mail; o nome dessa chave vem de uma sonda separada, não do código publicado aqui. A documentação atual do Agent Framework diz que as métricas de invocação de função nunca incluem argumentos nem resultados.

A lição não é sobre a 1.21.0. Teste de privacidade é teste de regressão: o comportamento mudou em sete dias, entre duas versões, e só uma contagem de sinais com a versão fixada mostra quando ele muda de novo.

Outras fontes de percurso: Fabric e Work IQ

Dois outros lugares guardam algo que parece um percurso, e vale saber o que cada um é.

No Fabric, a função evaluate_data_agent roda a avaliação de um agente de dados e cria duas tabelas: a de resumo e a <table_name>_steps, que, segundo a página, guarda o raciocínio detalhado e a execução passo a passo. O recurso está em preview e exige capacidade F2 ou maior. Pelo que a página descreve, a tabela nasce de uma rodada de avaliação, não do tráfego de produção; ela ajuda a entender um teste, não a investigar um caso real. O que a página descreve não foi executado.

No Work IQ, a página de visão geral diz que cada invocação de ferramenta é registrada e avaliada, para auditoria, análise de uso, limite de taxa e conformidade em tempo real. Que registro de invocação não é o raciocínio do agente é leitura minha. O desenho das tools do Work IQ MCP está no artigo sobre o tema; aqui só interessa o registro. O que a página descreve não foi executado.

Boas práticas para produção

  • Leia a estrutura antes do conteúdo. Ordem, status, error.type e tokens por volta mostram a maior parte do percurso sem expor dado de ninguém.
  • Deixe o conteúdo desligado em produção. A documentação manda ligar ENABLE_SENSITIVE_DATA só em desenvolvimento ou teste. Quando um incidente exigir o conteúdo, decida quem liga, por quanto tempo e quem lê.
  • Teste a privacidade como regressão, com a versão fixada. Entre a 1.21.0 e a 1.22.0, duas linhas da tabela de exposição mudaram; uma contagem de sinais com a versão fixada mostra a diferença assim que ela chega.
  • Atualize consultas, alertas e workbooks. Desde 30/09/2026, quem lê o valor de um dos sete atributos nas tabelas antigas encontra um ponteiro para AppGenAIContent.
  • Trate argumentos como dado pessoal, inclusive no log. No exemplo, o e-mail do cliente apareceu em duas linhas de log com a flag ligada. A documentação manda redigir dado pessoal, como e-mail e telefone.
  • Conte tentativas pelos spans. Sem uma métrica de tentativas, a repetição é dois spans execute_tool iguais em sequência.
  • Garanta o papel antes do incidente. Sem ele, a consulta devolve zero linhas, sem erro, e a equipe pode concluir que nada foi gravado.

O que precisa estar de pé

A ordem abaixo é causal, não cronológica: cada estágio existe porque o anterior ficou de pé.

Fundação — o percurso fica gravado. O tracing está ligado no projeto por quem pode ligar, e cada execução deixa spans de agente, de modelo e de ferramenta, com status e error.type.

Está de pé quando, para uma execução qualquer de ontem, alguém abre o trace e lê a ordem das ferramentas e qual delas falhou.

Produção com contexto — a política de conteúdo está decidida e testada. O conteúdo está desligado onde não deve estar, a tabela AppGenAIContent está protegida, e o teste de privacidade com a versão fixada roda a cada atualização.

Está de pé quando um teste falha se uma atualização do framework passar a expor argumento, resultado ou mensagem de exceção que a política não permite.

Escala e eficiência — quem investiga consegue consultar. O papel Privileged Monitoring Data Reader está com quem investiga, com PIM para o acesso temporário, e o runbook diz que zero linhas é motivo para conferir o papel antes de concluir que nada foi gravado.

Está de pé quando uma pessoa nova no plantão abre o conteúdo de um trace sem pedir ajuda ao time da plataforma.

Perguntas frequentes (FAQ)

O trace diz por que o modelo escolheu aquela ferramenta?

Não. A estrutura registra a ordem, o status e o tipo de erro; o motivo da escolha não é um atributo. Leitura minha: no melhor caso, as mensagens do modelo mostram o que ele pediu e o que recebeu, e elas só entram com o conteúdo ligado.

Preciso ligar o conteúdo para ver o percurso?

Não. A ordem dos spans, o status, o error.type e os tokens de entrada por volta são estrutura e ficam no trace com a flag desligada. O conteúdo responde a outra pergunta: com que argumentos e com que resultado.

Procurei o trace do caso e não achei. O que aconteceu?

Ele pode não existir. O tracing só grava depois de ligado, e retenção e amostragem seguem a configuração do Application Insights. Na aba Traces, a busca é por Response ID ou por Trace ID.

Se eu consultar AppGenAIContent sem o papel, recebo um erro?

Não. A documentação de tabelas protegidas descreve o teste: a consulta tem sucesso e devolve zero linhas. Por isso zero linhas pode ser lido, por engano, como “nada foi gravado”.

Existe uma métrica que conte as tentativas de ferramenta?

A convenção OpenTelemetry define gen_ai.invoke_agent.tool_calls, em status Development. O Agent Framework 1.22.0 não a emitiu nas três métricas do exemplo; a repetição aparece como spans execute_tool repetidos.

As convenções OpenTelemetry para GenAI são estáveis?

Não. A documentação do Foundry diz que elas têm status Development e podem mudar em versões futuras. Fixe a versão e teste a consulta que depende de um atributo.

O trace substitui a avaliação antes do deploy?

Não. A avaliação mede em agregado antes do deploy; o trace ajuda a investigar um caso depois do incidente. Um não faz o trabalho do outro.

O tracing cobre todos os tipos de agente?

Ainda não da mesma forma. O tracing é GA para agentes de prompt e hospedados e está em preview para agentes de workflow e externos.

Conclusão

O log da resposta guarda o que o agente disse; o trace guarda o caminho; e o conteúdo desse caminho fica em uma tabela que precisa de dono. Ligue o tracing, leia a estrutura antes do conteúdo, deixe o conteúdo desligado em produção e proteja AppGenAIContent com o papel certo. Antes do próximo incidente, teste duas coisas: se o percurso de uma execução de ontem se lê no trace, e se quem investiga abre o conteúdo sem pedir ajuda ao time da plataforma. Zero linhas não é o mesmo que nada gravado.

Referências oficiais

👉 Se você tem um agente em produção cujo log só guarda o texto da resposta, abra o trace de uma execução de ontem e veja se o percurso se lê nele, antes do próximo incidente. Quer trocar ideia sobre observabilidade de agentes no Foundry? Me chama no LinkedIn.

Compartilhe:

Eron Cavalcante

Eron Cavalcante atua como Cloud Solution Architect na Microsoft, com foco em plataformas de dados, IA generativa e arquitetura de nuvem para o setor financeiro. Combina experiência técnica em Azure, Databricks e engenharia de dados com uma visão de negócio orientada a valor, apoiando clientes na adoção responsável de IA em escala.

Deixe um comentário