Read in:
Русский

Как работают LLM-агенты — и как trip2g их запускает

LLM-агент — это языковая модель в цикле: она вызывает инструменты, читает результаты и повторяет, пока не вызовет finish или не упрётся в лимит бюджета. Флит trip2g позволяет описать таких агентов обычными заметками: фронтматтер — конфигурация, тело — инструкция. Агент запускается сам, когда в хранилище меняется подходящая заметка. Чтение и запись идут через токен доставки с ограниченным доступом — он строго задаёт, какие пути агенту можно трогать.

Как работает LLM-агент

Языковая модель сама по себе отвечает на один запрос и останавливается. Агент расширяет это циклом и инструментами.

Цикл:

  1. Модель читает текущий контекст (системный промпт + история разговора).
  2. Если информации достаточно — вызывает finish, и запуск заканчивается.
  3. Иначе — выбирает инструмент (поиск по базе знаний, чтение заметки, запись заметки) и рантайм его выполняет.
  4. Результат инструмента дописывается в историю, и модель запускается снова с шага 1.

«Инструменты» в современных LLM API — не плагины и не внешние сервисы. Это JSON-схемы функций, которые отправляются модели при каждом запросе. Модель возвращает структурированный вызов (имя функции + аргументы), рантайм его выполняет и передаёт результат обратно. Этот механизм называется function-calling (или tool-use). Агенты в trip2g работают именно так — не через MCP.

Почему важен скоуп. Модель может запросить любой вызов инструмента, который подсказывает инструкция. Без уровня ограничений плохо написанная инструкция или инжектированная заметка могут заставить агента прочитать или перезаписать то, чего он не должен касаться. Паттерны чтения и записи проверяет рантайм, а не только промпт. Только так границы доступа действительно работают.

Когда агент избыточен. Если задача детерминирована — переформатировать заметку, применить шаблон, посчитать значение, — достаточно простого промпта или скрипта. Агентный цикл нужен, когда правильная последовательность шагов неизвестна заранее и модель сама ищет путь по тому, что находит. Anthropic в Building Effective Agents формулирует это прямо: начинайте с простейшего инструмента, который работает.

Как это устроено в trip2g: флит

Роли как заметки

Агент в trip2g называется ролью. Роль — обычная заметка в хранилище, которую читает демон флита, а не человек:

  • Фронтматтер = конфигурация: модель, пути для чтения (read_patterns), пути для записи (write_patterns), что запускает агента (trigger_include, trigger_on), лимиты бюджета (max_tokens, max_steps), таймаут (timeout_seconds), политика конкурентных запусков (concurrency) и раскладка по нескольким файлам (for_each).
  • Тело = инструкция, которая рендерится как Jet-шаблон с четырьмя переменными: changed_files, change_file, attached_notes, depth.

Минимальная заметка-роль:

---
model: gpt-4o-mini
tools: [read_note, write_note]
read_patterns: ["drafts/**"]
write_patterns: ["published/**"]
mode: change
trigger_on: [update]
trigger_include: ["drafts/**"]
max_tokens: 4000
max_steps: 6
concurrency: skip
---
Ты редактор. Прочитай черновик по пути {{ change_file.Path }}, перепиши для ясности
и сохрани результат в published/{{ change_file.Title }}.md.

Запуск по изменению заметки

Когда заметка, совпадающая с trigger_include роли, сохраняется, trip2g отправляет доставку (delivery) через change-webhook во флит. Флит проверяет HMAC-подпись, рендерит тело роли как Jet-шаблон против контекста триггера и запускает цикл агента.

Флит регистрирует и поддерживает эти вебхуки автоматически через цикл reconcile — вебхуки вручную не настраиваются. Добавьте заметку-роль в папку агентов, и флит зарегистрирует соответствующий вебхук на следующем опросе. Удалите заметку — вебхук снимается.

Инструменты и границы доступа

У агента пять инструментов:

Инструмент Что делает
search(query) Полнотекстовый поиск в пределах доступа на чтение
read_note(path) Прочитать полное содержимое заметки (только в зоне чтения)
write_note(path, content) Создать или заменить заметку (только в зоне записи)
patch_note(path, find, replace) Точечная замена find→replace в существующей заметке (только в зоне записи); завершается ошибкой, если find не найден или встречается более одного раза
finish(answer) Завершить запуск с итоговым ответом

Каждое чтение и каждая запись проверяются по read_patterns и write_patterns через glob-сопоставление с двойной звёздочкой. Запрос за пределами доступа отклоняется, а ошибка возвращается модели — чтобы та скорректировала действия. Отказы никогда не проглатываются молча. В системном промпте модели явно указано, какие паттерны действуют, и любая попытка выйти за них подсвечивается.

Набор доступных инструментов можно ограничить через поле tools в фронтматтере. Инструмент finish доступен всегда.

Записи происходят в ходе запуска, а не после: каждый вызов write_note или patch_note сразу обновляет заметку через токен доставки с ограниченным доступом. В HTTP-ответе флит сообщает changes: [], потому что записи уже применены к моменту отправки ответа.

