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

В NanoAgent это реализовано как функция высшего уровня. Вам больше не нужно вручную писать код вроде «загрузить массив, сохранить массив» — вы просто подключаете к агенту  драйвер памяти. Он автоматически загружает беседу для сессии и сохраняет её после каждого завершенного вызова  chat()  /  stream() .

Одна строка, которая меняет всё

 $agent->setMemory(new FileMemory(__DIR__ . '/memory'), 'demo');
 

Вот и весь новый API.  setMemory()  принимает любой объект, реализующий  NanoAgent\Contracts\Memory , и идентификатор сессии. С этого момента агент загружает сохраненную историю в самом начале и сохраняет её после каждого ответа. Никакой ручной хореографии с  getHistory()  /  setHistory()  /  file_put_contents() .

Как это устроено в примере

Пример Memory Chat позволяет переключать драйверы с помощью выпадающего списка, чтобы вы могли почувствовать разницу:

 use NanoAgent\Agent;
use NanoAgent\Memory\ArrayMemory;
use NanoAgent\Memory\FileMemory;

// One shared conversation for the demo; in a real app use a user or chat id.
$sessionId = 'demo';

$drivers = [
    'file'  => 'FileMemory (persistent)',
    'array' => 'ArrayMemory (current request only)',
];
$driver = isset($_GET['driver'], $drivers[$_GET['driver']]) ? $_GET['driver'] : 'file';

$memory = match ($driver) {
    'array' => new ArrayMemory(),
    default => new FileMemory(__DIR__ . '/memory'),
};

$agent = new Agent(
    llm: [
        'provider' => $config['provider'] ?? 'groq',
        'model'    => $config['model']    ?? 'llama-3.3-70b-versatile',
        'api_key'  => $config['api_key']  ?? ''
    ],
    systemPrompt: "You are a helpful assistant with a perfect long-term memory."
);

// Loads any stored history and saves after every chat() call.
$agent->setMemory($memory, $sessionId);
 

Всё остальное — это стандартное использование агента, а забота о памяти берётся на себя:

 if ($_SERVER['REQUEST_METHOD'] === 'POST' && !empty($_POST['message'])) {
    // History is persisted automatically once the reply is complete.
    $agent->chat(trim($_POST['message']));
}
 

Сброс истории также делается одним вызовом.  clearHistory()  очищает историю в памяти процесса  и удаляет сохраненную строку/файл:

 if (isset($_POST['reset'])) {
    $agent->clearHistory();
    header("Location: " . $selfUrl);
    exit;
}
 

Три встроенных драйвера

Драйвер Хранилище Требования Используйте, когда
 ArrayMemory  Только текущий процесс - Тесты, долгоживущие воркеры (очереди, ReactPHP, Swoole, CLI-циклы)
 FileMemory  Один JSON-файл на сессию - Отсутствие инфраструктуры, один пользователь или небольшие утилиты
 PdoMemory  SQL-таблица (SQLite, MySQL, PostgreSQL)  ext-pdo  + драйвер Многопользовательские приложения, продакшн, когда нужны запросы/очистка

 ArrayMemory  — это базовая реализация («он всё забывает»), полезная для доказательства того, что именно драйвер отвечает за сохранение памяти.  FileMemory  именует файлы с помощью хэша от идентификатора сессии, поэтому произвольные идентификаторы (электронные почты, UUID, сырой ввод пользователя) не могут выйти за пределы директории; он записывает данные во временный файл, а затем переименовывает его, так что упавший запрос никогда не оставит наполовину записанный файл.  PdoMemory  сохраняет каждую сессию в виде одной строки с использованием нативных upsert-запросов в SQLite, MySQL/MariaDB и PostgreSQL:

 use NanoAgent\Memory\PdoMemory;

// SQLite file, created on first use (keep it outside your public web root):
$agent->setMemory(PdoMemory::sqlite(__DIR__ . '/memory/chat.sqlite'), "user-$userId");

// Or reuse an existing PDO connection (MySQL, Postgres, Laravel's, ...):
$agent->setMemory(new PdoMemory($existingPdo), $sessionId);
 

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

Выбор идентификатора сессии

Идентификатор сессии определяет, какая беседа будет загружена, поэтому он должен браться из источника, которому  доверяет ваш сервер, и никогда напрямую из пользовательского ввода — иначе один пользователь сможет подставить ID другого и прочитать его чат.

 $agent->setMemory($memory, 'user-' . $user->id);            // one per logged-in user
$agent->setMemory($memory, "user-{$user->id}-chat-{$chatId}"); // several per user
$agent->setMemory($memory, 'visitor-' . session_id());       // anonymous visitors
 

Почему это лучше ручного подхода с файлами

Вы по-прежнему можете делать это вручную —  setHistory()  при загрузке,  file_put_contents(getHistory())  после каждого ответа (и старая версия этой статьи показывала именно такой подход). Драйвер памяти предлагает ту же идею, но с уже встроенным циклом сохранения:

  •  Никакого шаблонного кода — один вызов  setMemory()  вместо логики загрузки/сохранения/очистки в каждом запросе.
  •  Меняйте хранилище, а не агента — переходите от  FileMemory  к  PdoMemory  (или вашему собственному драйверу), и в коде агента не изменится ни единой строки.
  •  Всё просто работает — драйвер сохраняет данные после каждого завершенного диалога, а вызов, вызвавший исключение посреди диалога, не сохраняется, поэтому в хранилище всегда остается последний успешно завершенный обмен репликами.

Нужен бэкенд, которого нет «из коробки» — Redis, кэш фреймворка, Eloquent? Реализуйте интерфейс  NanoAgent\Contracts\Memory , состоящий из трех методов ( load() ,  save() ,  clear() ), и передайте его прямо в  setMemory() .


Часть серии примеров NanoAgent. Лендинг + демо.