Hands-on guide

Open-source LLMs and agents, on your machine or in the cloud.

What a model, a server and an agent each are, how much hardware you need, how to run and connect them with open-source tools, and how to rent a GPU safely when your computer falls short. With exercises and expected results.

O mapa: modelo, servidor, agente e ferramentasThe map: model, server, agent and tools

Quase toda confusão sobre “rodar IA” vem de misturar quatro coisas diferentes. Separe-as e o resto fica simples.Almost all the confusion about “running AI” comes from mixing four different things. Separate them and the rest gets simple.

PeçaPieceO que é, em uma fraseWhat it is, in one sentenceExemplos open-sourceOpen-source examples
Modelo (pesos)Model (weights)Um arquivo grande de números que guarda o que o modelo “aprendeu”. Sozinho, não faz nada.A large file of numbers that stores what the model “learned”. On its own it does nothing.Qwen, Llama, Gemma, Mistral, gpt-oss
Servidor de inferênciaInference serverPrograma que carrega os pesos na memória e responde a pedidos HTTP: manda texto, recebe texto.A program that loads the weights into memory and answers HTTP requests: text in, text out.Ollama, llama.cpp, vLLM
AgenteAgentUm programa seu que chama o servidor em laço, deixando o modelo escolher ferramentas até terminar a tarefa.A program of yours that calls the server in a loop, letting the model pick tools until the task is done.o agent.py desta nota, smolagents, LangGraphthe agent.py in this note, smolagents, LangGraph
FerramentasToolsFunções que o agente executa no mundo real: ler arquivo, buscar na web, consultar um banco.Functions the agent runs in the real world: read a file, search the web, query a database.funções Python, servidores MCPPython functions, MCP servers
VocêYou terminal · chatterminal · chat AgenteAgent laço + ferramentasloop + tools agent.py · smolagents Servidor de LLMLLM server API HTTPHTTP API Ollama · llama.cpp · vLLM PesosWeights arquivo GGUFGGUF file GPU / CPU FerramentasTools arquivos · web · bancosfiles · web · databases /v1/chat/completions MCP Camada 1: o modelo é só um arquivo de números.Layer 1: the model is just a file of numbers. Camada 2: o servidor o carrega e abre uma API.Layer 2: the server loads it and opens an API. Camada 3: o agente chama a API em laço.Layer 3: the agent calls the API in a loop.
Quatro peças: você fala com o agente; o agente fala com o servidor pela API e com as ferramentas pelo MCP; o servidor carrega os pesos.Four pieces: you talk to the agent; the agent talks to the server over the API and to tools over MCP; the server loads the weights.

A Anthropic, no guia Building effective agents, define a diferença com precisão: em um workflow, o código decide a ordem dos passos; em um agente, é o próprio modelo que decide os próximos passos e quais ferramentas usar, usando o retorno do ambiente em um laço. O mesmo guia dá um conselho valioso: comece com a solução mais simples possível; muitas vezes, uma única chamada ao modelo já basta.Anthropic, in the guide Building effective agents, defines the difference precisely: in a workflow, code decides the order of the steps; in an agent, the model itself decides the next steps and which tools to use, using feedback from the environment in a loop. The same guide gives valuable advice: start with the simplest solution possible; often a single call to the model is enough.

Por que open-source? Três razões práticas: privacidade (os dados não saem da sua máquina), custo previsível (sem cobrança por token) e controle (você escolhe a versão e ela não muda sozinha). O preço é que você cuida do hardware e os melhores modelos fechados costumam ser mais capazes.Why open-source? Three practical reasons: privacy (data never leaves your machine), predictable cost (no per-token billing) and control (you pick the version and it does not change on its own). The price is that you look after the hardware, and the best closed models are usually more capable.

Cabe na minha máquina? A conta da memóriaWill it fit on my machine? The memory maths

Um modelo é medido em parâmetros (os números dentro do arquivo). “8B” significa 8 bilhões. Cada parâmetro ocupa alguns bits, e quantos bits depende da quantização: reduzir a precisão dos números (de 16 bits para 8 ou 4) para o modelo ocupar menos memória, com uma perda pequena de qualidade. Pesquisa de Dettmers e Zettlemoyer (2022) mostra que, para uma mesma memória, modelos maiores quantizados em 4 bits tendem a superar modelos menores em precisão maior, e por isso 4 bits virou o padrão no uso local.A model is measured in parameters (the numbers inside the file). “8B” means 8 billion. Each parameter takes a few bits, and how many depends on quantization: lowering the precision of the numbers (from 16 bits to 8 or 4) so the model takes less memory, with a small loss of quality. Research by Dettmers and Zettlemoyer (2022) shows that, for the same memory, larger models quantized to 4 bits tend to beat smaller models at higher precision, which is why 4 bits became the default for local use.

A regra de bolso: memória dos pesos ≈ parâmetros × bits ÷ 8. Some ainda 1 a 3 GB de folga para o contexto (o “cache KV”, que cresce com o tamanho da conversa) e para o sistema.The rule of thumb: weights memory ≈ parameters × bits ÷ 8. Add 1 to 3 GB of headroom for the context (the “KV cache”, which grows with conversation length) and for the system.

def gb(params_billion, bits):
    return params_billion * bits / 8

for size in (3, 8, 14, 32):
    print(f"{size:>3}B  fp16={gb(size,16):5.1f} GB   int8={gb(size,8):5.1f} GB   int4={gb(size,4):5.1f} GB")

Resultado esperado: o programa imprime 3B → 6,0 / 3,0 / 1,5 GB; 8B → 16 / 8 / 4 GB; 14B → 28 / 14 / 7 GB; 32B → 64 / 32 / 16 GB. Na prática, arquivos “4 bits” ficam perto de 5 bits por parâmetro: a página do Qwen3 no Ollama lista 5,2 GB para o 8b, 9,3 GB para o 14b e 20 GB para o 32b.Expected result: the program prints 3B → 6.0 / 3.0 / 1.5 GB; 8B → 16 / 8 / 4 GB; 14B → 28 / 14 / 7 GB; 32B → 64 / 32 / 16 GB. In practice “4-bit” files land near 5 bits per parameter: the Qwen3 page on Ollama lists 5.2 GB for 8b, 9.3 GB for 14b and 20 GB for 32b.

Seu hardwareYour hardwareMemória que contaMemory that countsModelo realista (4 bits)Realistic model (4-bit)
Sem GPU, 8 GB de RAMNo GPU, 8 GB RAMRAM (lento)RAM (slow)1B a 4B1B to 4B
Sem GPU, 16 GB de RAMNo GPU, 16 GB RAMRAM (lento, mas funciona)RAM (slow, but works)até 8Bup to 8B
GPU NVIDIA com 8 GB de VRAMNVIDIA GPU with 8 GB VRAMVRAMaté 8Bup to 8B
GPU com 24 GB de VRAMGPU with 24 GB VRAMVRAM14B a 32B14B to 32B
Mac com chip Apple Silicon (memória unificada)Mac with Apple Silicon (unified memory)a RAM toda, menos uns 25%most of the RAM, minus about 25%conforme a RAM: 16 GB → 8B; 32 GB → 14B a 32Bby RAM: 16 GB → 8B; 32 GB → 14B to 32B

Se o modelo não cabe na VRAM, o servidor despeja parte das camadas na RAM e a velocidade despenca. É o sinal de que está na hora de reduzir o modelo ou ir para a nuvem (seção 8).If the model does not fit in VRAM, the server spills some layers into RAM and speed collapses. That is the sign to pick a smaller model or move to the cloud (section 8).

