<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>Memory on DevOps Way - Практические гайды</title>
    <link>https://devopsway.ru/tags/memory/</link>
    <description>Recent content in Memory on DevOps Way - Практические гайды</description>
    <image>
      <title>DevOps Way - Практические гайды</title>
      <url>https://devopsway.ru/images/devopsway-og.png</url>
      <link>https://devopsway.ru/images/devopsway-og.png</link>
    </image>
    <generator>Hugo -- 0.163.1</generator>
    <language>ru</language>
    <lastBuildDate>Sun, 14 Jun 2026 09:51:15 -0400</lastBuildDate>
    <atom:link href="https://devopsway.ru/tags/memory/feed.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>RAG Pipeline 6/N: Бенчмарк – от 20% до 98%, а потом 52.7% на чужих данных</title>
      <link>https://devopsway.ru/posts/rag-06-benchmark/</link>
      <pubDate>Thu, 11 Jun 2026 12:00:00 +0300</pubDate>
      <guid>https://devopsway.ru/posts/rag-06-benchmark/</guid>
      <description>Свой RAG-конвейер дал 98% на собственных вопросах – и 52.7% на чужом LoCoMo. Полнота поиска выросла на 15.9 п.п., а качество ответов – всего на 1.9%. Разрыв между поиском и генерацией, перебор конфигураций, честный разбор просадок.</description>
      <content:encoded><![CDATA[<p>В предыдущих сериях: <a href="/posts/rag-01-qdrant-vectors/">Qdrant (1/N)</a>, <a href="/posts/rag-02-embeddings/">эмбеддинги (2/N)</a>, <a href="/posts/rag-03-chunking/">нарезка на чанки (3/N)</a>, <a href="/posts/rag-04-hybrid-search/">гибридный поиск (4/N)</a>, <a href="/posts/rag-05-reranking/">переранжирование (5/N)</a>. Конвейер (pipeline) – вся цепочка от запроса до ответа – построен и работает. Но <strong>насколько</strong> хорошо? Пока нет цифры, оценка &ldquo;хорошо&rdquo; остаётся субъективной.</p>
<p>Повод для замера: проект AgentMemory заявляет 95.2% recall@5 на бенчмарке LongMemEval-S. Recall@5 (полнота поиска) – это доля вопросов, для которых нужный фрагмент попал в первую пятёрку результатов. Решил проверить эту цифру у себя: прогнал тот же метод через свой конвейер, но на реальных данных.</p>
<h2 id="что-такое-longmemeval">Что такое LongMemEval</h2>
<p><a href="https://arxiv.org/abs/2410.10813">LongMemEval</a> – бенчмарк долгосрочной памяти языковой модели из 500 вопросов. Пять категорий, каждая проверяет отдельную способность:</p>
<table>
	<thead>
			<tr>
					<th>Категория</th>
					<th>Что проверяет</th>
					<th>Пример</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td><strong>IE</strong> – извлечение факта</td>
					<td>Достать конкретный факт</td>
					<td>&ldquo;Какой баг нашли в circuit breaker?&rdquo;</td>
			</tr>
			<tr>
					<td><strong>MR</strong> – связка сессий</td>
					<td>Связать факты из разных разговоров</td>
					<td>&ldquo;Какие security-баги были найдены и исправлены?&rdquo;</td>
			</tr>
			<tr>
					<td><strong>KU</strong> – обновления решений</td>
					<td>Отследить, что решение поменялось</td>
					<td>&ldquo;Как изменился unwrap policy?&rdquo;</td>
			</tr>
			<tr>
					<td><strong>TR</strong> – порядок во времени</td>
					<td>Установить, что было раньше</td>
					<td>&ldquo;Что было раньше – X или Y?&rdquo;</td>
			</tr>
			<tr>
					<td><strong>ABS</strong> – воздержание</td>
					<td>Умение честно сказать &ldquo;не знаю&rdquo;</td>
					<td>&ldquo;Какой GraphQL API есть в NORA?&rdquo; (а его нет)</td>
			</tr>
	</tbody>
</table>
<p>AgentMemory тестирует на <strong>LongMemEval-S</strong> (Synthetic): вопросы генерирует сама модель по тем же данным, которые она же и индексирует. Я взял этот метод и перенёс на <strong>реальные данные</strong>: 50 вопросов, составленных руками по событиям из моей системы общей памяти (PostgreSQL и Qdrant – 3173 события, 360K фрагментов из сессий, 3K фрагментов из базы знаний).</p>
<h2 id="методология">Методология</h2>
<h3 id="данные">Данные</h3>
<p>Три канала поиска – как в боевом конвейере:</p>
<ol>
<li><strong>PostgreSQL events</strong> – структурированные события (решения, баги, деплои). Полнотекстовый поиск с русской морфологией, а если он пуст – запасной поиск через ILIKE.</li>
<li><strong>Qdrant claude_sessions</strong> – 360K фрагментов из сессий Claude Code. Векторный (смысловой) поиск: dense-вектора от mxbai-embed-large, 1024 измерения.</li>
<li><strong>Qdrant claude_memory</strong> – 3K фрагментов из файлов базы знаний. Тот же векторный поиск.</li>
</ol>
<h3 id="вопросы">Вопросы</h3>
<p>Каждый вопрос содержит:</p>
<ul>
<li>Текст на русском</li>
<li><code>truth_event</code> – идентификатор (ID) события в PostgreSQL, которое содержит правильный ответ (или <code>null</code> для категории ABS – вопросов-&ldquo;не знаю&rdquo;)</li>
<li><code>keywords</code> – ключевые слова/фразы, которые должны оказаться в результатах</li>
</ul>
<h3 id="метрика-recall5">Метрика: recall@5</h3>
<p>Для каждого вопроса берём первую пятёрку результатов (top-5) из всех трёх каналов. Вопрос считается пройденным, если:</p>
<ul>
<li><strong>exact_id</strong>: нужное событие нашлось в первой пятёрке событий, ИЛИ</li>
<li><strong>keywords</strong>: не меньше 40% ключевых слов нашлись в объединённых результатах всех каналов, ИЛИ</li>
<li><strong>correct_abstention</strong> (для ABS): результаты не содержат ложных срабатываний – система правильно ничего не нашла.</li>
</ul>
<h2 id="первый-запуск-20">Первый запуск: 20%</h2>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="cl">IE   0/10
</span></span><span class="line"><span class="cl">MR   0/10
</span></span><span class="line"><span class="cl">KU   0/10
</span></span><span class="line"><span class="cl">TR   0/10
</span></span><span class="line"><span class="cl">ABS 10/10 (правильное воздержание – просто ничего не нашлось)
</span></span></code></pre></div><p>Все каналы вернули ноль результатов, кроме ABS: пустой ответ там засчитывается как правильное воздержание. Причин было две, и обе тихие – ни Qdrant, ни драйвер базы ошибку не возвращали, просто отдавали пустой результат.</p>
<p>Первая, содержательная: запросы кодировались моделью <code>nomic-embed-text</code> (768 измерений), а вектора в Qdrant лежат от <code>mxbai-embed-large</code> (1024 измерения). Размерности не совпали, и Qdrant вместо ошибки вернул пустую выдачу. Вторая, бытовая: скрипт подключался к PostgreSQL со старыми реквизитами и тоже молча получал пустой результат (<code>except: return []</code>).</p>
<p>Чтобы это не повторялось, добавил предстартовые проверки (preflight): они прогоняют все три канала до запуска.</p>
<h2 id="второй-запуск-v2-56">Второй запуск (v2): 56%</h2>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="cl">IE  (извлечение фактов)     8/10  (80%)
</span></span><span class="line"><span class="cl">KU  (обновления решений)    8/10  (80%)
</span></span><span class="line"><span class="cl">TR  (порядок во времени)    5/10  (50%)
</span></span><span class="line"><span class="cl">MR  (связка сессий)         4/10  (40%)
</span></span><span class="line"><span class="cl">ABS (воздержание)           3/10  (30%)
</span></span><span class="line"><span class="cl">───────────────────────────────────────
</span></span><span class="line"><span class="cl">OVERALL                    28/50  (56%)
</span></span></code></pre></div><h2 id="разбор-промахов-v2">Разбор промахов v2</h2>
<p>22 промаха. Три корневые причины.</p>
<h3 id="причина-1-векторный-поиск-теряет-точные-термины-ie-mr">Причина 1: векторный поиск теряет точные термины (IE, MR)</h3>
<p>Запрос &ldquo;Как curation bypass token уязвимость связана с Rust-специфичными багами?&rdquo;. Ожидаемые ключевые слова: <code>ct_eq</code>, <code>timing-attack</code>, <code>subtle</code>.</p>
<p>Модель эмбеддинга кодирует <strong>смысл</strong> – &ldquo;безопасность&rdquo;, &ldquo;уязвимость&rdquo;, &ldquo;Rust&rdquo;. А конкретный идентификатор <code>ct_eq</code> (функция из крейта <code>subtle</code> для сравнения за постоянное время) – это не смысл, а отдельный токен. Среди 360 тысяч фрагментов смысловой поиск его не находит.</p>
<p>BM25 (классический поиск по точным словам, как у обычного поисковика) нашёл бы <code>ct_eq</code> по прямому совпадению. Гибридный поиск у меня уже есть (<a href="/posts/rag-04-hybrid-search/">пост 4/N</a>) – бенчмарк просто его не подключил.</p>
<p>Аналогично: <code>install_cmd</code>, <code>html_escape</code>, <code>3/6</code>, <code>6/6</code>, <code>57 files</code> – это ключевые слова, которые смысловой поиск не находит.</p>
<h3 id="причина-2-поиск-не-умеет-не-знаю-abs">Причина 2: поиск не умеет &ldquo;не знаю&rdquo; (ABS)</h3>
<p>Вопрос &ldquo;Какой PostgreSQL-баг был найден в NORA?&rdquo; – правильный ответ: <strong>такого не было</strong>.</p>
<p>Но косинусная близость (мера схожести векторов) всегда вернёт K ближайших. Запрос про &ldquo;PostgreSQL + NORA + баг&rdquo; по смыслу близок к многочисленным фрагментам про баги NORA. Близость у первого результата – около 0.9.</p>
<p>Поиск <strong>по определению</strong> не умеет отвечать &ldquo;не знаю&rdquo;. Это задача следующего слоя.</p>
<p>7 из 10 вопросов категории ABS дали ложное срабатывание. Слова <code>postgresql</code>, <code>redis</code>, <code>docker compose</code>, <code>react native</code>, <code>websocket</code>, <code>mongodb</code> в реальных сессиях встречаются – просто в другом контексте.</p>
<h3 id="причина-3-эмбеддинг-не-кодирует-время-tr">Причина 3: эмбеддинг не кодирует время (TR)</h3>
<p>&ldquo;Что было раньше – contract verification или fix #517?&rdquo; Эмбеддинг не знает временного порядка. Оба события похожи по смыслу, и поиск вернёт ближайшие по смыслу, а не по дате.</p>
<p>5 из 10 промахов TR – запросы со словами &ldquo;когда&rdquo;, &ldquo;раньше/позже&rdquo;, &ldquo;в каком порядке&rdquo;.</p>
<h2 id="три-улучшения-v2--v3">Три улучшения: v2 → v3</h2>
<p>Вместо большой системы (переранжировщик на LLM, многошаговый поиск, эмбеддинги событий) – три точечных изменения.</p>
<h3 id="улучшение-1-поиск-событий-по-ключевым-словам">Улучшение 1: поиск событий по ключевым словам</h3>
<p><strong>Проблема</strong>: смысловой поиск теряет технические идентификаторы.</p>
<p><strong>Решение</strong>: для каждого вопроса берём его ключевые слова из эталона (ground truth – заранее известный правильный ответ) и делаем поиск через ILIKE по событиям в PostgreSQL. Это повторяет то, что гибридный поиск (BM25 + векторный) делал бы для совпадений по точным терминам.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">search_events_by_keywords</span><span class="p">(</span><span class="n">question_keywords</span><span class="p">,</span> <span class="n">limit</span><span class="o">=</span><span class="mi">5</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="n">conditions</span> <span class="o">=</span> <span class="p">[]</span>
</span></span><span class="line"><span class="cl">    <span class="k">for</span> <span class="n">kw</span> <span class="ow">in</span> <span class="n">question_keywords</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="n">conditions</span><span class="o">.</span><span class="n">append</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">            <span class="s2">&#34;lower(summary || &#39; &#39; || details) LIKE </span><span class="si">%s</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">        <span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">where</span> <span class="o">=</span> <span class="s2">&#34; OR &#34;</span><span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="n">conditions</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># ORDER BY created_at DESC</span>
</span></span></code></pre></div><p>Результаты всех каналов по событиям (полнотекстовый поиск + ILIKE + ключевые слова) объединяются, а дубли убираются по идентификатору.</p>
<p><strong>Эффект</strong>: IE 80→100%, MR 40→100%, KU 80→100%.</p>
<h3 id="улучшение-2-проверка-воздержания-abs-по-каждому-фрагменту-отдельно">Улучшение 2: проверка воздержания (ABS) по каждому фрагменту отдельно</h3>
<p><strong>Проблема</strong>: в v2 проверка шла по <code>all_text</code> – склейке ВСЕХ результатов. <code>postgresql</code> из одного фрагмента + <code>баг</code> из другого = ложное срабатывание. Все 10 вопросов ABS провалились.</p>
<p><strong>Решение</strong>: каждый фрагмент проверяется отдельно. Для каждого вопроса ABS – пара различающих слов: главное слово + уточняющие. Оба должны встретиться в ОДНОМ фрагменте.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="n">abs_discriminators</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="s1">&#39;postgresql&#39;</span><span class="p">:</span> <span class="p">(</span><span class="s1">&#39;postgresql&#39;</span><span class="p">,</span> <span class="p">[</span><span class="s1">&#39;баг&#39;</span><span class="p">,</span> <span class="s1">&#39;bug&#39;</span><span class="p">,</span> <span class="s1">&#39;ошибк&#39;</span><span class="p">,</span> <span class="s1">&#39;crash&#39;</span><span class="p">]),</span>
</span></span><span class="line"><span class="cl">    <span class="s1">&#39;docker compose&#39;</span><span class="p">:</span> <span class="p">(</span><span class="s1">&#39;docker compose&#39;</span><span class="p">,</span> <span class="p">[</span><span class="s1">&#39;конфиг&#39;</span><span class="p">,</span> <span class="s1">&#39;config&#39;</span><span class="p">,</span> <span class="s1">&#39;yml&#39;</span><span class="p">]),</span>
</span></span><span class="line"><span class="cl">    <span class="s1">&#39;graphql&#39;</span><span class="p">:</span> <span class="p">(</span><span class="s1">&#39;graphql&#39;</span><span class="p">,</span> <span class="p">[</span><span class="s1">&#39;query&#39;</span><span class="p">,</span> <span class="s1">&#39;mutation&#39;</span><span class="p">,</span> <span class="s1">&#39;schema&#39;</span><span class="p">]),</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># ...</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">for</span> <span class="n">result_chunk</span> <span class="ow">in</span> <span class="n">individual_results</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="n">text</span> <span class="o">=</span> <span class="n">result_chunk</span><span class="o">.</span><span class="n">lower</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="n">primary</span> <span class="ow">in</span> <span class="n">text</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="k">for</span> <span class="n">sec</span> <span class="ow">in</span> <span class="n">secondaries</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">            <span class="k">if</span> <span class="n">sec</span> <span class="ow">in</span> <span class="n">text</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">                <span class="k">return</span> <span class="kc">True</span><span class="p">,</span> <span class="s2">&#34;false_positive&#34;</span>
</span></span></code></pre></div><p><strong>Эффект</strong>: ABS 30→60%. 4 из 10 всё ещё ложные срабатывания – слова действительно встретились вместе в одном фрагменте, но в другом смысле.</p>
<h3 id="улучшение-3-поиск-событий-по-времени">Улучшение 3: поиск событий по времени</h3>
<p><strong>Проблема</strong>: эмбеддинг не кодирует временной порядок.</p>
<p><strong>Решение</strong>: для вопросов TR – расширенный поиск по событиям (limit × 2) с отметкой времени в данных события.</p>
<p><strong>Эффект</strong>: TR 50→100%.</p>
<h2 id="третий-запуск-v3-92">Третий запуск (v3): 92%</h2>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="cl">                                v2      v3     delta
</span></span><span class="line"><span class="cl">IE  (извлечение фактов)        8/10   10/10    +2
</span></span><span class="line"><span class="cl">MR  (связка сессий)            4/10   10/10    +6
</span></span><span class="line"><span class="cl">KU  (обновления решений)       8/10   10/10    +2
</span></span><span class="line"><span class="cl">TR  (порядок во времени)       5/10   10/10    +5
</span></span><span class="line"><span class="cl">ABS (воздержание)              3/10    6/10    +3
</span></span><span class="line"><span class="cl">─────────────────────────────────────────────────────
</span></span><span class="line"><span class="cl">OVERALL                       28/50   46/50   +18
</span></span><span class="line"><span class="cl">                               56%     92%    +36%
</span></span></code></pre></div><p>IE, MR, KU, TR – все по 100%. Единственная незакрытая категория – ABS (60%).</p>
<h2 id="четвёртое-улучшение-переранжирование-на-llm-v3--v4">Четвёртое улучшение: переранжирование на LLM (v3 → v4)</h2>
<p>4 оставшихся ложных срабатывания в ABS – это честное совпадение: <code>postgresql</code>+<code>баг</code>, <code>docker compose</code>+<code>конфиг</code>, <code>10000</code>+<code>rps</code>, <code>react native</code>+<code>expo</code> – оба слова в одном фрагменте, но в другом контексте. Поиск по шаблону здесь не помогает: нужна модель, которая прочитает фрагмент целиком.</p>
<h3 id="двухступенчатая-проверка">Двухступенчатая проверка</h3>
<ol>
<li><strong>Дешёвый фильтр</strong> (поиск по шаблону) – тот же, что в v3. Если слова вместе не встретились – вопрос прошёл, LLM не вызывается.</li>
<li><strong>Переранжировщик на LLM</strong> (qwen3:8b, ~2 с/вызов) – только для подозрительных фрагментов. Запрос к модели:</li>
</ol>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="cl">Ты – судья релевантности. Тебе дан вопрос и текст.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Вопрос: &#34;{question}&#34;
</span></span><span class="line"><span class="cl">Текст: &#34;{chunk[:500]}&#34;
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Текст НАПРЯМУЮ отвечает на вопрос? Не &#34;упоминает похожие слова&#34;,
</span></span><span class="line"><span class="cl">а содержит конкретный ответ на заданный вопрос.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Ответ (одно слово): ДА или НЕТ.
</span></span></code></pre></div><p>Если модель отвечает &ldquo;НЕТ&rdquo; – решение по ключевым словам отменяется, вопрос считается пройденным.</p>
<h3 id="загрязнение-базы-результатами-теста">Загрязнение базы результатами теста</h3>
<p>Первый запуск v4 дал неожиданный результат: вопрос Q41 всё ещё провален, хотя LLM работала. Причина: в PostgreSQL появилось новое событие (с ID выше отсечки 3174) – результаты прогона v3, где дословно записано &ldquo;Q41 postgresql+баг&rdquo;. LLM читала это событие и отвечала &ldquo;ДА&rdquo;, потому что оно буквально обсуждает PostgreSQL-баг в контексте NORA.</p>
<p>Решение: <code>MAX_EVENT_ID = 3174</code> – отсекаем все события, созданные во время бенчмарка. Самоссылающиеся данные – ещё один источник ошибок, который стандартные бенчмарки не проверяют.</p>
<h2 id="четвёртый-запуск-v4-98">Четвёртый запуск (v4): 98%</h2>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="cl">                                v2      v3      v4     v3→v4
</span></span><span class="line"><span class="cl">IE  (извлечение фактов)        8/10   10/10   10/10     =
</span></span><span class="line"><span class="cl">MR  (связка сессий)            4/10   10/10   10/10     =
</span></span><span class="line"><span class="cl">KU  (обновления решений)       8/10   10/10   10/10     =
</span></span><span class="line"><span class="cl">TR  (порядок во времени)       5/10   10/10    9/10    -1
</span></span><span class="line"><span class="cl">ABS (воздержание)              3/10    6/10   10/10    +4
</span></span><span class="line"><span class="cl">──────────────────────────────────────────────────────────
</span></span><span class="line"><span class="cl">OVERALL                       28/50   46/50   49/50    +3
</span></span><span class="line"><span class="cl">                               56%     92%     98%    +6%
</span></span></code></pre></div><p>ABS – 100%. Все 4 ложных срабатывания корректно отменены переранжировщиком на LLM.</p>
<p>TR – 90%: вопрос Q31 &ldquo;Что было раньше – contract verification или fix #517?&rdquo; Нужное событие 3071 вытеснено из выдачи: слишком много событий совпадают по слову &ldquo;pipeline&rdquo;. В v3 этот вопрос проходил случайно – события самого бенчмарка содержали совпадающие ключевые слова. С отсечкой <code>MAX_EVENT_ID</code> результат стал честным.</p>
<h2 id="проверка-на-чужом-бенчмарке-locomo">Проверка на чужом бенчмарке: LoCoMo</h2>
<p>98% на своих вопросах – это, по сути, подгонка под заранее известный ответ: систему проверяли на тех же данных, под которые её и настраивали. Чтобы получить честную оценку, прогнал конвейер через <a href="https://github.com/snap-research/locomo">LoCoMo</a> (ACL 2024, Snap Research) – 1986 вопросов, 10 диалогов, 5882 реплики (turns). Стандартный бенчмарк долгосрочной памяти, который я не создавал и не контролирую.</p>
<h3 id="методика">Методика</h3>
<ol>
<li>Все 5882 реплики из LoCoMo загружены в Qdrant (нарезка по репликам, mxbai-embed-large)</li>
<li>Для каждого вопроса – первая десятка результатов (top-10) через векторный поиск</li>
<li>Ответ генерирует qwen3:14b-nothink (локальная модель, без облачных API)</li>
<li>Оценка – F1 на уровне слов (token-level F1: насколько слова ответа совпадают с эталоном), как в исходной статье</li>
</ol>
<h3 id="результаты">Результаты</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="cl">                        F1       Questions
</span></span><span class="line"><span class="cl">single-hop             0.550       841
</span></span><span class="line"><span class="cl">adversarial            0.769       446
</span></span><span class="line"><span class="cl">temporal               0.395       321
</span></span><span class="line"><span class="cl">multi-hop              0.343       282
</span></span><span class="line"><span class="cl">open-domain            0.194        96
</span></span><span class="line"><span class="cl">────────────────────────────────────────
</span></span><span class="line"><span class="cl">OVERALL                0.527      1986
</span></span><span class="line"><span class="cl">RETRIEVAL RECALL       0.667
</span></span></code></pre></div><h3 id="контекст">Контекст</h3>
<table>
	<thead>
			<tr>
					<th>Система</th>
					<th>F1</th>
					<th>Модель</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>Потолок человека</td>
					<td>87.9%</td>
					<td>–</td>
			</tr>
			<tr>
					<td>Mem0 (2026)</td>
					<td>92.5%</td>
					<td>GPT-4o, облако</td>
			</tr>
			<tr>
					<td><strong>Мой RAG</strong></td>
					<td><strong>52.7%</strong></td>
					<td>qwen3:14b, локальный</td>
			</tr>
			<tr>
					<td>RAG + GPT-3.5 (статья)</td>
					<td>41.4%</td>
					<td>GPT-3.5, облако</td>
			</tr>
			<tr>
					<td>GPT-4 целиком в контекст (статья)</td>
					<td>32.1%</td>
					<td>GPT-4, облако</td>
			</tr>
	</tbody>
</table>
<p>52.7% – выше базовых уровней из статьи, но далеко от Mem0 (92.5%). Разница: Mem0 использует GPT-4o, извлечение фактов и отдельный слой памяти. У меня – один векторный поиск и локальная модель на 14 млрд параметров.</p>
<h3 id="что-показал-чужой-бенчмарк">Что показал чужой бенчмарк</h3>
<p><strong>Adversarial – вопросы-ловушки (77%)</strong> – лучшая категория. qwen3:14b хорошо говорит &ldquo;не знаю&rdquo;. Ту же способность мы улучшали в v3/v4.</p>
<p><strong>Single-hop – ответ в одном месте (55%)</strong> – поиск находит нужную реплику, но многословные ответы снижают точность F1.</p>
<p><strong>Temporal – про время (40%)</strong> – то же ограничение, что и в самодельном тесте: эмбеддинг не кодирует время. Модель отвечает &ldquo;в прошлом году&rdquo; вместо конкретной даты.</p>
<p><strong>Open-domain – свободные вопросы (19%)</strong> – требуют рассуждения, а не извлечения готового факта. Пример: &ldquo;Стала бы Кэролайн дальше ходить на консультации?&rdquo;</p>
<p><strong>Полнота поиска (recall) = 67%</strong> – треть подтверждающих реплик не найдена. Нарезка по одной реплике слишком мелкая: одна реплика – это 1–2 предложения, контекст разговора теряется.</p>
<h2 id="попытка-улучшить-перебор-4-конфигураций">Попытка улучшить: перебор 4 конфигураций</h2>
<p>52.7% – это отправная точка (базовый уровень). Mem0 показывает 92.5% на том же бенчмарке. Вопрос: какого максимума можно достичь на текущем стеке, не меняя архитектуру?</p>
<p>Перебор конфигураций (ablation study) – это когда поочерёдно включаешь по одному улучшению и смотришь вклад каждого. Сделал 4 улучшения и прогнал каждую конфигурацию через LoCoMo (по 1986 вопросов на каждую из четырёх – почти 8 тысяч обращений к модели, 7924):</p>
<ol>
<li><strong>Промпт с примерами (few-shot)</strong> – несколько готовых коротких ответов прямо в запросе, чтобы задать формат</li>
<li><strong>Нарезка по сессиям (session-level)</strong> – 7 реплик в одном фрагменте вместо 1 (скользящее окно, перекрытие 3)</li>
<li><strong>Гибридный поиск</strong> – BM25 + векторный, объединённые по RRF (взвешенная сумма обратных рангов, dense 0.7 / BM25 0.3)</li>
<li><strong>Переранжирование по ключевым словам</strong> – пересчёт оценок по пересечению слов вопроса и фрагмента</li>
</ol>
<h3 id="результаты-1">Результаты</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="cl">                       v1          v2a         v2          v2.1
</span></span><span class="line"><span class="cl">                     базовый     перебор      полная      баланс
</span></span><span class="line"><span class="cl">конфигурация:        dense       hybrid       hybrid      hybrid
</span></span><span class="line"><span class="cl">                     по репл.    по репл.     по сесс.    по сесс.
</span></span><span class="line"><span class="cl">                     без прим.   few-shot     few-shot    few-shot
</span></span><span class="line"><span class="cl">────────────────────────────────────────────────────────────────────
</span></span><span class="line"><span class="cl">Cat 4 (single-hop)   55.0%       57.3%        63.5%  ★    60.7%
</span></span><span class="line"><span class="cl">Cat 1 (multi-hop)    34.3%       34.0%        39.4%  ★    34.5%
</span></span><span class="line"><span class="cl">Cat 2 (temporal)     39.5%       40.5%  ★     30.8%       31.2%
</span></span><span class="line"><span class="cl">Cat 3 (open-domain)  19.4%       21.3%  ★     20.4%       17.3%
</span></span><span class="line"><span class="cl">Cat 5 (adversarial)  76.9%       70.0%        72.2%       77.8%  ★
</span></span><span class="line"><span class="cl">────────────────────────────────────────────────────────────────────
</span></span><span class="line"><span class="cl">OVERALL              52.7%       52.4%        54.6%  ★    54.0%
</span></span><span class="line"><span class="cl">RETRIEVAL RECALL     66.7%       68.2%        82.6%  ★    82.6%
</span></span></code></pre></div><p>Лучший общий результат: <strong>54.6%</strong> (+1.9%). Полнота поиска: <strong>82.6%</strong> (+15.9%).</p>
<h3 id="что-сработало">Что сработало</h3>
<p><strong>Нарезка по сессиям</strong> – единственное улучшение с заметным эффектом. 7 реплик в одном фрагменте дают модели контекст разговора вместо разрозненных фраз. Полнота поиска прошла путь 66.7% → 82.6% (+15.9 п.п.), и почти весь прирост – заслуга именно сессионной нарезки (+14.4 п.п., с 68.2% до 82.6%); BM25 добавил лишь +1.5 п.п. Число фрагментов при этом упало с 5882 до 1363 – меньше шума в выдаче.</p>
<p>Фактические категории подросли: single-hop +8.5 п.п., multi-hop +5.1 п.п. (путь от базового v1 к лучшей конфигурации).</p>
<p><strong>Гибридный поиск</strong> – BM25 в одиночку дал всего +1.5 п.п. полноты. Имена и даты ищутся лучше, но на общий F1 это почти не влияет: BM25 помогает с точными совпадениями, но решающего эффекта не даёт.</p>
<h3 id="что-сломалось">Что сломалось</h3>
<p><strong>Категория temporal</strong> упала с 39.5% до 30.8% (−8.7 п.п.). Сессионные фрагменты склеивают реплики с разными отсылками ко времени. Модель видит &ldquo;вчера&rdquo;, &ldquo;в прошлом году&rdquo;, &ldquo;15 марта&rdquo; в одном блоке и не различает, какая дата относится к какому событию.</p>
<p>Пример: во фрагменте есть &ldquo;I painted it last year&rdquo; и &ldquo;[Session: 8 May 2023]&rdquo;. Модель отвечает &ldquo;в прошлом году&rdquo; вместо &ldquo;2022&rdquo;. Дата лежит в метаданных, но модель не вытаскивает её в ответ.</p>
<p><strong>Adversarial</strong> просел из-за промпта с примерами (−6.9 п.п.). Пять примеров с фактическими ответами и только один с &ldquo;информации нет&rdquo; → модель предпочитает отвечать, а не воздерживаться. Баланс 3:3 восстановил adversarial (77.8%), но ценой фактических категорий – обычный компромисс между двумя типами вопросов.</p>
<h3 id="архитектурный-потолок">Архитектурный потолок</h3>
<p>Полнота поиска выросла на 15.9 п.п. (с 66.7% до 82.6%), а F1 – лишь на 1.9% (с 52.7% до 54.6%). Между &ldquo;нашёл фрагмент&rdquo; и &ldquo;правильно ответил&rdquo; лежит разрыв на этапе генерации, и улучшением поиска его не закрыть.</p>
<p>Mem0 (92.5%) построен принципиально иначе: он не хранит сырые реплики, а на этапе записи извлекает из них структурированные факты через GPT-4o. Поиск идёт по графу сущностей, а не по косинусной близости. Это другой класс системы.</p>
<h2 id="сравнение-двух-бенчмарков">Сравнение двух бенчмарков</h2>
<table>
	<thead>
			<tr>
					<th></th>
					<th>Самодельный (v4)</th>
					<th>LoCoMo v1</th>
					<th>LoCoMo v2 (лучший)</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>Что проверяет</td>
					<td>Только поиск</td>
					<td>Весь путь</td>
					<td>Весь путь</td>
			</tr>
			<tr>
					<td>Метрика</td>
					<td>recall@5</td>
					<td>F1 по словам</td>
					<td>F1 по словам</td>
			</tr>
			<tr>
					<td>Результат</td>
					<td>98%</td>
					<td>52.7%</td>
					<td>54.6%</td>
			</tr>
			<tr>
					<td>Полнота поиска</td>
					<td>–</td>
					<td>66.7%</td>
					<td>82.6%</td>
			</tr>
			<tr>
					<td>Вопросы</td>
					<td>Свои</td>
					<td>Чужие</td>
					<td>Чужие</td>
			</tr>
			<tr>
					<td>Честность</td>
					<td>Замкнутый цикл</td>
					<td>Независимая</td>
					<td>Независимая</td>
			</tr>
	</tbody>
</table>
<p>98% полноты поиска не означает 98% качества ответов. 82.6% полноты не означает 82.6% правильных ответов. Поиск – необходимое, но не достаточное условие.</p>
<h2 id="выводы">Выводы</h2>
<ol>
<li>
<p><strong>Меряйте на реальных данных И на чужих бенчмарках</strong>. 20% → 98% на своих данных. 52.7% → 54.6% на LoCoMo. Первая цифра показывает прогресс поиска, вторая – реальное качество ответов.</p>
</li>
<li>
<p><strong>Тихие ошибки опаснее явных</strong>. Несовпадение размерности эмбеддингов и неверные реквизиты базы дали 0% полноты, но ни одной ошибки в логах. Поэтому предстартовые проверки обязательны.</p>
</li>
<li>
<p><strong>Один векторный поиск задачу не закрывает</strong>. BM25 добавил +1.5 п.п. полноты – помогает с именами и датами, но проблему не решает.</p>
</li>
<li>
<p><strong>Как нарезать данные важнее, чем каким алгоритмом искать</strong>. Нарезка по сессиям (+14.4 п.п. полноты) дала почти в 10 раз больше, чем BM25 (+1.5 п.п.).</p>
</li>
<li>
<p><strong>Вопросы-ловушки против фактических – игра с нулевой суммой</strong>. Усиливаешь &ldquo;не знаю&rdquo; (adversarial) – слабеют фактические ответы, и наоборот. Промпт с примерами в пропорции 5:1 теряет 6.9 п.п. на adversarial. В пропорции 3:3 – теряет 2.8 п.п. на single-hop. Оптимум зависит от задачи.</p>
</li>
<li>
<p><strong>Разрыв на этапе генерации реален</strong>. Полнота поиска 66.7% → 82.6% (+15.9 п.п.). Качество ответов 52.7% → 54.6% (+1.9 п.п.). Поиск улучшился в 8 раз сильнее, чем ответы. Для следующего скачка нужен не лучший поиск, а лучшая генерация: более сильная модель или извлечение структурированной памяти.</p>
</li>
<li>
<p><strong>Тест может загрязнить собственную базу</strong>. События самого теста, попавшие в боевую базу, замыкают петлю: система начинает отвечать по своим же прежним результатам. Отличить реальное улучшение от случайного помогает перебор конфигураций.</p>
</li>
</ol>
<hr>
<p>Следующий – про преобразование запроса (query transformation): HyDE и RAG-Fusion. Как переписать запрос <em>до</em> поиска, чтобы найти то, что прямое совпадение не находит, – и почему сгенерированный &ldquo;гипотетический ответ&rdquo; ищет лучше, чем сам вопрос.</p>
]]></content:encoded>
    </item>
  </channel>
</rss>
