Pular para conteúdo

12. Assistente de IA

Objetivo

Adicionar o assistente de suporte do BoxOffice: um serviço Python 3.12 + FastAPI, gerenciado com uv, que responde às perguntas dos fãs com um LLM por trás de um pequeno gateway de LLM (LiteLLM: rotas, fallback, timeouts, custos); ancora as respostas no catálogo com RAG sobre pgvector; consulta dados ao vivo por um registro de ferramentas no formato do MCP, dentro de um loop de agente limitado; protege o modelo e os usuários com guardrails; exporta tokens, custo e tempo até o primeiro token para o Grafana do lab 8; e passa por teste de carga onde dói — no primeiro token.

No dia da venda, o suporte recebe milhares das mesmas perguntas: quando é o show? quanto custa? cadê o meu pedido? Um assistente consegue respondê-las em segundos, às 3 da manhã, em português e em inglês — se não inventar shows, não vazar dados e não custar mais que os ingressos. Este lab constrói um com a arquitetura da aula de sistemas de IA, rodando inteiramente na sua máquina com um modelo aberto local (Ollama) e, opcionalmente, no Claude para comparação. Na história da disciplina, este lab constrói o capítulo 13: ele implementa as decisões do gateway de LLM (rotas e um fallback), do loop do agente e das suas ferramentas só de leitura, da recuperação com pgvector e da malha de segurança.

Slides

Clique nos slides e use ← → para navegar; F abre em tela cheia e P abre a visão do apresentador, com as notas. Abrir os slides em uma nova aba

Antes de começar

Tempo cerca de 6 horas, incluindo o download dos modelos e a inferência local lenta
Ferramentas Docker com Compose v2; uv (uv --version, do lab 0); curl, jq. O Python é gerenciado pelo uv: nada de pip, nada de ambientes virtuais manuais
Recursos do Docker pelo menos 4 CPUs e 8 GB livres para este lab (≈ 1,5 GB de modelos); no macOS, planeje rodar o Ollama nativamente (passo 1)
Disco ≈ 3 GB para as imagens e os modelos
Contas e custos nenhuma obrigatória: tudo roda localmente e de graça. Opcional: uma chave de API da Anthropic para a rota do Claude (pago por uso, ≈ US$ 0,006 por pergunta com claude-opus-5-5)

Todo comando roda na raiz do seu repositório boxoffice, no estado em que o lab 11 o deixou:

Start state (end of lab 11)
boxoffice/
├── docker-compose.yaml     # the Compose stack of labs 1-9 (catalog behind gateway, order, ...)
├── .github/  k8s/          # labs 10-11
├── infra/  load/  scripts/
└── services/               # auth, catalog, gateway, inventory, notification, order, payment

Recupere o atraso

Pulou ou quebrou o lab 11? Baixe boxoffice-after-lab-11.zip, descompacte, cd boxoffice, sh scripts/get-otel-agent.sh e continue daqui. Este lab só precisa da stack do Compose (gateway, catalog, order, db, otel-lgtm), não do cluster.

Todo arquivo deste lab aparece completo nestas páginas e é publicado em https://insper.github.io/platform/2027.1/hands-on/12/code/<caminho>.

Arquitetura

flowchart LR
    fan[fã / interface de chat] -->|"POST /assistant/chat/stream"| api[assistant: FastAPI]
    api --> gin{guarda de entrada}
    gin --> orch[loop do agente]
    orch -->|"search_query: ..."| emb[modelo de embeddings]
    emb --> vdb[("pgvector: assistant.documents")]
    vdb -->|top-k eventos| orch
    orch -->|router do LiteLLM| gw[gateway de LLM]
    gw -->|local| ollama["Ollama: qwen2.5:1.5b"]
    gw -.->|"opcional, fallback para o local"| claude[API do Claude]
    orch -->|chamadas de ferramenta| reg[registro de ferramentas]
    reg -->|GET /catalog/events| cat[gateway -> catalog]
    reg -->|"GET /orders/id + X-User-Email"| ord[order]
    orch --> gout{guarda de saída}
    gout -->|tokens SSE + done| fan
    api -.->|métricas OTLP| lgtm[otel-lgtm]

