LLM-Agent
Bauen Sie in fünf Phasen einen Terminal-Chat-Agenten, vom einzelnen LLM-Aufruf bis zu Streaming-Antworten und Tool-Ausführung.
Klassifizierung: ausführbares Tutorial mit externem Provider. Jede Phase ist eine
kumulative Änderung desselben Projekts und kann ausgeführt werden, bevor Sie fortfahren.
Die Wippy-Verträge und der lokale Kontrollfluss lassen sich ohne Zugangsdaten prüfen;
die Generierung benötigt Netzwerkzugriff und einen gültigen OPENAI_API_KEY.
Was wir erstellen
Einen Terminal-Chat-Agenten, der:
- Text mit einem LLM generiert.
- Konversationen über mehrere Durchgänge führt.
- Antworten schrittweise streamt.
- Registrierte Tools aufruft.
Projektstruktur
llm-agent/
├── wippy.lock
└── src/
├── _index.yaml
├── ask.lua
├── chat.lua
└── tools/
├── _index.yaml
├── current_time.lua
└── calculate.lua
Phase 1: Einfache Generierung
Beginnen Sie mit einer einfachen Funktion, die llm.generate() mit einem String-Prompt aufruft.
Beginnen Sie in einem Wippy-Projekt mit dem Quellverzeichnis ./src. Setzen Sie
OPENAI_API_KEY in der Umgebung, die Wippy startet. Dieses Tutorial deklariert sein
Modell explizit; kopieren Sie nicht zusätzlich einen zweiten Eintrag mit demselben
Modellnamen aus einer anderen Anwendung.
Eintragsdefinitionen
Erstellen Sie src/_index.yaml:
version: "1.0"
namespace: app
entries:
- name: policy
kind: security.policy
policy:
actions: "*"
resources: "*"
effect: allow
- 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: dep.terminal
kind: ns.dependency
component: wippy/terminal
version: "*"
- name: ask
kind: process.lua
meta:
command:
name: ask
short: Ask a single question
security:
actor:
id: app:ask
policies:
- app:policy
source: file://ask.lua
method: main
modules:
- io
imports:
llm: wippy.llm:llm
Das LLM-Modul benötigt zwei Infrastruktur-Einträge:
env.storage.osstellt API-Keys aus Umgebungsvariablen bereit.process.hoststellt die Prozess-Runtime bereit, die das LLM-Modul intern verwendet.
Die Abhängigkeit wippy/terminal stellt den terminal.host bereit, auf dem Befehle ausgeführt werden und wohin io.print schreibt.
meta.command gibt dem Prozess einen Namen, sodass wippy run ask ihn mit den restlichen Argumenten als String-Payloads startet. Sein security-Block installiert den Actor und den Policy-Scope für diesen Start: Das LLM-Modul löst Modelle aus der Registry auf, und ein ohne Scope gestarteter Befehl liest nichts aus ihr.
Generierungscode
Erstellen Sie src/ask.lua:
local io = require("io")
local llm = require("llm")
local function main(input)
local response, err = llm.generate(input, {
model = "gpt-4.1-nano",
temperature = 0.7,
max_tokens = 512,
})
if err then
io.print("Error: " .. tostring(err))
return 1
end
io.print(response.result)
return 0
end
return { main = main }
Modelldefinition
Das LLM-Modul löst Modelle aus der Registry auf. Fügen Sie einen Modelleintrag zu _index.yaml hinzu:
- name: gpt-4o-mini
kind: registry.entry
meta:
name: gpt-4o-mini
type: llm.model
title: GPT-4o mini
comment: Fast, affordable model
capabilities:
- generate
- tool_use
- structured_output
class:
- fast
priority: 100
max_tokens: 128000
output_tokens: 16384
pricing:
input: 0.15
output: 0.6
providers:
- id: wippy.llm.openai:provider
provider_model: gpt-4o-mini
Initialisieren und testen
wippy init
wippy run ask "What is the capital of France?"
Dies führt den ask-Prozess auf dem Terminal-Host mit der Frage als Argument aus und gibt das Ergebnis aus. Die Modelldefinition teilt dem LLM-Modul mit, welchen Anbieter es verwenden und welchen Modellnamen es an die API senden soll.
Phase 2: Konversationen
Wechsle von einem einzelnen Aufruf zu einer Konversation mit mehreren Durchgängen mithilfe des Prompt-Builders. Registriere den Prozess als benannten Befehl.
Eintragsdefinitionen aktualisieren
Ersetze den ask-Eintrag durch einen chat-Prozess:
- name: chat
kind: process.lua
meta:
command:
name: chat
short: Start a terminal chat
security:
actor:
id: app:chat
policies:
- app:policy
source: file://chat.lua
method: main
modules:
- io
imports:
llm: wippy.llm:llm
prompt: wippy.llm:prompt
Ausführbare Lua-Einträge erhalten process als ambientes Runtime-Modul. Der folgende
Code verwendet es daher direkt; es gehört nicht in die Liste modules des Eintrags.
Chat-Prozess
Erstellen Sie 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-4o-mini",
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 }
Ausführen
wippy update
wippy install
wippy run chat
Der Prompt-Builder pflegt den vollständigen Konversationsverlauf. Jeder Durchgang fügt die Benutzernachricht und die Assistenzantwort an, sodass das Modell Kontext über vorherige Austausche hat.
Phase 3: Agent-Framework
Das Agent-Modul bietet eine höhere Abstraktionsebene über direkte LLM-Aufrufe. Agenten werden deklarativ mit Prompt, Modell und Tools definiert und dann über ein Context/Runner-Muster geladen und ausgeführt.
Agent-Abhängigkeit hinzufügen
Fügen Sie Folgendes zu _index.yaml hinzu:
- name: dep.agent
kind: ns.dependency
component: wippy/agent
version: "*"
parameters:
- name: process_host
value: app:processes
Einen Agenten definieren
Fügen Sie einen Agent-Eintrag hinzu:
- 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-4o-mini
max_tokens: 1024
temperature: 0.7
Chat-Prozess aktualisieren
Wechseln Sie zum Agent-Framework und aktualisieren Sie die Imports des Eintrags:
- name: chat
kind: process.lua
meta:
command:
name: chat
short: Start a terminal chat
security:
actor:
id: app:chat
policies:
- app:policy
source: file://chat.lua
method: main
modules:
- io
imports:
prompt: wippy.llm:prompt
agent_context: wippy.agent:context
Aktualisieren Sie 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 }
Das Agent-Framework trennt die Agentendefinition (Prompt, Modell und Parameter) von der Ausführungslogik. Derselbe Agent kann zur Laufzeit mit verschiedenen Kontexten, Tools und Modellen geladen werden.
Lösen Sie die neu hinzugefügte Agent-Abhängigkeit auf und führen Sie diese Phase aus:
wippy update
wippy install
wippy run chat
Phase 4: Streaming
Streamen Sie Antworten Token für Token, anstatt auf die vollständige Antwort zu warten.
Streaming-Implementierung
Aktualisieren Sie src/chat.lua:
local io = require("io")
local prompt = require("prompt")
local agent_context = require("agent_context")
local STREAM_TOPIC = "stream"
local stream_sequence = 0
local function stream_response(runner, conversation)
stream_sequence = stream_sequence + 1
local topic = STREAM_TOPIC .. ":" .. tostring(stream_sequence)
local stream_ch = process.listen(topic)
local done_ch = channel.new(1)
coroutine.spawn(function()
local response, err = runner:step(conversation, {
stream_target = {
reply_to = process.pid(),
topic = topic,
},
})
done_ch:send({ response = response, err = err })
end)
local full_text = ""
local response_result = nil
local stream_done = false
local function finish(text, response, err)
process.unlisten(stream_ch)
return text, response, err
end
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
response_result = result.value
else
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
stream_done = true
elseif chunk.type == "error" then
return finish(nil, nil, chunk.error and chunk.error.message or "stream error")
end
end
if response_result and response_result.err then
return finish(full_text, response_result.response, response_result.err)
end
if response_result and stream_done then
return finish(full_text, response_result.response, response_result.err)
end
end
return finish(full_text, nil, nil)
end
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 text, _, gen_err = stream_response(runner, conversation)
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
io.print("Bye!")
end
return { main = main }
Zentrale Muster:
coroutine.spawnführtrunner:step()separat aus, damit die Haupt-Coroutine Stream-Chunks verarbeiten kann.channel.selectwartet auf Stream- und Completion-Channel.- Jeder Durchgang verwendet ein eindeutiges Topic und entfernt seinen Listener, nachdem sowohl der Runner als auch der Stream dieses Durchgangs abgeschlossen sind.
- Der Prozess sammelt den gestreamten Text für den Konversationsverlauf.
Führen Sie die Streaming-Phase mit demselben Befehl aus:
wippy run chat
Phase 5: Tools
Geben Sie dem Agenten Tools, die er für externe Fähigkeiten aufrufen kann.
Tools definieren
Erstellen Sie 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: handler
Tool-Metadaten teilen dem LLM mit, was das Tool tut:
input_schemaist ein JSON-Schema, das die Argumente definiertllm_aliasist der Funktionsname, den das LLM siehtllm_descriptionerklärt, wann das Tool verwendet werden soll
Tools implementieren
Erstellen Sie 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 }
Erstellen Sie 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 }
Tools beim Agenten registrieren
Aktualisieren Sie den Agent-Eintrag in src/_index.yaml, sodass er die Tools referenziert:
- 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-4o-mini
max_tokens: 1024
temperature: 0.7
tools:
- app.tools:current_time
- app.tools:calculate
Tool-Ausführung hinzufügen
Ergänzen Sie die Prozessmodule des Chat-Prozesses um json und funcs:
modules:
- io
- json
- funcs
Ergänzen Sie src/chat.lua um die Tool-Ausführung:
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 stream_sequence = 0
local function stream_response(runner, conversation)
stream_sequence = stream_sequence + 1
local topic = STREAM_TOPIC .. ":" .. tostring(stream_sequence)
local stream_ch = process.listen(topic)
local done_ch = channel.new(1)
coroutine.spawn(function()
local response, err = runner:step(conversation, {
stream_target = {
reply_to = process.pid(),
topic = topic,
},
})
done_ch:send({ response = response, err = err })
end)
local full_text = ""
local response_result = nil
local stream_done = false
local function finish(text, response, err)
process.unlisten(stream_ch)
return text, response, err
end
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
response_result = result.value
else
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
stream_done = true
elseif chunk.type == "error" then
return finish(nil, nil, chunk.error and chunk.error.message or "stream error")
end
end
if response_result and response_result.err then
return finish(full_text, response_result.response, response_result.err)
end
if response_result and stream_done then
return finish(full_text, response_result.response, response_result.err)
end
end
return finish(full_text, nil, nil)
end
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 results
end
local function run_turn(runner, conversation)
while true do
local text, response, err = stream_response(runner, conversation)
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
end
end
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()
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)
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
io.print("Bye!")
end
return { main = main }
Die Tool-Ausführungsschleife:
runner:step()mit Streaming aufrufen.- Wenn die Antwort
tool_callsenthält, jedes Tool überfuncs.call()ausführen. - Tool-Aufrufe und Ergebnisse zur Konversation hinzufügen.
- Den Runner erneut aufrufen, damit er die Ergebnisse berücksichtigen kann.
- Den endgültigen Text zurückgeben, wenn die Antwort keine weiteren Tool-Aufrufe enthält.
Agenten ausführen
wippy update
wippy install
wippy run chat
Terminal Agent (type 'quit' to exit)
> what time is it?
[get_current_time] done
The current time is 17:20 UTC on February 12, 2026.
> what is 125 * 16?
[calculate] done
125 * 16 = 2000.
> quit
Bye!
Vollständigkeit und Grenzen
- Die Seite enthält alle Lua-Dateien und Registry-Einträge, die für die fünf Phasen
verfasst werden müssen.
wippy.lockund installierte Module entstehen durch die oben gezeigten Befehle. - Modellausgabe, Token-Nutzung, Reihenfolge der Tool-Auswahl und Wortlaut hängen vom Provider ab; die dargestellte Interaktion ist ein Beispiel und keine Zusage exakten Textes.
- Der Rechner ist bewusst ein kleiner Parser für Arithmetik und kein allgemeiner Ausdrucksauswerter. Behandeln Sie jedes echte Tool als Autoritätsgrenze und weisen Sie eng begrenzte Sicherheits-Policies zu, bevor Sie Seiteneffekte bereitstellen.
Nächste Schritte
- LLM-Modul — Referenz der LLM-API
- Agent-Modul — Referenz des Agent-Frameworks
- CLI-Anwendungen — Muster für Terminal-I/O
- Prozesse — Prozessmodell und Kommunikation