LLMエージェント本番2年で踏んだ地雷と、2026年のリアルなアーキテクチャ

「これで全部解決する」と思って本番投入したら現実は甘くなかった。7本のエージェントを2年稼働させて見えてきた、マルチエージェント・メモリ・コスト管理の実践的な知見をまとめました。

LLMエージェント本番運用2年で学んだ、2026年の現実的なアーキテクチャ設計

正直に言うと、2年前にLLMエージェントを本番に投入したとき、「これで全部解決する」と思ってた。今から振り返ると恥ずかしいくらい楽観的だった。

うちのチームは2024年初頭からカスタマーサポート自動化と社内ナレッジ検索にLLMエージェントを本番投入しはじめて、2026年現在は7本のエージェントが実稼働している。その2年間で踏んだ地雷と、それをどう乗り越えたかをここに書いておく。同じ轍を踏んでほしくないので。


2026年時点のLLMエージェント全体像:何が変わったか

2026年のLLM界隈は、正直「モデルの性能競争」から「いかに本番で安定して動かすか」に議論の中心が移ってきた感がある。GPT-4o Turbo・Claude 3.7 Sonnet・Gemini 2.5 Proあたりが主要モデルとして定着して、モデルそのものよりアーキテクチャ設計と運用の質が差を生む時代になってる。

実際にうちで測定したモデル別のパフォーマンスデータがこれ。

xychart-beta
  title "モデル別レイテンシ比較(うちの本番環境・p95、ms)"
  x-axis ["GPT-4o Turbo", "Claude 3.7 Sonnet", "Gemini 2.5 Pro", "Llama 3.3 70B(ローカル)"]
  y-axis "レイテンシ(ms)" 0 --> 4000
  bar [2800, 2200, 3100, 1400]

ローカルLLMの速さが際立つけど、精度とのトレードオフは依然としてある。ローカルLLM本番投入でハマった話|量子化・マルチGPU・コスト削減の実録2026にも書いたけど、ローカル運用は「安くて速い」だけじゃなくてインフラ維持コストが重くのしかかってくる。

2026年で特に変わったのはエージェントフレームワークの成熟度だと思う。LangChain 0.3系、LlamaIndex 0.12、そして新顔のLangGraph Cloudが本番ユースケースに耐えるレベルに仕上がってきた。以前は自前でツール呼び出しループを書いてたけど、今はだいぶ楽になってる。各フレームワークの現時点での評価をまとめるとこんな感じ。

フレームワークバージョン(2026/6)マルチエージェントストリーミング本番安定性うちの評価
LangGraph0.4.xAメイン採用
LlamaIndex Workflows0.12.xA-RAG特化で使用
AutoGen v33.2.xB+検証中
CrewAI1.5.xB一部採用
Semantic Kernel2.xA-.NETチームが使用

個人的にはLangGraphが今一番本番に向いてると感じてる。ただし0.4.x以降で設計思想がかなり変わってるので、古いブログ記事を参考にすると痛い目を見る。これは後述する。


マルチエージェント設計で2回本番を止めた話

2024年後半に「マルチエージェント最高じゃん」と思って設計したシステムが、本番で2回サービス停止を引き起こした。今でも思い出すだけで胃が痛い。

当時の構成はこんな感じ。

flowchart TB
    User[ユーザー] --> Orchestrator[オーケストレーターエージェント]
    Orchestrator --> Search[検索エージェント]
    Orchestrator --> Analysis[分析エージェント]
    Orchestrator --> Writer[ライターエージェント]
    Search --> VectorDB[(ベクトルDB)]
    Analysis --> ExternalAPI[外部API]
    Writer --> OutputFormatter[出力整形]
    OutputFormatter --> User
    Search -.->|エラー時| Orchestrator
    Analysis -.->|エラー時| Orchestrator

これの何が問題だったか。エラーハンドリングが甘かったのと、トークン消費の爆発だ。

オーケストレーターが中間エージェントのエラーを受け取ったとき、コンテキストをそのまま引き継いでリトライするもんだから、1リクエストで平均3.2回の不要なLLM呼び出しが発生してた。トークン消費が想定の4倍になって、月のAPI費用が当初見積もりの380%になった月があった。これは本当に焦った。

# 当時の地雷コード(再現)
async def orchestrate(task: str, context: dict) -> str:
    result = await search_agent.run(task, context)  # コンテキストを丸ごと渡してた
    if result.error:
        # エラー時もコンテキストをそのまま引き継いでリトライ
        result = await search_agent.run(task, context)  # ← これが問題
    ...

今の設計はこうなってる。

# 2026年版:トークン消費を意識した設計
from langchain_core.messages import HumanMessage, SystemMessage
from typing import TypedDict
import asyncio

