🎨

AIコーディングエージェント開発で学ぶコンテキストエンジニアリング入門

に公開

TL;DR

AIエージェントの性能は、モデルに情報をどう与えるか(コンテキストエンジニアリング)で決まります。この記事では、AIが自らファイル検索(Tools)、過去の対話内容の記憶(Memory)、タスクの委任(Processing)を行えるように設計することで、コンテキストウィンドウの制約を乗り越える具体的な手法を、実際のコードを交えて解説しました。

はじめに

こんにちは!ナレッジワークでAIエンジニアとして活動しているzawakin (@zawawahoge) です。AI技術戦略やAIエージェント開発に日々取り組んでいます。

近年、OpenAIの "AgentKit"Google Cloud の "Gemini Enterprise" など、多くの企業が「AIエージェント」の開発・利用を加速させています。AIエージェントは自律的にタスクを実行できる可能性を秘めていますが、その性能を最大限に引き出すには一つの大きな壁が存在します。それが コンテキストウィンドウの制約 です。

この記事では、AIエージェント開発におけるこの課題を乗り越えるための重要なアプローチである 「コンテキストエンジニアリング」 について、実際にAIコーディングエージェントを構築する過程を通して、その核心を掴んでいただくことを目指します。

課題:LLMが見ている「狭く不完全な窓」

LLMの挙動を理解する上で欠かせないのが 「コンテキストウィンドウ」 という概念です。これは、モデルが一度に処理できる情報量(トークン数)の上限を指します。


