Agente LLM
Agente LLM
Seção intitulada “Agente LLM”Construa um agente de chat no terminal passo a passo, progredindo de uma simples chamada LLM até um agente com streaming e ferramentas.
O Que Vamos Construir
Seção intitulada “O Que Vamos Construir”Um agente de chat no terminal que:
- Gera texto com um LLM
- Mantém conversas com múltiplos turnos
- Transmite respostas em tempo real
- Usa ferramentas para acessar capacidades externas
Estrutura do Projeto
Seção intitulada “Estrutura do Projeto”llm-agent/├── .wippy.yaml├── wippy.lock└── src/ ├── _index.yaml ├── ask.lua ├── chat.lua └── tools/ ├── _index.yaml ├── current_time.lua └── calculate.luaFase 1: Geração Simples
Seção intitulada “Fase 1: Geração Simples”Comece com uma função básica que chama llm.generate() com um prompt em string.
Criar o Projeto
Seção intitulada “Criar o Projeto”mkdir llm-agent && cd llm-agentmkdir -p srcDefinições de Entrada
Seção intitulada “Definições de Entrada”Crie src/_index.yaml:
version: "1.0"namespace: app
entries: - name: os_env kind: env.storage.os
- name: processes kind: process.host lifecycle: auto_start: true
- name: dep.llm kind: ns.dependency component: wippy/llm version: "*" parameters: - name: env_storage value: app:os_env - name: process_host value: app:processes
- name: ask kind: function.lua source: file://ask.lua method: handler imports: llm: wippy.llm:llmO módulo LLM precisa de duas entradas de infraestrutura:
env.storage.osfornece chaves de API a partir de variáveis de ambienteprocess.hostfornece o runtime de processos que o módulo LLM usa internamente
Código de Geração
Seção intitulada “Código de Geração”Crie src/ask.lua:
local llm = require("llm")
local function handler(input) local response, err = llm.generate(input, { model = "gpt-4.1-nano", temperature = 0.7, max_tokens = 512, })
if err then return nil, err end
return response.resultend
return { handler = handler }Definição do Modelo
Seção intitulada “Definição do Modelo”O módulo LLM resolve modelos a partir do registro. Adicione uma entrada de modelo ao _index.yaml:
- name: gpt-4.1-nano kind: registry.entry meta: name: gpt-4.1-nano type: llm.model title: GPT-4.1 Nano comment: Fast, affordable model capabilities: - generate - tool_use - structured_output class: - fast priority: 100 max_tokens: 1047576 output_tokens: 32768 pricing: input: 0.1 output: 0.4 providers: - id: wippy.llm.openai:provider provider_model: gpt-4.1-nanoInicializar e Testar
Seção intitulada “Inicializar e Testar”wippy initwippy run -x app:ask "What is the capital of France?"Isso chama a função diretamente e imprime o resultado. A definição do modelo diz ao módulo LLM qual provedor usar e qual nome de modelo enviar para a API.
Fase 2: Conversas
Seção intitulada “Fase 2: Conversas”Evolua de uma única chamada para uma conversa com múltiplos turnos usando o construtor de prompt. Altere a entrada de uma função para um processo com I/O de terminal.
Atualizar Definições de Entrada
Seção intitulada “Atualizar Definições de Entrada”Substitua a entrada ask por um processo chat e adicione a dependência do terminal:
- name: dep.terminal kind: ns.dependency component: wippy/terminal version: "*"
- name: chat kind: process.lua meta: command: name: chat short: Start a terminal chat source: file://chat.lua method: main modules: - io - process imports: llm: wippy.llm:llm prompt: wippy.llm:promptProcesso de Chat
Seção intitulada “Processo de Chat”Crie src/chat.lua:
local io = require("io")local llm = require("llm")local prompt = require("prompt")
local function main() io.print("Chat (type 'quit' to exit)") io.print("")
local conversation = prompt.new() conversation:add_system("You are a helpful assistant. Be concise and direct.")
while true do io.write("> ") io.flush() local input = io.readline() if not input or input == "quit" or input == "exit" then break end if input == "" then goto continue end
conversation:add_user(input)
local response, err = llm.generate(conversation, { model = "gpt-4.1-nano", temperature = 0.7, max_tokens = 1024, })
if err then io.print("Error: " .. tostring(err)) goto continue end
io.print(response.result) io.print("") conversation:add_assistant(response.result)
::continue:: end
io.print("Bye!")end
return { main = main }Executar
Seção intitulada “Executar”wippy updatewippy run chatO construtor de prompt mantém o histórico completo da conversa. Cada turno adiciona a mensagem do usuário e a resposta do assistente, fornecendo ao modelo o contexto das trocas anteriores.
Fase 3: Framework de Agentes
Seção intitulada “Fase 3: Framework de Agentes”O módulo de agentes fornece uma abstração de nível mais alto sobre chamadas LLM brutas. Agentes são definidos declarativamente com um prompt, modelo e ferramentas, e depois carregados e executados através de um padrão de contexto/runner.
Adicionar Dependência do Agente
Seção intitulada “Adicionar Dependência do Agente”Adicione ao _index.yaml:
- name: dep.agent kind: ns.dependency component: wippy/agent version: "*" parameters: - name: process_host value: app:processesDefinir um Agente
Seção intitulada “Definir um Agente”Adicione uma entrada de agente:
- name: assistant kind: registry.entry meta: type: agent.gen1 name: assistant title: Assistant comment: Terminal chat agent prompt: | You are a helpful terminal assistant. Be concise and direct. Answer questions clearly. If you don't know something, say so. Do not use emoji in responses. model: gpt-4.1-nano max_tokens: 1024 temperature: 0.7Atualizar o Processo de Chat
Seção intitulada “Atualizar o Processo de Chat”Mude para o framework de agentes. Atualize as importações da entrada:
- name: chat kind: process.lua meta: command: name: chat short: Start a terminal chat source: file://chat.lua method: main modules: - io - process imports: prompt: wippy.llm:prompt agent_context: wippy.agent:contextAtualize src/chat.lua:
local io = require("io")local prompt = require("prompt")local agent_context = require("agent_context")
local function main() io.print("Chat (type 'quit' to exit)") io.print("")
local ctx = agent_context.new() local runner, err = ctx:load_agent("app:assistant") if err then io.print("Failed to load agent: " .. tostring(err)) return end
local conversation = prompt.new()
while true do io.write("> ") io.flush() local input = io.readline() if not input or input == "quit" or input == "exit" then break end if input == "" then goto continue end
conversation:add_user(input)
local response, gen_err = runner:step(conversation) if gen_err then io.print("Error: " .. tostring(gen_err)) goto continue end
io.print(response.result) io.print("") conversation:add_assistant(response.result)
::continue:: end
io.print("Bye!")end
return { main = main }O framework de agentes separa a definição do agente (prompt, modelo, parâmetros) da lógica de execução. O mesmo agente pode ser carregado com diferentes contextos, ferramentas e modelos em tempo de execução.
Fase 4: Streaming
Seção intitulada “Fase 4: Streaming”Transmita respostas token por token em vez de esperar pela resposta completa.
Implementação de Streaming
Seção intitulada “Implementação de Streaming”Atualize src/chat.lua:
local io = require("io")local prompt = require("prompt")local agent_context = require("agent_context")
local STREAM_TOPIC = "stream"
local function stream_response(runner, conversation, stream_ch) local done_ch = channel.new(1)
coroutine.spawn(function() local response, err = runner:step(conversation, { stream_target = { reply_to = process.pid(), topic = STREAM_TOPIC, }, }) done_ch:send({ response = response, err = err }) end)
local full_text = ""
while true do local result = channel.select({ stream_ch:case_receive(), done_ch:case_receive(), }) if not result.ok then break end
if result.channel == done_ch then local r = result.value return full_text, r.response, r.err end
local chunk = result.value if chunk.type == "chunk" then io.write(chunk.content or "") full_text = full_text .. (chunk.content or "") elseif chunk.type == "done" then local r, ok = done_ch:receive() if ok and r then return full_text, r.response, r.err end return full_text, nil, nil elseif chunk.type == "error" then return nil, nil, chunk.error and chunk.error.message or "stream error" end end
return full_text, nil, nilend
local function main() io.print("Chat (type 'quit' to exit)") io.print("")
local ctx = agent_context.new() local runner, err = ctx:load_agent("app:assistant") if err then io.print("Failed to load agent: " .. tostring(err)) return end
local conversation = prompt.new() local stream_ch = process.listen(STREAM_TOPIC)
while true do io.write("> ") io.flush() local input = io.readline() if not input or input == "quit" or input == "exit" then break end if input == "" then goto continue end
conversation:add_user(input)
local text, _, gen_err = stream_response(runner, conversation, stream_ch) if gen_err then io.print("Error: " .. tostring(gen_err)) goto continue end
io.print("") if text and text ~= "" then conversation:add_assistant(text) end
::continue:: end
process.unlisten(stream_ch) io.print("Bye!")end
return { main = main }Padrões principais:
coroutine.spawnexecutarunner:step()em uma coroutine separada para que a coroutine principal possa processar chunks do streamchannel.selectmultiplexa o canal de stream e o canal de conclusão- Um único
process.listen()é criado uma vez e reutilizado entre os turnos - O texto é acumulado para adicionar ao histórico da conversa
Fase 5: Ferramentas
Seção intitulada “Fase 5: Ferramentas”Forneça ao agente ferramentas que ele pode chamar para acessar capacidades externas.
Definir Ferramentas
Seção intitulada “Definir Ferramentas”Crie src/tools/_index.yaml:
version: "1.0"namespace: app.tools
entries: - name: current_time kind: function.lua meta: type: tool title: Current Time input_schema: | { "type": "object", "properties": {}, "additionalProperties": false } llm_alias: get_current_time llm_description: Get the current date and time in UTC. source: file://current_time.lua modules: [time] method: handler
- name: calculate kind: function.lua meta: type: tool title: Calculate input_schema: | { "type": "object", "properties": { "expression": { "type": "string", "description": "Math expression to evaluate" } }, "required": ["expression"], "additionalProperties": false } llm_alias: calculate llm_description: Evaluate a mathematical expression and return the result. source: file://calculate.lua modules: [expr] method: handlerOs metadados da ferramenta informam ao LLM o que a ferramenta faz:
input_schemaé um JSON Schema definindo os argumentosllm_aliasé o nome da função que o LLM vêllm_descriptionexplica quando usar a ferramenta
Implementar Ferramentas
Seção intitulada “Implementar Ferramentas”Crie src/tools/current_time.lua:
local time = require("time")
local function handler() local now = time.now() return { utc = now:format("2006-01-02T15:04:05Z"), unix = now:unix(), }end
return { handler = handler }Crie src/tools/calculate.lua:
local expr = require("expr")
local function handler(args) local result, err = expr.eval(args.expression) if err then return { error = tostring(err) } end return { result = result }end
return { handler = handler }Registrar Ferramentas no Agente
Seção intitulada “Registrar Ferramentas no Agente”Atualize a entrada do agente em src/_index.yaml para referenciar as ferramentas:
- name: assistant kind: registry.entry meta: type: agent.gen1 name: assistant title: Assistant comment: Terminal chat agent prompt: | You are a helpful terminal assistant. Be concise and direct. Answer questions clearly. If you don't know something, say so. Use tools when they help answer the question. Do not use emoji in responses. model: gpt-4.1-nano max_tokens: 1024 temperature: 0.7 tools: - app.tools:current_time - app.tools:calculateAdicionar Execução de Ferramentas
Seção intitulada “Adicionar Execução de Ferramentas”Atualize os módulos do processo de chat para incluir json e funcs:
modules: - io - json - process - funcsAtualize src/chat.lua com a execução de ferramentas:
local io = require("io")local json = require("json")local funcs = require("funcs")local prompt = require("prompt")local agent_context = require("agent_context")
local STREAM_TOPIC = "stream"
local function stream_response(runner, conversation, stream_ch) local done_ch = channel.new(1)
coroutine.spawn(function() local response, err = runner:step(conversation, { stream_target = { reply_to = process.pid(), topic = STREAM_TOPIC, }, }) done_ch:send({ response = response, err = err }) end)
local full_text = ""
while true do local result = channel.select({ stream_ch:case_receive(), done_ch:case_receive(), }) if not result.ok then break end
if result.channel == done_ch then local r = result.value return full_text, r.response, r.err end
local chunk = result.value if chunk.type == "chunk" then io.write(chunk.content or "") full_text = full_text .. (chunk.content or "") elseif chunk.type == "done" then local r, ok = done_ch:receive() if ok and r then return full_text, r.response, r.err end return full_text, nil, nil elseif chunk.type == "error" then return nil, nil, chunk.error and chunk.error.message or "stream error" end end
return full_text, nil, nilend
local function execute_tools(tool_calls) local results = {} for _, tc in ipairs(tool_calls) do local args = tc.arguments if type(args) == "string" then args = json.decode(args) or {} end
io.write("[" .. tc.name .. "] ") io.flush()
local result, err = funcs.call(tc.registry_id, args) if err then results[tc.id] = { error = tostring(err) } io.print("error") else results[tc.id] = result io.print("done") end end return resultsend
local function run_turn(runner, conversation, stream_ch) while true do local text, response, err = stream_response(runner, conversation, stream_ch) if err then io.print("") return nil, err end
if text and text ~= "" then io.print("") end
local tool_calls = response and response.tool_calls if not tool_calls or #tool_calls == 0 then return text, nil end
if text and text ~= "" then conversation:add_assistant(text) end
local results = execute_tools(tool_calls)
for _, tc in ipairs(tool_calls) do local result = results[tc.id] local result_str = json.encode(result) or "{}" conversation:add_function_call(tc.name, tc.arguments, tc.id) conversation:add_function_result(tc.name, result_str, tc.id) end endend
local function main() io.print("Terminal Agent (type 'quit' to exit)") io.print("")
local ctx = agent_context.new() local runner, err = ctx:load_agent("app:assistant") if err then io.print("Failed to load agent: " .. tostring(err)) return end
local conversation = prompt.new() local stream_ch = process.listen(STREAM_TOPIC)
while true do io.write("> ") io.flush() local input = io.readline() if not input or input == "quit" or input == "exit" then break end if input == "" then goto continue end
conversation:add_user(input)
local text, gen_err = run_turn(runner, conversation, stream_ch) if gen_err then io.print("Error: " .. tostring(gen_err)) goto continue end if text and text ~= "" then conversation:add_assistant(text) end
::continue:: end
process.unlisten(stream_ch) io.print("Bye!")end
return { main = main }O loop de execução de ferramentas:
- Chama
runner:step()com streaming - Se a resposta contém
tool_calls, executa cada ferramenta viafuncs.call() - Adiciona as chamadas de ferramentas e resultados à conversa
- Volta ao passo 1 para o agente incorporar os resultados
- Quando não há mais chamadas de ferramentas, retorna o texto final
Executar o Agente
Seção intitulada “Executar o Agente”wippy updatewippy run chatTerminal Agent (type 'quit' to exit)
> what time is it?[get_current_time] doneThe current time is 17:20 UTC on February 12, 2026.
> what is 125 * 16?[calculate] done125 * 16 = 2000.
> quitBye!Próximos Passos
Seção intitulada “Próximos Passos”- Módulo LLM - Referência completa da API do LLM
- Módulo de Agentes - Referência do framework de agentes
- Aplicações CLI - Padrões de I/O de terminal
- Processos - Modelo de processos e comunicação