class AgentState(TypedDict):
    task: str
    compressed_context: str  # 圧縮済みコンテキストのみ引き継ぐ
    results: list[dict]
    retry_count: int
    max_retries: int

async def orchestrate_with_budget(
    state: AgentState,
    token_budget: int = 8000
) -> AgentState:
    """
    トークンバジェットを管理しながらエージェントを制御する。
    コンテキスト圧縮とリトライ上限を設けることで
    トークン爆発を防ぐ。
    """
    if state["retry_count"] >= state["max_retries"]:
        # リトライ上限に達したらグレースフルに失敗させる
        return {**state, "results": state["results"] + [{"error": "max_retries_exceeded"}]}
    
    # コンテキストを圧縮してから渡す
    compressed = await compress_context(
        state["compressed_context"],
        max_tokens=token_budget // 4  # バジェットの25%をコンテキストに割り当て
    )
    
    try:
        result = await search_agent.ainvoke({
            "task": state["task"],
            "context": compressed  # 圧縮済みのみ
        })
        return {
            **state,
            "results": state["results"] + [result],
            "compressed_context": compressed  # 圧縮版を引き継ぐ
        }
    except Exception as e:
        return {
            **state,
            "retry_count": state["retry_count"] + 1
        }

地味だけど、コンテキスト圧縮をちゃんと設計に組み込むのが本番で一番効いた変更だった。AIエージェント本番運用6ヶ月で火を噴いた話でも触れてるけど、状態管理の設計ミスがエージェントの失敗の大半を占めてる。

あと今になって思うのは、マルチエージェントはそもそも「必要になってから」導入すべきだったということ。最初からオーケストレーター構成にしようとしたのが根本的な間違いで、シンプルな単一エージェントから始めて、実際のボトルネックが見えてから分解する方が絶対に楽だった。


RAGの「惜しい」を超えた2026年の構成

「RAGを入れたのになんかイマイチ」ってなったことありませんか?うちも1年以上それで悩んでた。

RAG本番2年で「なんか惜しい」を乗り越えた話に詳細は書いたけど、2026年現在でうちが落ち着いた構成をアーキテクチャ図で示すとこんな感じ。

flowchart TD
    subgraph Ingestion["データ取り込みパイプライン"]
        Raw[生ドキュメント] --> Chunker[適応チャンキング]
        Chunker --> MetaExtractor[メタデータ抽出LLM]
        MetaExtractor --> Embedder[マルチモデルEmbedding]
        Embedder --> VDB[(ベクトルDB\nPinecone v3)]
        MetaExtractor --> KG[(知識グラフ\nNeo4j)]
    end

    subgraph Retrieval["ハイブリッド検索"]
        Query[ユーザークエリ] --> QueryExpander[クエリ拡張LLM]
        QueryExpander --> DenseSearch[Dense検索]
        QueryExpander --> SparseSearch[Sparse検索BM25]
        QueryExpander --> GraphSearch[グラフ検索]
        DenseSearch --> Reranker[Cross-Encoder Reranker]
        SparseSearch --> Reranker
        GraphSearch --> Reranker
        Reranker --> Context[最終コンテキスト]
    end

    subgraph Generation["生成"]
        Context --> LLM[GPT-4o Turbo]
        LLM --> Answer[回答]
        Answer --> Evaluator[自動評価LLM]
    end

    VDB --> DenseSearch
    KG --> GraphSearch

ポイントは3つある。

1. 適応チャンキング(Adaptive Chunking)

Fixedサイズでチャンクを切るのをやめた。2025年に登場した意味単位で区切るセマンティックチャンキングをLLM補助でやってる。実装コストは高いけど、チャンクの「中途半端に切れる問題」が激減した。検索精度がF1で0.67→0.82に上がった。これは体感でも明らかにわかるレベルの改善で、ここに工数をかけた価値は十分あった。

2. クエリ拡張の導入

async def expand_query(original_query: str) -> list[str]:
    """
    元のクエリから複数の検索クエリを生成する。
    HyDE(仮説ドキュメント生成)とサブクエリ分解を組み合わせた構成。
    """
    prompt = f"""以下のクエリに対して、3つのアプローチで検索クエリを生成してください:
1. 元のクエリをそのまま
2. 関連する専門用語に言い換えたクエリ  
3. このクエリに答えうる仮説的な文書の最初の一文

クエリ: {original_query}

JSON配列で返してください。"""
    
    response = await llm.ainvoke(prompt)
    return parse_json_response(response.content)

# 実際の使用例
queries = await expand_query("AWSのコスト削減方法")
# 出力例:
# [
#   "AWSのコスト削減方法",
#   "AWS EC2 Reserved Instances Savings Plans 費用最適化",
#   "AWSクラウドコストを30%削減した実践的な手順として、まずCompute Optimizerを..."
# ]