Atividade 1: meça o seu hardwareActivity 1: measure your hardware

# Linux
free -h                 # RAM total and available
nvidia-smi              # NVIDIA GPU, VRAM (skip if you have no NVIDIA GPU)
lscpu | grep "Model name"

# macOS
sysctl -n hw.memsize | awk '{print $1/1024/1024/1024 " GB"}'

Resultado esperado: você consegue completar a frase “tenho X GB de VRAM (ou de RAM)”. Com X, escolha na tabela o maior modelo que cabe e anote o nome. Esse é o seu alvo para as próximas atividades; se X for pequeno, use um modelo de 1B a 4B, como llama3.2:3b.Expected result: you can complete the sentence “I have X GB of VRAM (or RAM)”. With X, pick the largest model in the table that fits and write down its name. That is your target for the next activities; if X is small, use a 1B to 4B model such as llama3.2:3b.

Qual modelo? As famílias open-weights, incluindo as chinesasWhich model? The open-weights families, including the Chinese ones

Hoje, boa parte dos melhores modelos de pesos abertos vem de laboratórios chineses. Todos seguem o mesmo caminho das seções anteriores: baixar os pesos, servir por Ollama, llama.cpp ou vLLM e chamar pela API compatível com a da OpenAI. Só o nome do modelo muda.Today, a good share of the best open-weights models come from Chinese labs. They all follow the same path as the earlier sections: download the weights, serve them with Ollama, llama.cpp or vLLM and call them through the OpenAI-compatible API. Only the model name changes.

FamíliaFamilyQuem fezWho made itO que a distingueWhat sets it apartPara rodar localmenteTo run locally
DeepSeek (V3, R1)DeepSeek, laboratório de HangzhouDeepSeek, a lab in HangzhouO R1 popularizou o raciocínio treinado por aprendizado por reforço (o modelo “pensa” antes de responder). O V3 e o R1 completos são MoE de 671B parâmetros. Pesos sob licença MIT.R1 popularised reasoning trained with reinforcement learning (the model “thinks” before answering). The full V3 and R1 are 671B-parameter MoE models. Weights under the MIT license.O modelo completo (671b) tem 404 GB no Ollama. Para a sua máquina, use as versões destiladas (1.5b a 70b): modelos menores, de outras famílias, treinados para imitar o raciocínio do R1. Ex.: deepseek-r1:8b (5,2 GB).The full model (671b) is 404 GB on Ollama. For your machine use the distilled versions (1.5b to 70b): smaller models from other families, trained to imitate R1’s reasoning. E.g. deepseek-r1:8b (5.2 GB).
QwenAlibaba CloudAlibaba CloudA família mais variada: de 0,6B a centenas de bilhões, versões densas e MoE, bom suporte a ferramentas e a português. Em geral Apache 2.0.The widest family: from 0.6B to hundreds of billions, dense and MoE versions, good tool support and multilingual quality. Usually Apache 2.0.É o que este guia usa: qwen3:8b, qwen3:14b, qwen3:32b.It is what this guide uses: qwen3:8b, qwen3:14b, qwen3:32b.
GLMZhipu AI (Z.ai), da Universidade TsinghuaZhipu AI (Z.ai), a Tsinghua University spin-offFoco em agentes e programação, com chamada de ferramentas nativa.Focus on agents and coding, with native tool calling.Há variantes “Flash” menores; confira o tamanho na página do Ollama.There are smaller “Flash” variants; check the size on the Ollama page.
Kimi K2Moonshot AIMoE com cerca de 1 trilhão de parâmetros totais e 32B ativos, treinado para uso de ferramentas e tarefas longas. Licença MIT modificada.MoE with about 1 trillion total parameters and 32B active, trained for tool use and long tasks. Modified MIT license.Grande demais para um PC: use por GPUs alugadas ou por um provedor.Too big for a PC: use it on rented GPUs or through a provider.
MiniMaxMiniMaxMoE voltado a agentes e código.MoE aimed at agents and code.Também grande; veja a licença antes de qualquer uso comercial.Also large; read the license before any commercial use.

MoE (mixture of experts) explica os números enormes. O modelo é dividido em “especialistas” e, a cada token, só alguns são usados: o Kimi K2 tem ~1T de parâmetros no total, mas só 32B ativos por token. O que isso muda na conta da memória: a velocidade acompanha os parâmetros ativos, mas a memória acompanha os totais, porque todos os especialistas precisam estar carregados. Por isso um MoE é rápido para o tamanho, e ainda assim exige muita VRAM.MoE (mixture of experts) explains the huge numbers. The model is split into “experts” and, for each token, only a few are used: Kimi K2 has ~1T parameters in total but only 32B active per token. What it changes in the memory maths: speed follows the active parameters, but memory follows the total, because every expert has to be loaded. That is why a MoE is fast for its size and still demands a lot of VRAM.

Três cuidados. (1) Pesos abertos não é o mesmo que open-source no sentido estrito: em geral o código e os dados de treino não são publicados, e a licença varia por modelo e por versão. Leia a licença no cartão do modelo no Hugging Face antes de usar comercialmente. (2) Rodar os pesos na sua máquina não envia dados a ninguém; já usar a API hospedada do laboratório (por exemplo, a do DeepSeek) envia o seu texto aos servidores dele, sob a legislação do país dele. (3) Modelos treinados na China costumam recusar ou enviesar respostas sobre alguns temas políticos; teste com o seu caso de uso.Three cautions. (1) Open weights is not the same as open source in the strict sense: usually the code and training data are not published, and the license varies by model and version. Read the license on the model card on Hugging Face before commercial use. (2) Running the weights on your machine sends data to nobody; using the lab’s hosted API (for example DeepSeek’s) sends your text to their servers, under their country’s laws. (3) Models trained in China often refuse or skew answers on some political topics; test with your own use case.

Atividade 9: compare um modelo de raciocínio com o Qwen3Activity 9: compare a reasoning model with Qwen3

ollama pull deepseek-r1:8b
ollama run deepseek-r1:8b --verbose
>>> A bat and a ball cost $1.10 in total. The bat costs $1.00 more than the ball. How much is the ball?
>>> /bye

# the same question to the model from Activity 2, for comparison
ollama run qwen3:8b --verbose

Resultado esperado: o DeepSeek-R1 mostra primeiro um bloco de raciocínio (entre <think> e </think> ou numa seção “Thinking…”) e só depois a resposta: 5 centavos (e não os 10 da intuição apressada). Compare o eval rate e o tamanho da resposta: modelos de raciocínio gastam muitos tokens “pensando”, o que custa tempo. Para usar um modelo no agent.py, confirme na página dele no Ollama que ele tem a etiqueta tools; sem ela, o servidor recusa a chamada de ferramentas. Nesse caso, mantenha o Qwen3 como “motor” do agente.Expected result: DeepSeek-R1 first shows a reasoning block (between <think> and </think> or in a “Thinking…” section) and only then the answer: 5 cents (not the 10 of hasty intuition). Compare the eval rate and the answer length: reasoning models spend many tokens “thinking”, which costs time. To use a model in agent.py, check on its Ollama page that it has the tools tag; without it the server rejects tool calling. In that case keep Qwen3 as the agent’s “engine”.

Rodar na sua máquina: Ollama, llama.cpp e vLLMRun it on your machine: Ollama, llama.cpp and vLLM

Os três são open-source e expõem uma API compatível com a da OpenAI. A diferença é o público.All three are open-source and expose an OpenAI-compatible API. The difference is the audience.