Ограничения безопасности

Рантайм принудительно применяет три жёстких лимита, которые модель не может изменить:

  • Лимит токенов (max_tokens): если суммарный расход токенов достигает лимита до вызова finish, запуск останавливается со статусом capped.
  • Лимит шагов (max_steps): если цикл инструментов достигает лимита без finish, запуск останавливается со статусом max_steps.
  • Таймаут (timeout_seconds): контекст запуска отменяется и цикл останавливается. Значение по умолчанию — 300 секунд.

Флит накладывает потолок поверх настроек роли: effective = min(role.max_tokens, fleet.token_ceiling). Автор роли не может запросить больше максимума, заданного оператором флита.

Защита от петель (max_depth): trip2g передаёт счётчик depth с каждой доставкой. Когда запись агента запускает очередную доставку вебхука, depth увеличивается. Если depth >= max_depth, trip2g отбрасывает доставку без запуска. Для роли, которая пишет в собственный паттерн триггера, достаточно max_depth: 1, чтобы полностью исключить самоповтор.

Конкурентность (concurrency: skip): если доставка приходит, пока предыдущий запуск для той же роли ещё выполняется, новая доставка отбрасывается, а не ставится в очередь. Это сворачивает серию быстрых правок в один запуск агента.

Шаблонизация и fan-out

Тело роли — Jet-шаблон. При рендере доступны четыре переменные:

Переменная Содержимое
changed_files Все заметки, вызвавшие эту доставку
change_file Одна изменившаяся заметка (устанавливается только при for_each: changed_files)
attached_notes Контекстные заметки, предзагруженные через паттерны attach_notes
depth Текущая глубина доставки (0 = верхний уровень)

for_each: changed_files запускает агента по одному разу для каждого изменённого файла в доставке, при этом change_file указывает на текущий файл. for_each: attached_notes — по одному разу для каждой прикреплённой заметки. Без for_each агент запускается один раз со всеми списками.

Секреты никогда не передаются в шаблон. Обращение к неизвестной переменной в Jet — ошибка рендера, которая останавливает доставку до любого вызова LLM.

Рабочий пример: конвейер Krisp

Кейс Krisp показывает, как две связанные роли превращают сырые транскрипты звонков в структурированный граф знаний с WikiLinks.

transcripts/<id>.md   сырой транскрипт (пишет детерминированный инжест, не LLM)
   │  сохранение → change_webhook → роль transcript-segment
   ▼
segments/<id>.md      карта тем (заголовки по Минто + диапазоны [MM:SS–MM:SS])
   │  запись → change_webhook → роль transcript-wiki
   ▼
wiki/<id>.md          заметка знаний с [[WikiLinks]] в граф хранилища

transcript-segment срабатывает на transcripts/**, читает транскрипт из тела доставки (change_file.Content) и записывает карту сегментов в segments/ — только через инструмент write_note.

transcript-wiki срабатывает на segments/**, читает карту сегментов (тело доставки) и исходный транскрипт через read_note (покрыт read_patterns: ["segments/**", "transcripts/**"]) и записывает wiki-заметку с [[WikiLinks]] в wiki/.

Обе роли используют for_each: changed_files и max_depth: 3. Цепочка завершается, потому что ничто не срабатывает на wiki/**. В проверенном запуске на qwen/qwen3-14b через OpenRouter: шаг 1 — completed, ~7,6K токенов (2 шага); шаг 2 — completed, ~11,2K токенов (2 шага).

Важно: сырой транскрипт пишет детерминированный шаг инжеста — не LLM. Источник остаётся проверяемым и переобрабатываемым: если промпт или модель улучшились, достаточно перезапустить роли против той же исходной заметки.

Что впереди: тип роли executor

Второй вид роли (executor) будет запускать Python или shell для шагов, которым языковая модель не нужна: пагинация, маппинг полей, преобразование форматов. Тот же формат заметки-роли, тот же механизм триггеров — без расходов и задержки LLM.

Честные ограничения

Надёжность модели на многошаговых циклах. Более слабые или дешёвые модели иногда не вызывают finish корректно, игнорируют отказы скоупа или выдумывают пути. Лимиты шагов и токенов сдерживают ущерб, но не предотвращают его. Сначала тестируйте роли на дешёвой модели; переходите на более сильную, если результаты ненадёжны.

Стоимость. Каждый шаг цикла расходует токены. Роль с max_steps: 10 на большой модели и большом контексте может стоить ощутимо за одну доставку. Устанавливайте консервативные бюджеты, пока не измерили реальный расход в своей конфигурации.

Агент пишет в ваше хранилище. write_note заменяет всё содержимое заметки целиком. patch_note точечный, но требует уникального совпадения строки — если строка встречается более одного раза, вызов завершается ошибкой и заметка остаётся нетронутой. Если у роли широкий write_patterns, запутавшаяся модель может перезаписать нужные заметки. Сужайте write_patterns ровно до тех путей, которых должен касаться агент.