チャンク戦略が重要な理由

RAGシステムにおいて、ドキュメントチャンキング(分割)は、長いドキュメントをベクトル検索に適した小さな断片に分割するプロセスです。この一見単純なステップは、検索品質と最終的な回答の正確性に直接影響します。チャンクが大きすぎる場合(例:2000トークン)、取得された断片には無関係な情報が多すぎて、重要なコンテンツの重みが薄まる可能性があります。小さすぎる場合(例:100トークン)、コンテキストが失われ、取得された断片の意味が不完全になる可能性があります。さらに重要なのは、チャンク戦略によって、取得された断片がユーザーの質問と効果的な意味的マッチングを形成できるかどうかも決まることです。

多くのRAGプロジェクトが失敗する理由は、モデルが不十分であるからでも、ベクトルデータベースの選択を誤ったからでもなく、チャンク戦略が特定のシナリオに合わせて最適化されていないからです。法的契約書用のRAGシステムに必要なチャンク戦略は、技術文書用のRAGシステムとは完全に異なります。この記事では、4つの主要なチャンク戦略の原理、長所と短所、適用シナリオについて詳しく説明します。

戦略1:固定サイズチャンキング

固定サイズチャンキングは、最もシンプルで直接的な方法です。ドキュメントを固定トークン数で分割します。例えば、各チャンクは512トークンで、スライディングウィンドウで128トークンをオーバーラップさせます。この方法の利点は、実装が簡単で、パフォーマンスが予測可能なことです。欠点は、意味的な境界を完全に無視し、文の途中で切断される可能性があり、各チャンクの意味が不完全になることです。

実装時には、chunk_size(チャンクサイズ)とchunk_overlap(オーバーラップサイズ)という2つの重要なパラメータに注意する必要があります。オーバーラップは、重要な情報が2つのチャンクの境界に正確に落ちるのを防ぐために存在します。一般的には、chunk_sizeは256〜1024の間、オーバーラップはchunk_sizeの10%〜25%が推奨されます。中国語のドキュメントの場合、chunk_size=512(約800〜1000文字)、オーバーラップ=100が推奨されます。

from openai import OpenAI

client = OpenAI(api_key="your-deepseek-api-key", base_url="https://api.deepseek.com")

class FixedSizeChunker:
    def __init__(self, chunk_size=512, overlap=100):
        self.chunk_size = chunk_size
        self.overlap = overlap

    def chunk_text(self, text):
        """文字数による単純なチャンキング"""
        chunks = []
        start = 0
        while start < len(text):
            end = min(start + self.chunk_size, len(text))
            chunk = text[start:end]
            chunks.append({
                "text": chunk,
                "index": len(chunks),
                "start_char": start,
                "end_char": end
            })
            start = end - self.overlap
        return chunks

    def chunk_with_metadata(self, text, source="", title=""):
        """メタデータ付きチャンキング"""
        chunks = self.chunk_text(text)
        for c in chunks:
            c["source"] = source
            c["title"] = title
            c["char_count"] = len(c["text"])
        return chunks

chunker = FixedSizeChunker(chunk_size=800, overlap=120)
text = "DeepSeekは深度求索公司が開発した大規模言語モデルです..." * 50
chunks = chunker.chunk_with_metadata(text, source="deepseek_intro.md", title="DeepSeek紹介")
print(f"{len(chunks)}個のドキュメントチャンクを生成しました")

戦略2:セマンティックチャンキング

セマンティックチャンキングは、固定長で分割するのではなく、テキストの意味的な境界(段落、章、トピックの切り替わり点など)に基づいて分割位置を決定します。この方法で生成される各チャンクは意味がより完全であり、検索品質は通常、固定サイズチャンキングよりも優れています。ただし、実装はより複雑で、意味的な境界を判断するためのモデルが必要です。

