RAGの品質上限はコーパスの品質によって決まります。いくら洗練された埋め込みと検索アルゴリズムを使っても、元のデータが洗練されていなければ結果も悪い。このプロジェクトでチャンキングパイプラインが---区切り線と短絡境界を尊重できる理由は、データが収集段階で既にそのような構造に精製されているからである。
books/
├── 0. 자체 정리/ ← 가장 고품질: 1차 수집 → AI 재분류 → 주제별 MD
│ ├── 01_음양론/
│ ├── 02_오행론/
│ ├── ... (총 20개 주제 폴더 + 부록)
│ └── 20_실전사례/
├── 1. 나의 사주명리-심화편/ ← 고품질: 전자책 추출, 장/절 구조 유지
├── 2. 사주명리 실전 100구문/ ← 고품질: 전자책 추출
└── 99. 온라인 자료 (정리 X)/ ← 2차 수집 원본: PDF/TXT, 구조 불균일
核心観察:層0はオンラインで一次収集した資料をAIを活用してカテゴリ別に精密再分類した結果物である。原本をテーマ別(陰陽論、五行論、隔国論など)に分け、マークダウン構造(---区切り線、##ヘディング)を付与してチャンキング品質を最大化した。層99は、後に行われた二次オンライン収集であり、まだ整理されていない状態である。チャンキング品質は0〜2で高く、99で比較的低い。つまり、収集→AIベースの再分類→構造化という段階を経たデータほどRAG性能が良い。
このプロジェクトでオンライン資料を収集するときに強制する規則:
1。ソース必須:
모든 파일에 `출처: URL` 또는 `https://` 포함 필수.
없으면 자동 삭제:
grep -cE '(https?://|출처:)' "$f" → 0이면 rm
ソースがない文書は信頼できないため、コーパスには含まれません。
2。 --- 区切り線でトピックを分離:
1つのTXTファイルに複数のトピックがある場合は、---で明確に区切ります。これがチャンキング段階で「主題境界を越えるチャンク」が生じないようにする核心装置である。
# 좋은 예: 주제가 명확히 분리됨
갑목(甲木)은 양의 목기운으로, 큰 나무를 상징한다...
---
을목(乙木)은 음의 목기운으로, 풀이나 덩굴을 상징한다...
3。 AI生成コンテンツの禁止:
LLMが作成した内容をコーパスに入れると幻覚が再現される。すべての文書は、人間が作成した原文(学術論文、教材、専門家文)でなければならない。
4。自動収集時のファイルサイズ制限:
自動収集エージェント(saju-research-collector)が新しいファイルを作成するときは、ファイルあたり200行以下に制限します。これは、エージェントの安定性の問題(400行+作成時に失敗)によるものであり、コーパス全体の制約ではありません。実際のコーパスには400~1,700ジュール規模のファイルも正常に含まれている。ただし、1つのファイルにあまりにも多くのトピックが混在すると、チャンキング中にトピックの汚染が発生する可能性があるため、可能であればトピックごとにファイルを分離することが望ましいです。
IT会社でサービス企画書やKnowledge BaseをRAGコーパスで構築する場合も、同じ原則が適用される。
| 原則 | 社主プロジェクト | IT会社の適用 |
|---|---|---|
| 構造化マークアップ | ---区切り線、##ヘッダ |
Confluence ヘッダ、Notion ブロック、API doc のエンドポイント別分離 |
| ソース追跡 | URL、ページ番号 | 文書ID、バージョン、作成者、最終修正日 |
| AI生成可否表示 | 禁止 | タグ付け(source: human vs source: ai-draft) |
| 冗長管理 | dedup.pyコサイン類似度 | ドキュメントバージョン管理+以前のバージョンのアーカイブ |
データ収集パイプラインの例(Confluence→RAG):
Confluence API → 페이지 추출
→ HTML → Markdown 변환 (pandoc / html2text)
→ 메타데이터 추출 (제목, 스페이스, 라벨, 최종수정일)
→ 빈 페이지 / 템플릿 필터링
→ 구조화: 헤딩 기준 섹션 분리
→ 이전 버전 대비 diff → 변경분만 재처리
→ chunks.jsonl에 추가
# chunk.py 핵심 파라미터
--chunk-size 512 # 최대 512자 (글자 단위, 토큰이 아니라 char)
--overlap 64 # 64자 오버랩으로 문맥 단절 방지
overlap(オーバーラップ)とは: 前のチャンクの最後の64文字を次のチャンクの前部分に重複含めるものである。例えば、「…甲冑は強直な性格である。」で終わるチャンクがあれば、次のチャンクは「甲冑は剛直な性格である。したがってリーダーシップが…」のように続く。こうすることで、チャンクの境界で切れた文章をどちらのチャンクでも完全なコンテキストで検索することができる。
512文字を選択した理由:
埋め込みモデルとの関係 — 発見されたバグと教訓:
このプロジェクトを分析する過程で、Dense埋め込みのパフォーマンス損失のバグが発見されました。 paraphrase-multilingual-MiniLM-L12-v2のモデルアーキテクチャは512トークンをサポートするが(max_position_embeddings: 512)、sensence-transformersライブラリのデフォルト値が128トークンに設定されていてチャンクの前部分だけ埋め込まれ、残りが切り捨てられていた。
| アイテム | 値 |
|---|---|
| モデルアーキテクチャのサポート | 512トークン |
| ライブラリのデフォルト(バグ、修正完了) | 128トークン |
| 韓国語512文字→実際のトークン数 | ~337 トークン |
| 128トークン制限で実際の埋め込みを反映 | 前半〜200文字(全体の〜39%) |
| 128対512埋め込みコサイン類似度 | 0.55(情報が大きく異なる) |
修正方法: model.max_seq_length = 512 1行追加後のインデックスの再構築。ただし、このモデルが128トークン基準で学習された可能性があり、512に拡張時の品質向上の程度はベンチマークで確認が必要である。
それでもシステムが機能した理由:
教訓: ライブラリのデフォルト値を盲信しないでください。モデルアーキテクチャがサポートする範囲とライブラリ設定値が異なる場合があります。埋め込みパイプラインを構築するときは、必ずmax_seq_lengthと実際のチャンクのトークン数を比較確認する必要があります。この種の設定の不一致は、エラーを発生させずに静かに性能を削るため、発見が難しい。
64文字のオーバーラップを選択した理由:
単に512文字ごとに切らない。 意味単位を尊重する階層分割:
Level 1: --- 구분선으로 섹션 분리 (atomic boundary, 절대 넘지 않음)
Level 2: 빈 줄(\n\n)로 단락 분리
Level 3: 문장 단위 분할 (단락이 512자 초과 시)
コアコード(chunk.py:116-137):
def _split_into_paragraphs(text: str) -> list[str]:
# --- 구분선이 있으면 섹션별로 분리, 각 섹션을 atomic 단락으로
if _SECTION_SEPARATOR.search(text):
sections = _SECTION_SEPARATOR.split(text)
paragraphs = []
for section in sections:
section = section.strip()
if not section:
continue
paragraphs.append(section)
return paragraphs
# 없으면 빈 줄로 분리
parts = re.split(r"\n{2,}", text)
return [p.strip() for p in parts if p.strip()]
なぜ---区切り線が重要なのか:
---でテーマが区分される--- を atomic boundary に設定すると主題汚染を源泉遮断ITサービスと同じ構造:
APIドキュメント(Swagger/OpenAPI)はエンドポイントごとに自然な境界があり、Confluenceページは## 헤딩で区切られている。このような文書は---の代わりにヘディング(##)をアトミックboundaryとして使用すればよい。たとえば、GET /api/usersとPOST /api/usersが同じチャンクに混在している場合、検索時に混乱が発生しますが、エンドポイントごとにチャンキングすると正確な結果が得られます。
これが可能な前提条件:
第4章で説明されているように、収集フェーズ中のデータにすでに---区切り線が含まれている必要があります。区切り線のない非定型文書(スキャンPDF、レガシーWordファイルなど)では、この戦略は機能せず、追加の前処理(LLMベースのセクション検出、ルールベースのヘディング抽出)が必要です。
段落が512文字を超える場合は文単位で分割しますが、韓国語終結語を考慮します。
sentences = re.split(
r"(?<=[.!?。!?\n])\s*|(?<=[다요음함임됨것]\.)\s+",
para,
)
.!?。!? 後ろから分割(汎用)다., 요., 음., 함. など韓国語終結語+ピリオドの後から分割def _detect_lang(text: str) -> str:
"""텍스트 첫 2000자의 문자 유형 비율로 언어 판별"""
# 한글 음절(AC00-D7AF) > 20% → ko
# CJK 통합(4E00-9FFF) > 20% → zh
# ASCII 알파벳이 대다수 → en
言語タグはチャンクメタデータに保存され、検索時にトークナイザー選択とBM25クエリ拡張の方向を決定します。