📝

LangChainのRecursiveCharacterTextSplitter初心者向けガイド:RAGシステムの心臓部を理解する

に公開

⚠️この記事は個人開発中にぶつかった疑問をAIに壁打ちし、得られた情報をAIによってまとめた記事です。

はじめに

RAGシステムを構築する際、「テキスト分割」は検索精度を大きく左右する重要な要素です。LangChainのRecursiveCharacterTextSplitterは「推奨される分割方法」とされていますが、その動作原理を正確に理解している人は意外と少ないのではないでしょうか?

この記事では、実際のコード例を交えながら、RecursiveCharacterTextSplitter内部動作を完全に理解し、最適な設定方法まで詳しく解説します。

依存環境

python = "^3.11"
langchain = "^0.3.25"

RecursiveCharacterTextSplitterとは?

RecursiveCharacterTextSplitterは、大きなテキストを検索に適したサイズのチャンクに分割するLangChainのクラスです。

主な特徴

  • 🔄 再帰的分割:複数の分割方法を段階的に適用
  • 📝 意味保持:テキストの構造と文脈を可能な限り保持
  • ⚙️ 柔軟な設定:様々なパラメータで分割方式をカスタマイズ可能

基本的な使用方法

from langchain.text_splitter import RecursiveCharacterTextSplitter

def split_text(text: str, size=1000, overlap=200):
    splitter = RecursiveCharacterTextSplitter(
        chunk_size=size,                           # チャンクの最大サイズ
        chunk_overlap=overlap,                     # 重複させる文字数
        separators=["\n\n", "\n", " ", ""],       # 分割に使用する区切り文字
        length_function=len,                       # 長さの測定方法
    )
    return splitter.split_text(text)

🔍 動作原理の解説

1. 処理の順序:separators → chunk_size

多くの人が誤解しがちな点ですが、処理は以下の順序で行われます:

誤解: chunk_sizeで切ってからseparatorsを適用
正解: separatorsで分割してからchunk_sizeをチェック

2. 再帰的分割のステップ

separators = ["\n\n", "\n", " ", ""]