3. Cross-Encoder Rerankingで最終精度を上げる

Bi-Encoderで候補を100件取ってきて、Cross-Encoderで上位10件に絞る2段階構成にした。レイテンシは増えるけど、精度向上の費用対効果が高い。Dense検索だけで満足してた2024年は遠い昔で、クエリ拡張とRerankingまでセットにして初めて「惜しい」から脱出できた。ハイブリッドRAGはもうほぼ必須だと思ってる。


コスト管理:月のAPI費用が「なぜかわからない」状態から脱出した話

2024年末、LLM API費用が月130万円になった月があった。チームメンバーが「なんかLLM呼び出しが多い」と言ったのがきっかけで調査したら、ある機能のリトライロジックが無限ループ的になってたのが発覚した。「なんか多い」でしか気づけないってどういう状態だよって話なんだけど、それが当時の実態だった。

それ以来、LLMコスト可視化をCI/CDに組み込んで毎日計測するようにした。実際のコスト推移がこれ。

xychart-beta
  title "月間LLM API費用推移(万円)"
  x-axis ["2024/9", "2024/10", "2024/11", "2024/12", "2025/1", "2025/2", "2025/3", "2025/6", "2025/9", "2025/12", "2026/3"]
  y-axis "費用(万円)" 0 --> 160
  line [45, 62, 89, 130, 78, 71, 68, 65, 58, 52, 48]

2024/12のピークから2026/3にかけて63%削減できてる。やったことの核心がこれ。

# トークン使用量のリアルタイム追跡
from dataclasses import dataclass, field
from datetime import datetime
import asyncio

@dataclass
class TokenBudgetTracker:
    daily_budget: int = 5_000_000  # 1日500万トークン
    current_usage: int = 0
    alerts_sent: list = field(default_factory=list)
    
    async def track_and_alert(self, tokens_used: int, feature: str) -> bool:
        self.current_usage += tokens_used
        usage_ratio = self.current_usage / self.daily_budget
        
        # 70%・90%・100%でアラート
        for threshold in [0.7, 0.9, 1.0]:
            if usage_ratio >= threshold and threshold not in self.alerts_sent:
                await self._send_slack_alert(
                    f"⚠️ LLMトークン使用量が{int(threshold*100)}%に達しました\n"
                    f"機能: {feature}\n"
                    f"使用量: {self.current_usage:,} / {self.daily_budget:,}"
                )
                self.alerts_sent.append(threshold)
        
        # バジェット超過時はリクエストを拒否
        return usage_ratio < 1.0
    
    async def _send_slack_alert(self, message: str):
        # Slack Webhook呼び出し
        ...

# ミドルウェアとしてLLM呼び出しをラップ
tracker = TokenBudgetTracker()

async def llm_call_with_budget(prompt: str, feature: str) -> str:
    # 事前にトークン数を概算
    estimated_tokens = len(prompt.split()) * 1.3  # 簡易見積もり
    
    allowed = await tracker.track_and_alert(int(estimated_tokens), feature)
    if not allowed:
        raise BudgetExceededException(f"Daily token budget exceeded for {feature}")
    
    response = await llm.ainvoke(prompt)
    # 実際のトークン数で補正
    actual_tokens = response.response_metadata.get("token_usage", {}).get("total_tokens", 0)
    await tracker.track_and_alert(actual_tokens - int(estimated_tokens), feature)
    
    return response.content

これに加えて、プロンプトキャッシングの活用も大きかった。Claude 3.7 SonnetのPrompt Cachingは、長いシステムプロンプトの繰り返し呼び出しを90%以上削減できる。うちの場合、RAGのシステムプロンプトが2000トークン近くあったので、キャッシュヒット率を上げるだけで月14万円削減できた。地味に一番コスパが良かった改善かもしれない。

プロンプト設計はセンスじゃない——チーム運用2年で見えた構造化の話に書いた通り、プロンプトを構造化してキャッシュしやすい形にするのは設計段階から意識する必要がある。「あとからやろう」は絶対後悔する、というのはコスト管理全般に言えることで、月130万円の請求書を見てから動いた過去の自分を殴りたい。


2026年のLLMエージェント評価・モニタリング体制

「エージェントが正しく動いてるかどうか、どうやって確認してますか?」——これ、チームで一番議論になった話題だった。

正直まだ完全には解決してないけど、2026年現在うちが採用してる評価パイプラインを共有する。

sequenceDiagram
    participant Prod as 本番エージェント
    participant Logger as 構造化ロガー
    participant Eval as 評価LLM(Claude)
    participant Dashboard as Grafanaダッシュボード
    participant Slack as Slackアラート
    
    Prod->>Logger: 入力・出力・中間ステップを記録
    Logger->>Eval: 非同期で評価リクエスト
    Note over Eval: 以下を採点(1-5):\n・回答の正確性\n・ハルシネーション有無\n・ツール呼び出しの適切さ\n・レイテンシ
    Eval->>Logger: 評価スコア返却
    Logger->>Dashboard: メトリクス送信
    Dashboard->>Slack: スコア低下時アラート

