LLM-Agent
LLM-Agent
Abschnitt betitelt „LLM-Agent“Erstelle Schritt für Schritt einen Terminal-Chat-Agenten, von einem einfachen LLM-Aufruf bis zu einem Streaming-Agenten mit Tools.
Was wir erstellen
Abschnitt betitelt „Was wir erstellen“Einen Terminal-Chat-Agenten, der:
- Text mit einem LLM generiert
- Konversationen über mehrere Durchgänge führt
- Antworten in Echtzeit streamt
- Tools für externe Fähigkeiten nutzt
Projektstruktur
Abschnitt betitelt „Projektstruktur“llm-agent/├── .wippy.yaml├── wippy.lock└── src/ ├── _index.yaml ├── ask.lua ├── chat.lua └── tools/ ├── _index.yaml ├── current_time.lua └── calculate.luaPhase 1: Einfache Generierung
Abschnitt betitelt „Phase 1: Einfache Generierung“Beginne mit einer einfachen Funktion, die llm.generate() mit einem String-Prompt aufruft.
Projekt erstellen
Abschnitt betitelt „Projekt erstellen“mkdir llm-agent && cd llm-agentmkdir -p srcEintragsdefinitionen
Abschnitt betitelt „Eintragsdefinitionen“Erstelle 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:llmDas LLM-Modul benötigt zwei Infrastruktur-Einträge:
env.storage.osstellt API-Schlüssel aus Umgebungsvariablen bereitprocess.hoststellt die Prozess-Laufzeitumgebung bereit, die das LLM-Modul intern nutzt
Generierungscode
Abschnitt betitelt „Generierungscode“Erstelle 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 }Modelldefinition
Abschnitt betitelt „Modelldefinition“Das LLM-Modul löst Modelle aus der Registry auf. Füge einen Modelleintrag zur _index.yaml hinzu:
- 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-nanoInitialisieren und testen
Abschnitt betitelt „Initialisieren und testen“wippy initwippy run -x app:ask "What is the capital of France?"Dies ruft die Funktion direkt auf 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
Abschnitt betitelt „Phase 2: Konversationen“Wechsle von einem einzelnen Aufruf zu einer Konversation mit mehreren Durchgängen mithilfe des Prompt-Builders. Ändere den Eintrag von einer Funktion zu einem Prozess mit Terminal-I/O.
Eintragsdefinitionen aktualisieren
Abschnitt betitelt „Eintragsdefinitionen aktualisieren“Ersetze den ask-Eintrag durch einen chat-Prozess und füge die Terminal-Abhängigkeit hinzu:
- 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:promptChat-Prozess
Abschnitt betitelt „Chat-Prozess“Erstelle 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 }Ausführen
Abschnitt betitelt „Ausführen“wippy updatewippy run chatDer 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
Abschnitt betitelt „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
Abschnitt betitelt „Agent-Abhängigkeit hinzufügen“Füge zur _index.yaml hinzu:
- name: dep.agent kind: ns.dependency component: wippy/agent version: "*" parameters: - name: process_host value: app:processesEinen Agenten definieren
Abschnitt betitelt „Einen Agenten definieren“Füge 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-4.1-nano max_tokens: 1024 temperature: 0.7Chat-Prozess aktualisieren
Abschnitt betitelt „Chat-Prozess aktualisieren“Wechsle zum Agent-Framework. Aktualisiere die Eintrag-Imports:
- 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:contextAktualisiere 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 Agent-Definition (Prompt, Modell, Parameter) von der Ausführungslogik. Derselbe Agent kann zur Laufzeit mit verschiedenen Contexts, Tools und Modellen geladen werden.
Phase 4: Streaming
Abschnitt betitelt „Phase 4: Streaming“Streame Antworten Token für Token, anstatt auf die vollständige Antwort zu warten.
Streaming-Implementierung
Abschnitt betitelt „Streaming-Implementierung“Aktualisiere 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 }Zentrale Muster:
coroutine.spawnführtrunner:step()in einer separaten Coroutine aus, damit die Haupt-Coroutine Stream-Chunks verarbeiten kannchannel.selectmultiplext den Stream-Channel und den Done-Channel- Ein einzelnes
process.listen()wird einmal erstellt und über alle Durchgänge wiederverwendet - Text wird akkumuliert, um ihn dem Konversationsverlauf hinzuzufügen
Phase 5: Tools
Abschnitt betitelt „Phase 5: Tools“Gib dem Agenten Tools, die er aufrufen kann, um auf externe Fähigkeiten zuzugreifen.
Tools definieren
Abschnitt betitelt „Tools definieren“Erstelle 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: handlerTool-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
Abschnitt betitelt „Tools implementieren“Erstelle 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 }Erstelle 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
Abschnitt betitelt „Tools beim Agenten registrieren“Aktualisiere den Agent-Eintrag in src/_index.yaml, um die Tools zu referenzieren:
- 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:calculateTool-Ausführung hinzufügen
Abschnitt betitelt „Tool-Ausführung hinzufügen“Aktualisiere die Prozess-Module des Chat-Prozesses um json und funcs:
modules: - io - json - process - funcsAktualisiere src/chat.lua mit 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 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 }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
- Zurück zu Schritt 1, damit der Agent die Ergebnisse einbeziehen kann
- Wenn keine weiteren Tool-Aufrufe vorhanden sind, den finalen Text zurückgeben
Agenten ausführen
Abschnitt betitelt „Agenten ausführen“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!Nächste Schritte
Abschnitt betitelt „Nächste Schritte“- LLM-Modul - Vollständige LLM-API-Referenz
- Agent-Modul - Agent-Framework-Referenz
- CLI-Anwendungen - Terminal-I/O-Muster
- Prozesse - Prozessmodell und Kommunikation