🔄 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.
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.
🧰 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
💾 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.
📦 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.
🧹 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.
⚡ 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
Próximo Módulo:
6.4 - 🚀 Do Local ao Produção