FerramentaToolQuando usarWhen to usePorta padrãoDefault port
OllamaO caminho mais curto: baixa o modelo, gerencia a memória e já traz uma API. Ideal para começar e para uso pessoal.The shortest path: it downloads the model, manages memory and ships with an API. Ideal to start and for personal use.11434
llama.cpp (llama-server)Mais controle: roda arquivos GGUF direto, em CPU, GPU ou Apple Silicon, com muitas opções de quantização.More control: runs GGUF files directly on CPU, GPU or Apple Silicon, with many quantization options.você define (use --port 8080)you set it (use --port 8080)
vLLMServidor de produção em GPU: atende muitos usuários ao mesmo tempo. Usa o PagedAttention (Kwon et al., 2023) para aproveitar melhor a memória. É a escolha para a nuvem.Production server on GPU: serves many users at once. It uses PagedAttention (Kwon et al., 2023) to make better use of memory. It is the pick for the cloud.8000

Atividade 2: Ollama, do zero a uma conversaActivity 2: Ollama, from zero to a conversation

No Linux, o instalador oficial é um script (leia-o antes de executar, como com qualquer curl | sh). No macOS e no Windows, baixe o instalador em ollama.com.On Linux, the official installer is a script (read it before running, as with any curl | sh). On macOS and Windows, download the installer from ollama.com.

curl -fsSL https://ollama.com/install.sh | sh

ollama pull qwen3:8b          # download (about 5.2 GB); pick the size from Activity 1
ollama run qwen3:8b --verbose # chat in the terminal; --verbose prints speed at the end
>>> Explain what a GPU does in two sentences.
>>> /bye

ollama ps                     # which models are loaded, and on GPU or CPU

Resultado esperado: o modelo responde em português ou inglês (o Qwen3 pode mostrar um trecho de “raciocínio” antes da resposta). Com --verbose aparece a linha eval rate, em tokens por segundo. Em GPU, espere dezenas de tokens por segundo; só em CPU, de 2 a 10. O ollama ps mostra a coluna PROCESSOR: 100% GPU é o ideal; 100% CPU ou divisões como 40%/60% CPU/GPU indicam que faltou VRAM.Expected result: the model replies (Qwen3 may show a “thinking” section before the answer). With --verbose you get an eval rate line, in tokens per second. On a GPU expect tens of tokens per second; on CPU only, 2 to 10. ollama ps shows a PROCESSOR column: 100% GPU is ideal; 100% CPU or splits such as 40%/60% CPU/GPU mean VRAM ran short.

Atividade 3: a API HTTP por trás do chatActivity 3: the HTTP API behind the chat

O terminal é só um cliente. O servidor do Ollama escuta em 127.0.0.1:11434 e, segundo a documentação, oferece o endpoint compatível /v1/chat/completions, com suporte a streaming e a ferramentas.The terminal is just a client. The Ollama server listens on 127.0.0.1:11434 and, per its documentation, offers the compatible /v1/chat/completions endpoint, with streaming and tool support.

curl http://localhost:11434/v1/chat/completions \
  -H "Content-Type: application/json" \
  -d '{
    "model": "qwen3:8b",
    "messages": [{"role": "user", "content": "Say hello in five words."}]
  }' 

Resultado esperado: um JSON com choices[0].message.content contendo a resposta, mais usage com a contagem de tokens. Esse é todo o “segredo”: conversar com um modelo é um POST. Tudo o que vem depois (agentes, interfaces, frameworks) apenas chama esse endereço.Expected result: a JSON with choices[0].message.content holding the reply, plus usage with the token counts. That is the whole “secret”: talking to a model is a POST. Everything that follows (agents, interfaces, frameworks) just calls this address.

Atividade 4: o mesmo, com llama.cppActivity 4: the same, with llama.cpp

Instale o llama.cpp (pacotes, binários e Docker estão no README do projeto) e suba o servidor com um modelo do Hugging Face. A opção -hf baixa o arquivo GGUF; --jinja liga o template de chat necessário para chamada de ferramentas; --api-key exige uma chave.Install llama.cpp (packages, binaries and Docker are in the project README) and start the server with a Hugging Face model. The -hf option downloads the GGUF file; --jinja turns on the chat template needed for tool calling; --api-key requires a key.

llama-server -hf ggml-org/gemma-3-4b-it-GGUF --port 8080 --jinja --api-key my-secret-key

# in another terminal
curl http://127.0.0.1:8080/v1/chat/completions \
  -H "Authorization: Bearer my-secret-key" -H "Content-Type: application/json" \
  -d '{"messages": [{"role": "user", "content": "Say hello in five words."}]}' 

Resultado esperado: a primeira execução baixa o modelo e depois o log diz que o servidor está escutando em 127.0.0.1:8080. A resposta do curl tem o mesmo formato da Atividade 3. Sem o cabeçalho Authorization, o servidor responde 401: a chave funciona.Expected result: the first run downloads the model, then the log says the server is listening on 127.0.0.1:8080. The curl reply has the same shape as in Activity 3. Without the Authorization header the server answers 401: the key works.

O vLLM, o terceiro da lista, precisa de GPU NVIDIA e é o que usaremos na nuvem. Os comandos estão na seção 8.vLLM, the third on the list, needs an NVIDIA GPU and is what we will use in the cloud. The commands are in section 8.

Seu primeiro agente: um laço de 70 linhasYour first agent: a 70-line loop

Um agente parece mágico até você ver o código. O truque se chama chamada de ferramentas (tool calling): junto com a pergunta, você descreve funções que o modelo pode pedir. Se ele quiser usar uma, em vez de texto ele devolve um tool_calls com o nome da função e os argumentos. Quem executa é o seu código, não o modelo; o resultado volta como uma mensagem role: "tool", e o modelo continua. A ideia combina raciocínio e ação em passos alternados, como no artigo ReAct (Yao et al., 2022).An agent feels like magic until you see the code. The trick is called tool calling: along with the question, you describe functions the model may ask for. If it wants one, instead of text it returns a tool_calls with the function name and arguments. Your code runs it, not the model; the result goes back as a role: "tool" message, and the model continues. The idea mixes reasoning and acting in alternating steps, as in the ReAct paper (Yao et al., 2022).

PerguntaQuestion do usuáriofrom the user Modelo (LLM)Model (LLM) decide o próximo passodecides the next step Executa a ferramentaRuns the tool calc · read_file Resposta finalFinal answer sem tool_callsno tool_calls pede ferramentaasks for a tool terminafinishes resultado volta como mensagemresult goes back as a message role: tool
O laço do agente: o modelo pede uma ferramenta, o seu código a executa, o resultado volta ao modelo; repete até o modelo responder sem pedir nada.The agent loop: the model asks for a tool, your code runs it, the result goes back to the model; repeat until the model answers without asking for anything.

Atividade 5: escreva e rode o agenteActivity 5: write and run the agent

Só usa a biblioteca padrão do Python (3.9 ou mais novo). Duas ferramentas: calc (faz contas sem usar eval) e read_file (lê arquivos, mas só dentro da pasta workspace). Salve como agent.py.It uses only the Python standard library (3.9 or newer). Two tools: calc (does maths without using eval) and read_file (reads files, but only inside the workspace folder). Save it as agent.py.

agent.py

import ast, json, operator, os, sys, urllib.request

BASE_URL = os.environ.get("LLM_BASE_URL", "http://localhost:11434/v1")
MODEL = os.environ.get("LLM_MODEL", "qwen3:8b")
API_KEY = os.environ.get("LLM_API_KEY", "not-needed")
WORKDIR = os.path.abspath("workspace")
MAX_STEPS = 6