評価プロセスの核心部分:

from pydantic import BaseModel
from enum import IntEnum

class HallucinationLevel(IntEnum):
    NONE = 0
    MINOR = 1  # 小さな不正確さ
    MODERATE = 2  # 明確な誤情報
    SEVERE = 3  # 完全に作り話

class AgentEvaluation(BaseModel):
    accuracy_score: float  # 0.0-1.0
    hallucination_level: HallucinationLevel
    tool_usage_appropriate: bool
    answer_completeness: float  # 0.0-1.0
    reasoning: str  # 評価理由

async def evaluate_agent_response(
    user_query: str,
    agent_response: str,
    retrieved_context: list[str],
    tool_calls: list[dict]
) -> AgentEvaluation:
    """
    LLM-as-a-Judgeパターンで回答を評価する。
    本番では非同期で実行して、レイテンシに影響させない。
    """
    eval_prompt = f"""以下のエージェント応答を評価してください。
    
【ユーザークエリ】
{user_query}

【取得されたコンテキスト】
{chr(10).join(retrieved_context[:3])}  # 上位3件のみ渡す

【エージェントの回答】
{agent_response}

【実行されたツール呼び出し】
{tool_calls}

以下の観点で評価してください:
1. accuracy_score: コンテキストに基づいた正確性 (0.0-1.0)
2. hallucination_level: ハルシネーションレベル (0=なし, 1=軽微, 2=中程度, 3=深刻)
3. tool_usage_appropriate: ツール使用が適切だったか (true/false)
4. answer_completeness: 回答の完全性 (0.0-1.0)
5. reasoning: 評価理由(日本語で50字以内)

JSON形式で返してください。"""
    
    response = await eval_llm.ainvoke(eval_prompt)
    return AgentEvaluation.model_validate_json(response.content)

この評価パイプラインを入れてから、回答品質の低下を平均4時間以内に検知できるようになった。以前は「なんかユーザーからのクレームが増えた気がする」レベルでしか気づけなかったので、これは地味にマジで助かってる。

評価に使うモデルは意図的に本番とは別のモデルにしてる。Claude 3.7 SonnetをGPT-4oで評価する、みたいな構成だ。同じモデルで評価すると自分に甘い評価になりがちなので。個人的には、LLM-as-a-Judgeは参照回答との比較が難しいオープンエンドな質問に特に有効だと感じてる。一方で、数値計算や事実確認が明確なタスクは従来のルールベース評価と組み合わせた方がいい。このあたりは好みというよりユースケース次第で使い分けるのが正解だと思う。


まとめ

2年間のLLMエージェント本番運用で本当に役立った知見を整理するとこうなる。

  1. コンテキスト管理とトークンバジェットを設計の最優先に置く — 「あとからやろう」は絶対後悔する。月130万円の請求書を見てから動いた過去の自分を殴りたい

  2. ハイブリッドRAG(Dense + Sparse + Graph)はほぼ必須 — Dense検索だけで満足してた2024年は遠い昔。クエリ拡張とRerankingまでセットにして初めて「惜しい」から脱出できた

  3. LLM-as-a-Judgeによる自動評価を本番から切り離して非同期で走らせる — 評価を同期処理にするとレイテンシが死ぬので非同期前提で設計する

  4. マルチエージェントは「必要になってから」導入する — 最初からマルチエージェントにしようとするのは罠。シンプルな単一エージェントから始めて、実際のボトルネックが見えてから分解する

  5. フレームワークはLangGraphが今一番安定してる — ただし0.4.x以降で設計思想がかなり変わってるので、古い記事を参考にすると泣きを見る

次にやること:

  • Anthropicの新しいInteroperability Protocolに対応したツール呼び出し統一化(MCPまだ検証中)
  • ファインチューニング vs RAGのハイブリッド構成の本番実験(LoRAファインチューニングを本番投入して6ヶ月の続編になる予定)
  • LLMエージェントのA/Bテスト基盤整備(今は感覚で「この方が良さそう」判断してる部分があって、それを定量化したい)

LLMエージェントの本番運用、みなさんのチームではどうやって品質担保してますか?特に評価パイプラインは各社で全然違うアプローチを取ってると思うので、ぜひ聞いてみたい。

U

Untanbaby

ソフトウェアエンジニア|AWS / クラウドアーキテクチャ / DevOps

10年以上のIT実務経験をもとに、現場で使える技術情報を発信しています。 記事の誤りや改善点があればお問い合わせからお気軽にご連絡ください。

関連記事