Перейти к содержанию

12. Системы Памяти Агента

Зачем это нужно?

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

В этой главе разберём системы памяти, которые помогают агентам запоминать, извлекать и забывать информацию эффективно.

Реальный кейс

Ситуация: Пользователь спрашивает агента: "Какая была проблема с базой данных, которую мы исправили на прошлой неделе?" Агент отвечает: "У меня нет информации об этом."

Проблема: У агента нет памяти о прошлых разговорах. Каждое взаимодействие начинается с нуля, тратя контекст и время пользователя.

Решение: Система памяти сохраняет важные факты, достаёт их при необходимости и забывает устаревшее, чтобы не вылезать за лимиты контекста.

Теория простыми словами

Типы памяти

Кратковременная память:

  • История текущего разговора (хранится в runtime, не в долговременном хранилище)
  • Ограничена контекстным окном LLM
  • Теряется при завершении разговора
  • Примечание: Управление кратковременной памятью (саммаризация, отбор) описано в Context Engineering. Термин "рабочая память" используется в Context Engineering для обозначения недавних поворотов разговора в контексте.

Долговременная память (постоянное хранилище):

  • Факты, предпочтения, прошлые решения
  • Хранится в базе данных/файлах
  • Сохраняется между разговорами

Episodic память:

  • Конкретные события: "Пользователь спрашивал о месте на диске 2026-01-06"
  • Полезна для отладки и обучения

Semantic память:

  • Общие знания: "Пользователь предпочитает JSON ответы"
  • Извлекается из эпизодов

Операции с памятью

  1. Store — Сохранить информацию для будущего
  2. Retrieve — Найти релевантную информацию
  3. Forget — Удалить устаревшую информацию
  4. Update — Изменить существующую информацию

Как это работает (пошагово)

Шаг 1: Интерфейс памяти

type Memory interface {
    Store(key string, value any, metadata map[string]any) error
    Retrieve(query string, limit int) ([]MemoryItem, error)
    Forget(key string) error
    Update(key string, value any) error
}

type MemoryItem struct {
    Key      string
    Value    any
    Metadata map[string]any
    Created  time.Time
    Accessed time.Time
    TTL      time.Duration // Время жизни
}

Шаг 2: Сохранение информации

type SimpleMemory struct {
    store map[string]MemoryItem
    mu    sync.RWMutex
}

func (m *SimpleMemory) Store(key string, value any, metadata map[string]any) error {
    m.mu.Lock()
    defer m.mu.Unlock()

    m.store[key] = MemoryItem{
        Key:      key,
        Value:    value,
        Metadata: metadata,
        Created:  time.Now(),
        Accessed: time.Now(),
        TTL:      24 * time.Hour, // Дефолтный TTL
    }
    return nil
}

Шаг 3: Извлечение с поиском

func (m *SimpleMemory) Retrieve(query string, limit int) ([]MemoryItem, error) {
    m.mu.RLock()
    defer m.mu.RUnlock()

    // Простой поиск по ключевым словам (в проде используйте embeddings)
    results := make([]MemoryItem, 0, len(m.store))
    queryLower := strings.ToLower(query)

    for _, item := range m.store {
        // Проверяем, не истёк ли срок
        if item.TTL > 0 && time.Since(item.Created) > item.TTL {
            continue
        }

        // Простое сопоставление ключевых слов
        valueStr := fmt.Sprintf("%v", item.Value)
        if strings.Contains(strings.ToLower(valueStr), queryLower) {
            item.Accessed = time.Now() // Обновляем время доступа
            results = append(results, item)
        }
    }

    // Сортируем по времени доступа (самые свежие первыми)
    sort.Slice(results, func(i, j int) bool {
        return results[i].Accessed.After(results[j].Accessed)
    })

    if len(results) > limit {
        results = results[:limit]
    }

    return results, nil
}

Шаг 4: Забывание истёкших элементов

func (m *SimpleMemory) Cleanup() error {
    m.mu.Lock()
    defer m.mu.Unlock()

    now := time.Now()
    for key, item := range m.store {
        if item.TTL > 0 && now.Sub(item.Created) > item.TTL {
            delete(m.store, key)
        }
    }
    return nil
}

Шаг 5: Интеграция с агентом

func runAgentWithMemory(ctx context.Context, client *openai.Client, memory Memory, userInput string) (string, error) {
    // Извлекаем релевантные воспоминания
    memories, _ := memory.Retrieve(userInput, 5)

    // Строим контекст с воспоминаниями
    messages := []openai.ChatCompletionMessage{
        {Role: "system", Content: "Ты полезный ассистент с доступом к памяти."},
    }

    // Добавляем релевантные воспоминания как контекст
    if len(memories) > 0 {
        memoryContext := "Релевантная прошлая информация:\n"
        for _, mem := range memories {
            memoryContext += fmt.Sprintf("- %s: %v\n", mem.Key, mem.Value)
        }
        messages = append(messages, openai.ChatCompletionMessage{
            Role:    "system",
            Content: memoryContext,
        })
    }

    messages = append(messages, openai.ChatCompletionMessage{
        Role:    "user",
        Content: userInput,
    })

    resp, err := client.CreateChatCompletion(ctx, openai.ChatCompletionRequest{
        Model:    "gpt-4o-mini",
        Messages: messages,
    })
    if err != nil {
        return "", err
    }

    answer := resp.Choices[0].Message.Content

    // Сохраняем важную информацию из разговора
    if shouldStore(userInput, answer) {
        key := generateKey(userInput)
        memory.Store(key, answer, map[string]any{
            "user_input": userInput,
            "timestamp": time.Now(),
        })
    }

    return answer, nil
}

