古くならないRAG:新鮮なウェブデータとリフレッシュループ

RAGシステムは最後にクロールした時点での新鮮さに依存します。取得の計算は簡単ですが、回答が古くなる前に各ソースを再クロールし、再チャンクし、再埋め込みするリフレッシュループが難しい部分です。

取得拡張生成は現在、企業AIシステムの約半数に導入されており、最新の調査では採用率が51%に跳ね上がり、1年前の31%から増加しています。しかし、ほとんどのRAGプロジェクトは同じ失敗モードを共有しています:一度だけデータを取り込んで、その後世界が変わってしまったのです。取得の計算(クエリを埋め込み、ベクトルを検索し、トップチャンクをプロンプトに詰め込む)は簡単で、よく文書化されています。難しいのは、アシスタントが信頼できるか、または自信満々に間違っているかを決定する部分である新鮮さです。これは、回答が古くなる前に各ソースを再クロールし、再チャンクし、再埋め込みするパイプラインです。このガイドは、そのリフレッシュループを構築する方法について、markdown-firstの取り込みとソースごとのTTLを中心に説明します。

古くなった取得は取得しないよりも悪い

RAGの全体的な約束は、モデルが凍結されたトレーニングデータではなく、取得された証拠から回答することです。新鮮さを損なうと、約束も損なわれます:サポートボットが前四半期の返品ポリシーを引用し、価格アシスタントが廃止された数字を引用し、研究コパイロットが回答を変えた更新を見逃します。さらに悪いことに、モデルは取得されたチャンクが権威あるように見えるため、完全な自信を持ってそれを述べます。古くなったインデックスは大声で失敗するのではなく、静かにそしてもっともらしく失敗します。これは最も危険な種類です。コーパスを最新に保つことは、必須ではなく、基盤を築くか、余計なステップで幻覚を起こすかの違いです。

生のHTMLではなくmarkdownを取り込む

すべての下流の品質は取り込み時に決まります。生のHTMLは取得にとって災難です—ナビゲーション、クッキーバナー、フッター、スクリプトタグがノイズチャンクとなり、類似性検索を汚染し、コンテキストトークンを浪費します。クリーンなアプローチはmarkdown-firstです:各ページをクロールし、主要なコンテンツのみを構造化されたmarkdownとして抽出し、何もチャンク化される前に定型文を破棄します。当社のScraper APIはmarkdownを直接返し、JavaScriptをオンデマンドでレンダリングし、IPを回転させるので、ブロックされたページがコーパスに穴を開けることはありません—これは、ほとんどのRAGスタックが頼りにしている脆弱なPuppeteer-and-BeautifulSoup取り込みレイヤーを置き換えます。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日以内に取得されたチャンクのみを価格の質問に要求できるようにします。

markdownクロールからチャンクと埋め込み、インデックス作成とTTL駆動の再クロールまでのRAG新鮮さリフレッシュループの図
RAGを正直に保つループ:markdown-firstクロール、小さなメタデータ豊富なチャンク、そしてドリフト前に再埋め込みをトリガーするTTL。

ソースごとのTTL:新鮮さの中心

ほとんどのチームが犯す間違いは、すべてを一つのスケジュールでリフレッシュすることです—価格には遅すぎ、参考資料には無駄な夜間の完全再クロールです。異なるソースは異なる速度で古くなるので、それぞれに実際にどれだけ速くコンテンツが動くかに合ったタイム・トゥ・リブを与えます。ライブ価格と在庫は数分のTTLが必要かもしれません;ニュース、リスティング、フォーラムスレッドは1時間;ランキングとカタログは1日;ドキュメント、ポリシー、参考資料は1週間以上。各チャンクにそのソースの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を基盤とする全体像については、LLMsに新鮮なウェブデータを供給するガイドを参照し、単一のサイトを引用可能な知識ベースに変えるには、サポートボット知識ベースの構築を参照してください。

価格と在庫には数分、ニュースには1時間、カタログには1日、参考資料には1週間の階層化されたTTLバンド
コンテンツが実際に動く時計で各ソースをリフレッシュする—価格には数分、参考資料には1週間。

カバレッジのために取得する、類似性だけではない

クエリ時の最後のアップグレード。純粋なベクトル類似性は、製品コード、エラーストリング、固有名詞などの正確な用語を見逃します—キーワード検索がそれを捉えます。ハイブリッド取得は両方をブレンドし、その後最新性フィルターを適用して、時間に敏感なクエリが新鮮なチャンクを好むようにします。取得されたチャンクをすべての回答と共にログに記録し、モデルが実際に使用した証拠を監査できるようにします;そのトレーサビリティがRAGを説明可能にし、ユーザーが気づく前に古くなったチャンクをキャッチする方法です。LLM駆動の抽出からの技術は、取得されたページから構造化フィールドを必要とする場合にうまく組み合わさります。

よくある質問

RAGパイプラインのデータを新鮮に保つにはどうすればよいですか?

各ソースにそのコンテンツがどれだけ速く変わるかに合ったタイム・トゥ・リブを割り当て、すべてのチャンクにソースURLとタイムスタンプをタグ付けし、期限切れのソースのみを再クロールするスケジューラーを実行します。実際に変更されていないページの再埋め込みをスキップするためにコンテンツハッシュを使用し、取得時に最新性フィルタリングを有効にして、時間に敏感なクエリが新鮮なチャンクを好むようにします。

RAGのためにmarkdownをHTMLの代わりに取り込む理由は何ですか?

生のHTMLはナビゲーション、バナー、フッター、スクリプトを含んでおり、ノイズチャンクとなって類似性検索を汚染し、コンテキストトークンを浪費します。markdown-firstの取り込みは主要なコンテンツのみを抽出するので、チャンクはクリーンでトピックに沿っています。レンダリングされたmarkdownを返すスクレイピングAPIは、ほとんどのRAGスタックが頼りにしている脆弱なカスタムパーシングを取り除きます。

RAGに使用するチャンクサイズはどのくらいが良いですか?

約300〜500トークンが一般的なスイートスポットです:取得されたチャンクがしっかりと関連性を保ち、完全な考えを保持するのに十分大きいです。文や見出しの境界を尊重する再帰的なスプリッターを使用し、コンテキストが縫い目で失われないように少し重複を追加し、すべてのチャンクにソースメタデータを添付します。

RAGデータパイプラインを構築するのにプロキシが必要ですか?

オープンウェブをある程度の規模でクロールする場合、はい—サイトは一つのIPからの繰り返しリクエストをレート制限し、ブロックし、コーパスにギャップを残します。スクレイピングAPIまたは回転プロキシはクロールを信頼性のあるものに保ち、ソースが完全にリフレッシュされ、スケジュール通りに進行するようにします。これはまさにソースごとのTTLが依存するものです。

RAGは新鮮さに依存しています。クリーンなmarkdownを取り込み、メタデータを持たせた小さなチャンクを作成し、ソースごとのTTLから再埋め込みを駆動して、各ソースが独自の時計でリフレッシュされるようにします。そのループを一度構築すれば、アシスタントは先月のスナップショットではなく、今日のウェブから回答します。

Scraper APIで新鮮なRAGパイプラインを構築する