mozak.tech Engenharia de IA Corporativa 45%

Parte II — Sistemas que Raciocinam e Agem de Forma Autônoma

2.8 — Projeto Prático: Agente de Pesquisa Autônomo de Ponta a Ponta (Agente Autônomo)

Objetivo da Aula

Ao concluir esta aula, você será capaz de:

Implementar persistência de memória em arquivo JSON local (carregar e salvar)

Construir tools de pesquisa, notas e relatório com estado persistente entre sessões

Gerar relatório Markdown estruturado com notas agrupadas por tag

Completar os 5 TODOs do agente de pesquisa autônomo

Integrar todos os padrões anteriores (agent loop, tools, memória, logs) em um projeto coeso

Por que isso importa

Esta unidade integra tudo o que você aprendeu no módulo. O agente de pesquisa: - Tem memory persistente (JSON local — padrão para prototipagem, vector DB em produção) - Usa tools especializadas (buscar, anotar, listar, gerar relatório) - Persiste entre sessões (rodar novamente usa as notas da sessão anterior) - Gera output estruturado (relatório Markdown pronto para compartilhar)

Em produção, esse padrão é a base de assistentes de pesquisa, sistemas de knowledge management e copilotas de inteligência competitiva.

Conceitos Fundamentais

Memória Persistente com JSON

O padrão mais simples de persistência: um arquivo JSON no disco. Sem banco de dados, sem dependências.

from pathlib import Path

import json



MEMORIA_FILE = Path('memoria_agente.json')



# TODO 1: Carregar

def carregar_memoria() -> dict:

    if MEMORIA_FILE.exists():

        with open(MEMORIA_FILE, encoding='utf-8') as f:

            return json.load(f)

    return {'notas': []}  # estrutura inicial vazia



# TODO 2: Salvar

def salvar_memoria(memoria: dict):

    with open(MEMORIA_FILE, 'w', encoding='utf-8') as f:

        json.dump(memoria, f, indent=2, ensure_ascii=False)

ensure_ascii=False preserva acentos portugueses no arquivo. indent=2 torna o JSON legível quando você abrir com um editor.

TODO 3: salvar_nota() com Persistência

def salvar_nota(titulo: str, conteudo: str, tags: list[str] = None, memoria: dict = None) -> str:

    nota = {

        'id': len(memoria.get('notas', [])) + 1,

        'titulo': titulo,

        'conteudo': conteudo,

        'tags': tags or [],

        'criado_em': datetime.utcnow().isoformat(),

    }

    # TODO 3: adicionar e persistir

    memoria['notas'].append(nota)

    salvar_memoria(memoria)  # persiste imediatamente

    return f'Nota "{titulo}" salva com id={nota["id"]}'

Persistir imediatamente (não só ao final) garante que mesmo se o agente for interrompido no meio, as notas já estão salvas.

TODO 4: gerar_relatorio() em Markdown

def gerar_relatorio(titulo: str, conclusao: str, memoria: dict) -> str:

    notas = memoria.get('notas', [])

    data_atual = datetime.utcnow().strftime('%Y-%m-%d')

    

    # Agrupar por tag

    por_tag: dict[str, list] = {}

    sem_tag = []

    for nota in notas:

        if nota.get('tags'):

            for tag in nota['tags']:

                por_tag.setdefault(tag, []).append(nota)

        else:

            sem_tag.append(nota)

    

    # Construir Markdown

    linhas = [

        f'# {titulo}',

        f'',

        f'*Gerado em: {data_atual}*',

        f'',

        f'## Sumário',

        f'',

        f'- Total de notas: {len(notas)}',

        f'- Tags: {", ".join(sorted(por_tag.keys())) or "nenhuma"}',

        f'',

    ]

    

    for tag, notas_tag in sorted(por_tag.items()):

        linhas.append(f'## {tag.title()}')

        linhas.append('')

        for nota in notas_tag:

            linhas.append(f'### [{nota["id"]}] {nota["titulo"]}')

            linhas.append('')

            linhas.append(nota['conteudo'])

            linhas.append('')

    

    if sem_tag:

        linhas.append('## Sem Categoria')

        linhas.append('')

        for nota in sem_tag:

            linhas.append(f'### {nota["titulo"]}')

            linhas.append(nota['conteudo'])

            linhas.append('')

    

    linhas.extend(['## Conclusão', '', conclusao])

    

    return '\n'.join(linhas)

TODO 5: Resumo Final

# No final de rodar_agente():

notas_salvas = len(memoria.get('notas', []))