# ---- tools: plain Python functions -------------------------------------
OPS = {ast.Add: operator.add, ast.Sub: operator.sub, ast.Mult: operator.mul,
       ast.Div: operator.truediv, ast.Pow: operator.pow, ast.USub: operator.neg}

def calc(expression):
    def ev(n):
        if isinstance(n, ast.Constant) and isinstance(n.value, (int, float)):
            return n.value
        if isinstance(n, ast.BinOp) and type(n.op) in OPS:
            return OPS[type(n.op)](ev(n.left), ev(n.right))
        if isinstance(n, ast.UnaryOp) and type(n.op) in OPS:
            return OPS[type(n.op)](ev(n.operand))
        raise ValueError("only numbers and + - * / ** are allowed")
    return str(ev(ast.parse(expression, mode="eval").body))

def read_file(path):
    full = os.path.abspath(os.path.join(WORKDIR, path))
    if not full.startswith(WORKDIR + os.sep):          # never leave the workspace
        raise ValueError("path outside workspace")
    with open(full, encoding="utf-8") as f:
        return f.read()[:4000]                          # cap what goes back to the model

TOOLS = {"calc": calc, "read_file": read_file}
SCHEMAS = [
    {"type": "function", "function": {
        "name": "calc", "description": "Evaluate an arithmetic expression, e.g. 12*(3+4)",
        "parameters": {"type": "object", "required": ["expression"],
                       "properties": {"expression": {"type": "string"}}}}},
    {"type": "function", "function": {
        "name": "read_file", "description": "Read a text file from the workspace folder",
        "parameters": {"type": "object", "required": ["path"],
                       "properties": {"path": {"type": "string"}}}}},
]

# ---- the model call: one HTTP POST ---------------------------------------
def chat(messages):
    body = json.dumps({"model": MODEL, "messages": messages, "tools": SCHEMAS}).encode()
    req = urllib.request.Request(f"{BASE_URL}/chat/completions", data=body, headers={
        "Content-Type": "application/json", "Authorization": f"Bearer {API_KEY}"})
    with urllib.request.urlopen(req, timeout=300) as resp:
        return json.load(resp)["choices"][0]["message"]

# ---- the agent loop --------------------------------------------------------
def run(task):
    messages = [{"role": "system", "content": "You are a careful assistant. Use tools when needed."},
                {"role": "user", "content": task}]
    for step in range(1, MAX_STEPS + 1):
        msg = chat(messages)
        messages.append(msg)
        calls = msg.get("tool_calls") or []
        if not calls:                                   # no tool requested: this is the answer
            return msg["content"]
        for call in calls:
            name = call["function"]["name"]
            args = json.loads(call["function"]["arguments"])
            print(f"[step {step}] {name}({args})", file=sys.stderr)
            try:
                result = TOOLS[name](**args)
            except Exception as e:                      # give the error back so the model can recover
                result = f"error: {e}"
            messages.append({"role": "tool", "tool_call_id": call["id"], "content": result})
    return "stopped: step limit reached"

if __name__ == "__main__":
    print(run(" ".join(sys.argv[1:]) or "What is 17% of 2350?"))
mkdir workspace && echo "The deploy key rotates every 90 days." > workspace/notes.txt

python3 agent.py "What is 17% of 2350?"
python3 agent.py "Read notes.txt and tell me how often the deploy key rotates."
python3 agent.py "Read ../../etc/passwd"

Resultado esperado: na primeira, o terminal mostra em stderr uma linha como [step 1] calc({'expression': '2350*0.17'}) e a resposta final cita 399,5 (o modelo pode escrever “399.5”). Na segunda, aparece read_file e a resposta é “a cada 90 dias”. Na terceira, o seu código bloqueia: a ferramenta devolve error: path outside workspace, o modelo lê o erro e explica que não pôde ler. O laço funcionou como mostrado na figura, e a trava de segurança está no seu código, não na boa vontade do modelo. Modelos muito pequenos às vezes erram o formato da chamada: se acontecer, troque por um que liste “tools” (como o qwen3).Expected result: on the first, the terminal shows on stderr a line like [step 1] calc({'expression': '2350*0.17'}) and the final answer cites 399.5. On the second, read_file appears and the answer is “every 90 days”. On the third, your code blocks it: the tool returns error: path outside workspace, the model reads the error and explains it could not read the file. The loop worked as in the figure, and the safety guard lives in your code, not in the model’s goodwill. Very small models sometimes get the call format wrong: if that happens, switch to one that lists “tools” (such as qwen3).

Por que o try/except devolve o erro ao modelo em vez de parar? Porque um agente útil se recupera: ele lê “file not found”, tenta outro nome e segue. O MAX_STEPS impede um laço infinito (e uma conta inesperada, se o modelo for pago).Why does the try/except hand the error back to the model instead of stopping? Because a useful agent recovers: it reads “file not found”, tries another name and moves on. MAX_STEPS prevents an infinite loop (and a surprise bill, if the model is paid).

Conectando agente, modelo e ferramentasConnecting agent, model and tools

Há duas conexões diferentes, e cada uma tem o seu padrão aberto.There are two different connections, and each has its own open standard.

ConexãoConnectionPadrãoStandardNa práticaIn practice
Agente ↔ modeloAgent ↔ modelAPI compatível com a da OpenAIOpenAI-compatible APIVocê só troca a base_url (e a chave). Ollama, llama.cpp e vLLM falam o mesmo dialeto.You only change the base_url (and the key). Ollama, llama.cpp and vLLM speak the same dialect.
Agente ↔ ferramentasAgent ↔ toolsMCP (Model Context Protocol)Um servidor MCP expõe ferramentas, dados e prompts; qualquer agente que fale MCP os usa sem código novo.An MCP server exposes tools, data and prompts; any agent that speaks MCP uses them with no new code.

A documentação do MCP o descreve como um padrão aberto para conectar aplicações de IA a sistemas externos, e usa a analogia de uma porta USB-C: um conector único em vez de uma integração sob medida para cada par agente-ferramenta. É por isso que o agente da seção anterior, com funções Python escritas à mão, é só o começo: em projetos maiores você troca essas funções por servidores MCP prontos (arquivos, bancos, repositórios, navegador).The MCP documentation describes it as an open standard for connecting AI applications to external systems, and uses the USB-C analogy: one connector instead of a custom integration for every agent-tool pair. That is why the agent in the previous section, with hand-written Python functions, is only the start: in larger projects you swap those functions for ready-made MCP servers (files, databases, repositories, browser).

Atividade 6: troque o modelo sem mexer no agenteActivity 6: swap the model without touching the agent

O agent.py lê a base_url, o modelo e a chave de variáveis de ambiente. Rode a mesma tarefa contra cada servidor:The agent.py reads the base_url, the model and the key from environment variables. Run the same task against each server:

# 1) Ollama (defaults)
python3 agent.py "What is 17% of 2350?"

# 2) llama.cpp, from Activity 4
LLM_BASE_URL=http://127.0.0.1:8080/v1 LLM_API_KEY=my-secret-key LLM_MODEL=gemma-3-4b-it \
  python3 agent.py "What is 17% of 2350?"

# 3) any other OpenAI-compatible server (cloud, in Section 6)
LLM_BASE_URL=http://localhost:8000/v1 LLM_API_KEY=$VLLM_TOKEN LLM_MODEL=Qwen/Qwen2.5-7B-Instruct \
  python3 agent.py "What is 17% of 2350?"

