RAG отвечает по сегодняшним правилам на вопрос о прошлом: фильтруем дату действия

Клиент спрашивает ассистента: «Какой срок возврата был у тарифа Start 15 марта?» В базе есть два положения: до апреля — 14 дней, с апреля — 30. Поиск находит оба, а модель выбирает более свежий документ. Для вопроса о текущих условиях это разумно, для обращения о мартовской покупке получается неверный ответ.

Дата вопроса здесь является условием поиска. Её нужно сохранить отдельно от слов запроса и применить до передачи источников модели. Тогда мартовский вопрос получит мартовские условия, даже если новая редакция написана подробнее и лучше совпадает с запросом по смыслу.

Разведите дату загрузки и дату действия

У каждой редакции храните valid_from и valid_to: с какого дня правило действует и с какого уже перестаёт. Дата загрузки файла отвечает на другой вопрос — когда система получила документ. Файл могли загрузить в мае, хотя он описывает условия с января. Сортировка по времени загрузки не восстановит нужный исторический срез.

Удобное правило границ — включать начало и исключать конец: valid_from ≤ дата вопроса < valid_to. Тогда редакции до 1 апреля и с 1 апреля не пересекаются в полночь. Открытый конец означает, что следующая редакция пока неизвестна. Для условий, меняющихся внутри дня, понадобятся моменты времени и согласованный часовой пояс; календарную дату нельзя бездумно сравнивать с timestamp события.

Получите дату до поиска

Сначала извлеките из вопроса продукт и дату. Вход для модели: «Какой срок возврата был у Start 15 марта 2026 года?» Инструкция: «Верни product и as_of в формате YYYY-MM-DD. Не подставляй сегодняшнюю дату, если пользователь спрашивает о прошлом без достаточных данных. Если год или продукт неоднозначны, верни needs_clarification и вопрос пользователю». Для этого входа ожидаются Start и 2026-03-15.

Результат извлечения проверьте обычным кодом: дата существует, продукт известен, у пользователя есть доступ к его условиям. Если человек написал только «в марте», уточнение года полезнее уверенного выбора случайной редакции. После этого фильтр исторического периода должен пройти через все поисковые шаги, включая дополнительные запросы и получение полных документов.

Проверьте границу на работающем примере

Скрипт запускается стандартным Python и создаёт базу в памяти. SQL отбирает действующую редакцию для одного продукта. Это точный отбор источника перед смысловым поиском; вызов LLM ему не нужен. Текст выбранной строки затем можно передать модели для понятного ответа с цитатой.

import sqlite3
from datetime import date

db = sqlite3.connect(":memory:")
db.executescript("""
CREATE TABLE policies(id TEXT PRIMARY KEY, product TEXT,
                      valid_from TEXT, valid_to TEXT, body TEXT);
INSERT INTO policies VALUES
('return-v1', 'Start', '2026-01-01', '2026-04-01', 'Возврат: 14 дней'),
('return-v2', 'Start', '2026-04-01', NULL, 'Возврат: 30 дней');
""")

def source_for(product, as_of):
    day = date.fromisoformat(as_of).isoformat()
    rows = db.execute("""
        SELECT id, body FROM policies
        WHERE product=? AND valid_from<=?
          AND (valid_to IS NULL OR ?<valid_to)
    """, (product, day, day)).fetchall()
    if len(rows) != 1:
        raise ValueError(f"Expected one policy, found {len(rows)}")
    return rows[0]

assert source_for("Start", "2026-03-15")[0] == "return-v1"
assert source_for("Start", "2026-03-31")[0] == "return-v1"
assert source_for("Start", "2026-04-01")[0] == "return-v2"
try:
    source_for("Start", "2025-12-31")
except ValueError as exc:
    assert "found 0" in str(exc)
else:
    raise AssertionError("Missing period was accepted")

db.execute("INSERT INTO policies VALUES (?, ?, ?, ?, ?)",
           ("overlap", "Start", "2026-03-01", "2026-05-01", "Спорная редакция"))
try:
    source_for("Start", "2026-03-15")
except ValueError as exc:
    assert "found 2" in str(exc)
else:
    raise AssertionError("Overlap was accepted")
print("March=v1, April boundary=v2, gap and overlap rejected")

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

Сохраните исторический смысл в ответе

Передайте генератору вопрос, as_of, ID и текст выбранной редакции. Например: «Ответь о правилах на as_of=2026-03-15. Источник return-v1: Возврат: 14 дней. Укажи дату применимости и источник. Не заменяй правило текущими условиями». Полезный ответ начинается с «На 15 марта 2026 года для Start…», чтобы читатель видел, какой срез использован.

Держите отдельные сценарии для текущего вопроса, вопроса на конкретную дату и сравнения периодов. В сравнении нужны обе редакции, каждая со своей границей. Если исторические сведения исправляют задним числом, дополнительно решите, нужен ли ответ «как правило действовало» или «какие сведения система знала на тот момент». Для второго сценария одного valid_from недостаточно: понадобится история регистрации изменений.

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

Не перепутайте дату события с датой обращения

Пользователь может написать сегодня: «Покупка была 15 марта; что положено при возврате 10 апреля?» В такой задаче нельзя выбрать единственную дату из текста и забыть остальные. Сначала определите, к какому событию привязано конкретное правило: покупке, доставке или обращению. Сохраните поля purchase_date и request_date раздельно. Правило может зависеть от одного из них либо от обоих.

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

Подключите исторический срез к RAG

После отбора редакций потребуется собрать поиск по их содержимому и проверить, что модель сохраняет условия в ответе. В платном курсе LangChain вы создадите RAG по PDF с цитатами, гибридным поиском и реранжированием, добавите оценку качества и API приложения. На этой основе можно проверять реальные исторические вопросы целиком, от входной даты до ссылки на источник. Откройте программу платного курса «LangChain 1.0.x: c нуля до продакшн LLM-приложений».

Продолжить на практике

  • LangChain 1.0.x: c нуля до продакшн LLM-приложений ↗

    Практический курс по LangChain. Соберёте продакшн LLM-бота: RAG по PDF с цитатами, веб-поиск и трекинг заказов, многошаговый агент для анализа CSV и финальное веб-приложение (UI + API). Плюс профессиональные блоки: PromptOps и версионирование промптов, метрики качества RAG, гибридный поиск с реранком, observability/стоимость, надёжность (ретраи, таймауты), guardrails и отдельный модуль по LangGraph для устойчивых workflow. P.S. Полностью обновили материалы на актуальную версию LangChain