Zum Inhalt springen

LLM

Das Modul wippy/llm bietet eine einheitliche Schnittstelle zur Arbeit mit Large Language Models verschiedener Anbieter (OpenAI, Anthropic, Google, lokale Modelle). Es unterstuetzt Textgenerierung, Tool-Aufrufe, strukturierte Ausgabe, Embeddings und Streaming.

Fuege das Modul deinem Projekt hinzu:

Terminal-Fenster
wippy add wippy/llm
wippy install

Deklariere die Abhaengigkeit in deiner _index.yaml. Das LLM-Modul benoetigt einen Environment-Speicher (fuer API-Schluessel) und einen Process-Host:

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

Der Eintrag env.storage.os stellt OS-Umgebungsvariablen den LLM-Anbietern zur Verfuegung. Setze deine API-Schluessel als Umgebungsvariablen (z.B. OPENAI_API_KEY, ANTHROPIC_API_KEY).

Importiere die llm-Bibliothek in deinen Eintrag und rufe generate() auf:

entries:
- name: ask
kind: function.lua
source: file://ask.lua
method: handler
imports:
llm: wippy.llm:llm
local llm = require("llm")
local function handler()
local response, err = llm.generate("What are the three laws of robotics?", {
model = "gpt-4o"
})
if err then
return nil, err
end
return response.result
end
return { handler = handler }

Das erste Argument von generate() kann ein String-Prompt, ein Prompt-Builder oder eine Tabelle von Nachrichten sein. Das zweite Argument ist eine Options-Tabelle.

OptionTypBeschreibung
modelstringModellname oder -klasse (erforderlich)
temperaturenumberZufallskontrolle, 0-1
max_tokensnumberMaximale Anzahl zu generierender Tokens
top_pnumberNucleus-Sampling-Parameter
top_knumberTop-k-Filterung
thinking_effortnumberDenktiefe 0-100 (Modelle mit Denkfaehigkeit)
toolstableArray von Tool-Definitionen
tool_choicestring"auto", "none", "any" oder Tool-Name
streamtableStreaming-Konfiguration: { reply_to, topic, buffer_size }
timeoutnumberAnfrage-Timeout in Sekunden (Standard 600)
FeldTypBeschreibung
resultstringGenerierter Textinhalt
tokenstableToken-Nutzung: prompt_tokens, completion_tokens, thinking_tokens, total_tokens
finish_reasonstringGrund fuer das Ende der Generierung: "stop", "length", "tool_call", "filtered", "error"
tool_callstable?Array von Tool-Aufrufen (wenn das Modell Tools aufgerufen hat)
metadatatableAnbieterspezifische Metadaten
usage_recordtable?Nutzungsdatensatz

Fuer Konversationen mit mehreren Durchgaengen und komplexe Prompts verwende den Prompt-Builder:

imports:
llm: wippy.llm:llm
prompt: wippy.llm:prompt
local llm = require("llm")
local prompt = require("prompt")
local conversation = prompt.new()
conversation:add_system("You are a helpful assistant.")
conversation:add_user("What is the capital of France?")
local response, err = llm.generate(conversation, {
model = "gpt-4o",
temperature = 0.7,
max_tokens = 500
})
MethodeBeschreibung
prompt.new()Leeren Builder erstellen
prompt.with_system(content)Builder mit Systemnachricht erstellen
:add_system(content, meta?)Systemnachricht hinzufuegen
:add_user(content, meta?)Benutzernachricht hinzufuegen
:add_assistant(content, meta?)Assistenznachricht hinzufuegen
:add_developer(content, meta?)Entwicklernachricht hinzufuegen
:add_message(role, content_parts, name?, meta?)Nachricht mit Rolle und Inhaltsteilen hinzufuegen
:add_function_call(name, args, id?)Tool-Aufruf des Assistenten hinzufuegen
:add_function_result(name, result, id?)Tool-Ausfuehrungsergebnis hinzufuegen
:add_cache_marker(id?)Cache-Grenze markieren (Claude-Modelle)
:get_messages()Nachrichtenarray abrufen
:build(){ messages = ... }-Tabelle fuer llm.generate() abrufen
:clone()Tiefe Kopie des Builders
:clear()Alle Nachrichten entfernen

Alle add_*-Methoden geben den Builder fuer Verkettung zurueck.

Baue Kontext ueber mehrere Durchgaenge auf, indem du Nachrichten anfuegst:

local conversation = prompt.new()
conversation:add_system("You are a helpful assistant.")
-- first turn
conversation:add_user("What is Lua?")
local r1 = llm.generate(conversation, { model = "gpt-4o" })
conversation:add_assistant(r1.result)
-- second turn with full context
conversation:add_user("What makes it different from Python?")
local r2 = llm.generate(conversation, { model = "gpt-4o" })

Kombiniere Text und Bilder in einer einzelnen Nachricht:

