Перейти к содержимому

LLM

Модуль wippy/llm предоставляет унифицированный интерфейс для работы с большими языковыми моделями от различных провайдеров (OpenAI, Anthropic, Google, локальные модели). Поддерживается генерация текста, вызов инструментов, структурированный вывод, эмбеддинги и потоковая передача.

Добавьте модуль в проект:

Окно терминала
wippy add wippy/llm
wippy install

Объявите зависимость в _index.yaml. Модуль LLM требует хранилище окружения (для API-ключей) и хост процессов:

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

Запись env.storage.os предоставляет переменные окружения ОС для провайдеров LLM. Установите API-ключи как переменные окружения (например, OPENAI_API_KEY, ANTHROPIC_API_KEY).

Импортируйте библиотеку llm в запись и вызовите generate():

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 }

Первый аргумент generate() может быть строковым промптом, построителем промптов или таблицей сообщений. Второй аргумент — таблица параметров.

ПараметрТипОписание
modelstringИмя модели или класс (обязательный)
temperaturenumberКонтроль случайности, 0-1
max_tokensnumberМаксимальное количество токенов для генерации
top_pnumberПараметр nucleus-сэмплирования
top_knumberФильтрация top-k
thinking_effortnumberГлубина размышления 0-100 (модели с возможностью размышления)
toolstableМассив определений инструментов
tool_choicestring"auto", "none", "any" или имя инструмента
streamtableКонфигурация потоковой передачи: { reply_to, topic, buffer_size }
timeoutnumberТаймаут запроса в секундах (по умолчанию 600)
ПолеТипОписание
resultstringСгенерированный текстовый контент
tokenstableИспользование токенов: prompt_tokens, completion_tokens, thinking_tokens, total_tokens
finish_reasonstringПричина остановки генерации: "stop", "length", "tool_call", "filtered", "error"
tool_callstable?Массив вызовов инструментов (если модель использовала инструменты)
metadatatableМетаданные, специфичные для провайдера
usage_recordtable?Запись об использовании

Для многоходовых диалогов и сложных промптов используйте построитель промптов:

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
})
МетодОписание
prompt.new()Создать пустой построитель
prompt.with_system(content)Создать построитель с системным сообщением
:add_system(content, meta?)Добавить системное сообщение
:add_user(content, meta?)Добавить сообщение пользователя
:add_assistant(content, meta?)Добавить сообщение ассистента
:add_developer(content, meta?)Добавить сообщение разработчика
:add_message(role, content_parts, name?, meta?)Добавить сообщение с ролью и частями контента
:add_function_call(name, args, id?)Добавить вызов инструмента от ассистента
:add_function_result(name, result, id?)Добавить результат выполнения инструмента
:add_cache_marker(id?)Отметить границу кэша (модели Claude)
:get_messages()Получить массив сообщений
:build()Получить таблицу { messages = ... } для llm.generate()
:clone()Глубокая копия построителя
:clear()Удалить все сообщения

Все методы add_* возвращают построитель для цепочечного вызова.

Накапливайте контекст между ходами, добавляя сообщения:

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" })

Объединяйте текст и изображения в одном сообщении:

local conversation = prompt.new()
conversation:add_message(prompt.ROLE.USER, {
prompt.text("What's in this image?"),
prompt.image("https://example.com/photo.jpg")
})
ФункцияОписание
prompt.text(content)Текстовая часть контента
prompt.image(url, mime_type?)Изображение по URL
prompt.image_base64(mime_type, data)Изображение в кодировке Base64
КонстантаЗначение
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"

Клонируйте построитель для создания вариаций без изменения оригинала:

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?")

Передавайте ответы в реальном времени, используя систему межпроцессного взаимодействия. Для этого требуется запись типа process.lua:

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
ТипПоляОписание
"chunk"contentФрагмент текстового контента
"thinking"contentПроцесс размышления модели
"tool_call"name, arguments, idВызов инструмента
"error"error.message, error.typeОшибка потока
"done"metaПоток завершён
Потоковая передача требует запись типа process.lua, так как использует систему межпроцессного взаимодействия Wippy (process.pid(), process.listen()).

