コードとHTMLに特化したドキュメント戦略
PythonコードにはASTベースの関数分割器、HTMLにはタグ対応パーサー、Markdownには見出し階層を使った、特化型のチャンク分割を適用します。
「コードとHTMLに特化したドキュメント戦略」はCoddyKit上の無料AI Engineering Academyレッスンです。 これはレッスン4/4です。 下記で完全なレッスンを無料で読むことができます。その後、ブラウザ内の組み込みコードエディタと24時間対応のAIチューターでハンズオン演習できます。 これはAI Engineering Academy学習パスの一部であり、ウェブとCoddyKitアプリ全体で進捗が同期されます。 AI Engineering Academyコースには全4レッスンが含まれています。
汎用的なチャンク分割が特殊なドキュメントで失敗する理由
テキストベースのチャンク分割は文章向けに設計されていますが、実際のデータにはソースコード、HTML ページ、Markdown ドキュメントも含まれます。固定した文字境界でコードを分割すると、関数の本体の途中で切断され、検索に使えないチャンクになることがあります。特殊なドキュメントには、長さだけでなく内部構造を理解するチャンク分割器が必要です。
AST ベースの Python コード分割
Python ファイルの抽象構文木(AST)は、すべての関数、クラス、モジュールを構造化されたノードとして表します。AST を走査すると、各関数やメソッドを独立したチャンクとして抽出し、シグネチャ、docstring、本体をひとまとまりに保てます。LangChain の PythonCodeTextSplitter は内部でこの方法を使用しています。
import ast
import textwrap
def extract_functions(source_code: str) -> list[dict]:
tree = ast.parse(source_code)
chunks = []
for node in ast.walk(tree):
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
start = node.lineno - 1
end = node.end_lineno
lines = source_code.splitlines()[start:end]
chunks.append({
'name': node.name,
'code': '\n'.join(lines),
'start_line': node.lineno,
})
return chunksクラス境界によるチャンク分割
オブジェクト指向のコードベースでは、関数単位よりもクラス単位で分割したほうが適していることがよくあります。クラスチャンクなら、メソッド間の関係と、それらが操作する共有状態を保持できます。クラスの docstring とすべてのメソッド本体を1つのチャンクに含め、長いメソッドだけを対象に、より細かいチャンクを別途作成することもできます。
from langchain_text_splitters import Language, RecursiveCharacterTextSplitter
python_splitter = RecursiveCharacterTextSplitter.from_language(
language=Language.PYTHON,
chunk_size=1000,
chunk_overlap=100,
)
with open('my_module.py', 'r') as f:
source = f.read()
chunks = python_splitter.create_documents([source])
print(f'Created {len(chunks)} code chunks')チャンクへのコードメタデータの追加
生のコードチャンクは、メタデータがあって初めて有効に活用できます。コードチャンクをベクトルデータベースに保存する際は、ファイルパス、関数名、プログラミング言語、行範囲を含めてください。このメタデータにより、検索器は言語やファイルで絞り込め、LLM は回答内で正確なソース位置を示せるようになります。
from langchain_core.documents import Document
def chunk_python_file(filepath: str) -> list[Document]:
with open(filepath) as f:
source = f.read()
functions = extract_functions(source) # from previous example
docs = []
for fn in functions:
docs.append(Document(
page_content=fn['code'],
metadata={
'source': filepath,
'function': fn['name'],
'language': 'python',
'start_line': fn['start_line'],
}
))
return docsHTML:文字数よりも構造を重視する
HTML ドキュメントは、見出し、セクション、段落、リストによって階層的に構造化されています。文字数で HTML を分割すると、タグの途中で切れて不正な断片が生成されることがあります。適切な方法は、BeautifulSoup のようなパーサーで HTML を解析し、<article>、<section>、<p> タグなど、意味のある要素を抽出することです。
from bs4 import BeautifulSoup
def chunk_html_by_section(html: str) -> list[dict]:
soup = BeautifulSoup(html, 'html.parser')
chunks = []
for tag in soup.find_all(['h1', 'h2', 'h3', 'p', 'li']):
text = tag.get_text(separator=' ', strip=True)
if len(text) > 40: # skip trivial fragments
chunks.append({
'tag': tag.name,
'text': text,
})
return chunks見出し階層に基づく HTML チャンク分割
より高度な HTML 戦略では、コンテンツを最も近い見出しの下にまとめます。<h2> 見出しの後に続くすべての段落とリストは、そのセクションに属します。テキストを親見出しとともにまとめることで、単独の段落であれば失われてしまうトピックのコンテキストを保持できます。LangChain の HTMLHeaderTextSplitter はこの処理を自動的に実装しています。
from langchain_text_splitters import HTMLHeaderTextSplitter
headers_to_split_on = [
('h1', 'Header 1'),
('h2', 'Header 2'),
('h3', 'Header 3'),
]
splitter = HTMLHeaderTextSplitter(headers_to_split_on=headers_to_split_on)
with open('page.html') as f:
html = f.read()
sections = splitter.split_text(html)
for sec in sections[:3]:
print(sec.metadata)
print(sec.page_content[:200])
print('---')Markdown:見出し階層を尊重する
Markdown ドキュメントは、#、##、### の見出しで構成されています。MarkdownHeaderTextSplitter は見出しの境界で分割し、見出し階層をメタデータとして保存します。これにより、各チャンクが完全な見出しパスを持つため、ドキュメントの特定のセクションについて質問されたときに、取得したコンテキストの関連性が大幅に向上します。
from langchain_text_splitters import MarkdownHeaderTextSplitter
headers_to_split_on = [
('#', 'H1'),
('##', 'H2'),
('###', 'H3'),
]
md_splitter = MarkdownHeaderTextSplitter(headers_to_split_on=headers_to_split_on)
with open('README.md') as f:
markdown = f.read()
docs = md_splitter.split_text(markdown)
for doc in docs[:2]:
print('Metadata:', doc.metadata)
print('Content:', doc.page_content[:300])
print()見出し分割後の二次分割
見出しで分割した後も、個々のセクションが埋め込みモデルのトークン上限を超えるほど長い場合があります。推奨されるパターンは2段階の分割です。まず見出し階層で分割して意味的なコンテキストを保持し、次にチャンクサイズの上限を超えるセクションへ文字ベースの分割器を適用します。これにより、見出しメタデータを保持しながら、どのチャンクも大きくなりすぎないようにできます。
from langchain_text_splitters import MarkdownHeaderTextSplitter, RecursiveCharacterTextSplitter
header_splitter = MarkdownHeaderTextSplitter(
headers_to_split_on=[('#', 'H1'), ('##', 'H2')]
)
char_splitter = RecursiveCharacterTextSplitter(
chunk_size=500,
chunk_overlap=50,
)
with open('docs.md') as f:
md = f.read()
header_chunks = header_splitter.split_text(md)
final_chunks = char_splitter.split_documents(header_chunks)
print(f'{len(final_chunks)} final chunks produced')表を考慮した PDF のチャンク分割
PyMuPDF や pdfplumber などのツールで抽出した PDF は、表の構造が失われ、テキストの行が崩れてしまうことがあります。これに対処するには、表の境界ボックスを検出できるレイアウト対応の PDF パーサーを使用し、チャンク分割の前に表を Markdown または CSV 形式へ変換します。各表は1つのチャンクとして扱い、通常の文章ではなく表であることを示す構造化メタデータを付与します。
import pdfplumber
def extract_pdf_chunks(pdf_path: str) -> list[dict]:
chunks = []
with pdfplumber.open(pdf_path) as pdf:
for page_num, page in enumerate(pdf.pages):
# Extract tables separately
for table in page.extract_tables():
rows = ['|'.join(str(c) for c in row) for row in table]
chunks.append({
'type': 'table',
'content': '\n'.join(rows),
'page': page_num + 1,
})
# Extract prose text
text = page.extract_text()
if text:
chunks.append({'type': 'text', 'content': text, 'page': page_num + 1})
return chunks混在コーパスの言語・ファイル形式検出
エンタープライズのナレッジベースでは、Python スクリプト、HTML の API ドキュメント、Markdown のアーキテクチャノート、CSV のデータエクスポートなど、異なるファイル形式が混在することがよくあります。堅牢なチャンク分割パイプラインでは、拡張子や MIME タイプからファイル形式を検出し、各ドキュメントを適切な専用チャンク分割器へ振り分ける必要があります。これにより、文章にコード分割のロジックを適用したり、その逆を行ったりすることを防げます。
from pathlib import Path
def route_document(filepath: str) -> list[dict]:
ext = Path(filepath).suffix.lower()
if ext == '.py':
return chunk_python_file(filepath)
elif ext in ('.html', '.htm'):
with open(filepath) as f:
return chunk_html_by_section(f.read())
elif ext == '.md':
# use MarkdownHeaderTextSplitter
return chunk_markdown(filepath)
elif ext == '.pdf':
return extract_pdf_chunks(filepath)
else:
# fallback: plain text recursive splitter
return chunk_plain_text(filepath)周辺行によるコンテキストの保持
コードを関数単位で分割する場合、ファイル先頭の import 文や、メソッドを含むクラス定義など、数行の周辺コンテキストを含めると有効なことがよくあります。このコンテキストによって、LLM は利用可能なライブラリや、その関数がより大きなクラス内で果たす役割を理解しやすくなり、生成される回答の品質が向上します。
def chunk_with_imports(source_code: str, fn_node, lines: list[str]) -> str:
# Gather top-of-file imports (first block before first non-import)
import_lines = []
for line in lines:
stripped = line.strip()
if stripped.startswith('import ') or stripped.startswith('from '):
import_lines.append(line)
elif stripped and not stripped.startswith('#'):
break
fn_body = '\n'.join(lines[fn_node.lineno - 1:fn_node.end_lineno])
return '\n'.join(import_lines) + '\n\n' + fn_body理解度チェック
このレッスンで学んだドキュメント固有のチャンク分割戦略について、理解度を確認しましょう。
レッスンのまとめ
このレッスンでは、AST ベースのチャンク分割によって Python の関数とクラスの境界を保持できること、HTMLHeaderTextSplitter と MarkdownHeaderTextSplitter が見出し階層を尊重し、セクションとともにコンテキストを保持すること、そして2段階のアプローチ(見出し分割の後に文字分割を行う方法)によって、構造メタデータを失わずに大きすぎるセクションを処理できることを学びました。次は、密な検索と疎な検索を組み合わせるハイブリッド検索について学びます。
よくある質問
「コードとHTMLに特化したドキュメント戦略」レッスンは無料ですか?
はい。「コードとHTMLに特化したドキュメント戦略」の完全なテキストはこのウェブで無料で読めます。インタラクティブに演習し(組み込みコードエディタと24時間対応のAIチューター)、AI Engineering Academyコースの残りをアンロックするには、CoddyKit PROにアップグレードしてください。 AI Engineering Academyコースには全4レッスンが含まれています。
「コードとHTMLに特化したドキュメント戦略」で何を学びますか?
PythonコードにはASTベースの関数分割器、HTMLにはタグ対応パーサー、Markdownには見出し階層を使った、特化型のチャンク分割を適用します。 ブラウザで直接実行するハンズオンコードでAI Engineering Academyを演習し、24時間対応のAIチューターがレッスンを進める中での質問に答えます。
AI Engineering Academyを始めるのに経験は必要ですか?
事前経験は必要ありません。CoddyKitのAI Engineering Academyは初級者から上級者向けに構成されているため、ここから始めるか最初から始めて、自分のペースで進むことができます。 これはレッスン4/4です。
「コードとHTMLに特化したドキュメント戦略」レッスンにはどのくらい時間がかかりますか?
ほとんどのCoddyKitレッスンは約5~10分かかります。各レッスンはコンパクトでインタラクティブなので、着実に進歩し、ウェブとアプリ全体で正確に前回の場所から再開できます。
このAI Engineering Academyレッスンでコードを書いて実行できますか?
はい。すべてのAI Engineering Academyレッスンに組み込みコードエディタが含まれているため、ブラウザでリアルコードを書いて実行し、即座のAIフィードバックを取得できます。ローカル設定は不要です。
このコースのすべてのレッスン
- 素朴なチャンク分割が検索に悪影響を与える理由
- 埋め込み類似度による意味的チャンク分割
- 親子チャンクと小から大への検索
- コードとHTMLに特化したドキュメント戦略