Resultado esperado: as respostas têm o mesmo formato e chegam ao mesmo número, mesmo com modelos e servidores diferentes. O texto exato muda (modelos são probabilísticos), o contrato não. Esse desacoplamento é o que permite desenvolver no notebook e rodar na nuvem sem reescrever nada.Expected result: the answers have the same shape and reach the same number, even with different models and servers. The exact wording changes (models are probabilistic), the contract does not. This decoupling is what lets you develop on a laptop and run in the cloud without rewriting anything.

E se eu não quiser escrever o laço?What if I do not want to write the loop?

  • smolagents (Hugging Face): biblioteca open-source minúscula, com agentes que escrevem código como ação (CodeAgent) ou chamam ferramentas em JSON (ToolCallingAgent). Funciona com modelos locais, via Ollama, e aceita ferramentas de servidores MCP.smolagents (Hugging Face): a tiny open-source library, with agents that write code as their action (CodeAgent) or call tools in JSON (ToolCallingAgent). It works with local models through Ollama and accepts tools from MCP servers.
  • LangGraph: modela o agente como um grafo de estados, útil quando o fluxo tem ramificações, repetições e pontos de aprovação humana.LangGraph: models the agent as a state graph, useful when the flow has branches, repeats and human-approval points.
  • Open WebUI: uma interface de chat web, open-source, que se conecta a Ollama ou a qualquer API compatível com a da OpenAI.Open WebUI: an open-source web chat interface that connects to Ollama or to any OpenAI-compatible API.

Aprenda primeiro o laço de 70 linhas: com ele entendido, qualquer framework deixa de ser caixa-preta, e você sabe depurar quando algo der errado.Learn the 70-line loop first: once you understand it, any framework stops being a black box, and you know how to debug when something goes wrong.

Agentes open-source prontos: quem são e como são feitosReady-made open-source agents: who they are and how they are built

Todos os agentes abaixo são variações do mesmo desenho que você escreveu na seção 5: um laço que chama o modelo, ferramentas que ele pode pedir, um prompt de sistema que define o comportamento e, conforme o projeto, memória entre sessões, uma sandbox para executar código com segurança e canais (terminal, IDE, Telegram). A diferença está em qual desses pedaços cada um caprichou.Every agent below is a variation on the same design you wrote in section 5: a loop that calls the model, tools it can ask for, a system prompt that sets its behaviour and, depending on the project, memory across sessions, a sandbox to run code safely and channels (terminal, IDE, Telegram). What differs is which of those pieces each one polished.

AgenteAgentQuem fezWho made itO que éWhat it isLicençaLicense
Hermes AgentNous ResearchAgente de uso geral que “cresce com você”: guarda memória entre sessões, cria e refina skills a partir da experiência, agenda tarefas em linguagem natural e delega a subagentes. Traz dezenas de ferramentas (busca na web, navegador, visão), conversa por Telegram, Discord, Slack e outros, e executa em 5 tipos de sandbox (local, Docker, SSH, Singularity, Modal). Usa o seu provedor de modelo.General-purpose agent that “grows with you”: keeps memory across sessions, creates and refines skills from experience, schedules tasks in natural language and delegates to subagents. It ships dozens of tools (web search, browser, vision), talks through Telegram, Discord, Slack and more, and runs in 5 kinds of sandbox (local, Docker, SSH, Singularity, Modal). It uses your own model provider.MIT
OpenHandsAll Hands AI e comunidade (antes OpenDevin)All Hands AI and community (formerly OpenDevin)Engenheiro de software autônomo: recebe uma tarefa, edita arquivos, roda comandos e navega na web dentro de uma sandbox em contêiner. Também roda sem interface, em CI.Autonomous software engineer: takes a task, edits files, runs commands and browses the web inside a container sandbox. It also runs headless, in CI.MIT
OpenClawProjeto da comunidadeCommunity projectAssistente pessoal que roda no seu computador e responde em WhatsApp, Telegram, Slack, Discord e dezenas de outros canais. Extensível por skills da comunidade, que merecem desconfiança (veja a seção de segurança).Personal assistant that runs on your computer and answers in WhatsApp, Telegram, Slack, Discord and dozens of other channels. Extensible through community skills, which deserve suspicion (see the security section).MIT
GooseBlock (hoje na Agentic AI Foundation, da Linux Foundation)Block (now at the Linux Foundation’s Agentic AI Foundation)Agente local para automatizar tarefas de código e de uso geral; suas extensões são servidores MCP.Local agent to automate coding and general tasks; its extensions are MCP servers.Apache 2.0
ClineClineAgente de programação dentro do VS Code (e no terminal): lê o projeto, propõe edições e comandos e pede a sua aprovação a cada passo.Coding agent inside VS Code (and the terminal): reads the project, proposes edits and commands and asks your approval at each step.Apache 2.0
AiderPaul Gauthier“Par de programação” no terminal, integrado ao git: cada edição vira um commit que você pode desfazer.Terminal “pair programmer” built on git: every edit becomes a commit you can undo.Apache 2.0
smolagentsHugging FaceBiblioteca mínima para construir o seu agente; o CodeAgent escreve código Python como ação.Minimal library to build your own agent; the CodeAgent writes Python code as its action.Apache 2.0
LangGraphLangChainBiblioteca para modelar agentes como grafos de estados, com pontos de aprovação humana e retomada.Library to model agents as state graphs, with human-approval points and resuming.MIT
Não confunda “Hermes” modelo com “Hermes” agente. A Nous Research também faz a série de modelos Hermes (o Hermes 3, por exemplo, é um ajuste fino do Llama 3.1 com um formato próprio de chamada de ferramentas). O Hermes Agent é o programa que usa um modelo; pode funcionar com qualquer um. Modelo é o cérebro; agente é o corpo.Do not confuse the “Hermes” model with the “Hermes” agent. Nous Research also makes the Hermes model series (Hermes 3, for example, is a fine-tune of Llama 3.1 with its own tool-calling format). Hermes Agent is the program that uses a model; it can work with any. The model is the brain; the agent is the body.

Como escolher: para programar, Aider (terminal) ou Cline (VS Code); para entregar tarefas inteiras de engenharia a um agente isolado, OpenHands; para um assistente que fica ligado e lembra de você, Hermes Agent ou OpenClaw; para construir o seu, smolagents ou LangGraph. Estrelas no GitHub e funções mudam rápido: antes de escolher, confira o repositório e a data do último commit.How to choose: for coding, Aider (terminal) or Cline (VS Code); to hand whole engineering tasks to an isolated agent, OpenHands; for an always-on assistant that remembers you, Hermes Agent or OpenClaw; to build your own, smolagents or LangGraph. GitHub stars and features change fast: check the repository and the date of the last commit before choosing.

Atividade 10: ligue um agente pronto ao seu OllamaActivity 10: plug a ready-made agent into your Ollama

O Aider é o mais simples de testar. Dentro de um repositório git qualquer (um de teste, de preferência):Aider is the simplest to try. Inside any git repository (a throwaway one, preferably):

python3 -m pip install aider-install && aider-install

export OLLAMA_API_BASE=http://127.0.0.1:11434
aider --model ollama_chat/qwen3:8b
> add a function is_prime(n) to a new file primes.py

git log --oneline -3     # in another terminal