Определите инструменты как встроенные схемы и передайте их в 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
ПолеТипОписание
idstringУникальный идентификатор вызова
namestringИмя инструмента
argumentstableРазобранные аргументы, соответствующие схеме
ЗначениеПоведение
"auto"Модель решает, когда использовать инструменты (по умолчанию)
"none"Никогда не использовать инструменты
"any"Обязательно использовать хотя бы один инструмент
"tool_name"Обязательно использовать указанный инструмент

Генерация валидированного JSON, соответствующего схеме:

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
Для моделей OpenAI все свойства должны быть перечислены в массиве required. Для необязательных полей используйте union-типы: type = {"string", "null"}. Установите additionalProperties = false.

Модели определяются как записи реестра с meta.type: llm.model:

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
ПолеОписание
meta.nameИдентификатор модели, используемый в API-вызовах
meta.typeДолжен быть llm.model
meta.capabilitiesСписок возможностей: generate, tool_use, structured_output, embed, thinking, vision, caching
meta.classПринадлежность к классу: fast, balanced, reasoning и т.д.
meta.priorityЧисловой приоритет для разрешения по классу (больше — выше приоритет)
max_tokensМаксимальное контекстное окно
output_tokensМаксимальное количество выходных токенов
pricingСтоимость за миллион токенов: input, output
providersМассив с id (запись провайдера) и provider_model (имя модели у провайдера)

Для локально размещённых моделей (LM Studio, Ollama) определите отдельную запись провайдера с пользовательским 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

На модели можно ссылаться по точному имени, классу или явному префиксу класса:

-- 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" })

Порядок разрешения:

  1. Совпадение по точному meta.name
  2. Совпадение по имени класса (побеждает наивысший meta.priority)
  3. С префиксом class: поиск только в указанном классе

Запрашивайте доступные модели и их возможности во время выполнения:

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

Генерация векторных эмбеддингов для семантического поиска:

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

Проверьте провайдера перед отправкой задач. Полезно для проверок готовности и лёгкого мониторинга состояния:

local status, err = llm.status({
model = "gpt-4o",
})
ПараметрОписание
modelОбязательный. Модель для проверки.
provider_idНеобязательный. Пропускает разрешение модели и обращается к конкретному провайдеру.

Возвращает StatusResponse провайдера (содержимое зависит от провайдера).

Ошибки возвращаются как второе возвращаемое значение. При ошибке первое значение равно nil:

local response, err = llm.generate("Hello", { model = "gpt-4o" })
if err then
io.print("Error: " .. tostring(err))
return
end
io.print(response.result)
КонстантаОписание
llm.ERROR_TYPE.INVALID_REQUESTНекорректный запрос
llm.ERROR_TYPE.AUTHENTICATIONНеверный API-ключ
llm.ERROR_TYPE.RATE_LIMITПревышен лимит запросов провайдера
llm.ERROR_TYPE.SERVER_ERRORСерверная ошибка провайдера
llm.ERROR_TYPE.CONTEXT_LENGTHВходные данные превышают контекстное окно
llm.ERROR_TYPE.CONTENT_FILTERКонтент отфильтрован системой безопасности
llm.ERROR_TYPE.TIMEOUTТаймаут запроса
llm.ERROR_TYPE.MODEL_ERRORНекорректная или недоступная модель
КонстантаОписание
llm.FINISH_REASON.STOPНормальное завершение
llm.FINISH_REASON.LENGTHДостигнут лимит токенов
llm.FINISH_REASON.CONTENT_FILTERКонтент отфильтрован
llm.FINISH_REASON.TOOL_CALLМодель выполнила вызов инструмента
llm.FINISH_REASON.ERRORОшибка при генерации
КонстантаОписание
llm.CAPABILITY.GENERATEГенерация текста
llm.CAPABILITY.TOOL_USEВызов инструментов/функций
llm.CAPABILITY.STRUCTURED_OUTPUTСтруктурированный вывод в формате JSON
llm.CAPABILITY.EMBEDВекторные эмбеддинги
llm.CAPABILITY.THINKINGРасширенное размышление
llm.CAPABILITY.VISIONПонимание изображений
llm.CAPABILITY.CACHINGКэширование промптов