【実践】LLM活用における4つの設計パターン - 大規模テキスト処理の課題と解決策

この記事でわかること
- 入力を先頭から切り捨てる「圧縮」をやめ、コンテキスト長から分割幅を動的に計算する方法
- 文字数↔トークン数の係数を推測で置くとコンテキスト超過が必発することと、tiktoken での実測手順
- 出力が毎回同じ長さに収束する原因と、セクション分割生成での対処
- 引用マーカーを強制して、出力を元文書と突き合わせられる状態にする設計
Python で LLM の API(OpenAI / Azure OpenAI など)を叩いたことがあること。トークンとコンテキストウィンドウの概念を知っていると読みやすいです。
LLM(大規模言語モデル)を使ったドキュメント処理システムを開発する中で直面した課題と、その解決策を4つの設計パターンとして紹介します。
入力制限の克服、出力品質の向上、ハルシネーション防止など、実務で使えるテクニックを実装例とともに解説。
はじめに
LLMを使ったドキュメント処理システムを開発していると、「入力が長すぎて処理できない」「出力が毎回同じサイズになる」「LLMが嘘をつく」といった課題に直面します。
本記事では、これらの課題に対する実践的なアプローチを、実際のプロジェクトでの適用事例とともに紹介します。紹介するパターンは特定のプロジェクトに依存せず、LLMを活用するシステム全般に応用可能です。
1. Semantic Map-Reduce パターン
課題: データ廃棄という名の「圧縮」
LLMには入力トークン数の制限があります。大量のドキュメントを処理する際、以下のようなコードをよく見かけます:
# よくある問題のあるコード
combined_content = all_documents[:8000] # 8000文字以降は完全に消失これは「圧縮」ではなく「データ廃棄」です。8000文字を超える情報は完全に失われ、重要な内容が後半にあった場合は致命的な情報欠落が発生します。
解決策: 3ステップの Map-Reduce
Step 1: 動的トークン分割
ハードコード制限を撤廃し、モデルのコンテキストウィンドウに基づいて動的に計算します。
MAX_CONTEXT_TOKENS = 128000
PROMPT_OVERHEAD = 3000
RESPONSE_TOKENS = 16384
SAFETY_MARGIN = 0.8
# 日本語は 1 トークン ≒ 1 文字強。o200k_base(GPT-4o) の実測で 1.15〜1.4 文字/トークン、
# cl100k_base だと 0.69〜1.10 文字/トークン。安全側に倒して 1.0 で見積もる。
JP_CHARS_PER_TOKEN = 1.0
def calculate_max_chars(num_sections: int = 1) -> int:
"""利用可能なトークン数から最大文字数を動的に計算"""
available = (MAX_CONTEXT_TOKENS - PROMPT_OVERHEAD - RESPONSE_TOKENS) * SAFETY_MARGIN
return int(available * JP_CHARS_PER_TOKEN / num_sections)文字数 ↔ トークン数の係数を推測で置かないこと。 日本語は 1 トークン ≒ 1 文字強で、3 倍ではありません。tiktoken での実測値は、o200k_base(GPT-4o / GPT-4o-mini)で技術文書 1.32 文字/トークン、仕様書風 1.38、ひらがな主体 1.21、漢字熟語主体 1.15。cl100k_base ではさらに厳しく 0.69〜1.10 文字/トークンです。
ここを 3.0 にすると 2〜2.6 倍の過大見積もりになり、コンテキスト超過が必発します。
係数は自分のコーパスで実測してから決めます。
import tiktoken
enc = tiktoken.get_encoding("o200k_base") # GPT-4o 系。GPT-4 / GPT-3.5 系は cl100k_base
chars = sum(len(t) for t in sample_texts)
tokens = sum(len(enc.encode(t)) for t in sample_texts)
print(f"{chars / tokens:.2f} 文字/トークン") # この実測値以下を JP_CHARS_PER_TOKEN にするStep 2: 引用マーカーベースの重複排除
分割処理すると同じ内容が複数バッチに含まれることがあります。引用マーカー(出典情報)をキーにして重複を検出・排除します。
- 同一ソース・同一行番号の内容は1つにまとめる
- マーカーなしコンテンツはハッシュ値で重複検出
Step 3: セマンティッククラスタリング
Embedding APIでコンテンツをベクトル化し、類似度の高いコンテンツを同一バッチにグループ化します。関連する情報が同じ文脈で処理されるため、LLMの理解精度が向上します。
効果
| 指標 | Before | After |
|---|---|---|
| 入力制限 | 8,000〜15,000文字 | 約87,000文字 |
| 情報保持率 | 5-20% | 70-100% |
2. 出力収束問題の解決パターン
課題: 暗黙的な情報圧縮
改善後も出力が50-70行程度に収束する問題が発生しました。原因を分析すると:
max_tokens: 4096(出力制限)
÷ 3セクション(要件/仕様/設計を一括生成)
= 各セクション約1300トークン(50-70行)LLMは与えられた出力枠に収まるよう、暗黙的に情報を圧縮していたのです。
解決策
1. 出力トークン制限の拡張
# Before
max_tokens = 4096
# After(モデルの最大出力を活用)
max_tokens = 163842. プロンプトでの明示的な指示
【最重要ルール - 情報保持】 ・すべての技術情報を漏れなく含めてください。省略は禁止です。 ・「整理」とは重複削除と構造化のみを意味します。要約・圧縮は行わないでください。 ・テーブルの行数を減らさないでください。
3. セクション分割生成
複数セクションを一括生成するのではなく、セクションごとに独立して生成します。
# 各セクションが独立して最大トークン数を使用可能
requirements = generate_section("requirements", max_tokens=16384)
specifications = generate_section("specifications", max_tokens=16384)
design = generate_section("design", max_tokens=16384)効果
| 指標 | Before | After |
|---|---|---|
| 平均出力行数 | 50-70行 | 100-200行 |
| 引用マーカー数 | 511件 | 1,431件(+180%) |
3. 品質保証パターン(ハルシネーション防止)
課題: LLMの「補完」傾向
LLMは与えられた情報を「補完」しようとする傾向があり、元ドキュメントに存在しない情報を生成(ハルシネーション)することがあります。技術ドキュメントでこれが起きると致命的です。
解決策
1. 引用マーカーの強制
すべての技術情報に出典を付与させます。
【出力ルール】
すべての技術的な記述には引用マーカーを付与してください。
形式: [元ファイル:ファイル名.md:行n]
例:
- 最大速度は100km/hです。[元ファイル:仕様書.md:行42]2. プロンプトでの明確な禁止
【最重要原則】
- 嘘をつかない: 元文書に記載のない情報を捏造しないでください
- 推測を書かない: 「〜と思われる」「おそらく〜」は禁止
- 理由は出典がある場合のみ: 根拠が元文書にない場合は理由を書かない
3. 検証可能性の確保
引用マーカーがあることで、出力内容と元文書を照合して自動検証できます。
def verify_output(output: str, source_docs: dict) -> list[str]:
"""引用マーカーと元文書を照合して検証"""
markers = parse_citation_markers(output)
errors = []
for marker in markers:
if not exists_in_source(marker, source_docs):
errors.append(f"検証失敗: {marker}")
return errors4. 整合性チェックパターン
課題: 解析手法間の乖離
コード解析において、静的解析ツールとLLMの解析結果が乖離することがあります。例えば:
- 静的解析: 「関数数: 0」
- LLM出力: 「13個の関数仕様を生成」
この乖離は、C++のマクロやテンプレートなど、静的解析が苦手とするパターンで発生します。
解決策: 自動整合性チェック
@dataclass
class ConsistencyCheck:
is_consistent: bool = True
warnings: list[str] = field(default_factory=list)
static_function_count: int = 0
llm_function_count: int = 0
def check_consistency(static_result, llm_result) -> ConsistencyCheck:
check = ConsistencyCheck()
check.static_function_count = static_result.function_count
check.llm_function_count = count_functions_in_llm_output(llm_result)
# 50%以上の乖離で警告
if check.static_function_count > 0:
diff_rate = abs(check.llm_function_count - check.static_function_count) / check.static_function_count
if diff_rate > 0.5:
check.warnings.append(
f"静的解析({check.static_function_count}個)とLLM出力({check.llm_function_count}個)に乖離があります"
)
check.is_consistent = False
return check警告の可視化
ドキュメント冒頭に警告を表示し、ユーザーに注意を促します。
⚠️ 注意: 以下の整合性警告が検出されました: ・静的解析で関数が検出されませんでしたが、LLMが13個の関数仕様を生成しました。 ・マクロやテンプレートによる関数定義の可能性があります。
まとめ
| パターン | 解決する課題 | キーポイント |
|---|---|---|
| Semantic Map-Reduce | 入力制限による情報損失 | 動的分割 + クラスタリング |
| 出力収束対策 | 出力の固定サイズ化 | トークン拡張 + プロンプト強化 |
| 品質保証 | ハルシネーション | 引用マーカー + 明示的禁止 |
| 整合性チェック | 解析手法間の乖離 | 自動比較 + 警告可視化 |
これらのパターンは組み合わせることで、より堅牢なLLM活用システムを構築できます。特に「入力制限の克服」と「出力品質の向上」は、多くのLLMアプリケーションで共通する課題であり、ぜひ参考にしてみてください。
参考情報
- 使用モデル: GPT-4o / GPT-4o-mini(Azure OpenAI)
- トークナイザ: o200k_base(tiktoken)
- コンテキストウィンドウ: 128Kトークン
- Embedding: text-embedding-ada-002
更新履歴
- 見出しがすべて同じ階層で、目次に「課題/解決策/効果」が繰り返し並ぶ状態だったのを、各パターンの下に入れ子にする形に修正。コードブロックの言語指定を plain text から text に修正。「この記事でわかること」を概要プロパティへ移動。
- calculate_max_chars の係数を訂正。「日本語は約3文字/トークン」は誤りで、tiktoken 実測では o200k_base で 1.15〜1.4 文字/トークン(cl100k_base は 0.69〜1.10)。係数 3.0 → JP_CHARS_PER_TOKEN = 1.0 に変更し、実測手順のコードと警告を追加。これに伴い効果表の入力制限を「96,000〜100,000文字」から「約87,000文字」へ修正(計算式との整合)。参考情報にトークナイザを明記。


