RAG, который не устаревает: свежие веб-данные и циклы обновления

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

Генерация с дополнением извлечением теперь используется примерно в половине корпоративных AI-систем — внедрение выросло до 51% в последних опросах, по сравнению с 31% год назад. Однако большинство проектов RAG имеют один и тот же режим отказа: они были заполнены один раз, а затем мир изменился. Математика извлечения (встраивание запроса, поиск векторов, вставка верхних фрагментов в подсказку) — это лёгкая, хорошо задокументированная часть. Сложная часть, та, которая решает, будет ли ваш помощник надёжным или уверенно ошибочным, — это свежесть — конвейер, который продолжает повторно обходить, разбивать и повторно встраивать каждый источник, прежде чем его ответы устареют. Это руководство о построении этого цикла обновления, с первичной загрузкой в формате markdown и TTL для каждого источника в его основе.

Устаревшее извлечение хуже, чем его отсутствие

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

Загружайте markdown, а не сырой HTML

Качество всего, что идёт дальше, определяется на этапе загрузки. Сырой HTML — это катастрофа для извлечения — навигация, баннеры с куки, футеры и теги скриптов становятся шумовыми фрагментами, которые загрязняют поиск по сходству и тратят контекстные токены. Чистый подход — это первичная загрузка в формате markdown: обходите каждую страницу и извлекайте только основное содержание в виде структурированного markdown, отбрасывая шаблонные элементы до того, как что-либо будет разбито. Наш Scraper API возвращает markdown напрямую, рендерит JavaScript по запросу и меняет IP, чтобы заблокированные страницы не оставляли пробелов в вашем корпусе — он заменяет хрупкий слой загрузки Puppeteer-and-BeautifulSoup, на который полагается большинство стэков RAG. Конечная точка web-data-for-LLMs объединяет обход, очистку и структурирование в один вызов, созданный именно для этого.

import requests

# markdown-first ingestion: clean text, JS rendered, IPs rotated
def fetch_markdown(url):
    r = requests.post(
        "https://api.quantumproxies.io/scrape",
        headers={"Authorization": f"Bearer {QP_KEY}"},
        json={"url": url, "format": "markdown", "render": True},
        timeout=60,
    )
    doc = r.json()
    return {
        "url": url,
        "markdown": doc["markdown"],       # no nav, no boilerplate
        "fetched_at": doc["fetchedAt"],     # timestamp for TTL logic
        "content_hash": doc["contentHash"], # skip re-embed if unchanged
    }

Подавайте вашему стэку RAG чистые веб-данные

Разбивайте на маленькие фрагменты, добавляйте метаданные

Как только у вас есть чистый markdown, разделите его на фрагменты примерно по 300-500 токенов — достаточно маленькие, чтобы извлечённый фрагмент был строго по теме, и достаточно большие, чтобы сохранить целостную мысль. Используйте рекурсивный разделитель (LangChain или LlamaIndex оба поставляются с ним), который уважает границы предложений и заголовков, а не разрезает посреди слова. Критический, часто пропускаемый шаг — это метаданные: каждый фрагмент должен содержать URL источника, временную метку и хэш содержимого. Эти метаданные делают свежесть возможной — извлечение может фильтровать по свежести, а ваш цикл обновления может определить, какие фрагменты принадлежат источнику, который только что изменился.

from langchain.text_splitter import RecursiveCharacterTextSplitter

splitter = RecursiveCharacterTextSplitter(
    chunk_size=1800, chunk_overlap=200,  # ~400 tokens per chunk
)

def to_chunks(doc):
    parts = splitter.split_text(doc["markdown"])
    return [{
        "text": p,
        "source": doc["url"],
        "fetched_at": doc["fetched_at"],   # drives recency filtering
        "ttl": ttl_for(doc["url"]),          # per-source refresh clock
    } for p in parts]

Встраивайте эти фрагменты с помощью актуальной модели — text-embedding-3-large, модели Cohere или открытого варианта Sentence-BERT — и храните векторы вместе с метаданными в векторной базе данных, такой как Pinecone, Weaviate, Milvus или FAISS. Настройте пороги сходства и, что важно, включите фильтрацию метаданных, чтобы запрос мог требовать, например, только фрагменты, извлечённые за последние 30 дней для вопроса о ценах.

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

TTL для каждого источника: сердце свежести

Ошибка, которую допускают большинство команд, заключается в обновлении всего по одному расписанию — ночной полный повторный обход, который одновременно слишком медленный для цен и расточительный для справочных документов. Разные источники стареют с разной скоростью, поэтому дайте каждому время жизни, соответствующее тому, как быстро его содержание на самом деле меняется. Живые цены и запасы могут нуждаться в TTL в несколько минут; новости, списки и темы форумов — в часе; рейтинги и каталоги — в дне; документация, политики и справочные материалы — в неделю или более. Пометьте каждый фрагмент TTL его источника, и планировщик будет повторно обходить только те источники, чьи часы истекли. Хэш содержимого — это ваш клапан эффективности: если повторно обойдённая страница идентична по байтам, пропустите шаг встраивания и просто сбросьте временную метку.

import time

TTL = {"prices": 300, "news": 3600, "catalog": 86400, "docs": 604800}

def needs_refresh(chunk, now=None):
    now = now or time.time()
    return (now - chunk["fetched_at"]) > chunk["ttl"]

# refresh only what has expired; re-embed only what actually changed
for src in due_sources(TTL):
    doc = fetch_markdown(src.url)
    if doc["content_hash"] != src.last_hash:
        reindex(to_chunks(doc))     # re-chunk + re-embed
    src.touch(doc["content_hash"])  # reset the clock either way

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

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

Извлекайте для охвата, а не только для сходства

Последнее улучшение на этапе запроса. Чистое векторное сходство упускает точные термины — коды продуктов, строки ошибок, собственные имена — которые ключевой поиск улавливает. Гибридное извлечение сочетает оба метода, затем применяет ваш фильтр свежести, чтобы запрос, чувствительный ко времени, предпочитал свежие фрагменты. Записывайте извлечённые фрагменты с каждым ответом, чтобы вы могли проверить, какие доказательства модель действительно использовала; эта прослеживаемость делает RAG объяснимым, и это то, как вы ловите устаревший фрагмент до того, как это сделает пользователь. Техники из извлечения на основе LLM хорошо сочетаются здесь, когда вам нужны структурированные поля из извлечённых страниц, а не проза.

Часто задаваемые вопросы

Как я могу поддерживать свежесть данных в конвейере RAG?

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

Почему для RAG следует загружать markdown вместо HTML?

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

Какой размер фрагмента я должен использовать для RAG?

Примерно 300–500 токенов — это общая золотая середина: достаточно маленькие, чтобы извлечённый фрагмент оставался строго релевантным, и достаточно большие, чтобы сохранить целостную мысль. Используйте рекурсивный разделитель, который уважает границы предложений и заголовков, добавьте небольшое перекрытие, чтобы контекст не терялся на стыках, и прикрепите метаданные источника к каждому фрагменту.

Нужны ли мне прокси для построения конвейера данных RAG?

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

RAG живёт или умирает на свежести. Загружайте чистый markdown, разбивайте на маленькие фрагменты с метаданными и управляйте повторным встраиванием с помощью TTL для каждого источника, чтобы каждый источник обновлялся по своему собственному расписанию. Постройте этот цикл один раз, и ваш помощник будет отвечать на основе сегодняшнего веба, а не снимка прошлого месяца.

Постройте свежий конвейер RAG на Scraper API