⏱️ Timeline completa do trace (alvo: < 3s para "oi tudo bem?")
Toda mensagem percorre 6 hops. Em condições normais (modelo quente, FTS5 indexado, sem tool calls pesadas), o budget é:
t=0ms ┌─ Telegram emite update
t=30ms │ H1: Webhook recebe POST (rede + TLS handshake reuso)
t=35ms │ H2: Gateway router → toolset (lookup em RAM)
t=180ms │ H3: FTS5 lookup + context build (SQLite + Honcho cache)
t=2100ms │ H4: LLM inference (Sonnet 4.6) (TTFT ~400ms + streaming)
t=2150ms │ H5: Tool exec (skip se sem tool) (0ms neste caso)
t=2200ms └─ H6: Resposta + memory write (Telegram sendMessage)
TOTAL: ~2.2s (P50) · 4-6s (P95) · 10s+ (P99 cold)
Os próximos 6 tópicos abrem cada hop com latências medidas em produção, sequence diagram, e os 1-2 lugares onde 90% das pessoas erram a otimização.
📨 Hop 1 — Telegram → Webhook (~30ms)
O Telegram Bot API faz POST HTTPS para o endpoint registrado em setWebhook. Esse é o único hop que você não controla — o tempo é dominado pela distância geográfica entre os datacenters da Telegram (Holanda/Singapura) e seu servidor.
Telegram DC Seu webhook
│ │
│── POST /hermes ───▶│ t=0 (TCP já estabelecido — keep-alive)
│ {update_id, │
│ message:{ │
│ text:"oi", │
│ from:{id:42}, │
│ chat:{id:99} │
│ }} │
│ │── parse JSON t=2ms
│ │── ack 200 OK t=3ms ◀── DEVE responder <5s
│◀── 200 ────────────│
│ │── enfileira no gateway (async)
📊 Latências reais (medido com HERMES_TRACE=1)
- P50: 28ms (servidor em Frankfurt, Telegram NL)
- P95: 85ms (renegociação TLS, pico de tráfego)
- P99: 320ms (cold connection, packet loss)
- Timeout do Telegram: 60s — se não devolver 200 em <5s ele retry com mesmo
update_id
✓ O que FAZER
- ✓Responder
200 OKimediatamente e processar em background (fila/asyncio task) - ✓Deduplicar por
update_id— Telegram retry em caso de timeout - ✓Validar
secret_tokenno headerX-Telegram-Bot-Api-Secret-Token - ✓HTTP/2 + keep-alive para reduzir 100ms de handshake
✗ O que NÃO fazer
- ✗Processar o LLM antes de devolver 200 — Telegram vai retry e você processa 2x
- ✗Expor webhook sem secret token (qualquer um envia POST falso)
- ✗Long polling em produção — gateway perde mensagens em restart
- ✗Hospedar em região distante do seu público (lat 300ms+ é norma)
💡 Dica
Ative HERMES_TRACE=1 e cada hop loga hop=H1 dur_ms=28 update_id=12345. Plot esses números num histograma semanal — você descobre cold connection peaks antes do usuário reclamar.
🚪 Hop 2 — Gateway router → toolset (~5ms)
O gateway normaliza o update do Telegram em um Message interno, identifica o canal/usuário e resolve qual toolset carregar. Tudo em RAM — esse hop só estoura se você tiver lookup em DB remoto (anti-padrão).
Webhook handler Gateway ToolsetRegistry
│ │ │
│── Message( │ │
│ channel="tg", │ │
│ user_id=42, │ │
│ text="oi" │ │
│ ) ──────────────────▶ │
│ │── route(user=42) ─────▶│
│ │ │── lookup
│ │◀── ["chat","memory"] ──│ profile=default
│ │ │ tools=2 (cached)
│ │── attach context ──────│ t=5ms
│ │ (RAM lookup)
📊 Latências P50/P95/P99
- P50: 3ms (toolset em RAM, registry warm)
- P95: 12ms (primeira mensagem do usuário após restart)
- P99: 80ms (lookup de profile em SQLite + parse YAML)
- Anti-padrão observado: lookup em Postgres remoto → 200ms+ (não faça isso)
💡 Dica
O registry é lazy-loaded mas cacheia para sempre. Se você editar config.yaml e adicionar um toolset, precisa /reload ou reiniciar — não há TTL.
🧠 Hop 3 — Memory FTS5 lookup + context build (~50-200ms)
Aqui o Hermes monta o contexto que vai para o LLM: últimas N mensagens da sessão, fatos relevantes via FTS5, user representation do Honcho, e prompts de sistema. É o hop mais variável — onde mora 80% da otimização.
Gateway ContextBuilder SQLite FTS5 Honcho
│ │ │ │
│── build(msg) ──▶│ │ │
│ │── recent(N=20) ──▶│ t=8ms │
│ │◀── messages ──────│ │
│ │── fts5 query ────▶│ t=15ms │
│ │ "oi tudo bem" │ BM25 rank │
│ │◀── 3 facts ───────│ │
│ │── user_repr ─────────────────────▶│ t=120ms
│ │ (dialectic agent → LLM call) │ ⚠️ HOTSPOT
│ │◀── "prefere PT-BR, técnico" ──────│
│ │── assemble prompt │
│ │ (sys + repr + facts + history) │
│◀── ChatRequest ─│ tokens=2400 t=180ms │
📊 Latências reais — onde o tempo vai
- FTS5 recent (N=20): P50 8ms · P95 25ms — SQLite local, índice quente
- FTS5 search (BM25): P50 15ms · P95 60ms — depende do tamanho do corpus
- Honcho user_repr local: P50 45ms · P95 180ms — chama LLM Haiku para sintetizar
- Honcho cloud: P50 200ms · P95 800ms — rede + inferência remota
- Assemble final: P50 2ms (concatenação de string)
- TOTAL hop 3: P50 70ms (local) · 250ms (cloud)
✓ O que FAZER para otimizar
- ✓Cachear user_repr por sessão (TTL 5min) — economiza 150ms por turno
- ✓Limitar FTS5 a top-K=5 com BM25 rank — mais não ajuda o LLM
- ✓Indexar com
tokenize='porter unicode61'para PT-BR - ✓Prompt caching do provider — sys+repr é estável, candidato perfeito
✗ O que NÃO fazer
- ✗Recomputar user_repr em todo turno (200ms+ de Honcho desperdiçados)
- ✗Mandar TODO o histórico — passa de 32K tokens, custo explode
- ✗Buscar embeddings vetoriais se FTS5 já resolve 90% dos casos
- ✗Bloquear na call do Honcho — use timeout 500ms e fallback vazio
🤖 Hop 4 — LLM inference (latência variável)
O hop dominante. Cada modelo tem TTFT (time-to-first-token) e tokens/s distintos. O total é TTFT + output_tokens / tokens_per_sec. Streaming não muda o total mas melhora UX porque o usuário vê o primeiro token em <500ms.
📊 Latência por modelo (medido via OpenRouter, prompt=2K, output=200 tokens)
| Modelo | TTFT P50 | tok/s | Total P50 | $/1M out |
|---|---|---|---|---|
claude-haiku-4.5 | 280ms | 120 | 1.9s | $1.25 |
claude-sonnet-4.6 | 420ms | 80 | 2.9s | $15 |
claude-opus-4.7 | 650ms | 45 | 5.1s | $75 |
gpt-5 | 380ms | 95 | 2.5s | $10 |
gemini-2.5-pro | 450ms | 140 | 1.9s | $5 |
deepseek-v3.2 | 520ms | 60 | 3.8s | $1.10 |
llama-4-maverick (local) | 120ms | 200 | 1.1s | $0 + GPU |
⚠️ Atenção: cold-start em providers serverless
Modelos open-source pouco usados no OpenRouter (qualquer :free ou :nitro com baixo throughput) sofrem cold-start de 5-15s no primeiro request. Você vê P99 disparar para 20s+. Solução: warm-up cron a cada 4min OU não use tier free em produção.
💡 Dica de roteamento por intenção
Configure model.router no YAML: chat trivial → Haiku, raciocínio → Sonnet, código difícil → Opus. Economiza 60-80% do custo mantendo qualidade percebida.
🛠️ Hop 5 — Tool execution + (opcional) re-prompt
Quando o LLM retorna tool_calls em vez de texto, o gateway executa as ferramentas (em paralelo quando possível) e re-prompta o modelo com os resultados. Cada round-trip = mais um Hop 4. É aqui que requests "rápidos" viram 15s.
LLM ToolExecutor Tool (ex: web_search)
│ │ │
│── tool_calls ──▶│ │
│ [search("X"), │── parallel.gather ──▶│ t=600ms
│ fetch("Y")] │ │ (HTTP + parse)
│ │◀── results ──────────│
│ │── re-prompt │
│◀── full ctx ────│ (tokens cresceram)
│ + tool out
│── final text ──▶ resposta t+=2.5s (Hop 4 de novo)
Sem tool call
"oi tudo bem?" → resposta direta
Hop 5 = 0ms. Total fica em ~2.2s. Caso ideal.
1 tool call (read_file)
"o que tem em README.md?"
Hop 5 = ~10ms exec + ~2s re-prompt. Total ~5s.
Chain de 3 tools (search → fetch → summarize)
"pesquise X e me resuma"
3 round-trips de LLM + 3 execs. Total 10-18s. Stream feedback intermediário ou usuário abandona.
💡 Paralelismo é grátis
Se o LLM retorna 3 tool_calls independentes, o executor roda os 3 em asyncio.gather(). 3x600ms vira 600ms. Mas só funciona se as tools forem independentes — não passe output de uma como input de outra na mesma rodada.
✅ Hop 6 — Resposta → Telegram + memory write (~80ms)
Resposta finalizada: chama sendMessage do Telegram, persiste turno na memória (INSERT em SQLite + trigger FTS5), e dispara reflexão assíncrona se o threshold de salvamento for atingido. Memory write deve ser fire-and-forget — usuário não espera por ele.
Gateway Telegram API SQLite ReflectionQueue
│ │ │ │
│── sendMessage ▶│ t=45ms │ │
│ │ (chat_id=99, │ │
│ │ text="...") │ │
│◀── 200 ────────│ │ │
│ │ │
│── async write ───────────────────▶│ t=3ms (INSERT) │
│ (não bloqueia) │ + FTS5 trigger │
│ │ t=8ms total │
│── enqueue reflect ──────────────────────────────────▶│
│ (se turnos % 10 == 0) │ background
📊 Latências P50/P95/P99
- sendMessage: P50 45ms · P95 180ms · P99 1.2s (rate-limit ou rede)
- INSERT + FTS5 trigger: P50 8ms · P95 25ms (com WAL ativado)
- Reflection enqueue: <1ms (só append em fila in-memory)
- Reflection real (async): 3-8s — não conta no trace do usuário
✓ O que FAZER
- ✓Streaming via
editMessageTexta cada N tokens (UX excelente) - ✓SQLite com
PRAGMA journal_mode=WAL(leituras concorrentes) - ✓Memory write em
asyncio.create_task— nãoawait - ✓Retry exponencial em
sendMessagecom 429/502 (max 3x)
✗ O que NÃO fazer
- ✗Awaitar o write — usuário sente +10ms grátis
- ✗Editar a mensagem 100x/s — Telegram dá 429 (limit ~1x/s por chat)
- ✗Rodar reflection inline — bloqueia próximo turno
- ✗Persistir conteúdo bruto com PII sem hash/redação
✅ Resumo do Módulo
H1 webhook 30ms · H2 router 5ms · H3 contexto 70-250ms · H4 LLM 1.5-5s · H5 tools variável · H6 envio 80ms.
Otimize cacheando user_repr e escolhendo modelo certo por intenção. Não micro-otimize H1/H2/H6.
Cada re-prompt = +1 Hop 4. Paralelize tools independentes; faça streaming feedback.
Nunca await. Reflection roda em background quando o threshold dispara.
Providers serverless com baixo throughput → 5-15s extras. Warm-up cron ou pague pelo tier dedicado.
Próximo módulo:
Módulo 2.4 — 🛡️ Modelo de segurança e privacidade: onde dados vivem, o que vaza, prompt injection, exfiltration via tool e supply chain de skills.