Resultado esperado: o Aider cria o primes.py e registra um commit com a mudança; git log mostra o commit, e /undo no Aider o desfaz. Foi o mesmo laço da Atividade 5, só que com ferramentas de edição de arquivo e git. Com um modelo pequeno, espere erros: agentes de código rendem muito mais com modelos de 14B ou mais. O Hermes Agent e o OpenClaw seguem a mesma ideia (apontar para um servidor compatível); o procedimento exato está na documentação de cada um.Expected result: Aider creates primes.py and records a commit with the change; git log shows it, and /undo in Aider reverts it. It was the same loop as Activity 5, only with file-editing and git tools. With a small model expect mistakes: coding agents do far better with models of 14B or more. Hermes Agent and OpenClaw follow the same idea (point to a compatible server); the exact steps are in each one’s documentation.

Quando o seu computador não basta: subir na nuvemWhen your computer is not enough: move to the cloud

Sinais de que chegou a hora: o modelo que você precisa não cabe na memória (o ollama ps mostra divisão CPU/GPU), a velocidade é pequena demais para trabalhar (poucos tokens por segundo), ou várias pessoas precisam usar o mesmo modelo. Existem três caminhos:Signs it is time: the model you need does not fit in memory (ollama ps shows a CPU/GPU split), the speed is too low to work with (few tokens per second), or several people need the same model. There are three paths:

CaminhoPathComo funcionaHow it worksQuando escolherWhen to choose it
Alugar uma VM com GPURent a GPU VMVocê liga uma máquina com GPU (AWS EC2, ou provedores especializados), instala Ollama ou vLLM e paga por hora enquanto ela estiver ligada.You start a GPU machine (AWS EC2, or specialised providers), install Ollama or vLLM and pay by the hour while it is on.Mais controle, mesmo software do local. É o caminho desta nota.Most control, same software as local. This is the path in this note.
Endpoint gerenciadoManaged endpointO provedor hospeda o modelo aberto e cobra por uso ou por hora (por exemplo, Amazon Bedrock ou Hugging Face Inference Endpoints).The provider hosts the open model and bills per use or per hour (for example Amazon Bedrock or Hugging Face Inference Endpoints).Você não quer administrar servidor.You do not want to run a server.
HíbridoHybridO agente roda no notebook e só o modelo vai para a nuvem (a Atividade 7).The agent runs on the laptop and only the model goes to the cloud (Activity 7).Quer manter ferramentas e arquivos locais.You want tools and files to stay local.
Seu notebookYour laptop agent.py localhost:11434 sem GPUno GPU Túnel SSHSSH tunnel criptografadoencrypted porta 22port 22 VM com GPUGPU VM ex.: g5.xlarge (A10G, 24 GB)e.g. g5.xlarge (A10G, 24 GB) Ollama / vLLM 127.0.0.1:11434 só aceita conexão localaccepts local connections only firewall: só o SSH, só o seu IPfirewall: SSH only, your IP only
O agente continua no seu notebook e acha que o modelo é local; o túnel SSH leva a porta 11434 até a GPU alugada, que nunca é exposta à internet.The agent stays on your laptop and thinks the model is local; the SSH tunnel carries port 11434 to the rented GPU, which is never exposed to the internet.

Como referência de tamanho, a instância g5.xlarge da AWS traz uma GPU NVIDIA A10G com 24 GB de memória, 4 vCPUs e 16 GiB de RAM (página oficial da família G5): dá para um modelo de até 14B a 32B em 4 bits, conforme a tabela da seção 2. Os preços mudam por região e por tempo; consulte a página de preços da AWS antes de ligar, e leia a nota de AWS deste site para criar a conta, os alertas de orçamento e o acesso seguro.As a size reference, the AWS g5.xlarge instance has an NVIDIA A10G GPU with 24 GB of memory, 4 vCPUs and 16 GiB of RAM (official G5 family page): enough for a model of 14B to 32B in 4 bits, per the table in section 2. Prices vary by region and time; check the AWS pricing page before you start, and read the AWS note on this site to set up the account, budget alerts and secure access.

Antes de ligar qualquer GPU: crie um alerta de orçamento (AWS Budgets) e anote como desligar a máquina. Uma GPU esquecida ligada é a forma mais comum de levar um susto na fatura.Before starting any GPU: create a budget alert (AWS Budgets) and note how to stop the machine. A GPU left running is the most common way to get a surprise bill.

Atividade 7: Ollama numa GPU alugada, acessado por túnel SSHActivity 7: Ollama on a rented GPU, reached through an SSH tunnel

  1. Ligue a máquina. No console da AWS, inicie uma instância g5.xlarge com uma imagem que já traga os drivers NVIDIA (por exemplo, uma Deep Learning AMI de Ubuntu) e disco de 100 GB. No grupo de segurança, libere apenas a porta 22 (SSH) e apenas para o seu IP.Start the machine. In the AWS console, launch a g5.xlarge instance with an image that already includes NVIDIA drivers (for example an Ubuntu Deep Learning AMI) and a 100 GB disk. In the security group, open only port 22 (SSH) and only to your IP.
  2. Entre e confirme a GPU: ssh -i key.pem ubuntu@IP_DA_MAQUINA e depois nvidia-smi. Deve listar a A10G com cerca de 24 GB.Log in and confirm the GPU: ssh -i key.pem ubuntu@MACHINE_IP and then nvidia-smi. It should list the A10G with about 24 GB.
  3. Instale o Docker e o NVIDIA Container Toolkit (a documentação do Ollama indica este kit para usar GPU em contêiner) e suba o Ollama preso ao localhost da VM:Install Docker and the NVIDIA Container Toolkit (the Ollama documentation points to this toolkit for GPU in containers) and start Ollama bound to the VM’s localhost:
# on the GPU VM
docker run -d --gpus=all -v ollama:/root/.ollama -p 127.0.0.1:11434:11434 --name ollama ollama/ollama
docker exec ollama ollama pull qwen3:32b
docker exec ollama ollama ps
  1. Abra o túnel no seu notebook e rode o mesmo agente:Open the tunnel on your laptop and run the same agent:
# on your laptop, terminal 1 (leave it open)
ssh -i key.pem -N -L 11434:127.0.0.1:11434 ubuntu@MACHINE_IP

# terminal 2: the agent does not change, it still talks to localhost:11434
LLM_MODEL=qwen3:32b python3 agent.py "What is 17% of 2350?"

Resultado esperado: o nvidia-smi da VM mostra o processo do Ollama usando a VRAM durante a resposta, o ollama ps mostra 100% GPU e a resposta do agente chega mais rápida do que no notebook, com um modelo maior. Do ponto de vista do agent.py, nada mudou: a porta local 11434 agora é o túnel até a GPU.Expected result: the VM’s nvidia-smi shows the Ollama process using VRAM during the answer, ollama ps shows 100% GPU, and the agent’s answer arrives faster than on the laptop, with a larger model. From agent.py’s point of view nothing changed: local port 11434 is now the tunnel to the GPU.

Alternativa para vários usuários: vLLMAlternative for several users: vLLM

Para atender uma equipe, o vLLM é mais eficiente. Na VM, instale-o (pip install vllm, de preferência num ambiente virtual) e suba o servidor. Segundo a documentação, ligar a chamada de ferramentas exige --enable-auto-tool-choice e um --tool-call-parser que combine com o modelo (para o Qwen2.5, o hermes):To serve a team, vLLM is more efficient. On the VM, install it (pip install vllm, preferably in a virtual environment) and start the server. Per the documentation, enabling tool calling requires --enable-auto-tool-choice and a --tool-call-parser that matches the model (for Qwen2.5, hermes):

