RAGの精度を決める「チャンク分割」 — 食べすぎも食べなさすぎもダメ
RAG(Retrieval-Augmented Generation)を構築したとき、期待通りの回答が返ってこない。原因の大部分は、検索ではなくチャンク分割にあります。
EmbeddingモデルやLLMを高精度なものに変えても、チャンク分割が不適切だと「正しい文書を取得しても、必要な部分が含まれていない」という事態になります。
本記事では、チャンク分割の基本考え方から実装パターンまでを解説します。
チャンク分割とは何か
RAGの処理フローは以下の通りです:
- 文書を小さな断片(チャンク)に分割する
- 各チャンクをEmbeddingでベクトル化する
- ユーザーの質問もベクトル化し、ベクトルDBから類似チャンクを検索する
- 検索したチャンクをLLMのコンテキストとして渡し、回答を生成する
このステップ1がチャンク分割です。RAGの精度は、この分割方法で8割決まると言っても過言ではありません。
なぜ分割が必要なのか
LLMにはコンテキスト長の制限があります。数万トークンを超える文書をそのまま渡すと、コストが跳ね上がり、精度も低下します(Lost in the Middle問題 — コンテキストの中央にある情報が無視されやすくなる現象)。
だから文書を小さく分割し、関連する部分だけをLLMに渡す。これがRAGの基本戦略です。
しかし「小さく」にも程があります。
チャンクサイズのジレンマ
大きすぎるチャンクの問題
1チャンクに3000文字詰め込んだ場合:
- 1つのチャンクに複数トピックが混在し、ベクトル検索の精度が下がる
- 「売上」で検索したのに、人事制度の段落も入ってきてノイズになる
- コストが無駄に増える
小さすぎるチャンクの問題
1チャンクを50文字にした場合:
- 文脈が切断され、「それ」が何を指すか分からない
- 「売上は前年比120%でした」だけ取得しても、いつの売上か分からない
- LLMが正しい回答を組み立てられない
目安
実務での経験則:
| チャンクサイズ | 適している文書 |
|---|---|
| 200〜400文字 | FAQ、短文ベースの知識ベース |
| 500〜1000文字 | マニュアル、技術文書 |
| 1000〜2000文字 | 長文レポート、論文 |
これが正解ではなく、文書の性質によって調整が必要です。
4つの分割戦略
1. 固定長分割(最もシンプル)
指定した文字数で機械的に切る方法です。
def fixed_chunk(text, chunk_size=500, overlap=50):
chunks = []
start = 0
while start < len(text):
end = start + chunk_size
chunks.append(text[start:end])
start = end - overlap # 重複を取る
return chunks
メリット: 実装が最も簡単
デメリット: 文の途中で切断されることがある
ポイント: overlapパラメータで隣接チャンクと重複を持たせることで、文脈切断を緩和できます。50〜100文字のオーバーラップがよく使われます。
2. 文単位分割
文の境界で切る方法です。日本語なら句点「。」、英語ならピリオド「.」を区切りにします。
import re
def sentence_chunk(text, max_chars=800):
sentences = re.split(r'(?<=[。])', text)
sentences = [s for s in sentences if s.strip()]
chunks = []
current = ""
for sentence in sentences:
if len(current) + len(sentence) <= max_chars:
current += sentence
else:
if current:
chunks.append(current)
current = sentence
if current:
chunks.append(current)
return chunks
メリット: 文の途中で切断されない
デメリット: 文の長さにばらつきがあるとチャンクサイズが不均一になる
3. 段落単位分割
改行(段落)を区切りにする方法です。技術文書やマニュアルに適しています。
def paragraph_chunk(text, max_chars=1500):
paragraphs = text.split('\n\n')
chunks = []
current = ""
for para in paragraphs:
if len(current) + len(para) <= max_chars:
current += "\n\n" + para if current else para
else:
if current:
chunks.append(current)
current = para
if current:
chunks.append(current)
return chunks
メリット: 1つのチャンクに1つの話題が収まりやすい
デメリット: 段落が長すぎる場合はさらに分割が必要
4. 意味ベース分割(高度)
LLMを使って意味的なまとまりで分割する方法です。
プロンプト例:
以下のテキストを、意味的にまとまった単位に分割してください。
各チャンクは200〜500文字程度にし、一つのトピックが完結するようにしてください。
テキスト: {text}
メリット: 最も精度が高い
デメリット: コストが高い、処理時間が長い
実務では、まず段落単位分割で試し、精度が足りない部分だけ意味ベースに切り替えるハイブリッド運用が現実的です。
実務で使えるツール
LangChainのRecursiveCharacterTextSplitter
from langchain.text_splitter import RecursiveCharacterTextSplitter
splitter = RecursiveCharacterTextSplitter(
chunk_size=800,
chunk_overlap=100,
separators=["\n\n", "\n", "。", "、", " ", ""]
)
chunks = splitter.split_text(text)
separatorsを上から順に試し、最適な位置で分割してくれます。段落→改行→句点→読点の順で階層的に分割するため、文の途中で切れるのを最小限に抑えられます。
LlamaIndexのSentenceSplitter
from llama_index.core.node_parser import SentenceSplitter
splitter = SentenceSplitter(
chunk_size=800,
chunk_overlap=100
)
nodes = splitter.get_nodes_from_documents(documents)
チャンク分割でよくある失敗
失敗1: 表が分割される
表を行で跨いで分割されると、列の意味が分からなくなります。表を含む文書では、表を1つのチャンクとして扱う前処理が必要です。
失敗2: 見出しが切り離される
「## 売上データ」という見出しと、その本文が別チャンクになると、検索時に本文だけ取得されて「何のデータか分からない」状態になります。見出しを各チャンクの先頭に付与するメタデータ付与が有効です。
def add_heading_context(chunks, heading):
return [f"[{heading}]\n{chunk}" for chunk in chunks]
失敗3: コードブロックが分割される
プログラムのコードブロックの途中で分割されると、構文が不完全になります。コードブロックを検出して1つのチャンクにまとめる処理を入れましょう。
チャンク分割後の品質チェック
分割が終わったら、以下を確認しましょう:
- チャンク数: 文書全体が何チャンクになったか
- チャンク長の分布: 極端に短い・長いチャンクがないか
- サンプリング確認: ランダムに5〜10チャンクを読んで、意味が完結しているか
この3点をチェックするだけで、大半の分割問題に気づけます。
まとめ
チャンク分割は地味ですが、RAGの精度を左右する最重要工程です。
- まず段落単位分割で始める
- 精度が足りなければRecursiveCharacterTextSplitterに切り替える
- 表・見出し・コードブロックには前処理を入れる
- オーバーラップ100文字程度を基本にする
RAGの構築において、Embeddingモデル選びよりもチャンク分割の方が効果が大きいことが多いです。ぜひここに時間をかけてください。
本記事の内容は、Kindle本 「RAG構築入門 — 自社データでAIを賢くする」 でより深く解説しています。チャンク分割だけでなく、Embedding選定、ベクトルDB比較、ハイブリッド検索、評価手法まで、RAG構築の全工程を実装付きで網羅しています。
Amazonで購入する(¥1,650・Kindle / Kindle Unlimitedなら無料)
著者: 葉山悠希(はやま ゆうき)