LLM-агент
LLM-агент
Заголовок раздела «LLM-агент»Пошаговое создание терминального чат-агента: от простого вызова LLM до потокового агента с инструментами.
Что мы создаём
Заголовок раздела «Что мы создаём»Терминальный чат-агент, который:
- Генерирует текст с помощью LLM
- Поддерживает многоходовые диалоги
- Передаёт ответы в реальном времени
- Использует инструменты для доступа к внешним возможностям
Структура проекта
Заголовок раздела «Структура проекта»llm-agent/├── .wippy.yaml├── wippy.lock└── src/ ├── _index.yaml ├── ask.lua ├── chat.lua └── tools/ ├── _index.yaml ├── current_time.lua └── calculate.luaФаза 1: Простая генерация
Заголовок раздела «Фаза 1: Простая генерация»Начнём с базовой функции, которая вызывает llm.generate() со строковым промптом.
Создание проекта
Заголовок раздела «Создание проекта»mkdir llm-agent && cd llm-agentmkdir -p srcОпределение записей
Заголовок раздела «Определение записей»Создайте 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:llmМодулю LLM нужны две инфраструктурные записи:
env.storage.osпредоставляет API-ключи из переменных окруженияprocess.hostпредоставляет среду выполнения процессов, используемую модулем LLM
Код генерации
Заголовок раздела «Код генерации»Создайте 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 }Определение модели
Заголовок раздела «Определение модели»Модуль LLM разрешает модели из реестра. Добавьте запись модели в _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-nanoИнициализация и тестирование
Заголовок раздела «Инициализация и тестирование»wippy initwippy run -x app:ask "What is the capital of France?"Это вызывает функцию напрямую и выводит результат. Определение модели указывает модулю LLM, какой провайдер использовать и какое имя модели отправлять в API.
Фаза 2: Диалоги
Заголовок раздела «Фаза 2: Диалоги»Переход от одиночного вызова к многоходовому диалогу с использованием построителя промптов. Запись меняется с функции на процесс с терминальным вводом-выводом.
Обновление определений записей
Заголовок раздела «Обновление определений записей»Замените запись ask на процесс chat и добавьте зависимость терминала:
- 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:promptПроцесс чата
Заголовок раздела «Процесс чата»Создайте 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 }wippy updatewippy run chatПостроитель промптов хранит полную историю диалога. На каждом ходе добавляются сообщение пользователя и ответ ассистента, обеспечивая модели контекст предыдущих обменов.
Фаза 3: Фреймворк агентов
Заголовок раздела «Фаза 3: Фреймворк агентов»Модуль агентов предоставляет более высокоуровневую абстракцию над прямыми вызовами LLM. Агенты определяются декларативно с промптом, моделью и инструментами, а затем загружаются и выполняются через паттерн контекст/раннер.
Добавление зависимости агента
Заголовок раздела «Добавление зависимости агента»Добавьте в _index.yaml:
- name: dep.agent kind: ns.dependency component: wippy/agent version: "*" parameters: - name: process_host value: app:processesОпределение агента
Заголовок раздела «Определение агента»Добавьте запись агента:
- 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.7Обновление процесса чата
Заголовок раздела «Обновление процесса чата»Переключаемся на фреймворк агентов. Обновите импорты записи:
- 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:contextОбновите 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 }Фреймворк агентов отделяет определение агента (промпт, модель, параметры) от логики выполнения. Один и тот же агент может загружаться с разными контекстами, инструментами и моделями во время выполнения.
Фаза 4: Потоковая передача
Заголовок раздела «Фаза 4: Потоковая передача»Передача ответов токен за токеном вместо ожидания полного ответа.
Реализация потоковой передачи
Заголовок раздела «Реализация потоковой передачи»Обновите 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 }Ключевые паттерны:
coroutine.spawnзапускаетrunner:step()в отдельной корутине, чтобы основная корутина могла обрабатывать чанки потокаchannel.selectмультиплексирует канал потока и канал завершения- Один
process.listen()создаётся один раз и переиспользуется между ходами - Текст накапливается для добавления в историю диалога
Фаза 5: Инструменты
Заголовок раздела «Фаза 5: Инструменты»Предоставьте агенту инструменты, которые он может вызывать для доступа к внешним возможностям.
Определение инструментов
Заголовок раздела «Определение инструментов»Создайте 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Метаданные инструмента сообщают LLM, что делает инструмент:
input_schema— JSON Schema, определяющая аргументыllm_alias— имя функции, видимое для LLMllm_description— описание, когда использовать инструмент
Реализация инструментов
Заголовок раздела «Реализация инструментов»Создайте 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 }Создайте 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 }Регистрация инструментов в агенте
Заголовок раздела «Регистрация инструментов в агенте»Обновите запись агента в src/_index.yaml, добавив ссылки на инструменты:
- 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:calculateДобавление выполнения инструментов
Заголовок раздела «Добавление выполнения инструментов»Обновите модули процесса чата, добавив json и funcs:
modules: - io - json - process - funcsОбновите src/chat.lua с выполнением инструментов:
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 }Цикл выполнения инструментов:
- Вызов
runner:step()с потоковой передачей - Если ответ содержит
tool_calls, выполнить каждый инструмент черезfuncs.call() - Добавить вызовы инструментов и результаты в диалог
- Вернуться к шагу 1, чтобы агент обработал результаты
- Когда вызовов инструментов больше нет, вернуть финальный текст
Запуск агента
Заголовок раздела «Запуск агента»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!Следующие шаги
Заголовок раздела «Следующие шаги»- Модуль LLM - Полный справочник API LLM
- Модуль агентов - Справочник фреймворка агентов
- CLI-приложения - Паттерны терминального ввода-вывода
- Процессы - Модель процессов и взаимодействие