# on the GPU VM
export VLLM_TOKEN=$(openssl rand -hex 24)      # a long random key; save it
vllm serve Qwen/Qwen2.5-7B-Instruct \
  --enable-auto-tool-choice --tool-call-parser hermes \
  --host 127.0.0.1 --port 8000 --api-key "$VLLM_TOKEN"

# on your laptop
ssh -i key.pem -N -L 8000:127.0.0.1:8000 ubuntu@MACHINE_IP
LLM_BASE_URL=http://localhost:8000/v1 LLM_API_KEY=<the-token> LLM_MODEL=Qwen/Qwen2.5-7B-Instruct \
  python3 agent.py "What is 17% of 2350?"

Resultado esperado: o log do vLLM termina com o servidor escutando em 127.0.0.1:8000. Uma chamada sem a chave devolve 401, e com a chave o agente responde como antes. Duas camadas de proteção: o servidor só escuta no localhost da VM e ainda exige a chave.Expected result: the vLLM log ends with the server listening on 127.0.0.1:8000. A call without the key returns 401, and with the key the agent answers as before. Two layers of protection: the server only listens on the VM’s localhost and still requires the key.

Atividade 8: desligue e confirmeActivity 8: shut down and confirm

Terminou? Encerre o túnel (Ctrl+C), e no console da AWS encerre (terminate) a instância, ou pare (stop) se quiser voltar depois. Parar interrompe a cobrança da computação, mas o disco continua cobrado; encerrar apaga tudo.Done? Close the tunnel (Ctrl+C), and in the AWS console terminate the instance, or stop it if you want to return later. Stopping ends compute billing, but the disk is still billed; terminating deletes everything.

Resultado esperado: no console, a instância aparece como terminated (ou stopped), a lista de volumes está vazia quando encerrada, e o painel de custos deixa de somar horas de GPU. Seu agente continua funcionando no notebook com um modelo local menor.Expected result: in the console the instance shows as terminated (or stopped), the volume list is empty when terminated, and the cost dashboard stops adding GPU hours. Your agent keeps working on the laptop with a smaller local model.

Prática: dar memória de documentos ao agente com RAGHands-on: give the agent document memory with RAG

RAG (Retrieval-Augmented Generation) é buscar, antes de responder, os trechos relevantes dos seus documentos e colocá-los no contexto do modelo. Serve para três coisas: responder sobre dados privados ou recentes que o modelo nunca viu, reduzir invenções (a resposta pode citar a fonte) e evitar re-treinar o modelo a cada mudança nos documentos. A explicação completa, com medições, está na nota AI Engineering; aqui vai só o que precisa para ligá-lo ao agente.RAG (Retrieval-Augmented Generation) means fetching, before answering, the relevant passages from your documents and putting them into the model’s context. It serves three purposes: answering about private or recent data the model never saw, cutting down invention (the answer can cite its source) and avoiding retraining the model whenever the documents change. The full explanation, with measurements, is in the AI Engineering note; here is only what you need to wire it to the agent.

Como funciona: um modelo de embeddings transforma cada trecho de texto em um vetor (uma lista de números) em que textos de sentido parecido ficam perto. Você guarda os vetores num banco vetorial. Na pergunta, transforma-a em vetor também e pede ao banco os vizinhos mais próximos. Para o agente, a busca vira só mais uma ferramenta, ao lado de calc e read_file.How it works: an embedding model turns each piece of text into a vector (a list of numbers) where texts with similar meaning end up close. You store the vectors in a vector database. At question time you turn the question into a vector too and ask the database for the nearest neighbours. For the agent, search becomes just one more tool, next to calc and read_file.

Onde guardar os vetoresWhere to store the vectorsQuando escolherWhen to choose it
PostgreSQL + pgvectorPadrão recomendado para começar: você já tem (ou terá) um Postgres, os vetores ficam ao lado dos dados de negócio, e você usa SQL, transações, backup e permissões que já conhece. Atende bem de milhares a alguns milhões de trechos.Recommended default to start: you already have (or will have) a Postgres, vectors sit next to your business data, and you use the SQL, transactions, backups and permissions you already know. Handles thousands to a few million chunks well.
Qdrant, Milvus, WeaviateBancos feitos só para vetores (open-source). Valem a pena com volumes muito grandes, filtros complexos ou necessidade de escalar a busca separadamente do resto.Databases built only for vectors (open-source). Worth it with very large volumes, complex filters or a need to scale search separately from everything else.
Chroma, SQLite-vecProtótipos e projetos pequenos, sem servidor para administrar.Prototypes and small projects, with no server to run.

Atividade 11: RAG com Postgres + pgvector, usado pelo agenteActivity 11: RAG with Postgres + pgvector, used by the agent

Passo 1: suba um Postgres com a extensão (a imagem oficial do projeto já a traz) e baixe um modelo de embeddings pequeno.Step 1: start a Postgres with the extension (the project’s official image already includes it) and pull a small embedding model.

docker run -d --name pg-rag -e POSTGRES_PASSWORD=dev -p 127.0.0.1:5432:5432 pgvector/pgvector:pg17
ollama pull nomic-embed-text      # embedding model, 768 dimensions
pip install "psycopg[binary]"

Passo 2: salve como rag.py. Ele cria a tabela e o índice, indexa os arquivos da pasta workspace (um trecho por parágrafo) e expõe a função de busca.Step 2: save as rag.py. It creates the table and the index, indexes the files in the workspace folder (one chunk per paragraph) and exposes the search function.

rag.py

import glob, json, os, sys, urllib.request
import psycopg

BASE_URL = os.environ.get("LLM_BASE_URL", "http://localhost:11434/v1")
EMBED_MODEL = os.environ.get("EMBED_MODEL", "nomic-embed-text")
DSN = os.environ.get("PG_DSN", "postgresql://postgres:dev@127.0.0.1:5432/postgres")

def embed(text):
    body = json.dumps({"model": EMBED_MODEL, "input": text}).encode()
    req = urllib.request.Request(f"{BASE_URL}/embeddings", data=body,
                                 headers={"Content-Type": "application/json"})
    with urllib.request.urlopen(req, timeout=120) as resp:
        vec = json.load(resp)["data"][0]["embedding"]
    return "[" + ",".join(map(str, vec)) + "]"          # pgvector text format

def setup(conn):
    conn.execute("CREATE EXTENSION IF NOT EXISTS vector")
    conn.execute("""CREATE TABLE IF NOT EXISTS chunks (
        id bigserial PRIMARY KEY, source text, content text, embedding vector(768))""")
    conn.execute("CREATE INDEX IF NOT EXISTS chunks_hnsw ON chunks "
                 "USING hnsw (embedding vector_cosine_ops)")

def ingest():
    with psycopg.connect(DSN, autocommit=True) as conn:
        setup(conn)
        conn.execute("TRUNCATE chunks")                  # simple: re-index everything
        for path in glob.glob("workspace/*.txt") + glob.glob("workspace/*.md"):
            for para in open(path, encoding="utf-8").read().split("\n\n"):
                if para.strip():
                    conn.execute("INSERT INTO chunks (source, content, embedding) "
                                 "VALUES (%s, %s, %s::vector)",
                                 (os.path.basename(path), para.strip(), embed(para)))

def search_docs(query, k=3):
    with psycopg.connect(DSN) as conn:
        rows = conn.execute("SELECT source, content, embedding <=> %s::vector AS dist "
                            "FROM chunks ORDER BY dist LIMIT %s", (embed(query), k)).fetchall()
    return "\n\n".join(f"[{src}] {txt}" for src, txt, _ in rows) or "no results"

if __name__ == "__main__":
    ingest(); print("indexed")

