Самое важное из мира AI — в канале MAX AgentLabОтдельные разборы, инструменты и практические схемы Читайте Agent Lab в Telegram Разборы, кейсы и новости о практических AI-агентах без лишней воды.

Как честно сравнить системы памяти для AI-агентов

Как честно сравнить системы памяти для AI-агентов
Temporary fallback cover; replace in editorial pass.

Уровень: продвинутый · Чтение: до 12 минут · Обновлено: 17 октября 2026

Почти любое сравнение памяти агентов, которое попадается в обзорах, на самом деле сравнивает сразу три вещи: саму память, модель, которая пишет ответ, и способ, которым этот ответ оценивают. Если у системы A ответы пишет одна модель со своим промптом, а у системы B другая, разница в итоговом балле ничего не говорит о памяти. Ниже собран воспроизводимый стенд. Все системы подключаются к нему через один интерфейс add/search, а всё, что идёт после поиска, у них общее.

Что именно мы измеряем

Под системой памяти здесь понимается компонент, который принимает реплики диалога и по запросу возвращает релевантный контекст. Точное определение и соседние термины есть в статье память агента в глоссарии. Честный тест держит переменной только одну часть конвейера:

  1. Запись: система получает одни и те же сообщения в одном и том же порядке.
  2. Поиск: система получает один и тот же вопрос и возвращает не больше K фрагментов или токенов.
  3. Ответ: одна фиксированная модель, один шаблон промпта, температура 0.
  4. Оценка: одна процедура для всех систем, судья с зафиксированной версией и промптом, плюс детерминированные метрики.

Меняется только реализация шагов 1 и 2. Если на шагах 3 и 4 у систем что-то различается, сравнение уже нечестное.

Откуда берутся искажения

Шаг 1. Структура стенда

Дальше идёт пример структуры проекта. Названия файлов условные, адаптируйте их под свой репозиторий.

memory-bench/
  data/
    dialogs.jsonl        # сессии диалогов
    questions.jsonl      # вопросы с эталонными ответами
  adapters/
    base.py              # интерфейс MemorySystem
    full_context.py      # базовая линия: весь диалог
    no_memory.py         # нижняя граница: пустой контекст
    my_vector_store.py   # ваша система
  harness.py             # прогон: add → search → answer
  evaluate.py            # общая оценка
  config.yaml            # зафиксированные параметры

Формат данных в примере такой:

// dialogs.jsonl — одна строка = одно сообщение
{"conv_id": "c01", "session": 1, "turn": 3, "role": "user",
 "text": "Я переехал в Казань в марте", "ts": "2026-03-14T10:00:00Z"}

// questions.jsonl
{"conv_id": "c01", "qid": "c01-q4", "question": "В каком городе живёт пользователь?",
 "answer": "Казань", "type": "single_hop"}

Поле type понадобится для разбивки результатов: одношаговые вопросы, многошаговые, временные, вопросы на обновление факта, вопросы без ответа. Если в отчёте только общий балл, он почти наверняка скрывает, где именно система ошибается.

Шаг 2. Единый интерфейс Add/Search

# adapters/base.py
from abc import ABC, abstractmethod
from dataclasses import dataclass

@dataclass
class Message:
    conv_id: str
    role: str
    text: str
    ts: str

@dataclass
class Hit:
    text: str
    score: float | None = None

class MemorySystem(ABC):
    name: str

    @abstractmethod
    def reset(self, conv_id: str) -> None:
        """Полностью очистить память для conv_id."""

    @abstractmethod
    def add(self, msg: Message) -> None:
        """Записать одно сообщение. Вопросы сюда не попадают."""

    @abstractmethod
    def search(self, conv_id: str, query: str, k: int) -> list[Hit]:
        """Вернуть не более k фрагментов. Без генерации ответа."""

Главное правило: search возвращает только извлечённый контекст. Если библиотека умеет лишь «ответить на вопрос», для честного теста её нужно обернуть так, чтобы отдавались сырые воспоминания. Если это невозможно, отметьте в отчёте, что для этой системы шаг ответа не изолирован.

Две обязательные базовые линии:

# adapters/full_context.py — верхний ориентир
class FullContext(MemorySystem):
    name = "full_context"
    def __init__(self): self.store = {}
    def reset(self, conv_id): self.store[conv_id] = []
    def add(self, msg): self.store[msg.conv_id].append(f"[{msg.ts}] {msg.role}: {msg.text}")
    def search(self, conv_id, query, k):
        return [Hit(text="\n".join(self.store[conv_id]))]

# adapters/no_memory.py — нижняя граница
class NoMemory(MemorySystem):
    name = "no_memory"
    def reset(self, conv_id): pass
    def add(self, msg): pass
    def search(self, conv_id, query, k): return []