Checkpoint и Resume

Агент может работать часами над сложной задачей. Если процесс упадёт посередине, потеряется весь прогресс. Checkpoint (чекпоинт) сохраняет состояние разговора периодически. При сбое агент возобновляет работу с последней сохранённой точки.

Базовая реализация Checkpoint (структура, сохранение/загрузка, интеграция с agent loop) описана в Главе 09: Анатомия Агента. Продвинутые стратегии (гранулярность, валидация, ротация) — в Главе 11: State Management.

Здесь мы рассматриваем, как Checkpoint связан с памятью агента:

  • Что сохранять: историю сообщений (messages[]), содержимое памяти, состояние инструментов, текущий шаг выполнения.
  • Когда сохранять: после каждого значимого шага (вызов инструмента, ответ пользователю). Для коротких задач (2-3 итерации) Checkpoint избыточен. Для длинных задач (10+ итераций) — обязателен.
  • TTL: устанавливайте TTL на Checkpoint (например, 24 часа), чтобы устаревшие снимки не накапливались.

Shared Memory между агентами

В мульти-агентных системах агенты обмениваются информацией через общее хранилище памяти. Каждый агент читает и пишет в общий store, разграничивая данные по namespace:

type SharedMemoryStore struct {
    store CheckpointStore
}

// Запись с namespace агента
func (s *SharedMemoryStore) Put(ctx context.Context, agentID, key string, value any) error {
    fullKey := fmt.Sprintf("shared:%s:%s", agentID, key)
    data, _ := json.Marshal(value)
    return s.store.Set(ctx, fullKey, data, 0)
}

// Чтение данных другого агента
func (s *SharedMemoryStore) Get(ctx context.Context, agentID, key string) (any, error) {
    fullKey := fmt.Sprintf("shared:%s:%s", agentID, key)
    data, err := s.store.Get(ctx, fullKey)
    if err != nil {
        return nil, err
    }
    var result any
    return result, json.Unmarshal(data, &result)
}

// Получить все записи всех агентов (для supervisor)
func (s *SharedMemoryStore) ListAll(ctx context.Context) (map[string]any, error) {
    keys, _ := s.store.Keys(ctx, "shared:*")
    result := make(map[string]any)
    for _, key := range keys {
        val, _ := s.store.Get(ctx, key)
        var parsed any
        json.Unmarshal(val, &parsed)
        result[key] = parsed
    }
    return result, nil
}

Связь: Подробнее об управлении состоянием агента — в Главе 11: State Management. Checkpoint — это частный случай персистенции состояния.

Типовые ошибки

Ошибка 1: Нет TTL (Time To Live)

Симптом: Память растёт бесконечно, потребляя хранилище и контекст.

Причина: Не забывается устаревшая информация.

Решение: Реализуйте TTL и периодическую очистку.

Ошибка 2: Сохранение всего

Симптом: Память заполняется нерелевантной информацией, делая извлечение шумным.

Причина: Нет фильтрации того, что сохранять.

Решение: Сохраняйте только важные факты, не каждый поворот разговора.

Ошибка 3: Нет оптимизации извлечения

Симптом: Извлечение возвращает нерелевантные результаты или пропускает важную информацию.

Причина: Простое сопоставление ключевых слов вместо семантического поиска.

Решение: Используйте embeddings для семантического поиска по сходству.

Мини-упражнения

Упражнение 1: Реализуйте Memory Store

Создайте хранилище памяти, которое сохраняется на диск:

type FileMemory struct {
    filepath string
    // Ваша реализация
}

func (m *FileMemory) Store(key string, value any, metadata map[string]any) error {
    // Сохранить в файл
}

Ожидаемый результат:

  • Память сохраняется между перезапусками
  • Можно загрузить из файла при старте

Упражнение 2: Семантический поиск

Реализуйте извлечение с использованием embeddings:

func (m *Memory) RetrieveSemantic(query string, limit int) ([]MemoryItem, error) {
    // Используйте embeddings для поиска семантически похожих элементов
}

Ожидаемый результат:

  • Находит релевантные элементы даже без точного совпадения ключевых слов
  • Возвращает самые похожие элементы первыми

Критерии сдачи / Чек-лист

Сдано:

  • Понимаете разные типы памяти
  • Можете сохранять и извлекать информацию
  • Реализуете TTL и очистку
  • Интегрируете память с агентом

Не сдано:

  • Нет TTL, память растёт бесконечно
  • Сохранение всего без фильтрации
  • Только простой поиск по ключевым словам

Связь с другими главами

ВАЖНО: Память (эта глава) отвечает за хранение и извлечение информации. Управление тем, как эта информация включается в контекст LLM, описано в Context Engineering.

Что дальше?

После понимания систем памяти переходите к:

  • 13. Context Engineering — Узнайте, как эффективно управлять контекстом из памяти, состояния и retrieval