MÓDULO 6.3

🏗️ Montando o Agente Completo

Do agent loop ao modelo otimizado: montando todas as peças em um agente local funcional com Ollama e Qwen3.

6
Tópicos
30
Minutos
Avançado
Nível
Prática
Tipo
1

🔄 O Agent Loop

O agent loop é o coração de qualquer agente de IA. Em ~30 linhas de Python, você implementa o ciclo completo: receber tarefa, chamar o LLM, executar ferramentas e retornar o resultado. É o mesmo padrão que o Claude Code usa internamente — a diferença é que aqui rodamos 100% local com Ollama.

Diagrama do Query Loop do agente
O query loop — ciclo principal que conecta LLM, ferramentas e contexto.

Agent Loop Completo (~30 linhas)

from ollama import chat
import json, re

def agent_loop(task, model='qwen3:8b', max_iterations=30):
    messages = [
        {'role': 'system', 'content': 'You are a coding agent. Use tools to complete tasks.'},
        {'role': 'user', 'content': task}
    ]
    for i in range(max_iterations):
        response = chat(
            model=model, messages=messages,
            tools=TOOL_SCHEMAS, stream=False,
            options={'num_ctx': 65536, 'temperature': 0.6, 'keep_alive': '10m'}
        )
        msg = response.message
        content = re.sub(r'<think>.*?</think>', '', msg.content or '', flags=re.DOTALL).strip()
        messages.append({'role': 'assistant', 'content': content})

        if not msg.tool_calls:
            return content  # Done!

        for tc in msg.tool_calls:
            result = TOOL_MAP[tc.function.name](**tc.function.arguments)
            messages.append({'role': 'tool', 'content': str(result)[:10000], 'name': tc.function.name})
    return "Max iterations reached"

System prompt: define o comportamento base do agente — mantenha curto e direto

max_iterations: limite de segurança para evitar loops infinitos — 30 é generoso para a maioria das tarefas

tool_calls: quando o LLM não pede nenhuma ferramenta, a tarefa está concluída

💡 ~30 Linhas = Agente Funcional

Sim, é só isso. O agent loop inteiro cabe em ~30 linhas de Python. A complexidade real está nas ferramentas, no gerenciamento de contexto e nas otimizações — mas o núcleo é surpreendentemente simples. O Claude Code segue exatamente este padrão, apenas com muito mais polish em cima.

2

🧰 Integrando Ferramentas

O agent loop acima referencia TOOL_MAP e TOOL_SCHEMAS — estes são os registros de ferramentas que construímos na Trilha 5. Agora, conectamos tudo. O dispatch é automático: o LLM escolhe a ferramenta pelo nome, e o TOOL_MAP mapeia para a função Python correspondente.

Conectando TOOL_MAP e TOOL_SCHEMAS

from tools import TOOL_MAP, TOOL_SCHEMAS, read_file, write_file, run_shell, search_in_files

# O dispatch é automático:
# 1. LLM retorna tool_calls com nome e argumentos
# 2. TOOL_MAP localiza a função Python pelo nome
# 3. Executa com os argumentos do LLM
result = TOOL_MAP[tc.function.name](**tc.function.arguments)

# Exemplo de fluxo completo:
# LLM decide: "preciso ler o arquivo main.py"
# tool_call: name="read_file", arguments={"path": "main.py"}
# TOOL_MAP["read_file"] → função read_file
# read_file(path="main.py") → conteúdo do arquivo
# Resultado volta para o LLM como mensagem de tool

TOOL_MAP: dicionário {'nome': funcao} — dispatch em O(1)

TOOL_SCHEMAS: lista de schemas JSON que o Ollama passa ao LLM para tool calling

Erro handling: sempre capture exceções no dispatch — uma ferramenta que crasheia mata o loop

📊 Reuse da Trilha 5

tools.py: importar diretamente o módulo construído na Trilha 5 — zero retrabalho

4 ferramentas base: read_file, write_file, run_shell, search_in_files — suficiente para 90% das tarefas de código

Extensível: adicionar novas ferramentas é só criar a função + schema e registrar no TOOL_MAP

3

💾 Memória com Persistência

Um agente que esquece tudo entre execuções é limitado. Com persistência de memória, o agente mantém contexto entre sessões — sabe o que já fez, quais arquivos modificou e quais decisões tomou. Reutilizamos os componentes AgentMemory e PermissionController da Trilha 5.

LocalAgent com Memória Persistente

class LocalAgent:
    def __init__(self, model='qwen3:8b'):
        self.model = model
        self.memory = AgentMemory()  # da T5
        self.permissions = PermissionController()  # da T5

    def run(self, task):
        self.memory.add({'role': 'user', 'content': task})

        # Agent loop com memória
        messages = self.memory.get_messages()
        for i in range(30):
            response = chat(model=self.model, messages=messages, tools=TOOL_SCHEMAS)
            msg = response.message
            content = clean_thinking(msg.content or '')
            self.memory.add({'role': 'assistant', 'content': content})

            if not msg.tool_calls:
                self.memory.save()  # persiste ao final
                return content

            for tc in msg.tool_calls:
                if self.permissions.request_permission(tc.function.name, tc.function.arguments):
                    result = TOOL_MAP[tc.function.name](**tc.function.arguments)
                else:
                    result = "Permission denied"
                self.memory.add({'role': 'tool', 'content': str(result)[:10000]})
            messages = self.memory.get_messages()

        self.memory.save()
        return "Max iterations reached"

