2026-05-31
Estou mudando meu título de engenheiro de software para babá de agentes. Eu nem gosto muito de codar com IA, mas com a atual expectativa dos superiores não me restam alternativas. Além disso, sobra mais tempo pra ver tiktok.
Ainda asssim, não queria deixar essa caixa preta me dominar. Queria entender como funciona. Então fiz o que sempre faço: tentei fazer o meu próprio.
number go up
Desde a primeira vez que ouvi a palavra agente no contexto de IA já senti um trem ruim. Um misto de confusão com a certeza que tão me enganando pra aumentar algum número: engajamento, valor de mercado, etc.
Turns out agente é um trem muito simples. É só uma IA muito determinada. Vamos recapitular o funcionamento do motor de inferência de uma LLM: um loop de multiplicação de matriz até um token de parada.
function llm(context) is
while true do
logits = get_probability(context)
next_token = sort(logits)[0] # amostragem gulosa 😋
context.append(next_token)
if next_token == EOS then
break
return context
Pra transformar a LLM em um agente, basta colocar um loop em volta. Todo agente tem uma tarefa. Ao invés de parar quando EOS, você tenta de novo, até a tarefa estar concluída. E tá pronto seu agente!
function agent(task) is
while not task.done? do
result = llm(task.context)
task.context.append(result)
agent.fnl
Escolhi fennel, um LISP de lua, para escrever esse agente.
Usei a lib lua-openai pra abstrair a comunicação com o provedor. No meu caso
o provedor é o ollama rodando na minha máquina. Estou usando o modelo
qwen3.5:9b, que é o que cabeu na minha gpu.
(local oai (require :openai))
(local client (oai.new (os.getenv :OPENAI_API_KEY)))
(tset client :api_base (os.getenv :OPENAI_BASE_URL))
(local chat (client:new_chat_session {:model "qwen3.5:9b"}))
Agora, um REPL básico:
(each [prompt #(io.read)]
(print (chat:send prompt)))
> fennel agent.fnl
say hi
Hi there! 👋 How's it going?
🛠️ tools
LLMs são apenas pesos mortos em uma VRAM, não conseguem interagir com o mundo lá fora. Tudo que sabem é emitir texto. Por isso inventaram tool calls: é como se o modelo emitisse uma procuração. Essas procurações podem conter uma busca na web, edição de arquivo, ou até um comando bash. É dever do código do agente atuar como procurador, realizando a ação no lugar da LLM e devolvendo o resultado pro modelo.
; uma definição de tool usando json schema, pro modelo saber o que é possível fazer
(local bash {:type :function
:function {:name :bash
:description "Run a shell command and return its ouput"
:parameters {:type :object
:properties {:command {:type :string
:description "The shell command to run"}}
:required [:command]}}})
(local chat (client:new_chat_session {:model "qwen3.5:9b" :tools [bash]}))
Vamos criar algumas funções helper para lidar com tools calls. handle-tools
faz o decoding da requisição da LLM, enquanto handle-tool chama a ferramenta
correta e devolve seu resultado.
(local agent {})
(fn agent.handle-tool [name arguments]
(match name
:bash (: (io.popen arguments.command) :read :*a)))
(fn agent.handle-tools [tool_calls]
(each [_ {:function {: name : arguments} :id tool_call_id} (ipairs tool_calls)]
(let [result (agent.handle-tool name (cjson.decode arguments))
content (cjson.encode result)]
(print (chat:send {:role :tool : tool_call_id : content})))))
(fn agent.handle [data]
(match (chat:send data)
{: tool_calls} (agent.handle-tools tool_calls)
message message))
(each [prompt #(io.read)]
(print (agent.handle prompt)))
> fennel basic.fnl
which dir am i?
You are currently in the `/workspace` directory.
⚠️ tool calls são perigosas! Inteligências artificiais são burras, e podem apagar seu drive, banco de dados, etc. Rode tudo dentro de um container se quiser se proteger!
amarrando tudo
Estamos quase lá. Segundo simonw, um agente roda ferramentas em um loop para atingir um objetivo. Nosso pseudoagente já roda ferramentas, só falta um loop:
- (print (chat:send {:role :tool : tool_call_id : content})))))
+ (agent.handle {:role :tool : tool_call_id : content}))))
Isso criará uma recursão mútua, efetivamente criando um loop. Não se procupe com a stack, lua possui tail call optimization.
A condição de parada é o match do agent.handle não casar mais, ou seja, o
modelo não possui mais chamadas de ferramenta a fazer. Esse é um ótimo proxy para
conclusão da tarefa: ou o objetivo foi alcaçado, ou o modelo não sabe mais o
que fazer. Nesse último caso, o loop do REPL dá ao usuário a chance de
providenciar mais contexto pra tarefa, dando o ponta-pé pra mais uma aventura
do agente.
claudio-bode
Eu achava que o claude code era um grande musse. No frigir dos ovos é só um loop dentro de um loop. Então eu fiz meu próprio musse, claudio-bode, o GOAT dos agentes.