Если ваша система не обгоняет no_memory заметно, проблема в данных или в стенде. Если она обгоняет full_context на коротких диалогах, проверьте, нет ли утечки.

Шаг 3. Общий harness

# harness.py (сокращённо)
import json, hashlib, yaml
from tiktoken import get_encoding  # или токенизатор вашей модели

cfg = yaml.safe_load(open("config.yaml"))
enc = get_encoding(cfg["tokenizer"])

ANSWER_PROMPT = open("prompts/answer.txt").read()   # один для всех

def truncate(hits, budget):
    out, used = [], 0
    for h in hits:
        n = len(enc.encode(h.text))
        if used + n > budget:
            break
        out.append(h.text); used += n
    return "\n---\n".join(out), used

def run(system, dialogs, questions, llm):
    rows = []
    for conv_id, msgs in dialogs.items():
        system.reset(conv_id)
        for m in msgs:                       # одинаковый порядок
            system.add(m)
        for q in questions[conv_id]:
            hits = system.search(conv_id, q["question"], cfg["k"])
            ctx, used = truncate(hits, cfg["context_budget_tokens"])
            prompt = ANSWER_PROMPT.format(context=ctx, question=q["question"])
            ans = llm(prompt, model=cfg["answer_model"], temperature=0, seed=cfg["seed"])
            rows.append({"system": system.name, "qid": q["qid"], "type": q["type"],
                         "context_tokens": used, "prediction": ans,
                         "gold": q["answer"],
                         "prompt_sha": hashlib.sha256(ANSWER_PROMPT.encode()).hexdigest()[:12]})
    return rows

Функцию llm реализуйте один раз через клиент вашего провайдера и отдавайте её всем системам. Обрезка по бюджету делается снаружи адаптера, поэтому лишний контекст не даст системе преимущества.

Пример config.yaml. Значения нужно подставить свои, это не рекомендация:

answer_model: "<зафиксированная версия модели ответов>"
judge_model:  "<зафиксированная версия модели-судьи>"
tokenizer: "cl100k_base"
k: 10
context_budget_tokens: 2000
seed: 42
runs: 3

Шаг 4. Общая процедура оценки

Используйте сразу два слоя оценки:

# evaluate.py (фрагмент)
import random
JUDGE_PROMPT = open("prompts/judge.txt").read()

def judge_all(rows, llm, cfg):
    random.Random(cfg["seed"]).shuffle(rows)      # перемешиваем системы
    for r in rows:
        p = JUDGE_PROMPT.format(question=r["qid"], gold=r["gold"], pred=r["prediction"])
        r["judge"] = llm(p, model=cfg["judge_model"], temperature=0).strip().upper() == "YES"
    return rows

Для вопросов без ответа правильным считается явный отказ («в памяти нет данных»). Эти вопросы считайте отдельно: так видно, какие системы склонны галлюцинировать по нерелевантному контексту.

Шаг 5. Запуск

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

python -m venv .venv && . .venv/bin/activate
pip install -r requirements.txt
export LLM_API_KEY=...            # не коммитить

python harness.py --systems no_memory,full_context,my_vector_store \
                  --runs 3 --out results/run.jsonl
python evaluate.py --in results/run.jsonl --out results/report.csv
git rev-parse HEAD > results/commit.txt

Проверка результата

Прежде чем публиковать цифры, пройдите по списку:

  1. У всех строк в run.jsonl одинаковый prompt_sha и одна и та же answer_model.
  2. context_tokens ни у одной строки не больше бюджета. Посмотрите и среднее по системам: если одна стабильно использует половину бюджета, это тоже надо указать.
  3. Порядок систем: no_memory < ваша система ≤ примерно full_context (на диалогах, которые помещаются в бюджет). Если порядок другой, ищите баг.
  4. Расхождение между прогонами меньше разницы между системами. Сообщайте среднее и разброс по runs, а не лучший прогон.
  5. Вручную проверьте 30–50 случайных вердиктов судьи. Если судья заметно расходится с вами, поправьте его промпт и перезапустите оценку для всех систем.
  6. Таблица результатов разбита по type вопросов и содержит задержку add/search, если она для вас важна.

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

Ограничения подхода

Итог

Как честно сравнить системы памяти для AI-агентов: поставить их в одинаковые условия везде, кроме самой памяти. Одинаковые данные и порядок записи, одинаковый бюджет контекста, одна модель ответов с одним промптом, один слепой судья, несколько прогонов и две базовые линии. Всё остальное, что попадает в «сравнение», скорее мешает, чем помогает.

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