local conversation = prompt.new()
conversation:add_message(prompt.ROLE.USER, {
prompt.text("What's in this image?"),
prompt.image("https://example.com/photo.jpg")
})
FunktionBeschreibung
prompt.text(content)Textinhalt-Teil
prompt.image(url, mime_type?)Bild von URL
prompt.image_base64(mime_type, data)Base64-kodiertes Bild
KonstanteWert
prompt.ROLE.SYSTEM"system"
prompt.ROLE.USER"user"
prompt.ROLE.ASSISTANT"assistant"
prompt.ROLE.DEVELOPER"developer"
prompt.ROLE.FUNCTION_CALL"function_call"
prompt.ROLE.FUNCTION_RESULT"function_result"
prompt.ROLE.CACHE_MARKER"cache_marker"

Klone einen Builder, um Variationen zu erstellen, ohne das Original zu veraendern:

local base = prompt.new()
base:add_system("You are a helpful assistant.")
local conv1 = base:clone()
conv1:add_user("What is AI?")
local conv2 = base:clone()
conv2:add_user("What is ML?")

Streame Antworten in Echtzeit mithilfe der Prozesskommunikation. Dies erfordert einen process.lua-Eintrag:

local llm = require("llm")
local TOPIC = "llm_stream"
local function main()
local stream_ch = process.listen(TOPIC)
local response = llm.generate("Write a short story", {
model = "gpt-4o",
stream = {
reply_to = process.pid(),
topic = TOPIC,
},
})
while true do
local chunk, ok = stream_ch:receive()
if not ok then break end
if chunk.type == "chunk" then
io.write(chunk.content)
elseif chunk.type == "thinking" then
io.write(chunk.content)
elseif chunk.type == "error" then
io.print("Error: " .. chunk.error.message)
break
elseif chunk.type == "done" then
break
end
end
process.unlisten(stream_ch)
end
TypFelderBeschreibung
"chunk"contentTextinhalt-Fragment
"thinking"contentDenkprozess des Modells
"tool_call"name, arguments, idTool-Aufruf
"error"error.message, error.typeStream-Fehler
"done"metaStream abgeschlossen
Streaming erfordert einen process.lua-Eintrag, da es das Prozesskommunikationssystem von Wippy verwendet (process.pid(), process.listen()).

Definiere Tools als Inline-Schemas und uebergib sie an generate():

local llm = require("llm")
local prompt = require("prompt")
local json = require("json")
local tools = {
{
name = "get_weather",
description = "Get current weather for a location",
schema = {
type = "object",
properties = {
location = { type = "string", description = "City name" },
},
required = { "location" },
},
},
}
local conversation = prompt.new()
conversation:add_user("What's the weather in Tokyo?")
local response = llm.generate(conversation, {
model = "gpt-4o",
tools = tools,
tool_choice = "auto",
})
if response.tool_calls and #response.tool_calls > 0 then
for _, tc in ipairs(response.tool_calls) do
-- execute the tool and get a result
local result = { temperature = 22, condition = "sunny" }
-- add the exchange to the conversation
conversation:add_function_call(tc.name, tc.arguments, tc.id)
conversation:add_function_result(tc.name, json.encode(result), tc.id)
end
-- continue generation with tool results
local final = llm.generate(conversation, { model = "gpt-4o" })
print(final.result)
end
FeldTypBeschreibung
idstringEindeutiger Aufruf-Identifikator
namestringTool-Name
argumentstableGeparste Argumente gemaess dem Schema
WertVerhalten
"auto"Modell entscheidet, wann Tools verwendet werden (Standard)
"none"Niemals Tools verwenden
"any"Muss mindestens ein Tool verwenden
"tool_name"Muss das angegebene Tool verwenden

Generiere validiertes JSON gemaess einem Schema:

local llm = require("llm")
local schema = {
type = "object",
properties = {
name = { type = "string" },
age = { type = "number" },
hobbies = {
type = "array",
items = { type = "string" },
},
},
required = { "name", "age", "hobbies" },
additionalProperties = false,
}
local response, err = llm.structured_output(schema, "Describe a fictional character", {
model = "gpt-4o",
})
if not err then
print(response.result.name)
print(response.result.age)
end
Bei OpenAI-Modellen muessen alle Properties im required-Array enthalten sein. Verwende Union-Typen fuer optionale Felder: type = {"string", "null"}. Setze additionalProperties = false.

Modelle werden als Registry-Eintraege mit meta.type: llm.model definiert:

entries:
- name: gpt-4o
kind: registry.entry
meta:
name: gpt-4o
type: llm.model
title: GPT-4o
comment: OpenAI's flagship model
capabilities:
- generate
- tool_use
- structured_output
- vision
class:
- balanced
priority: 100
max_tokens: 128000
output_tokens: 16384
pricing:
input: 2.5
output: 10
providers:
- id: wippy.llm.openai:provider
provider_model: gpt-4o
FeldBeschreibung
meta.nameModellbezeichner fuer API-Aufrufe
meta.typeMuss llm.model sein
meta.capabilitiesFeature-Liste: generate, tool_use, structured_output, embed, thinking, vision, caching
meta.classKlassenzugehoerigkeit: fast, balanced, reasoning, etc.
meta.priorityNumerische Prioritaet fuer klassenbasierte Aufloesung (hoeher gewinnt)
max_tokensMaximales Kontextfenster
output_tokensMaximale Ausgabe-Tokens
pricingKosten pro Million Tokens: input, output
providersArray mit id (Anbieter-Eintrag) und provider_model (anbieterspezifischer Modellname)