ステップ1:段落区切りで分割(\n\n

元テキスト:
段落1の内容が続きます...(1500文字)

段落2の内容です。(300文字)

段落3の内容が続きます...(1200文字)

ステップ2:サイズチェック

  • 段落1: 1500文字 → ❌ オーバー(1000文字制限)
  • 段落2: 300文字 → ✅ OK
  • 段落3: 1200文字 → ❌ オーバー

ステップ3:再帰的分割
オーバーした段落を次のseparator(\n)で再分割:

段落1 → 改行で分割 → 800文字 + 700文字 → ✅ 両方OK
段落3 → 改行で分割 → 600文字 + 600文字 → ✅ 両方OK

🔗 chunk_overlapの機能

よくある誤解と正しい理解

誤解: chunk_overlapによってchunk_sizeを超える
正解: chunk_sizeは絶対制限、overlapは重複を作る

実際の動作例

chunk_size = 1000
chunk_overlap = 200

3000文字のテキストの場合:

チャンク1: 文字位置    0 ~ 1000 (1000文字)
チャンク2: 文字位置  800 ~ 1800 (1000文字)← 200文字重複
チャンク3: 文字位置 1600 ~ 2600 (1000文字)← 200文字重複
チャンク4: 文字位置 2400 ~ 3000 (600文字)

重複は必ずしも正確ではない

# 目標の重複:200文字
# 実際の重複:separatorsによって調整される

# 例:段落境界で調整
チャンク1の末尾: "...クラス名の命名規則について"
チャンク2の先頭: "クラス名の命名規則について詳しく説明します..."
# → 実際の重複:180文字(段落境界を優先)

⚡ 文字数 vs トークン数

デフォルトは文字数測定

length_function=len,  # 文字数で測定

トークン数での測定

LLMではトークン数が重要なため、以下のように変更可能:

import tiktoken

def count_tokens(text: str, model="gpt-4o-mini"):
    enc = tiktoken.encoding_for_model(model)
    return len(enc.encode(text))

splitter = RecursiveCharacterTextSplitter(
    chunk_size=1000,  # 1000トークン
    chunk_overlap=200,
    separators=["\n\n", "\n", " ", ""],
    length_function=lambda x: count_tokens(x, "gpt-4o-mini"),  # トークン数で測定
)

🎯 実践的な設定方法

日本語文書に最適化した設定

def create_japanese_splitter(chunk_size=1000, overlap=200):
    return RecursiveCharacterTextSplitter(
        chunk_size=chunk_size,
        chunk_overlap=overlap,
        separators=[
            "\n\n",  # 段落区切り(最優先)
            "\n",    # 改行
            "。",    # 日本語の句点
            "!",    # 感嘆符
            "?",    # 疑問符
            " ",     # 空白
            "",      # 文字単位(最後の手段)
        ],
        length_function=len,
    )

コード文書用の設定

from langchain.text_splitter import Language

# Python用の設定
python_splitter = RecursiveCharacterTextSplitter.from_language(
    language=Language.PYTHON,
    chunk_size=1000,
    chunk_overlap=200
)

📊 パラメータの最適化ガイド

chunk_size の選び方

# 用途別推奨値
document_qa = 1000    # 文書QA:詳細な情報が必要
summary = 2000        # 要約:長めの文脈が必要  
code_search = 500     # コード検索:関数単位での分割

chunk_overlap の選び方

# chunk_sizeの10-20%が目安
chunk_size = 1000
chunk_overlap = chunk_size * 0.2  # 200文字(20%)

# 文書の種類による調整
technical_doc = chunk_size * 0.1   # 技術文書:重複少なめ
narrative_text = chunk_size * 0.3  # 物語:文脈重視で重複多め

🚀 実際のRAGシステムでの活用

import streamlit as st
from langchain_community.vectorstores import Chroma

def process_pdf_for_rag(pdf_text: str):
    # 最適化されたスプリッター
    splitter = RecursiveCharacterTextSplitter(
        chunk_size=1000,
        chunk_overlap=200,
        separators=["\n\n", "\n", "。", " ", ""],
        length_function=len,
    )
    
    # チャンクに分割
    chunks = splitter.split_text(pdf_text)
    
    # ベクトルストアに保存
    vectorstore = Chroma.from_texts(
        texts=chunks,
        embedding=embeddings,
    )
    
    return vectorstore, chunks

# 分割結果の確認
st.write(f"📊 分割統計:")
st.write(f"- 元文書: {len(pdf_text):,}文字")
st.write(f"- チャンク数: {len(chunks)}")
st.write(f"- 平均チャンクサイズ: {sum(len(c) for c in chunks) // len(chunks)}文字")

💡 よくあるトラブルと対処法

1. チャンクが大きすぎる/小さすぎる

# 問題:チャンクサイズのばらつきが大きい
# 対処:separatorsを文書の構造に合わせて調整

# Markdown文書の場合
markdown_separators = [
    "\n## ",   # 見出し2
    "\n### ",  # 見出し3
    "\n\n",    # 段落
    "\n",      # 改行
    " ",       # 空白
    "",        # 文字
]

2. 重複が想定通りにならない

# 問題:overlapが期待値と大きく異なる
# 対処:separatorの優先順位を見直す

# 厳密な重複が必要な場合
strict_splitter = RecursiveCharacterTextSplitter(
    chunk_size=1000,
    chunk_overlap=200,
    separators=[" ", ""],  # 空白と文字のみ(構造を無視)
    length_function=len,
)

まとめ

RecursiveCharacterTextSplitterの核心:

  1. 🔄 再帰的処理: separators → chunk_size の順序で段階的分割
  2. 📏 厳密な制限: chunk_sizeは絶対に超えない
  3. 🔗 柔軟な重複: chunk_overlapは目標値、実際はseparatorsで調整
  4. 📝 意味保持: テキストの構造を尊重した分割
  5. ⚙️ カスタマイズ: 文書の特性に合わせた設定が重要

適切な設定により、RAGシステムの検索精度と回答品質を大幅に向上させることができます。ぜひ自分のプロジェクトに合わせて最適化してみてください!


参考資料

Discussion