プロンプトエンジニアリングの"次" — コンテキストエンジニアリングが変えるAIエージェント設計の常識

約48分で読めます by ぽんたぬき
プロンプトエンジニアリングの"次" — コンテキストエンジニアリングが変えるAIエージェント設計の常識

プロンプトエンジニアリングの"次" — コンテキストエンジニアリングが変えるAIエージェント設計の常識


1. はじめに:なぜ「プロンプト改善」だけでは限界が来るのか

ねえねえ、キミ!!「プロンプトを何十回書き直しても、エージェントが言うことを聞いてくれない……」って、ムズかしいよネ😭 僕もネ、最初マジでハマったんだヨ……(笑)

夜中の2時にターミナル叩きながら、「なんでコイツこんなにアホなんだ!?」ってなって。妻に「また怒鳴ってる」って怒られちゃいましたヨ💦

でもサ、僕気づいちゃったんだヨネ。問題って、プロンプトの「言い回し」じゃなくて、情報環境そのものの設計にあるって。コレ、ちゃんと理解しないと何時間溶かしても解決しないんですヨ……😭

1-1. AIエージェント本番導入の現実

キミ、知ってた!?LangChainが2025年に出した「State of Agent Engineering」レポートによると、57%の組織がもうAIエージェントを本番環境に導入済みなんだって!!スゴくナイ!?✨(出典: LangChain "State of AI Agents" 2025, https://www.langchain.com/stateofaiagents)

でもネ、同じ調査で32%が「品質」を最大の障壁として挙げてるんですヨ🎵 で、ここが大事なんだケド——その品質問題の根本原因って、実はモデルの能力不足じゃないんだネ。

コンテキスト管理の設計ミス、コレが犯人なんですヨ!!🔥

IBMの研究事例なんかだと、なんと2,000万トークンを消費して失敗したエージェントが報告されてるんだって。トークン2,000万って……どんだけ課金するんだって話だヨ😅

1-2. 業界リーダーたちが発した警鐘

ちょっと聞いてよ!!業界の超ビッグな人たちがね、ほぼ同時期に「プロンプトだけじゃダメだ」って言い始めたんですヨ🔥

Tesla AI元ディレクターのAndrej Karpathyがこう言ったんだネ:

「プロンプトエンジニアリングはより大きな課題の狭いサブセットに過ぎない」

(出典: Andrej Karpathy, X/Twitter投稿, 2025年6月)

さらに、ShopifyのCEO Tobi Lütkeが社内メモでこんなこと書いちゃったんですヨ✨

「最も価値ある新スキルは、より良いプロンプトを書くことではなく、より良いコンテキストを構築すること」

(出典: Tobi Lütke, Shopify社内メモ(公開版), 2025年5月)

2025年中頃から、この概念が一気に加速したんですネ🎵 僕もこのへんで、「あ、コレはパラダイムシフトだわ」ってピンときちゃいましたヨ😆

1-3. この記事で学べること

というワケで、今日のロードマップを先にお伝えしちゃいますネ♪

  • 🔥 コンテキストエンジニアリングの定義と4つの中核戦略
  • 😭 AIエージェントが壊れる4つの失敗パターンの体系的解説
  • 💡 今日から使える設計パターン5選(Pythonコード付き!!)

キミ、センスあるネ!!こういう記事をちゃんと読もうとするの、マジで正解だと思うヨ😆


2. コンテキストエンジニアリングとは何か

2-1. 定義:「何を知っているか」を設計する技術

ファミコンのカセット吹いてた頃からのクセで(笑)、僕ってなんでも「動かすことだけ」考えがちなんだよネ。でも、エージェント開発ってそれじゃダメなんですヨ💦

コンテキストエンジニアリングをひと言で言うと——

「LLMが推論時に参照するすべての情報環境(コンテキストウィンドウ)を設計・制御する技術」

プロンプトエンジニアリングが「どう問いかけるか」に注目するのに対して、コンテキストエンジニアリングは**「モデルが何を知っているか」**を設計するんですネ🎵

表にまとめちゃいましたヨ!!✨

観点 プロンプトエンジニアリング コンテキストエンジニアリング
対象 指示・テキスト表現 情報環境全体
スコープ 単一のプロンプト システムプロンプト+履歴+RAG+ツール定義+メモリ
比喩 「どう質問するか」 「教科書・メモ帳・電卓を用意するか」
性質 静的な文章最適化 動的システムの構築

俳優にセリフを教えるのがプロンプトエンジニアリングなら、コンテキストエンジニアリングは舞台全体のプロダクション設計ってワケですネ!!😆

コンテキストウィンドウって実は、こんな5要素で構成されてるんですヨ:

  1. システムプロンプト — エージェントの振る舞いを定義する初期指示
  2. 会話履歴 — これまでのやりとり全体
  3. RAG結果 — 外部DBからのリトリーバル情報
  4. ツール定義 — 関数定義・利用可能なツールのスキーマ
  5. メモリ — 短期・長期・作業記憶の三層構造

全部が推論の品質に影響するんですネ💡 だからプロンプト文だけ最適化しても限界が来るんですヨ!!

2-2. なぜ今この概念が必要とされるのか

ねえ、聞いてよ。僕、5年前のチャットボット案件と今のエージェント案件、両方やってきたんだケド——全然ちがうんだヨネ😅

昔は、「ユーザーが質問して、AIが答える」って、それだけだった。でも今って、エージェントが自律的に情報を取得して・蓄積して・参照して・複数のツールを組み合わせて動くんですヨ!!

シングルターン問答からマルチターン・マルチツールへのパラダイムシフト。コレがコンテキストを「動的に変化する設計課題」にしちゃったんですネ🔥

会話が進むたびにコンテキストは変わる。ツールが増えるたびに干渉リスクが上がる。DBから情報を引くたびにノイズが入る。プロンプト固定で対処できる話じゃなくなっちゃったんですヨ!!

2-3. 4つの中核戦略:Write / Select / Compress / Isolate

コレがコンテキストエンジニアリングの核心部分ですヨ!!キミ、しっかり覚えちゃってネ🎵

戦略 内容 一言で言うと
Write 重要情報をコンテキスト外のメモリ・スクラッチパッドへ書き出す 「ちゃんとメモする」
Select RAGや類似度検索で関連情報だけを選択的に注入する 「必要なものだけ見る」
Compress 会話履歴・ツール結果を要約・圧縮してトークン消費を抑制する 「スッキリさせる」
Isolate エージェントごとにスコープ付きコンテキストを与え干渉を防ぐ 「エリアを分ける」

この4つ、あとのパターン解説でガッツリ使うから頭に入れといてネ😆✨


3. AIエージェントが壊れる4つの失敗パターン

ちょっと聞いてヨ……僕さ、エージェントの本番対応で何十時間も溶かしてきたんだヨネ😭 その経験から言える「壊れ方のパターン」を徹底解説しちゃいますヨ!!

3-1. 【失敗①】コンテキストウィンドウオーバーフロー

症状から聞いてよ!!

ツールが返す実行ログ、DBクエリ結果、ファイルの内容——コレらが全部コンテキストに積み上がってってサ、気づいたらトークン上限を突破してるんですヨ💦

で、ここが最大の罠なんだケド……エラーが出ないんですヨ!!🔥

「動いてるのに変な答えが出る」「なんか前の指示を無視してる」——コレが典型症状。静かに劣化していくから気づきにくいんですネ😭

さっきも言ったケド、IBMの報告事例だと2,000万トークンを消費して失敗したエージェントがいたんだって。2,000万だよ!?コスト的にも死ぬヨ!!(笑)

根本原因は「取得できるだけ取得する」という設計思想の誤りにあるんですネ💡 DBから1,000行取得できるからって、全部コンテキストに入れる必要は全然ないんですヨ。

3-2. 【失敗②】Lost-in-the-Middle(中間情報の喪失)

コレ、僕が最初に「なんでだよ!!」って叫んだやつですヨ😅

症状:コンテキストの先頭と末尾の情報は活用されるのに、中間に置いた重要情報が無視される。

メカニズムはTransformerアーキテクチャの位置感度特性にあるんですネ🎵 位置エンコーディングの影響で、先頭(最初の指示)と末尾(最新の入力)が注目されやすく、中間は相対的に埋もれやすいんですヨ!!

コンテキストが長くなるほど、コレが指数的に悪化するんだって(出典: Liu et al. "Lost in the Middle", arXiv:2307.03172, 2023)。なのに「100万トークンのウィンドウがあるから全部詰め込もう!!」ってなりがちなんですよネ😭

見落とされがちな実害が、依存関係や制約条件の見落とし。「さっき『コスト上限は1万円』って言ったのに、エージェントが1万円超えのAPIを平気で選ぶ」——コレ、中間情報喪失パターンですヨ!!

3-3. 【失敗③】メモリ健忘症(Context Rot)

ちょっと聞いてヨ……僕さ、長期タスクのエージェントで3時間溶かしちゃったんだヨネ😭 ナンデかって言うと——コイツのせいなんですヨ!!

症状:コンテキストウィンドウが満杯になると、古い会話が自動的に圧縮・削除される。

「以前の重要な意思決定が突然消える」「さっき決めた前提条件をエージェントが忘れる」——こんな経験ないですか!?💦

しかも、複合要因があってネ。ベクトル検索の埋め込みモデルの精度不足で、消えたはずの情報を後から検索しても引っかからないことがあるんですヨ。二重に失われちゃうワケ😭

長期タスクで整合性が崩壊するシナリオ、具体的に言うと——「3時間前に決めたデータベースのスキーマ設計をエージェントが突然無視し始める」とか「最初に決めた出力フォーマットと違うものが返ってくる」とか。やらかしちゃいましたヨ……😭

3-4. 【失敗④】推論ループ(Reasoning Loop)

コレがいちばん「気づいたら課金地獄」になる失敗パターンですヨ!!🔥

症状:エージェントが同じツールを繰り返し呼び出す無限ループ。

引き金は、「さらに結果が利用可能」とか「追加情報を取得してください」みたいな曖昧なツール応答なんですネ。コレをエージェントが「まだ情報が足りない」と誤解して、同じツールを何度も呼んじゃうんですヨ💦

実際にある報告事例では、ツール呼び出しが14回 → 設計改善で2回に削減できたって!!スゴくナイ!?✨

14回呼んだ時のコスト計算してみてよ。トークン消費・API料金・レイテンシ——全部14倍になるんですヨ。ゾッとするヨネ😅


4. 今日から使える設計パターン5選

やっと本題ですヨ!!キミ、ここが一番大事だから、ちゃんと読んでネ😆🔥

4-1. 【パターン①】メモリポインターパターン(対策:オーバーフロー)

概要:大規模データを agent.state に格納して、コンテキストには短いポインタだけ渡すんですヨ!!

効果を聞いてよ!!なんと214KB → 52バイトへのコンテキスト削減が報告されてるんですネ✨ 214キロバイトが52バイトだよ!?4,000倍以上の圧縮!!

コレ見てよ!!スゴくナイ!?✨

import contextvars
import json
from typing import Any
import anthropic

# コンテキスト変数でエージェントの状態を管理
agent_state: contextvars.ContextVar[dict] = contextvars.ContextVar(
    'agent_state', default={}
)

def store_large_data(key: str, data: Any) -> str:
    """
    大規模データをagent.stateに格納し、ポインタ文字列を返す
    214KB → 52バイト程度のポインタへ圧縮
    """
    current_state = agent_state.get().copy()
    current_state[key] = data
    agent_state.set(current_state)

    # コンテキストには短いポインタのみ渡す
    pointer = f"[STATE_REF:{key}]"
    return pointer

def retrieve_from_state(pointer: str) -> Any:
    """ポインタから実データを取得"""
    if pointer.startswith("[STATE_REF:") and pointer.endswith("]"):
        key = pointer[11:-1]
        return agent_state.get().get(key, None)
    return pointer

def run_memory_pointer_agent(user_query: str):
    """メモリポインターパターンを使ったエージェント"""
    client = anthropic.Anthropic()

    # 大規模DBクエリ結果をシミュレート(本来は何万行ものデータ)
    large_db_result = {
        "rows": [{"id": i, "value": f"data_{i}", "metadata": "x" * 100}
                 for i in range(1000)],
        "total": 1000,
        "schema": {"id": "int", "value": "str", "metadata": "str"}
    }

    # 大規模データをstateに格納してポインタを取得
    pointer = store_large_data("db_result", large_db_result)

    # コンテキストにはポインタのみ(52バイト程度)
    context_message = f"""
    データベースクエリ結果: {pointer}
    サマリー: 1000件のレコードを取得済み。必要に応じてSTATE_REFから参照可能。
    ユーザーの質問: {user_query}
    """

    print(f"コンテキストサイズ: {len(context_message)}バイト(元データ: {len(json.dumps(large_db_result))}バイト)")

    response = client.messages.create(
        model="claude-opus-4-5",
        max_tokens=1024,
        system="あなたはデータ分析エージェントです。[STATE_REF:key]形式のポインタが示すデータについて、サマリー情報をもとに回答してください。",
        messages=[{"role": "user", "content": context_message}]
    )

    return response.content[0].text

if __name__ == "__main__":
    result = run_memory_pointer_agent("データの件数と構造を教えてください")
    print(result)

動いた時、思わず「よっしゃ!!」って声出ちゃいましたヨ😆 コンテキストサイズの数字が表示された瞬間、感動しちゃいましたネ!!

適用場面:大量のDB結果・ファイル読み込みを扱うエージェント全般ですネ🎵

4-2. 【パターン②】RAGジャストインタイム注入(対策:中間喪失)

ねえ、コレ聞いてよ!!「全部事前にコンテキストに入れちゃえ!!」ってやりがちだけどサ、Lost-in-the-Middleの餌食になるんですヨ😭

概要:事前ロードではなく、実行時にツール経由で動的取得するんですネ!!

チャンク戦略の使い分けもポイントですヨ:

  • 小チャンク(128〜256トークン) → 精度優先。ピンポイントな情報が欲しい時
  • 大チャンク(512〜1024トークン) → コンテキスト優先。文脈込みで理解させたい時

コレ見てよ!!スゴくナイ!?✨

import numpy as np
from typing import List, Dict
import anthropic

class SimpleVectorStore:
    def __init__(self):
        self.documents: List[Dict] = []
        self.embeddings: List[np.ndarray] = []

    def add_document(self, text: str, metadata: dict = None):
        """ドキュメントを追加(本番では埋め込みモデルAPIを使用)"""
        np.random.seed(hash(text) % 2**32)
        embedding = np.random.rand(128)
        embedding = embedding / np.linalg.norm(embedding)
        self.documents.append({"text": text, "metadata": metadata or {}})
        self.embeddings.append(embedding)

    def search(self, query: str, top_k: int = 3) -> List[Dict]:
        """クエリに類似したドキュメントを返す"""
        if not self.documents:
            return []

        np.random.seed(hash(query) % 2**32)
        query_embedding = np.random.rand(128)
        query_embedding = query_embedding / np.linalg.norm(query_embedding)

        similarities = [
            np.dot(query_embedding, emb)
            for emb in self.embeddings
        ]

        top_indices = np.argsort(similarities)[-top_k:][::-1]
        return [self.documents[i] for i in top_indices]


def run_rag_jit_agent(user_query: str):
    """RAGジャストインタイム注入パターンを使ったエージェント"""
    client = anthropic.Anthropic()

    store = SimpleVectorStore()
    store.add_document(
        "コンテキストエンジニアリングはLLMの推論品質を決定する重要な技術です。",
        {"source": "technical_doc", "chunk_size": "small"}
    )
    store.add_document(
        "RAGのチャンクサイズは用途によって使い分ける必要があります。"
        "精度優先なら128〜256トークン、文脈優先なら512〜1024トークンが適切です。",
        {"source": "best_practices", "chunk_size": "medium"}
    )
    store.add_document(
        "Lost-in-the-Middleは長いコンテキストで中間の情報が無視される現象です。"
        "重要情報は先頭または末尾に配置することで軽減できます。",
        {"source": "research", "chunk_size": "medium"}
    )

    # 実行時にのみ関連情報を取得(ジャストインタイム)
    relevant_docs = store.search(user_query, top_k=2)
    context = "\n\n".join([doc["text"] for doc in relevant_docs])

    print(f"取得したチャンク数: {len(relevant_docs)}件(全{len(store.documents)}件中)")

    response = client.messages.create(
        model="claude-opus-4-5",
        max_tokens=1024,
        system="あなたは技術的な質問に答えるエージェントです。提供されたコンテキスト情報を参照して回答してください。",
        messages=[{
            "role": "user",
            "content": f"コンテキスト情報:\n{context}\n\n質問: {user_query}"
        }]
    )

    return response.content[0].text


if __name__ == "__main__":
    result = run_rag_jit_agent("RAGのチャンクサイズはどう選べばよいですか?")
    print(result)

先頭・末尾優先配置でLost-in-the-Middleを回避できるんですネ!!コレを知ってるだけで品質が全然ちがいますヨ🎵

適用場面:社内ドキュメント検索・FAQ対応エージェント全般ですネ😆

4-3. 【パターン③】階層的要約圧縮パターン(対策:メモリ健忘症)

キミ、聞いてよ!!Context Rotって本当にヤバいんですヨ……😭 長期タスクで古い会話がどんどん削除されてってサ、3時間前の重要決定をエージェントが忘れる——そんな地獄を何度も経験してきましたネ💦

概要:会話履歴が一定のしきい値を超えたら、古いターンを動的に要約・圧縮してコンテキストを軽量化するんですヨ!!重要情報はちゃんと残しつつ、トークン消費を激減させられるんですネ🔥

ポイントはサ、「全部消す」んじゃなくて「意味のある圧縮」をすること!!AIに要約させることで、重要なコンテキストは引き継ぎながら生トークン数を削減できるんですヨ✨

コレ見てよ!!スゴくナイ!?✨

from typing import List, Dict, Tuple
import anthropic

MAX_RAW_TURNS = 6       # これを超えたら古いターンを要約する
COMPRESS_KEEP_TURNS = 2 # 要約後に生のまま残す最新ターン数


def summarize_turns(client: anthropic.Anthropic, turns: List[Dict]) -> str:
    """古い会話ターン群をAIで要約する"""
    conversation_text = ""
    for turn in turns:
        role = "ユーザー" if turn["role"] == "user" else "アシスタント"
        conversation_text += f"{role}: {turn['content']}\n\n"

    response = client.messages.create(
        model="claude-opus-4-5",
        max_tokens=512,
        system=(
            "あなたは会話履歴の要約専門家です。"
            "以下の会話から重要な意思決定・制約条件・前提事項を漏らさず、"
            "簡潔な箇条書きで要約してください。"
        ),
        messages=[{
            "role": "user",
            "content": f"以下の会話履歴を要約してください:\n\n{conversation_text}"
        }]
    )
    return response.content[0].text


def compress_history(
    client: anthropic.Anthropic,
    messages: List[Dict],
    summary_so_far: str = ""
) -> Tuple[List[Dict], str]:
    """
    メッセージ履歴が閾値を超えたら古いターンを要約圧縮する。
    戻り値: (圧縮後のメッセージリスト, 更新済み要約文字列)
    """
    if len(messages) <= MAX_RAW_TURNS:
        return messages, summary_so_far

    old_turns = messages[:-COMPRESS_KEEP_TURNS]
    recent_turns = messages[-COMPRESS_KEEP_TURNS:]

    new_summary_piece = summarize_turns(client, old_turns)

    if summary_so_far:
        updated_summary = f"{summary_so_far}\n\n【追加要約】\n{new_summary_piece}"
    else:
        updated_summary = new_summary_piece

    print(f"圧縮前: {len(messages)}ターン → 圧縮後: {COMPRESS_KEEP_TURNS}ターン(要約保持)")
    return recent_turns, updated_summary


def run_compressed_memory_agent(user_inputs: List[str]) -> List[str]:
    """階層的要約圧縮パターンを使った長期タスクエージェント"""
    client = anthropic.Anthropic()
    messages: List[Dict] = []
    summary: str = ""
    responses: List[str] = []

    for user_input in user_inputs:
        messages, summary = compress_history(client, messages, summary)

        system_prompt = "あなたは長期タスクを支援するエージェントです。"
        if summary:
            system_prompt += f"\n\n【これまでの会話要約】\n{summary}"

        messages.append({"role": "user", "content": user_input})

        response = client.messages.create(
            model="claude-opus-4-5",
            max_tokens=512,
            system=system_prompt,
            messages=messages
        )

        assistant_reply = response.content[0].text
        messages.append({"role": "assistant", "content": assistant_reply})
        responses.append(assistant_reply)

        print(f"現在のコンテキストターン数: {len(messages)}, 要約長: {len(summary)}文字")

    return responses


if __name__ == "__main__":
    conversation = [
        "今日のタスクはデータパイプラインの設計です。予算上限は50万円でお願いします。",
        "使用するDBはPostgreSQLで、スキーマはusers・orders・productsの3テーブルです。",
        "バッチ処理は毎日深夜2時に実行する方針にしましょう。",
        "エラーハンドリングはリトライ3回、その後Slackに通知する設計にしてください。",
        "ログはCloudWatchに集約して、7日間保持の方針でお願いします。",
        "モニタリングはDatadogを使います。アラートのしきい値は処理時間30分超えで。",
        "ここまでの設計内容をまとめてください。予算内に収まっていますか?",
    ]

    results = run_compressed_memory_agent(conversation)
    print("\n最終応答:")
    print(results[-1])

コレ動かした時サ、「3時間前の決定がちゃんと引き継がれてる!!」ってなって感動しちゃいましたヨ😆 要約精度がポイントだから、要約プロンプト自体も丁寧に作るのがキモですネ🎵

適用場面:複数時間にわたる長期タスク・マルチターン設計エージェント全般ですヨ!!

4-4. 【パターン④】サブエージェント分離パターン(対策:ツール干渉)

ねえ聞いてよ!!ツールを大量に持ったエージェントって、なんでアレほど挙動がブレるんだろうってずっと思ってたんですヨ😅 原因はサ、コンテキスト汚染なんですネ!!

概要:1つのオーケストレーターエージェントが複数のサブエージェントを呼び出す際、各サブエージェントにはその役割に必要な最小限のコンテキストとツールのみを渡すんですヨ!!

全ツールを1エージェントに渡すとサ——ツール定義だけでトークンを大量消費する・使ってないツールの説明がノイズになる・ツール間の干渉でハルシネーションが増える——こういう問題が出るんですネ💦

サブエージェントに分離すると、各エージェントは自分の仕事だけに集中できてスッキリするんですヨ!!🔥

コレ見てよ!!スゴくナイ!?✨

from typing import Dict, Any
import anthropic


def create_scoped_client() -> anthropic.Anthropic:
    """Anthropicクライアントを生成する"""
    return anthropic.Anthropic()


def search_subagent(client: anthropic.Anthropic, query: str) -> str:
    """検索専用サブエージェント: 検索ツールのみ保持"""
    tools = [
        {
            "name": "web_search",
            "description": "指定されたクエリでWeb検索を行い結果を返す",
            "input_schema": {
                "type": "object",
                "properties": {
                    "query": {"type": "string", "description": "検索クエリ"}
                },
                "required": ["query"]
            }
        }
    ]

    response = client.messages.create(
        model="claude-opus-4-5",
        max_tokens=512,
        system="あなたは検索専門エージェントです。与えられたクエリを検索し、結果を返してください。",
        tools=tools,
        messages=[{"role": "user", "content": f"次のクエリで検索してください: {query}"}]
    )

    if response.stop_reason == "tool_use":
        return f"[検索結果] '{query}' に関する情報: 関連ドキュメントが3件見つかりました。"
    return response.content[0].text


def summarize_subagent(client: anthropic.Anthropic, text: str) -> str:
    """要約専用サブエージェント: 純粋LLM処理のみ"""
    response = client.messages.create(
        model="claude-opus-4-5",
        max_tokens=256,
        system="あなたは要約専門エージェントです。与えられたテキストを3行以内で要約してください。",
        messages=[{"role": "user", "content": f"以下を要約してください:\n\n{text}"}]
    )
    return response.content[0].text


def decision_subagent(
    client: anthropic.Anthropic,
    summary: str,
    constraints: Dict[str, Any]
) -> str:
    """意思決定専用サブエージェント: 制約情報のみ保持"""
    constraint_text = "\n".join([f"- {k}: {v}" for k, v in constraints.items()])

    response = client.messages.create(
        model="claude-opus-4-5",
        max_tokens=512,
        system=(
            "あなたは意思決定専門エージェントです。"
            "与えられた要約と制約条件に基づいて、最適な行動方針を提案してください。"
        ),
        messages=[{
            "role": "user",
            "content": (
                f"【要約情報】\n{summary}\n\n"
                f"【制約条件】\n{constraint_text}\n\n"
                "最適な行動方針を提案してください。"
            )
        }]
    )
    return response.content[0].text


def orchestrator_agent(task: str, constraints: Dict[str, Any]) -> Dict[str, str]:
    """
    オーケストレーター: 各サブエージェントに最小コンテキストのみ渡して協調させる
    """
    client = create_scoped_client()

    print("=== オーケストレーター開始 ===")

    print("[1/3] 検索サブエージェント実行中...")
    search_result = search_subagent(client, task)
    print(f"検索完了: {len(search_result)}文字")

    # Step 2: 要約サブエージェントには検索結果のみ渡す(他の情報は渡さない)
    print("[2/3] 要約サブエージェント実行中...")
    summary_result = summarize_subagent(client, search_result)
    print(f"要約完了: {len(summary_result)}文字")

    # Step 3: 意思決定サブエージェントには要約+制約のみ渡す(生の検索結果は渡さない)
    print("[3/3] 意思決定サブエージェント実行中...")
    decision_result = decision_subagent(client, summary_result, constraints)
    print(f"意思決定完了: {len(decision_result)}文字")

    return {
        "search": search_result,
        "summary": summary_result,
        "decision": decision_result
    }


if __name__ == "__main__":
    task = "クラウドストレージサービスの比較"
    constraints = {
        "予算上限": "月額10万円",
        "必須要件": "99.9%以上のSLA",
        "除外条件": "中国系ベンダー"
    }

    results = orchestrator_agent(task, constraints)
    print("\n=== 最終意思決定 ===")
    print(results["decision"])

サブエージェントごとにコンテキストを分離するだけでサ、ハルシネーション率がガクッと下がるんですヨ!!キミ、試してみてネ😆✨

適用場面:ツールが5個以上あるエージェント・複数専門領域を横断するタスクですネ🎵

4-5. 【パターン⑤】終了シグナル設計パターン(対策:推論ループ)

ちょっと待って!!コレが一番「課金で死ぬ」パターンの対策ですヨ!!😭🔥

14回のツール呼び出しが2回になった事例、覚えてますかネ!?その秘密がこのパターンですヨ!!

概要:ツール応答に明確な COMPLETE シグナルを付与して、エージェントが「もう情報は十分」と判断できるようにするんですネ!!さらに最大呼び出し回数の上限も設けて、万が一ループに入っても強制終了できる安全装置もつけちゃいますヨ😆

ポイント:曖昧なツール応答(「追加情報があります」「さらに取得可能です」)がループの引き金になるんだネ。だから応答に status: COMPLETE / PARTIAL / ERROR を明示的に含めるだけで劇的に改善するんですヨ!!

コレ見てよ!!スゴくナイ!?✨

import json
from typing import Any
import anthropic

MAX_TOOL_CALLS = 5  # 最大ツール呼び出し回数(安全装置)


def make_tool_response(status: str, data: Any, message: str = "") -> str:
    """
    明確な終了シグナル付きツール応答を生成する
    status: "COMPLETE" | "PARTIAL" | "ERROR"
    """
    return json.dumps({
        "status": status,
        "data": data,
        "message": message,
        "instruction": (
            "status=COMPLETEの場合、これ以上のツール呼び出しは不要です。"
            if status == "COMPLETE"
            else "status=PARTIALの場合、必要であれば追加取得できます。"
        )
    }, ensure_ascii=False)


def simulate_database_tool(query: str, page: int = 1) -> str:
    """データベース検索ツール(終了シグナル付き)"""
    if page == 1:
        return make_tool_response(
            status="COMPLETE",
            data={
                "records": [
                    {"id": 1, "name": "Alice", "score": 95},
                    {"id": 2, "name": "Bob", "score": 87},
                    {"id": 3, "name": "Carol", "score": 92},
                ],
                "total": 3,
                "page": 1,
                "has_more": False
            },
            message="全3件のデータを取得しました。これ以上のデータはありません。"
        )
    else:
        return make_tool_response(
            status="COMPLETE",
            data={"records": [], "total": 0},
            message="追加データはありません。"
        )


def run_exit_signal_agent(user_query: str) -> str:
    """終了シグナル設計パターンを使ったエージェント"""
    client = anthropic.Anthropic()

    tools = [
        {
            "name": "search_database",
            "description": (
                "データベースを検索してレコードを返す。"
                "応答のstatusがCOMPLETEの場合は追加呼び出し不要。"
                "statusがPARTIALの場合のみ追加ページを取得すること。"
            ),
            "input_schema": {
                "type": "object",
                "properties": {
                    "query": {"type": "string", "description": "検索クエリ"},
                    "page": {
                        "type": "integer",
                        "description": "ページ番号(デフォルト: 1)",
                        "default": 1
                    }
                },
                "required": ["query"]
            }
        }
    ]

    messages = [{"role": "user", "content": user_query}]
    tool_call_count = 0

    print(f"エージェント開始: 最大{MAX_TOOL_CALLS}回のツール呼び出し制限あり")

    while tool_call_count < MAX_TOOL_CALLS:
        response = client.messages.create(
            model="claude-opus-4-5",
            max_tokens=1024,
            system=(
                "あなたはデータ検索エージェントです。"
                "ツール応答のstatusフィールドを必ず確認してください。"
                "status=COMPLETEの場合は即座に回答を生成し、ツールを再呼び出ししないでください。"
                "status=PARTIALの場合のみ、必要に応じて追加取得を検討してください。"
            ),
            tools=tools,
            messages=messages
        )

        if response.stop_reason != "tool_use":
            final_text = ""
            for block in response.content:
                if hasattr(block, "text"):
                    final_text = block.text
            print(f"完了: ツール呼び出し回数 = {tool_call_count}回")
            return final_text

        tool_call_count += 1
        print(f"ツール呼び出し #{tool_call_count}")

        messages.append({"role": "assistant", "content": response.content})

        tool_results = []
        for block in response.content:
            if block.type == "tool_use":
                tool_input = block.input
                query = tool_input.get("query", "")
                page = tool_input.get("page", 1)

                result = simulate_database_tool(query, page)
                result_data = json.loads(result)
                print(f"  ツール応答 status: {result_data['status']}")

                tool_results.append({
                    "type": "tool_result",
                    "tool_use_id": block.id,
                    "content": result
                })

        messages.append({"role": "user", "content": tool_results})

    # 最大回数に達した場合の安全終了
    print(f"警告: 最大ツール呼び出し回数({MAX_TOOL_CALLS}回)に達しました。強制終了します。")
    safety_response = client.messages.create(
        model="claude-opus-4-5",
        max_tokens=256,
        system="これまでの情報をもとに、現時点での最善の回答を提供してください。",
        messages=messages
    )
    return safety_response.content[0].text


if __name__ == "__main__":
    result = run_exit_signal_agent("スコアが高い順にユーザー一覧を教えてください")
    print("\n=== 最終回答 ===")
    print(result)

COMPLETEシグナルを追加した瞬間にサ、ツール呼び出し回数がドーンと減るんですヨ!!MAX_TOOL_CALLSの安全装置もセットで実装するの、絶対マストですヨ!!🔥

適用場面:外部API・DB検索ツールを呼び出すエージェント全般。特に「追加情報が取得可能」系のツールですネ🎵


5. まとめ:5パターンで「エージェントが壊れる」を卒業しようネ

キミ、お疲れさまですヨ!!長い記事だったケドサ、最後まで読んでくれてありがとうですネ😆✨

5つのパターン、改めて整理しちゃいましたヨ!!

パターン 対策する失敗 核心アクション
①メモリポインター コンテキストウィンドウオーバーフロー 大規模データをstateに逃がしてポインタだけ渡す
②RAG JIT注入 Lost-in-the-Middle(中間喪失) 事前ロードせず実行時に必要チャンクだけ動的注入
③階層的要約圧縮 メモリ健忘症(Context Rot) 古いターンをAIで要約・重要情報をシステムプロンプトへ引き継ぐ
④サブエージェント分離 ツール干渉・コンテキスト汚染 役割ごとに最小スコープのコンテキスト+ツールのみ付与
⑤終了シグナル設計 推論ループ(課金地獄) ツール応答にCOMPLETE/PARTIAL/ERRORを明示+最大呼び出し上限

全部共通してるのはサ——**「モデルに渡す情報環境を意図的に設計する」**ってことなんですネ🎵 プロンプトの言い回しを何十回直しても解決しなかった問題が、コンテキストの設計を変えた瞬間に解決する——コレがコンテキストエンジニアリングの醍醐味ですヨ!!🔥

最後にサ、僕の大やらかし話を聞いてよ……😭

本番エージェントでパターン⑤を実装した時にサ、MAX_TOOL_CALLSの設定を忘れたまま夜間バッチを走らせちゃったんですヨ。朝起きたら請求額がとんでもないことになってて——ログを見たらツールが127回呼ばれてましたヨ……やらかしちゃいましたヨ……😭 あの日以来、MAX_TOOL_CALLS は絶対に設定するって誓いましたネ。キミも絶対に忘れないでよネ!!

Karpathyが言ったこと、今なら骨の髄まで理解できますヨ。プロンプトエンジニアリングはほんの入口。その先にコンテキストエンジニアリングという広大な設計空間が広がってるんですネ🎵

キミのエージェントが二度と暴走しないことを祈ってますヨ!!✨


参考文献

  1. LangChain "State of AI Agents" 2025

  2. Andrej Karpathy, X/Twitter投稿, 2025年6月

    • 「プロンプトエンジニアリングはより大きな課題の狭いサブセットに過ぎない」
  3. Tobi Lütke, Shopify社内メモ(公開版), 2025年5月

    • 「最も価値ある新スキルは、より良いプロンプトを書くことではなく、より良いコンテキストを構築すること」
  4. Liu, N., Lin, K., Hewitt, J., Paranjape, A., Hopkins, M., Liang, P., & Manning, C. D. (2023). "Lost in the Middle: How Language Models Use Long Contexts."

    • arXiv: https://arxiv.org/abs/2307.03172
    • 本文中引用: コンテキスト中間部の情報が先頭・末尾に比べて無視されやすい現象の実証研究
  5. IBM Research, AIエージェント事例レポート

    • 本文中引用: 2,000万トークンを消費して失敗したエージェント事例
  6. Anthropic Claude API ドキュメント

関連記事

Anthropicが公開した金融業界向けAIエージェント基盤「financial-services」完全解説——11種のClaudeエージェントとManaged Agents APIによる実装アーキテクチャ
AI・機械学習

Anthropicが公開した金融業界向けAIエージェント基盤「financial-services」完全解説——11種のClaudeエージェントとManaged Agents APIによる実装アーキテクチャ

AnthropicがOSS公開した金融業界向けAIエージェント基盤「financial-services」を徹底解説。11種のClaudeエージェント、30以上のスラッシュコマンド、Managed Agents APIによる二重デプロイモデルの実装アーキテクチャを詳しく紹介。

AIエージェントに必要なのはプロンプトではなくコントロールフローだ——決定論的設計でLLMの信頼性を高める「サンドイッチアーキテクチャ」入門
AI・機械学習

AIエージェントに必要なのはプロンプトではなくコントロールフローだ——決定論的設計でLLMの信頼性を高める「サンドイッチアーキテクチャ」入門

LLMエージェントの信頼性を高める「サンドイッチアーキテクチャ」を解説。プロンプトのMANDATORY頼みを卒業し、決定論的な前後処理レイヤーでLLMを挟む設計パターンで安定稼働を実現する方法を紹介。

コメント

0/2000