図: コンテキストウィンドウ(https://docs.claude.com/en/docs/build-with-claude/context-windows の画像を加工)

近年のモデルではこのウィンドウサイズが飛躍的に増大しているものの、依然として物理的な上限は存在します。例えば、大規模なコードベース全体を一度に読み込ませることは不可能です。


図: LLMは、コンテキストウィンドウという狭い窓を通してしか世界を見ることができない(Gemini生成)

さらに、「Lost-in-the-middle」問題[1]として知られるように、LLMはコンテキストの 冒頭と末尾の情報を重視し、中間の情報を忘れやすい 傾向があります。つまり、情報をただ長く入力すれば良いというわけではなく、AIエージェントが「知るべき」広大な文脈(ツール情報、過去の対話履歴、プロジェクト知識など)を、この「狭く不完全な窓」を通して、いかに賢く伝えるかが鍵となります。

解決策:コンテキストエンジニアリング

この課題を解決するアプローチが 「コンテキストエンジニアリング」 です。これは、限られたコンテキストウィンドウに 「どの情報を、どのように配置するか」を戦略的に設計する 技術体系を指します。

本記事では、コンテキストエンジニアリングの主要な3つのアプローチを、AIコーディングエージェントの実装例と共に解説します。

  1. Tools(ツール) : 外部の世界から必要な情報だけを取得する。
  2. Memory(記憶) : 蓄積された知識から関連情報を引き出す。
  3. Processing(加工) : 情報を要約・変換したり、他のエージェントに処理を委任する。

実践:AIコーディングエージェントを構築する

ここからは、実際に構築したAIコーディングエージェントのコードを追いながら、3つのアプローチがどのように実装されているかを見ていきましょう。

このエージェントは、ユーザーとの対話を通じて、ローカルのファイル検索、読み書きなどを自律的に行うCLIツールです。

動作イメージ

プロジェクト構造の概要

エージェントは主にagent/ディレクトリ内に実装されており、その能力の中核はagent/tools/に定義された各ツール群です。

agent/
├── tools/
│   ├── delegate.py     # アプローチ3: サブエージェントへのタスク委譲
│   ├── fs.py           # アプローチ1: ファイルの読み書き
│   ├── memory.py       # アプローチ2: 長期記憶の保存
│   ├── search.py       # アプローチ1: ファイル・コンテンツ検索
│   ├── registry.py     # 全ツールの管理・実行
│   └── ...
├── orchestrator.py     # LLMとの対話ループを管理
├── system_prompts.py   # システムプロンプトの構築
└── main.py             # CLIエントリーポイント

アプローチ1: 外部の世界から情報を得る (Tools)

AIエージェントがコードベースについて何か作業をするには、まず現状を把握する必要があります。しかし、全ファイルをコンテキストに入れることはできません。そこで、必要な情報だけを動的に取得するための 「ツール」 を定義します。

ファイルシステムへのアクセス

agent/tools/search.pyagent/tools/fs.py は、エージェントがファイルシステムという「外部世界」と対話するためのツールです。

  • search_files : globパターンを使ってファイル名を検索します。これにより、エージェントはまず関連しそうなファイルの「ありか」を探ることができます。

  • read_file: 特定のファイルの内容を読み込みます。検索で見つけたファイルを具体的に調べるために使います。

agent/tools/search.py より抜粋:

def search_files_definition() -> Dict[str, Any]:
    """
    Returns the JSON Schema definition for the search_files tool.
    """
    return {
        "name": "search_files",
        "description": (
            "List files by glob-style pattern relative to the repository root. "
            "Use to discover modules by name. Returns up to 200 matches."
        ),
        "input_schema": { ... }
    }

これらのツールがあることで、エージェントは「まず src/**/*.py でファイルを探し、見つかった src/auth/login.py の中身を読んでみよう」といった段階的な情報収集が可能になり、 コンテキストの消費を最小限に抑えられます


アプローチ2: 蓄積された記憶から情報を得る (Memory)

一度の対話で得た学びや決定事項が、次のセッションで失われてしまうのは非効率です。そこで、重要な情報を永続化させる 「記憶」 の仕組みを導入します。

save_memory ツール

agent/tools/memory.py で定義された save_memory ツールは、エージェントが学んだことを AGENTS.md というファイルに追記する機能を提供します。

agent/tools/memory.py より抜粋:

def definition() -> Dict[str, Any]:
    """Return the tool definition for save_memory."""
    return {
        "name": "save_memory",
        "description": (
            "Save important information, learnings, or decisions to persistent memory. "
            "Use this when you discover key facts about the codebase, user preferences, "
            "project conventions, or other information that should be remembered across sessions. "
        ),
        "input_schema": { ... }
    }

def run(inputs: Dict[str, Any], root: str) -> List[Dict[str, str]]:
    """Save memory content to AGENTS.md in the project root."""
    content = inputs["content"]
    memory_path = Path(root) / MEMORY_FILE
    timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
    entry = f"\n## {timestamp}\n\n{content}\n"
    with open(memory_path, "a", encoding="utf-8") as f:
        f.write(entry)
    # ...

記憶の読み込み

そして、エージェントの起動時に agent/system_prompts.py がこの AGENTS.md を読み込み、システムプロンプトの一部としてLLMに与えます。これにより、エージェントは過去のセッションで得た知識(例:「このプロジェクトではレスポンスの語尾に『だゾ』をつける」など)を思い出すことができます。

agent/system_prompts.py より抜粋:

def base_system_prompt(repo_root: str = None) -> List[Dict[str, str]]:
    # ... (基本プロンプト)

    # Load memory from AGENTS.md if available
    memory_text = ""
    if repo_root:
        memory_path = Path(repo_root) / "AGENTS.md"
        if memory_path.exists():
            memory_content = memory_path.read_text(encoding="utf-8")
            if memory_content.strip():
                memory_text = (
                    "\n\n<agent_memory>\n"
                    "The following contains learnings and important information from previous sessions:\n\n"
                    f"{memory_content}\n"
                    "</agent_memory>"
                )
    # ...
    return [{"type": "text", "text": base_text + memory_text}]

これは、 コンテキストの冒頭(システムプロンプト)に重要な情報を配置することで、「Lost-in-the-middle」問題を回避する実践的なテクニック でもあります。


アプローチ3: 情報を加工して新たな情報を得る (Processing)

複雑なタスクは、そのまま一つのコンテキストで処理しようとすると情報が溢れてしまいます。このような場合、タスクを分割し、別のエージェントに 「委譲」 するアプローチが有効です。

delegate_task ツール

agent/tools/delegate.py は、まさにこの委譲を実現するためのツールです。このツールが呼び出されると、現在のエージェントとは別に、読み取り専用の権限を持つ**「サブエージェント」** が生成され、与えられたサブタスクを独立して実行します。

agent/tools/delegate.py より抜粋:

def definition() -> Dict[str, Any]:
    """Return the tool definition for delegate_task."""
    return {
        "name": "delegate_task",
        "description": (
            "Delegate a specific task to a sub-agent with read-only access. "
            "Use this when you need to:\n"
            "- Perform independent analysis or research in parallel\n"
            "- Break down complex tasks into smaller subtasks\n"
        ),
        "input_schema": { ... }
    }

def run(inputs: Dict[str, Any], repo_root: str, model: str) -> List[Dict[str, str]]:
    # ...
    # Create a read-only sub-agent
    tool_registry = ToolRegistry(repo_root=repo_root, allow_write=False, ...)
    sub_agent = ClaudeOrchestrator(...)

    # Execute the delegated task
    result = sub_agent.run_once(prompt, tool_registry)
    # ...

例えば、「アーキテクチャの視点」と「開発者体験の視点」から同時にプロジェクトを分析させたい場合、エージェントは2つのdelegate_taskを並列で実行できます。サブエージェントがそれぞれの分析を行い、その 結果だけ をメインのエージェントに返すことで、 メインのコンテキストはサブタスクの詳細で汚染されることなく、 最終的な要約作業に集中できます。


まとめ

本記事では、AIエージェント開発におけるコンテキストウィンドウの制約という課題に対し、「コンテキストエンジニアリング」という解決策を3つのアプローチ(Tools, Memory, Processing )に分けて、具体的なコードと共に解説しました。

  • Tools: search_filesread_file で、広大な情報源から必要なものだけをコンテキストに引き込む。
  • Memory: save_memory とシステムプロンプトの連携で、セッションをまたいで知識を維持する。
  • Processing: delegate_task で複雑なタスクを分割・委譲し、コンテキストをクリーンに保つ。

AIとの対話は、単に指示を出すだけでなく、AIが自ら情報を**「取りに行ける」環境をいかにデザインするか** という、体系的なシステム設計へと進化しています。この「環境設計」には無限の工夫の余地があり、 これからのAI開発における最も面白く、創造的な領域の一つ だと感じています。

この記事が、皆さんがAIエージェントを開発する上での一助となれば幸いです。

今回ご紹介したAIコーディングエージェントの全ソースコードは、以下のリポジトリからご覧いただけます。

https://github.com/zawakin/ai-coding-agent-demo

(注:あくまでデモ用に即席で作ったものなので実用というより教育用途での扱いでの利用をお願いします)


XでAI関連の情報を発信していますので、ぜひフォローをお願いします!

https://x.com/zawawahoge

脚注
  1. Lost in the Middle: How Language Models Use Long Contexts(arXiv) ↩︎

株式会社ナレッジワーク

Discussion