Agente LLM
Agente LLM
Sección titulada «Agente LLM»Construye un agente de chat para terminal paso a paso, progresando desde una simple llamada LLM hasta un agente con streaming y herramientas.
Lo Que Construiremos
Sección titulada «Lo Que Construiremos»Un agente de chat para terminal que:
- Genera texto con un LLM
- Mantiene conversaciones multi-turno
- Transmite respuestas en tiempo real
- Usa herramientas para acceder a capacidades externas
Estructura del Proyecto
Sección titulada «Estructura del Proyecto»llm-agent/├── .wippy.yaml├── wippy.lock└── src/ ├── _index.yaml ├── ask.lua ├── chat.lua └── tools/ ├── _index.yaml ├── current_time.lua └── calculate.luaFase 1: Generación Simple
Sección titulada «Fase 1: Generación Simple»Comienza con una función básica que llama a llm.generate() con un prompt de texto.
Crear el Proyecto
Sección titulada «Crear el Proyecto»mkdir llm-agent && cd llm-agentmkdir -p srcDefiniciones de Entradas
Sección titulada «Definiciones de Entradas»Crea 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:llmEl módulo LLM necesita dos entradas de infraestructura:
env.storage.osproporciona claves API desde variables de entornoprocess.hostproporciona el runtime de procesos que el módulo LLM usa internamente
Código de Generación
Sección titulada «Código de Generación»Crea 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 }Definición del Modelo
Sección titulada «Definición del Modelo»El módulo LLM resuelve modelos desde el registro. Agrega una entrada de modelo a _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 y Probar
Sección titulada «Inicializar y Probar»wippy initwippy run -x app:ask "What is the capital of France?"Esto llama a la función directamente e imprime el resultado. La definición del modelo le indica al módulo LLM qué proveedor usar y qué nombre de modelo enviar a la API.
Fase 2: Conversaciones
Sección titulada «Fase 2: Conversaciones»Pasa de una sola llamada a una conversación multi-turno usando el constructor de prompts. Cambia la entrada de una función a un proceso con E/S de terminal.
Actualizar Definiciones de Entradas
Sección titulada «Actualizar Definiciones de Entradas»Reemplaza la entrada ask con un proceso chat y agrega la dependencia de 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:promptProceso de Chat
Sección titulada «Proceso de Chat»Crea 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 }Ejecutar
Sección titulada «Ejecutar»wippy updatewippy run chatEl constructor de prompts mantiene el historial completo de la conversación. Cada turno agrega el mensaje del usuario y la respuesta del asistente, dando al modelo contexto de los intercambios anteriores.
Fase 3: Framework de Agentes
Sección titulada «Fase 3: Framework de Agentes»El módulo de agentes proporciona una abstracción de nivel superior sobre las llamadas LLM directas. Los agentes se definen declarativamente con un prompt, modelo y herramientas, y luego se cargan y ejecutan a través de un patrón de contexto/runner.
Agregar Dependencia del Agente
Sección titulada «Agregar Dependencia del Agente»Agrega a _index.yaml:
- name: dep.agent kind: ns.dependency component: wippy/agent version: "*" parameters: - name: process_host value: app:processesDefinir un Agente
Sección titulada «Definir un Agente»Agrega una 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.7Actualizar el Proceso de Chat
Sección titulada «Actualizar el Proceso de Chat»Cambia al framework de agentes. Actualiza los imports de la 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:contextActualiza 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 }El framework de agentes separa la definición del agente (prompt, modelo, parámetros) de la lógica de ejecución. El mismo agente puede cargarse con diferentes contextos, herramientas y modelos en tiempo de ejecución.
Fase 4: Streaming
Sección titulada «Fase 4: Streaming»Transmite respuestas token por token en lugar de esperar la respuesta completa.
Implementación de Streaming
Sección titulada «Implementación de Streaming»Actualiza 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 }Patrones clave:
coroutine.spawnejecutarunner:step()en una corrutina separada para que la corrutina principal pueda procesar los chunks del streamchannel.selectmultiplexa el canal de stream y el canal de completado- Se crea un solo
process.listen()una vez y se reutiliza entre turnos - El texto se acumula para agregarlo al historial de la conversación
Fase 5: Herramientas
Sección titulada «Fase 5: Herramientas»Dale al agente herramientas que pueda llamar para acceder a capacidades externas.
Definir Herramientas
Sección titulada «Definir Herramientas»Crea 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: handlerLos metadatos de las herramientas le indican al LLM qué hace cada herramienta:
input_schemaes un JSON Schema que define los argumentosllm_aliases el nombre de función que ve el LLMllm_descriptionexplica cuándo usar la herramienta
Implementar Herramientas
Sección titulada «Implementar Herramientas»Crea 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 }Crea 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 Herramientas en el Agente
Sección titulada «Registrar Herramientas en el Agente»Actualiza la entrada del agente en src/_index.yaml para referenciar las herramientas:
- 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:calculateAgregar Ejecución de Herramientas
Sección titulada «Agregar Ejecución de Herramientas»Actualiza los módulos del proceso de chat para incluir json y funcs:
modules: - io - json - process - funcsActualiza src/chat.lua con ejecución de herramientas:
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 }El bucle de ejecución de herramientas:
- Llama a
runner:step()con streaming - Si la respuesta contiene
tool_calls, ejecuta cada herramienta víafuncs.call() - Agrega las llamadas a herramientas y resultados a la conversación
- Vuelve al paso 1 para que el agente incorpore los resultados
- Cuando no hay más llamadas a herramientas, retorna el texto final
Ejecutar el Agente
Sección titulada «Ejecutar el 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!Siguientes Pasos
Sección titulada «Siguientes Pasos»- Módulo LLM - Referencia completa de la API LLM
- Módulo de Agentes - Referencia del framework de agentes
- Aplicaciones CLI - Patrones de E/S de terminal
- Procesos - Modelo de procesos y comunicación