7月1日、Towards Data Scienceが「Context Engineering for RAG : The Four Typed Inputs Behind Every RAG Answer」と題した記事を公開した。RAGパイプラインの回答品質を左右するのはプロンプトの磨き方ではなく、LLMのコンテキストウィンドウに何を・どこから・どう組み立てて渡すかというアーキテクチャ設計だ——本記事はその主張を、4種の型付き入力という具体的な実装パターンで裏付けている。
「プロンプトエンジニアリング」から「コンテキストエンジニアリング」へ
2025年6月、ShopifyのCEO Tobi Lütkeが「プロンプトエンジニアリングという言葉は間違いだ」とXに投稿し、代わりにコンテキストエンジニアリングという言葉を提唱した。定義は「LLMがタスクをそれらしく解けるよう、すべてのコンテキストを提供する技法」だ。翌週、Andrej Karpathyがこれを支持し「コンテキストウィンドウを次のステップに最適な情報で満たす繊細な技術と科学」と表現した。この概念はほどなくO'Reilly Media書籍『Context Engineering』の表紙を飾り、LangChainがタクソノミーとして整理した。
本記事はその概念を、単一ドキュメントを対象としたRAGパイプライン(ドキュメントパース・質問パース・検索・生成の4ブロック)に適用して解説したものだ。プロンプトという「1つのテキストブロック」を磨く話ではなく、LLMのコンテキストウィンドウに何を・どこから・どう組み立てて渡すかという、ソフトウェアアーキテクチャとしての設計の話である。
各ブロックが「型付きコンテキスト」を出力する
記事の核心は、パイプラインの各ブロックがPydanticクラスやpandas DataFrameという型付きオブジェクトを出力し、それが最終的なLLM呼び出しに収束するという設計だ。
- パースブロック:
line_df(行ごとのテキストとbbox)、page_df(ページ種別・列数)、toc_df(目次エントリ)、image_df(埋め込み画像)、parsing_summary(ドキュメント種別・ページ数・典型フィールド)を出力する。 - 質問パースブロック:
ParsedQuestionを出力する。keywords(検索用の名詞フレーズリスト)、intent(固定enumによるラベル)、structural_hints.pages_hint(「3ページ目に」などの明示的ページ指定)、answer_shape(期待する出力形式:text/amount/date/list/table/address)の各フィールドは、それぞれ異なる下流ブロックが消費する。 - 検索ブロック:
filtered_line_df(LLMが実際に読む行のサブセット)とretrieval_audit(どの手法でどのページを選んだかの記録)を出力する。200ページの契約書も10ページ程度の関連行に絞られ、ユーザーコンテンツは数千トークン以内に収まる。 - 生成ブロック:コンテキストの消費者であり出力者ではない。型付きPydanticオブジェクトとして回答を返す。
LLMに渡る4つの型付き入力
LLM呼び出しに実際に渡るのは以下の4つだ。
1. 固定システムプロンプト
モジュールレベルのPython定数として定義され、呼び出しをまたいで変化しない。
PARSE_QUESTION_SYSTEM_PROMPT = (
"You extract content noun phrases from the user's question..."
)
def parse_question(question, *,
system_prompt: str = PARSE_QUESTION_SYSTEM_PROMPT,
...):
変化しないため、LLMプロバイダーのキャッシュ対象になる。AnthropicのPrompt CachingやOpenAIのキャッシュ機構では、キャッシュ済み入力は通常の約10分の1のコストで処理される。また、Gitでdiff可能な安定したシンボルとして監査にも対応する。
2. 検索済み行(フィルタ済みDataFrame)
ParsedQuestion.keywordsとstructural_hintsを使って検索メソッド(キーワード・目次・LLMアービター)を選択し、フィルタ済みフレームを返す。LLMが読むのはこのフレームだけで、ドキュメント全体は渡さない。
3. ドキュメントコンテキストブロック(コンパクトJSON)
DocContextクラスのas_prompt_json()メソッドが、ドキュメント種別・ページ数・典型フィールド・サマリーを最小JSONとして出力する。
class DocContext(BaseModel):
doc_type: str | None = None
n_pages: int | None = None
typical_fields: list[str] = []
summary: str | None = None
def as_prompt_json(self) -> str:
payload = {k: v for k, v in self.model_dump().items()
if v is not None and v != []}
return json.dumps(payload, separators=(",", ":"))
CV(履歴書)の場合で200文字以下。全フィールドがnullなら空オブジェクト{}となり、ユーザーコンテンツからそのブロックごと除外される。
4. PromptContextアグリゲーター
各LLM呼び出しブロックがcontext: PromptContextという単一のkwargで受け取る集約クラス。現在はdoc_contextフィールドのみ有効で、corpus_contextとproject_contextは将来の拡張用にreserved状態で定義されている。
class PromptContext(BaseModel):
doc_context: DocContext | None = None
# corpus_context: CorpusContext | None = None # reserved
# project_context: ProjectContext | None = None # reserved
新レイヤーの追加はコメントアウトを外すだけで済み、すべてのブロックが自動的に新レイヤーを受け取れる設計だ。
命名によって変わる3つの実務上の効果
記事は「命名によってコードは変わらないが、実務上の問いが変わる」と指摘する。この一文が示すのは、コンテキストエンジニアリングが単なるリファクタリングではなく、障害調査・コスト管理・将来拡張のそれぞれに異なる問いの立て方をもたらすという点だ。
- 監査:回答が誤っていたとき、問うべきは「プロンプトに何を書いたか」ではなく「あの呼び出しでコンテキストウィンドウに何が入っていたか」になる。各ブロックの出力は
parsing/・questions/<hash>/・retrieval/<hash>/以下に永続化され、監査者がコンテキストペイロードを事後的に再構築できる。これはRAGの「なぜこの回答が返ったか」をトレースする上で、ログにプロンプト文字列を残すよりもはるかに構造的なアプローチだ。 - コスト:システムプロンプトのキャッシュ・
as_prompt_jsonによる圧縮・検索による絞り込みの3つが組み合わさり、変動コストを最小化する。100ドキュメント×10問=1000呼び出しのコーパスでは、この差は無視できない規模になる。 - 拡張性:
PromptContextのインターフェースが安定しているため、コーパス対応・複数ターン会話・ツール呼び出しへの拡張時も、既存ブロックのシグネチャを変えずに済む。reservedフィールドのコメントアウトを外すだけで全ブロックに新レイヤーが伝播する設計は、成長するRAGシステムの保守コストを抑える実用的な工夫といえる。
実装コードはGitHubリポジトリdoc-intel/notebooks-vol1で公開されている。
詳細はContext Engineering for RAG : The Four Typed Inputs Behind Every RAG Answerを参照していただきたい。