Por que agentes são difíceis de debugar
Não-determinismo, dependência de LLM e loops: os três inimigos do desenvolvedor.
Não-determinismo
O mesmo input pode gerar outputs diferentes. Temperature > 0 garante variação. Reproduzir um bug exige capturar o estado exato.
Dependência de LLM
O comportamento muda com versões de modelo, mudanças de prompt e até carga do servidor. O que funciona hoje pode falhar amanhã.
Loops infinitos
O agente chama a mesma tool repetidamente, interpreta o resultado errado ou nunca decide que terminou. Sem limite, o custo explode.
Por que print() não é suficiente
Abordagem ingênua
agent.run("Pesquise sobre IA")
# Saída: uma string final
# Você não sabe:
# - Quantas chamadas ao LLM?
# - Qual tool foi chamada?
# - Qual foi o custo?
# - Por que parou?
Com rastreamento
with langsmith.trace("agent-run"):
agent.run("Pesquise sobre IA")
# Você vê:
# - 3 chamadas ao GPT-4o
# - 2 tool calls (search + calc)
# - $0.042 de custo
# - 8.3s de latência total
O Princípio da Observabilidade em Agentes
Um sistema é observável quando você consegue entender seu estado interno apenas analisando suas saídas externas. Para agentes, isso significa capturar:
Input exato de cada chamada
Output e tokens usados
Latência por etapa
Erros e exceções
Configurando LangSmith em 3 minutos
Da conta gratuita ao primeiro trace capturado — sem alterar código de produção.
Crie a conta e gere a API key
Acesse smith.langchain.com, crie uma conta gratuita e vá em Settings → API Keys → Create API Key.
# Tier gratuito: 5.000 traces/mês # Sem necessidade de cartão de crédito
Configure as variáveis de ambiente
export LANGCHAIN_TRACING_V2="true" export LANGCHAIN_API_KEY="ls__xxxxxxxxxxxxxxxx" export LANGCHAIN_PROJECT="meu-agente-producao" # Ou via .env + python-dotenv # NUNCA commite sua API key no git
Execute seu agente normalmente
from langchain_openai import ChatOpenAI
from langchain.agents import AgentExecutor, create_openai_tools_agent
from langchain_core.prompts import ChatPromptTemplate
# NENHUMA mudança de código necessária!
# O tracing é automático quando LANGCHAIN_TRACING_V2=true
llm = ChatOpenAI(model="gpt-4o-mini")
agent = create_openai_tools_agent(llm, tools, prompt)
executor = AgentExecutor(agent=agent, tools=tools)
result = executor.invoke({"input": "Qual é a capital do Brasil?"})
# → Trace aparece automaticamente no LangSmith
Dica: Tracing seletivo em produção
Ative o tracing apenas para uma amostra das requisições em produção (ex: 10%) para controlar custos e volume. Use a variável LANGCHAIN_SAMPLING_RATE=0.1.
Visualizando uma trace completa
Anatomia de um trace no LangSmith: o que cada nó representa e como lê-lo.
Estrutura de um trace hierárquico
O que inspecionar primeiro
- →Status: Success, Error ou Timeout?
- →Latência total: onde está o gargalo?
- →Número de iterações: loops excessivos?
- →Custo: dentro do orçamento esperado?
Adicionando metadata ao trace
from langsmith import traceable
@traceable(
run_type="chain",
name="email-classifier",
tags=["prod", "v2.1"],
metadata={
"user_id": "usr_123",
"feature_flag": "gpt4o"
}
)
def classify_email(text: str) -> str:
return agent.run(text)
Usando o decorador @traceable para funções customizadas
from langsmith import traceable
from langchain_openai import ChatOpenAI
@traceable # Aparece como nó separado na árvore do trace
def my_preprocessing(text: str) -> str:
"""Limpeza e normalização do input."""
text = text.strip().lower()
return text[:2000] # Trunca textos muito longos
@traceable
def my_postprocessing(response: str) -> dict:
"""Estrutura a resposta final."""
return {"answer": response, "confidence": 0.9}
# Estas funções agora aparecem na hierarquia do trace LangSmith
Identificando a chamada de LLM problemática
Técnicas para isolar qual etapa causou falha, resposta incorreta ou custo elevado.
Sintoma 1: Resposta incorreta
Inspecione o prompt enviado ao LLM. O contexto estava correto? A instrução era clara?
→ Tab "Input"
→ Veja o System Message
→ Veja o contexto injetado
Sintoma 2: Custo alto
Qual chamada consumiu mais tokens? Contexto desnecessariamente longo?
→ Identifique o maior
→ Verifique se todos os
mensagens são necessárias
Sintoma 3: Timeout
Qual nó demorou mais? Tool externa lenta? LLM sobrecarregado?
→ Identifique gargalo
→ Tool ou LLM?
→ Adicione timeout local
Replay e comparação de traces
Um dos superpoderes do LangSmith é comparar dois traces lado a lado para ver exatamente o que mudou entre versões de prompt:
from langsmith import Client
client = Client()
# Listar traces de um projeto
runs = client.list_runs(
project_name="meu-agente",
filter='and(eq(status, "error"), gt(total_tokens, 1000))',
limit=10
)
for run in runs:
print(f"ID: {run.id}")
print(f"Status: {run.status}")
print(f"Tokens: {run.total_tokens}")
print(f"Latência: {run.end_time - run.start_time}")
print("---")
# Acessar input/output de uma run específica
run = client.read_run("run-id-aqui")
print(run.inputs) # Input exato
print(run.outputs) # Output retornado
Adicione feedback humano ao trace
Com client.create_feedback(run_id, "quality", score=1) você pode associar avaliações humanas (thumbs up/down) a traces específicos e criar datasets para fine-tuning ou avaliação automática.
Métricas de qualidade para agentes
Latência, custo e success rate: as três métricas que todo agente em produção precisa monitorar.
Latência
⏱️Monitore percentis, não só médias. Uma média de 5s pode esconder P99 de 60s.
Custo
💰Defina alertas para tarefas que custam 3× acima da média — indica loop ou prompt infinito.
Success Rate
✅Inclua falhas silenciosas: respostas fora do formato esperado também são falhas.
Dashboard de métricas com LangSmith SDK
from langsmith import Client
from datetime import datetime, timedelta
import statistics
client = Client()
# Coleta runs das últimas 24h
runs = list(client.list_runs(
project_name="meu-agente",
start_time=datetime.now() - timedelta(hours=24)
))
# Calcula métricas
latencias = [
(r.end_time - r.start_time).seconds
for r in runs if r.end_time
]
custos = [r.total_cost for r in runs if r.total_cost]
sucessos = sum(1 for r in runs if r.status == "success")
print(f"Total de runs: {len(runs)}")
print(f"Success rate: {sucessos/len(runs)*100:.1f}%")
print(f"Latência mediana: {statistics.median(latencias):.1f}s")
print(f"Latência P95: {sorted(latencias)[int(len(latencias)*0.95)]:.1f}s")
print(f"Custo médio: ${statistics.mean(custos):.4f}")
print(f"Custo total: ${sum(custos):.2f}")
Avaliação automática com LangSmith Evaluators
from langsmith.evaluation import evaluate, LangChainStringEvaluator
# Cria um evaluator baseado em LLM
qa_evaluator = LangChainStringEvaluator(
"qa",
config={"llm": ChatOpenAI(model="gpt-4o-mini")}
)
# Roda avaliação em um dataset existente
results = evaluate(
lambda x: agent.invoke(x),
data="meu-dataset-de-teste",
evaluators=[qa_evaluator],
experiment_prefix="v2.1-test"
)
print(results.to_pandas()[["input", "output", "score"]].head())
Logging estruturado para produção
Registre cada tool call com input, output e custo — e combine com métricas de negócio.
Logging não estruturado (evitar)
print("Iniciando agente...")
print("Chamando search tool")
print(f"Resultado: {result}")
print("Agente terminou")
# Problemas:
# - Impossível filtrar por user_id
# - Sem timestamp preciso
# - Sem custo ou tokens
# - Impossível agregar em dashboards
Logging estruturado (fazer)
import structlog, time
logger = structlog.get_logger()
logger.info("tool_call",
tool="search",
input=query[:200],
output=result[:200],
duration_ms=int((end-start)*1000),
tokens_used=423,
cost_usd=0.0012,
user_id="usr_123",
session_id="sess_456",
trace_id=run_id
)
Callback handler completo para produção
import time, structlog
from langchain_core.callbacks import BaseCallbackHandler
from langchain_core.outputs import LLMResult
logger = structlog.get_logger()
class ProductionCallbackHandler(BaseCallbackHandler):
def __init__(self, user_id: str, session_id: str):
self.user_id = user_id
self.session_id = session_id
self._tool_start_times = {}
self._llm_start_times = {}
def on_llm_start(self, serialized, prompts, run_id, **kwargs):
self._llm_start_times[str(run_id)] = time.time()
logger.info("llm_start",
model=serialized.get("name"),
prompt_chars=sum(len(p) for p in prompts),
user_id=self.user_id
)
def on_llm_end(self, response: LLMResult, run_id, **kwargs):
duration = time.time() - self._llm_start_times.get(str(run_id), time.time())
usage = response.llm_output.get("token_usage", {})
logger.info("llm_end",
duration_ms=int(duration * 1000),
prompt_tokens=usage.get("prompt_tokens", 0),
completion_tokens=usage.get("completion_tokens", 0),
total_tokens=usage.get("total_tokens", 0),
user_id=self.user_id,
session_id=self.session_id
)
def on_tool_start(self, serialized, input_str, run_id, **kwargs):
self._tool_start_times[str(run_id)] = time.time()
logger.info("tool_start",
tool_name=serialized.get("name"),
input_preview=str(input_str)[:300],
user_id=self.user_id
)
def on_tool_end(self, output, run_id, **kwargs):
duration = time.time() - self._tool_start_times.get(str(run_id), time.time())
logger.info("tool_end",
duration_ms=int(duration * 1000),
output_preview=str(output)[:300],
user_id=self.user_id,
session_id=self.session_id
)
def on_tool_error(self, error, run_id, **kwargs):
logger.error("tool_error",
error=str(error),
user_id=self.user_id,
session_id=self.session_id
)
# Uso
handler = ProductionCallbackHandler(user_id="usr_123", session_id="sess_456")
result = executor.invoke(
{"input": "Analise este contrato"},
config={"callbacks": [handler]}
)
✓ Boas práticas de logging
- ✓Logue input e output truncados (máx. 300 chars)
- ✓Sempre inclua user_id, session_id e trace_id
- ✓Registre duração em milissegundos
- ✓Use formato JSON estruturado (structlog, loguru)
- ✓Capture erros com stack trace completo
✗ Anti-padrões de logging
- ✗Logar o conteúdo completo de cada mensagem
- ✗Ignorar erros de tools (só logar sucesso)
- ✗Logar dados pessoais sensíveis (PII)
- ✗Usar print() sem contexto de sessão
- ✗Logar de forma síncrona em hot path
Stack de observabilidade recomendada
🔬
LangSmith
Traces detalhados, comparação de experimentos, datasets de avaliação
📊
Grafana + Loki
Dashboards de métricas de negócio, alertas de anomalia, logs de produção
🚨
Sentry / Datadog
Alertas de erros em tempo real, APM, correlação com traces do LangSmith
Resumo do Módulo 2.8
Você concluiu o último módulo da Trilha 2. Agora sabe como tornar seus agentes observáveis, mensuráveis e depuráveis em produção.
O que você aprendeu
- ◆Por que agentes são difíceis de debugar (não-determinismo, loops, dependência de LLM)
- ◆Como configurar o LangSmith em menos de 3 minutos
- ◆Como ler e interpretar uma trace hierárquica completa
- ◆Como identificar a chamada problemática usando filtros e metadata
- ◆Métricas de qualidade: latência (P50/P95/P99), custo e success rate
- ◆Logging estruturado com callback handler para produção
Checklist de conclusão
- Criou conta no LangSmith e configurou variáveis
- Executou um agente e visualizou o trace
- Identificou o custo de uma run completa
- Implementou o ProductionCallbackHandler
- Definiu alertas de latência e custo
- Calculou o success rate do seu agente