Passo 3: registre a busca como ferramenta no agent.py, com três acréscimos (o resto do arquivo não muda):Step 3: register the search as a tool in agent.py, with three additions (the rest of the file stays the same):

from rag import search_docs                                   # 1) import

TOOLS = {"calc": calc, "read_file": read_file, "search_docs": search_docs}   # 2) register

SCHEMAS.append({"type": "function", "function": {                          # 3) describe
    "name": "search_docs",
    "description": "Search the user's indexed documents. Use it for questions about their notes.",
    "parameters": {"type": "object", "required": ["query"],
                   "properties": {"query": {"type": "string"}}}}})
cat > workspace/policy.md << 'EOF'
The deploy key rotates every 90 days.

Production database backups run at 03:00 UTC and are kept for 14 days.

On-call engineers are paged only for severity 1 incidents.
EOF

python3 rag.py
python3 agent.py "How long are production backups kept?"

Resultado esperado: rag.py imprime indexed; o agente mostra em stderr uma linha como [step 1] search_docs({'query': 'production backups retention'}) e responde que os backups ficam 14 dias. Para ver o banco trabalhando: docker exec -it pg-rag psql -U postgres -c "SELECT source, left(content,40) FROM chunks". Se o agente responder sem chamar a ferramenta, o modelo é pequeno demais para decidir sozinho; teste um modelo maior ou diga na pergunta “search the documents”.Expected result: rag.py prints indexed; the agent shows on stderr a line such as [step 1] search_docs({'query': 'production backups retention'}) and answers that backups are kept for 14 days. To see the database at work: docker exec -it pg-rag psql -U postgres -c "SELECT source, left(content,40) FROM chunks". If the agent answers without calling the tool, the model is too small to decide on its own; try a larger model or say “search the documents” in the question.

Detalhes que importam. O modelo de embeddings usado para indexar e para buscar tem de ser o mesmo (e a dimensão da coluna, vector(768), tem de bater com a dele). O operador <=> é a distância de cosseno, o mesmo que o índice vector_cosine_ops acelera; o HNSW é um índice aproximado, que troca um pouco de precisão por muita velocidade. Em produção, divida os documentos em trechos de algumas centenas de palavras com sobreposição, guarde metadados (fonte, data, dono) e filtre por permissão na própria consulta SQL, para o agente nunca recuperar o que o usuário não pode ver. Também trate o texto recuperado como dado não confiável: um documento pode conter instruções maliciosas (prompt injection).Details that matter. The embedding model used to index and to search must be the same (and the column dimension, vector(768), must match it). The <=> operator is cosine distance, the same the vector_cosine_ops index speeds up; HNSW is an approximate index, trading a little accuracy for a lot of speed. In production, split documents into chunks of a few hundred words with overlap, store metadata (source, date, owner) and filter by permission inside the SQL query itself, so the agent never retrieves what the user may not see. Also treat retrieved text as untrusted data: a document can contain malicious instructions (prompt injection).

Segurança: o que muda quando o modelo tem “mãos”Security: what changes when the model has “hands”

  • Nunca exponha o servidor à internet. A documentação do Ollama explica que ele escuta só em 127.0.0.1 por padrão e que mudar o OLLAMA_HOST para 0.0.0.0 não adiciona nenhuma autenticação. Use túnel SSH, VPN ou um proxy reverso com autenticação.Never expose the server to the internet. The Ollama documentation explains it listens only on 127.0.0.1 by default and that changing OLLAMA_HOST to 0.0.0.0 adds no authentication. Use an SSH tunnel, a VPN or an authenticated reverse proxy.
  • Menor privilégio nas ferramentas. Dê ao agente só o que a tarefa exige: uma pasta, não o disco inteiro (como o read_file faz); leitura, não escrita; uma conta de banco somente leitura.Least privilege on tools. Give the agent only what the task needs: one folder, not the whole disk (as read_file does); read, not write; a read-only database account.
  • Injeção de prompt. Qualquer texto que o agente leia (página web, e-mail, arquivo) pode conter instruções escondidas. O OWASP lista a injeção de prompt e o excesso de autonomia entre os principais riscos de aplicações com LLMs. Peça confirmação humana antes de ações irreversíveis (apagar, enviar, pagar).Prompt injection. Any text the agent reads (a web page, an email, a file) may carry hidden instructions. OWASP lists prompt injection and excessive agency among the top risks of LLM applications. Ask for human confirmation before irreversible actions (delete, send, pay).
  • Segredos fora do código. Chaves em variáveis de ambiente ou no gerenciador de segredos, nunca no repositório.Secrets out of code. Keys in environment variables or a secrets manager, never in the repository.

Para ir mais fundo, veja a nota Secure web apps: headers, auth and AI agents deste site.To go deeper, see the Secure web apps: headers, auth and AI agents note on this site.

Cola e revisãoCheat sheet and review

ColaCheat sheet

Quero…I want…UsoI use
estimar a memóriaestimate memoryparâmetros × bits ÷ 8, mais 1–3 GBparameters × bits ÷ 8, plus 1–3 GB
rodar um modelo rápidorun a model quicklyollama run qwen3:8b
ver se está na GPUsee if it is on the GPUollama ps
chamar por HTTPcall over HTTPPOST /v1/chat/completions
servir GGUF com chaveserve GGUF with a keyllama-server -hf … --port 8080 --api-key …
servir vários usuários na GPUserve several users on GPUvllm serve … --enable-auto-tool-choice --tool-call-parser …
trocar de servidorswitch serverLLM_BASE_URL, LLM_MODEL, LLM_API_KEY
um modelo de raciocínio locala local reasoning modelollama run deepseek-r1:8b
um agente pronto no meu Ollamaa ready-made agent on my OllamaOLLAMA_API_BASE=… aider --model ollama_chat/qwen3:8b
busca por significado no Postgressearch by meaning in PostgresORDER BY embedding <=> $1::vector LIMIT 3
usar a GPU da nuvem sem expô-lause the cloud GPU without exposing itssh -N -L 11434:127.0.0.1:11434 user@ip

Perguntas de revisãoReview questions

  • Qual a diferença entre modelo, servidor e agente? Qual deles é “só um arquivo”?What is the difference between model, server and agent? Which one is “just a file”?
  • Por que um modelo de 8B em 4 bits ocupa perto de 5 GB e não 16 GB?Why does an 8B model in 4 bits take close to 5 GB and not 16 GB?
  • No laço do agente, quem executa a ferramenta: o modelo ou o seu código? Por que isso importa para a segurança?In the agent loop, who runs the tool: the model or your code? Why does that matter for security?
  • O que muda no agent.py ao sair do Ollama local para o vLLM na nuvem?What changes in agent.py when moving from local Ollama to vLLM in the cloud?
  • Por que o Kimi K2, com 32B ativos, não cabe numa GPU de 24 GB? Qual a diferença entre parâmetros totais e ativos?Why does Kimi K2, with 32B active, not fit a 24 GB GPU? What is the difference between total and active parameters?
  • O que o Hermes Agent, o OpenHands e o Aider têm em comum com o agent.py? O que cada um acrescenta?What do Hermes Agent, OpenHands and Aider have in common with agent.py? What does each add?
  • Por que o RAG precisa usar o mesmo modelo de embeddings para indexar e para buscar? Onde se filtra por permissão?Why must RAG use the same embedding model to index and to search? Where do you filter by permission?
  • Por que usar um túnel SSH em vez de abrir a porta 11434 para a internet?Why use an SSH tunnel instead of opening port 11434 to the internet?