AgentMemory: armazena mensagens em JSON — sobrevive a restarts

PermissionController: verifica permissão antes de cada tool call — segurança integrada

save(): chamado ao final de cada tarefa — garante que nada se perde

💡 Composição, Não Herança

O LocalAgent usa composição: ele TEM um AgentMemory e TEM um PermissionController. Cada componente é independente e testável. Se quiser trocar o armazenamento de JSON para SQLite, só muda a implementação do AgentMemory — o LocalAgent nem percebe.

4

📦 Contexto para Janelas Menores

Modelos locais têm context windows menores que a API: 8K-32K tokens vs 200K do Claude. Isso exige compressão agressiva — sem ela, o agente quebra em 3-4 iterações quando os resultados de ferramentas enchem a janela. A preparação de mensagens é obrigatória para agentes locais.

Compressão Agressiva para Modelos Locais

# Modelos locais: 8K-32K vs 200K da API
# Compressão agressiva é OBRIGATÓRIA

def prepare_messages(messages, max_tokens=28000):
    # 1. Truncar outputs longos de ferramentas
    for m in messages:
        if m['role'] == 'tool' and len(m['content']) > 5000:
            m['content'] = m['content'][:5000] + '\n...[truncated]'
    # 2. Sliding window
    return trim_messages(messages, max_tokens)

Truncar tool results: saídas > 5000 chars são cortadas — a maioria da informação útil está no início

Sliding window: reutiliza o trim_messages() da Trilha 5 — mantém as mensagens mais recentes

28K budget: deixa margem para o modelo gerar resposta dentro do num_ctx de 32K

⚠️ Sem Compressão = Agente Quebrado

Sem compressão, um agente local com num_ctx de 32K quebra em 3-4 iterações. Um único run_shell("find . -name '*.py'") pode retornar 20K tokens de output. Some dois tool calls desses e o contexto inteiro está consumido. A compressão não é otimização — é requisito de funcionamento.

5

🧹 Strip de <think> Blocks

O Qwen3 (e outros modelos "thinking") inclui blocos <think>...</think> nas respostas. Esses blocos contêm o raciocínio interno do modelo — útil para debug, mas desastroso para o contexto. Se não forem removidos antes de salvar no histórico, consomem tokens preciosos sem nenhum valor para iterações futuras.

Removendo Thinking Blocks

import re

def clean_thinking(content: str) -> str:
    """Remove Qwen3 thinking blocks from response."""
    return re.sub(r'<think>.*?</think>', '', content, flags=re.DOTALL).strip()

# USAR em todo assistant message antes de salvar no histórico:
clean_content = clean_thinking(msg.content or '')
messages.append({'role': 'assistant', 'content': clean_content})

re.DOTALL: flag essencial — permite que .*? capture newlines dentro do bloco think

Lazy match: .*? (não greedy) para capturar cada bloco individualmente

strip(): remove espaços em branco residuais após remoção dos blocos

💡 Thinking Tokens Consomem Contexto

Um bloco <think> típico do Qwen3 consome 200-2000 tokens. Em 10 iterações do agent loop, isso pode significar 20K tokens desperdiçados — mais da metade da janela de contexto de um modelo 32K. Limpar antes de salvar no histórico é obrigatório para agentes locais.

6

⚡ Otimizações

Cada parâmetro do Ollama impacta a qualidade e velocidade do agente. Configurar errado significa respostas ruins, alucinações ou lentidão desnecessária. O Qwen3 tem configurações recomendadas específicas que diferem dos defaults do Ollama.

Configurações Otimizadas para Qwen3

options = {
    'num_ctx': 65536,       # Context window (SEMPRE configurar!)
    'temperature': 0.6,     # Recomendado Qwen3
    'top_p': 0.95,          # Recomendado Qwen3
    'top_k': 20,            # Recomendado Qwen3
    'keep_alive': '10m',    # Não descarrega modelo entre calls
    # NÃO usar temperature=0 (greedy) com Qwen3!
}

num_ctx: define o tamanho real da janela de contexto — o default do Ollama (2048) é muito pequeno para agentes

temperature 0.6: recomendado pela equipe Qwen — balanceia criatividade com consistência

keep_alive: mantém o modelo carregado na VRAM entre chamadas — evita reload de 5-15 segundos

Fazer

  • Sempre configurar num_ctx explicitamente
  • Usar keep_alive para evitar reload entre calls
  • Seguir as recomendações oficiais do modelo (temp, top_p, top_k)
  • Testar com stream=False primeiro (mais simples de debugar)

Evitar

  • Usar defaults do Ollama sem ajuste (num_ctx=2048 é muito pouco)
  • Usar temperature=0 (greedy) com Qwen3 — causa repetição
  • Ignorar keep_alive e pagar 5-15s de reload a cada chamada
  • Copiar configs de outro modelo sem verificar compatibilidade

📋 Resumo do Módulo

Agent loop completo em ~30 linhas — ciclo LLM + ferramentas + retorno
TOOL_MAP e TOOL_SCHEMAS integrados da Trilha 5 — dispatch automático
LocalAgent com memória persistente e permissões integradas
Compressão agressiva obrigatória para janelas de contexto menores
Strip de <think> blocks economiza centenas de tokens por iteração
Otimizações Qwen3: num_ctx, temperature 0.6, keep_alive obrigatório

Próximo Módulo:

6.4 - 🚀 Do Local ao Produção