Componentes do BoxOffice envolvidos

Componente da paleta Neste lab
LLM Gateway Router do LiteLLM: rotas, fallback, timeouts, contabilização de custo
Orchestrator o loop de agente limitado de agent.py
Tool Registry três ferramentas só de leitura com JSON schemas (no estilo do MCP)
Memory Fabric RAG: embeddings dos eventos do catálogo no pgvector
Vector DB PostgreSQL 17 + pgvector, índice HNSW
Safety Mesh guardrails de entrada e de saída, mascaramento de PII

O que você vai medir

Pergunta Métrica Ferramenta
Quanto tempo até o fã ver a primeira palavra? TTFT p50/p95 sob concorrência scripts/assistant_load.py
Com que rapidez os tokens são produzidos? tokens/s, requisições/min o mesmo
A recuperação encontra os eventos certos? hit@5, MRR scripts/eval_retrieval.py
As respostas são ancoradas? fração de grounded: true a API, a métrica do guarda
Quanto custa uma resposta? tokens e US$ por requisição a API, assistant_llm_cost_usd_total

Pré-requisitos

Passos

  1. O serviço e o gateway de LLM — Ollama, pgvector, o projeto uv, as rotas e o fallback do LiteLLM, a rota opcional do Claude.
  2. RAG com pgvector — embeddings, o índice HNSW, a ingestão do catálogo, a avaliação da recuperação.
  3. Ferramentas e o loop do agente — o registro de ferramentas, o loop limitado, streaming, a API.
  4. Guardrails e métricas — injection e PII na entrada, ancoragem na saída; tokens, custo e TTFT no Grafana.
  5. Meça — TTFT sob carga, qualidade da recuperação e das respostas, escolha do modelo.

Experimente no playground

Experimente: rode o cenário ai-assistant com o perfil load. Aumente os steps do orquestrador de 1 para 4 e veja a latência e o custo por requisição se multiplicarem — cada step é mais uma chamada ao LLM. Depois ative o cache semântico do gateway de LLM e veja que fração do tráfego nunca chega ao modelo.

O simulador completo está no playground.

Meça

O passo Meça tem os scripts e a tabela de resultados para guardar em results/lab12.md.

Solução de problemas

Sintoma Causa provável Correção
as respostas levam minutos o modelo roda numa CPU (sempre o caso no Docker no macOS) rode o Ollama nativamente e defina OLLAMA_URL=http://host.docker.internal:11434; feche outras stacks; faça perguntas mais curtas
litellm.APIConnectionError … Connection refused nos logs o Ollama não está rodando ou OLLAMA_URL está errada docker compose ps ollama / ollama list; confira a variável no docker-compose.yaml
model "qwen2.5:1.5b" not found o modelo não foi baixado docker compose up ollama-init (ou ollama pull qwen2.5:1.5b nativamente)
a inicialização falha com type "vector" does not exist o db ainda roda a imagem postgres:17 simples docker compose up -d db depois de atualizar o arquivo compose; docker compose exec db psql -U boxoffice -c "\dx" lista vector
/assistant/ingest retorna Internal Server Error ou {"ingested":0} o gateway não está rodando (Name or service not known em docker compose logs assistant), ou o catálogo atrás dele caiu docker compose up -d --wait gateway; docker compose ps gateway catalog-1; docker compose exec assistant python -c "import urllib.request;print(urllib.request.urlopen('http://gateway:8080/catalog/events/1').status)"
uv sync --frozen falha no build o uv.lock não corresponde ao pyproject.toml rode uv lock em services/assistant (na sua máquina) e refaça o build
nenhuma métrica assistant_* no Grafana OTEL_EXPORTER_OTLP_ENDPOINT não definida, ou nenhuma pergunta respondida ainda confira as variáveis do assistant; as métricas são exportadas a cada 10 s
Bind for 0.0.0.0:11434 failed: port is already allocated o Ollama já roda nativamente na sua máquina, e o docker compose up -d também subiu o contêiner ollama mantenha o nativo: docker compose stop ollama ollama-init, e suba os serviços pelo nome (docker compose up -d assistant) com OLLAMA_URL exportada
litellm.Timeout … Timeout passed=120.0 nos logs; errors no teste de carga o modelo roda numa CPU e as requisições ficam na fila diante dele por mais que os 120 s do gateway diminua a concorrência (--concurrency 1 2 --requests 2), ou rode o Ollama nativamente; o timeout=120 está em app/llm.py
422 blocked para uma pergunta normal um falso positivo dos padrões de injection reformule, e anote: falsos positivos fazem parte da qualidade medida do guardrail