print(f'\n[MEMÓRIA] {notas_salvas} nota(s) salva(s) em {MEMORIA_FILE}')



# Estimativa de custo (opcional mas útil)

# O agente não acumula custo explicitamente no starter, mas podemos estimar

# com base no número de iterações × custo médio por iteração

Aprofundamento Técnico

O Ciclo Completo de Sessões

Sessão 1:

  carregar_memoria() → {'notas': []}  (arquivo não existe)

  agente pesquisa → salva 3 notas

  salvar_memoria() → cria memoria_agente.json



Sessão 2:

  carregar_memoria() → {'notas': [nota1, nota2, nota3]}  (carrega da sessão anterior)

  agente pesquisa → salva mais 2 notas  

  salvar_memoria() → atualiza com 5 notas totais



Sessão 3:

  listar_notas(tag='ia-generativa') → filtra por tag

  gerar_relatorio(...) → combina todas as notas em Markdown

Isso é um knowledge accumulator — o agente fica mais rico com cada sessão.

Sistema de Tags para Organização

def listar_notas(memoria: dict, tag: str = None) -> str:

    notas = memoria.get('notas', [])

    if tag:

        notas = [n for n in notas if tag in n.get('tags', [])]

    if not notas:

        return f'Nenhuma nota encontrada{f" com tag \'{tag}\'" if tag else ""}.'

    return '\n'.join(

        f'[{n["id"]}] {n["titulo"]} — tags: {n.get("tags", [])} — {n["criado_em"][:10]}'

        for n in notas

    )

O agente pode chamar listar_notas(tag='rag') antes de pesquisar para verificar se já tem notas sobre o tema.

Tool de Relatório como Tool “Terminal”

A tool gerar_relatorio é uma “terminal tool” — quando o agente a chama, indica que a pesquisa está completa. O system prompt instrui:

system='Você é um agente de pesquisa. Use as tools para pesquisar, anotar descobertas '

       'e gerar um relatório completo ao final.'

“ao final” instrui o agente a chamar gerar_relatorio como última ação.

Exemplos Anotados

Exemplo 1: Todos os TODOs Resolvidos

from pathlib import Path

from datetime import datetime

import json

import os

import anthropic



client = anthropic.Anthropic(api_key=os.environ.get('ANTHROPIC_API_KEY'))

MEMORIA_FILE = Path('memoria_agente.json')



# TODO 1: carregar

def carregar_memoria() -> dict:

    if MEMORIA_FILE.exists():

        with open(MEMORIA_FILE, encoding='utf-8') as f:

            return json.load(f)

    return {'notas': []}



# TODO 2: salvar

def salvar_memoria(memoria: dict):

    with open(MEMORIA_FILE, 'w', encoding='utf-8') as f:

        json.dump(memoria, f, indent=2, ensure_ascii=False)



# TODO 3: adicionar nota e persistir

def salvar_nota(titulo: str, conteudo: str, tags: list[str] = None, memoria: dict = None) -> str:

    nota = {

        'id': len(memoria.get('notas', [])) + 1,

        'titulo': titulo,

        'conteudo': conteudo,

        'tags': tags or [],

        'criado_em': datetime.utcnow().isoformat(),

    }

    memoria['notas'].append(nota)

    salvar_memoria(memoria)

    return f'Nota "{titulo}" salva com id={nota["id"]}'



# TODO 4: gerar relatório Markdown

def gerar_relatorio(titulo: str, conclusao: str, memoria: dict) -> str:

    notas = memoria.get('notas', [])

    data = datetime.utcnow().strftime('%Y-%m-%d')

    

    por_tag: dict[str, list] = {}

    sem_tag = []

    for n in notas:

        if n.get('tags'):

            for t in n['tags']:

                por_tag.setdefault(t, []).append(n)

        else:

            sem_tag.append(n)

    

    md = [f'# {titulo}', f'', f'*{data} | {len(notas)} notas*', f'']

    

    for tag, ns in sorted(por_tag.items()):

        md.append(f'## {tag.replace("-", " ").title()}')

        md.append('')

        for n in ns:

            md.append(f'### {n["titulo"]}')

            md.append('')

            md.append(n['conteudo'])

            md.append('')

    

    md.extend(['## Conclusão', '', conclusao])

    return '\n'.join(md)