Fuer lokal gehostete Modelle (LM Studio, Ollama) definiere einen separaten Anbieter-Eintrag mit einer eigenen base_url:

- name: local_provider
kind: registry.entry
meta:
name: ollama
type: llm.provider
title: Ollama Local
driver:
id: wippy.llm.openai:driver
options:
api_key_env: none
base_url: http://127.0.0.1:11434/v1
- name: local-llama
kind: registry.entry
meta:
name: local-llama
type: llm.model
title: Local Llama
capabilities:
- generate
max_tokens: 4096
output_tokens: 4096
pricing:
input: 0
output: 0
providers:
- id: app:local_provider
provider_model: llama-3.2

Modelle koennen per exaktem Namen, Klasse oder explizitem Klassenpraefix referenziert werden:

-- exact model name
llm.generate("Hello", { model = "gpt-4o" })
-- model class (picks highest priority in that class)
llm.generate("Hello", { model = "fast" })
-- explicit class syntax
llm.generate("Hello", { model = "class:reasoning" })

Aufloesungsreihenfolge:

  1. Abgleich per exaktem meta.name
  2. Abgleich per Klassenname (hoechste meta.priority gewinnt)
  3. Mit class:-Praefix wird nur in dieser Klasse gesucht

Verfuegbare Modelle und ihre Faehigkeiten zur Laufzeit abfragen:

local llm = require("llm")
-- all models
local models = llm.available_models()
-- filter by capability
local tool_models = llm.available_models("tool_use")
local embed_models = llm.available_models("embed")
-- list model classes
local classes = llm.get_classes()
for _, c in ipairs(classes) do
print(c.name .. ": " .. c.title)
end

Generiere Vektor-Embeddings fuer semantische Suche:

local llm = require("llm")
-- single text
local response = llm.embed("The quick brown fox", {
model = "text-embedding-3-small",
dimensions = 512,
})
-- response.result is a float array
-- multiple texts
local response = llm.embed({
"First document",
"Second document",
}, { model = "text-embedding-3-small" })
-- response.result is an array of float arrays

Pruefe einen Anbieter, bevor Arbeit gesendet wird. Nuetzlich fuer Bereitschaftspruefungen und leichtgewichtiges Health-Monitoring:

local status, err = llm.status({
model = "gpt-4o",
})
OptionBeschreibung
modelErforderlich. Zu pruefendes Modell.
provider_idOptional. Ueberspringt die Modellaufloesung und zielt auf einen bestimmten Anbieter.

Gibt die StatusResponse des Anbieters zurueck (Inhalt ist anbieterabhaengig).

Fehler werden als zweiter Rueckgabewert zurueckgegeben. Bei einem Fehler ist der erste Rueckgabewert nil:

local response, err = llm.generate("Hello", { model = "gpt-4o" })
if err then
io.print("Error: " .. tostring(err))
return
end
io.print(response.result)
KonstanteBeschreibung
llm.ERROR_TYPE.INVALID_REQUESTFehlerhafte Anfrage
llm.ERROR_TYPE.AUTHENTICATIONUngueltiger API-Schluessel
llm.ERROR_TYPE.RATE_LIMITRate-Limit des Anbieters ueberschritten
llm.ERROR_TYPE.SERVER_ERRORServerfehler des Anbieters
llm.ERROR_TYPE.CONTEXT_LENGTHEingabe ueberschreitet das Kontextfenster
llm.ERROR_TYPE.CONTENT_FILTERInhalt durch Sicherheitssysteme gefiltert
llm.ERROR_TYPE.TIMEOUTAnfrage-Zeitueberschreitung
llm.ERROR_TYPE.MODEL_ERRORUngueltiges oder nicht verfuegbares Modell
KonstanteBeschreibung
llm.FINISH_REASON.STOPNormale Fertigstellung
llm.FINISH_REASON.LENGTHMaximale Token-Anzahl erreicht
llm.FINISH_REASON.CONTENT_FILTERInhalt gefiltert
llm.FINISH_REASON.TOOL_CALLModell hat einen Tool-Aufruf ausgefuehrt
llm.FINISH_REASON.ERRORFehler waehrend der Generierung
KonstanteBeschreibung
llm.CAPABILITY.GENERATETextgenerierung
llm.CAPABILITY.TOOL_USETool-/Funktionsaufrufe
llm.CAPABILITY.STRUCTURED_OUTPUTStrukturierte JSON-Ausgabe
llm.CAPABILITY.EMBEDVektor-Embeddings
llm.CAPABILITY.THINKINGErweitertes Denken
llm.CAPABILITY.VISIONBildverstaendnis
llm.CAPABILITY.CACHINGPrompt-Caching