セマンティックチャンキングの核心は、テキスト内の「自然な区切り」を識別することです:段落区切り(二重改行)、見出しマーカー(Markdownの#)、トピックの移行文(例:「一方」「次に説明する」)、および埋め込み類似度によって検出される意味的ドリフトポイントです。実用的な戦略としては、最初に段落や章を第一レベルの分割として使用し、段落がまだしきい値を超える場合は、より細かい方法でさらに分割します。

import numpy as np

class SemanticChunker:
    def __init__(self, max_chunk_size=1000, similarity_threshold=0.7):
        self.max_chunk_size = max_chunk_size
        self.similarity_threshold = similarity_threshold

    def get_embedding(self, text):
        """DeepSeekを使用してテキストの埋め込みを取得"""
        response = client.embeddings.create(
            model="deepseek-chat",
            input=text[:8000]
        )
        return np.array(response.data[0].embedding)

    def chunk_by_paragraphs(self, text):
        """最初に段落で分割"""
        paragraphs = [p.strip() for p in text.split("\n\n") if p.strip()]
        return paragraphs

    def merge_similar(self, paragraphs):
        """意味的に類似した段落を結合"""
        if len(paragraphs) <= 1:
            return paragraphs
        chunks = []
        current = paragraphs[0]
        for i in range(1, len(paragraphs)):
            combined = current + "\n\n" + paragraphs[i]
            if len(combined) <= self.max_chunk_size:
    current = combined
            else:
                chunks.append(current)
                current = paragraphs[i]
        chunks.append(current)
        return chunks

chunker = SemanticChunker(max_chunk_size=1000)
paras = chunker.chunk_by_paragraphs(long_document)
merged = chunker.merge_similar(paras)
print(f"{len(paras)} パラグラフから {len(merged)} のセマンティックチャンクにマージしました")

re>

戦略3:再帰的チャンキング

再帰的チャンキングは妥協案です。まず粗い粒度の区切り文字(例:二重改行)で分割を試み、それでもチャンクが大きすぎる場合は、より細かい粒度の区切り文字(例:単一改行、句点、カンマ)でさらに分割します。この段階的な細分化は、意味的な境界を考慮しつつ、チャンクサイズを制御可能な範囲に保ちます。LangChainのRecursiveCharacterTextSplitterは、この戦略の代表的な実装です。

class RecursiveChunker:
    def __init__(self, chunk_size=512, overlap=50):
        self.chunk_size = chunk_size
        self.overlap = overlap
        self.separators = ["\n\n", "\n", "。", ".", ";", ";", ",", ",", " ", ""]

    def split(self, text):
        """再帰的分割"""
        return self._recursive_split(text, self.separators)

    def _recursive_split(self, text, separators):
        if len(text) <= self.chunk_size:
            return [text] if text.strip() else []
        if not separators:
            # 最終手段:強制分割
            return [text[i:i+self.chunk_size] for i in range(0, len(text), self.chunk_size-self.overlap)]
        sep = separators[0]
        remaining = separators[1:]
        parts = text.split(sep)
        chunks = []
        current = ""
        for part in parts:
            if len(current) + len(part) + len(sep) <= self.chunk_size:
                current = (current + sep + part) if current else part
            else:
                if current:
                    chunks.extend(self._recursive_split(current, remaining))
                current = part
        if current:
            chunks.extend(self._recursive_split(current, remaining))
        return chunks

rchunker = RecursiveChunker(chunk_size=512, overlap=50)
chunks = rchunker.split(long_chinese_text)
print(f"再帰的チャンキング結果:{len(chunks)} チャンク、平均長 {sum(len(c) for c in chunks)//len(chunks)} 文字")

戦略4:文単位のチャンキング

文単位のチャンキングは、各チャンクの境界が文の末尾に正確に一致するようにし、文の途中で切れないようにします。この戦略は中国語にとって特に重要です。中国語の文の境界は英語ほど明確ではありません(英語では大文字が文の開始マーカーとして機能します)。句点、疑問符、感嘆符などの句読点を使用して文の境界を識別する必要があります。spaCyやjiebaなどのNLPツールと組み合わせることで、より正確な文境界検出が可能になります。

文単位のチャンキングは通常、他の戦略の補完として使用されます。固定サイズまたは再帰的チャンキングに基づいて、分割ポイントが文の末尾に来るようにします。利点は、各チャンクが1つ以上の完全な文の組み合わせとなり、意味がより自己完結的で、検索結果の可読性が向上することです。

チャンキング戦略の選択ガイド

技術文書/APIドキュメント:固定サイズのチャンキング(512-1024トークン)をお勧めします。このような文書は構造が明確で、コードとテキストが交互に現れ、意味的な境界が明確ではありません。法的契約書/ポリシー文書:意味的チャンキングをお勧めします。条項と段落の間には明確な意味的境界があり、条項ごとに分割することで意味の完全性が保証されます。ニュース/ブログ記事:再帰的または文単位のチャンキングをお勧めします。記事の構造は多様であり、意味の完全性とチャンクサイズのバランスが必要です。会話ログ:ターンごとのチャンキングをお勧めします。各ターンの対話を1つのチャンクとし、対話の相互作用を保持します。混合ドキュメントセット:適応的チャンキングをお勧めします。まずルールでドキュメントタイプを検出し、次に対応するチャンキング戦略を選択します。

銀の弾丸はありません。最適なチャンキング戦略は実験によって決定する必要があります。各候補戦略について検索品質(例:Recall@k、MRR)を評価し、データセットで最も良いパフォーマンスを示すものを選択することをお勧めします。また、チャンキング戦略は固定ではなく、ドキュメントセットの更新や追加に伴い、定期的に再評価して調整することが必要です。

実践における一般的な誤解

誤解1:チャンクが大きいほど良い。多くの開発者は「大きなチャンクはより多くのコンテキストを含み、検索品質が高い」と考えています。しかし、実際のテストでは、chunk_sizeを512から2048に増やすと、検索関連性は通常最初に上昇し、その後低下し、最適な範囲は512-1024です。大きすぎるチャンクはベクトル表現が「平均化」されすぎて、細かい意味情報が失われます。誤解2:オーバーラップが多いほど良い。オーバーラップは境界情報の損失を減らしますが、過度のオーバーラップ(>30%)はストレージコストと検索ノイズを大幅に増加させます。同じ情報が複数のチャンクに現れ、検索時に非常に類似した複数のチャンクが返される可能性があります。誤解3:チャンキング戦略を一度設定すれば完了。ドキュメントセットの変化(新しいドキュメントタイプ、スタイルの変化)に伴い、元の戦略が最適でなくなる可能性があります。四半期ごと、またはドキュメントセットの大幅な更新後に、チャンキング戦略の効果を再評価することをお勧めします。

高度:コンテンツベースの動的チャンキング

多様なドキュメントタイプがあるシナリオでは、固定戦略はすべてをカバーすることは困難です。高度なアプローチは動的チャンキングです。まず小さなモデルでドキュメント構造を分析し(見出しレベル、テーブル境界、コードブロック範囲などを検出)、次にドキュメントの局所的な特徴に基づいてチャンク粒度を動的に選択します。例えば、コードブロックは完全に保持し(分割しない)、長い段落は文の境界で分割し、テーブルは全体を保持します。この方法は実装の複雑さが高いですが、文書品質の要求が非常に高いシナリオ(例:法的文書検索)では大幅な品質向上をもたらします。まず固定戦略から始め、十分なフィードバックデータを蓄積した後、徐々に動的戦略へ移行することをお勧めします。

このスキルチェーンを自分で操作してみませんか?

スキルチェーンで開く →