def rodar_agente(tarefa: str):

    memoria = carregar_memoria()

    mensagens = [{'role': 'user', 'content': tarefa}]

    max_iter = 10

    

    print(f'\n[AGENTE] Tarefa: {tarefa[:100]}')

    print(f'[MEMÓRIA] {len(memoria.get("notas", []))} nota(s) existente(s)')

    

    for iteracao in range(1, max_iter + 1):

        print(f'\n[Iter {iteracao}]')

        response = client.messages.create(

            model='claude-haiku-4-5-20251001',

            max_tokens=1500,

            tools=TOOLS,

            system='Você é um agente de pesquisa. Pesquise, anote descobertas e gere relatório ao final.',

            messages=mensagens,

        )

        

        if response.stop_reason == 'end_turn':

            texto = next((b.text for b in response.content if hasattr(b, 'text')), '')

            print(f'\n[CONCLUÍDO] {texto[:200]}')

            break

        

        tool_use_blocks = [b for b in response.content if b.type == 'tool_use']

        if not tool_use_blocks:

            break

        

        mensagens.append({'role': 'assistant', 'content': response.content})

        tool_results = []

        

        for block in tool_use_blocks:

            print(f'  → {block.name}({list(block.input.keys())})')

            resultado = executar_tool(block.name, block.input, memoria)

            print(f'  ← {resultado[:100]}')

            tool_results.append({'type': 'tool_result', 'tool_use_id': block.id, 'content': resultado})

        

        mensagens.append({'role': 'user', 'content': tool_results})

    

    # TODO 5: resumo final

    notas_salvas = len(memoria.get('notas', []))

    print(f'\n[MEMÓRIA] {notas_salvas} nota(s) salva(s) em {MEMORIA_FILE}')

Padrões e Armadilhas

Padrões

Padrão 1: Persistir nota imediatamente após criação

memoria['notas'].append(nota)

salvar_memoria(memoria)  # imediatamente — não apenas ao final

Se o agente for interrompido, notas já escritas são preservadas.

Padrão 2: ID sequencial baseado em tamanho da lista

'id': len(memoria.get('notas', [])) + 1

Simples e incrementa automaticamente. Em produção, use uuid4() para evitar conflitos em acesso concorrente.

Padrão 3: ensure_ascii=False + indent=2 em json.dump

json.dump(memoria, f, indent=2, ensure_ascii=False)

Arquivo legível por humanos + suporte a português.

Armadilhas

⚠️ Armadilha 1: Carregar sem checar existência do arquivo

# ERRADO: FileNotFoundError se MEMORIA_FILE não existe

with open(MEMORIA_FILE) as f:

    return json.load(f)



# CORRETO:

if MEMORIA_FILE.exists():

    with open(MEMORIA_FILE) as f:

        return json.load(f)

return {'notas': []}

⚠️ Armadilha 2: executar_tool precisa receber memoria como parâmetro

def executar_tool(nome: str, args: dict, memoria: dict) -> str:

    if nome == 'salvar_nota':

        return salvar_nota(args['titulo'], args['conteudo'], args.get('tags', []), memoria)

    # ...

A memoria deve fluir por referência para que salvar_nota modifique a instância correta.

⚠️ Armadilha 3: Relatório gerado com memoria desatualizada

# ERRADO: memoria carregada no início pode não ter notas salvas nesta sessão

# se salvar_memoria() não foi chamado corretamente

def gerar_relatorio(titulo: str, conclusao: str, memoria: dict) -> str:

    notas = memoria.get('notas', [])  # ← usa a mesma instância que foi modificada

Como memoria é passada por referência e salvar_nota modifica memoria['notas'], o relatório sempre tem as notas mais recentes.

⚗ Laboratório prático — mozak.tech
Se não for realizar o laboratório, pule para o próximo capítulo.

Ponte para o Lab

5 TODOs mapeados diretamente:

TODO 1 — carregar_memoria():

if MEMORIA_FILE.exists():

    with open(MEMORIA_FILE, encoding='utf-8') as f:

        return json.load(f)

return {'notas': []}

TODO 2 — salvar_memoria():

with open(MEMORIA_FILE, 'w', encoding='utf-8') as f:

    json.dump(memoria, f, indent=2, ensure_ascii=False)

TODO 3 — em salvar_nota(), após criar nota:

memoria['notas'].append(nota)

salvar_memoria(memoria)

TODO 4 — gerar_relatorio(): implementar o Markdown com agrupamento por tag (veja Exemplo 1 desta aula).

TODO 5 — após o loop em rodar_agente():

print(f'\n[MEMÓRIA] {len(memoria.get("notas", []))} nota(s) salva(s) em {MEMORIA_FILE}')

Agora você está pronto para o lab.