Boas práticas neste lab

  • Rotas em vez de nomes de modelo, com um fallback — o app/llm.py pede local ou claude, e claude cai para local; por quê: LLM Gateway.
  • Um loop limitado que sempre termina com uma resposta — o app/agent.py para em MAX_STEPS = 4 e faz a última chamada sem ferramentas; por quê: o loop do agente.
  • Ferramentas só de leitura, identidade vinda da plataforma — o app/tools.py pega o fã do X-User-Email e nunca escreve; por quê: ferramentas são efeitos colaterais.
  • Recuperação medida por si só — o scripts/eval_retrieval.py calcula hit@k e MRR antes que qualquer modelo responda; por quê: RAG e memória.
  • Mascare antes do modelo, confira as citações depois dele, meça o TTFT em segundos — app/guardrails.py e app/metrics.py; por quê: Segurança, avaliações e observabilidade.

Limpeza

docker compose stop assistant ollama        # free the CPU/RAM of the model
docker compose down -v                      # or remove everything (including the downloaded models)

O docker compose down -v também apaga o volume ollama-models: a próxima execução baixa os modelos de novo. Mantenha-o se for continuar para o lab 13, que pode usar o mesmo modelo local. Com o Ollama nativo, ollama stop qwen2.5:1.5b libera a memória do modelo na hora (ele também sai da memória após 5 minutos ocioso), e ollama rm qwen2.5:1.5b nomic-embed-text apaga os modelos quando você não precisar mais deles.

Estado final

End state (what this lab added or changed)
boxoffice/
├── .gitignore              # + __pycache__/, .venv/
├── docker-compose.yaml     # db/db-replica -> pgvector image; + ollama, ollama-init, assistant
├── scripts/
│   ├── assistant_load.py   # TTFT load test (uv inline script)
│   └── eval_retrieval.py   # hit@k / MRR (uv inline script)
├── results/lab12.md
└── services/assistant/     # new: pyproject.toml, uv.lock, .python-version, Dockerfile, .dockerignore,
    └── app/                # __init__, config, llm, db, rag, tools, agent, guardrails, metrics, main

Faça o commit e a tag: git add -A && git commit -m "lab 12: AI assistant" && git tag lab12.

Checklist

  • ollama, ollama-init e assistant rodam no docker-compose.yaml (ou o Ollama nativamente, com OLLAMA_URL); o db usa a imagem do pgvector
  • services/assistant é um projeto uv com pyproject.toml e uv.lock commitados; a imagem é construída com uv sync --frozen
  • o /assistant/ingest carregou pelo menos 300 eventos; o scripts/eval_retrieval.py reporta hit@5 e MRR
  • /assistant/chat e /assistant/chat/stream respondem com citações conferidas pelo guarda de saída; uma tentativa de injection retorna 422
  • get_order_status só responde para o usuário em X-User-Email
  • métricas assistant_* (tokens, custo, TTFT) visíveis no Grafana
  • tabela de resultados preenchida em results/lab12.md, incluindo onde o modelo rodou
  • autoverificação: docker compose up -d --build assistant a partir de um clone limpo, e então curl localhost:8088/assistant/health retorna "status":"UP"; push com a tag lab12

Começar: O serviço e o gateway de LLM