n8nのAIエージェントノードでのツールの使い方が悪くて無駄なトークンを使っていた件

n8nのAIエージェントノードでのツールの使い方が悪くて無駄なトークンを使っていた件

2025.12.17

自然言語からSQLを生成、実行、結果の表示をn8nで実験していて、Too many tokensエラーで動かなくなりました。

大きかった原因ですが、RAGの検索結果がそのままLLMのコンテキストに入っていたことでした。

以下のような感じ。

スクリーンショット 2025-12-16 16.02.26

試していたこと

n8nのAIエージェントノードとGemini File Search APIを組み合わせ、スキーマ情報をRAGで取得してSQLを生成する仕組みに取り組んでいました。

Gemini File Search APIでRAG構築を試した時の記事

https://dev.classmethod.jp/articles/gemini-api-file-search-tool-rag/

スキーマ情報(テーブル定義やカラム情報)、ナレッジ情報はGemini File Searchのストアーに保存し、API経由でRAG検索するようにしました。

曖昧な言葉があるとユーザーに聞き返すようにしていたため、会話履歴もメモリーに保存されるようになっています。

当初の設計:

[ユーザー質問] → [AIエージェント] → Tool: Gemini File Search / Memory: Simple Memory(会話履歴)
                      ↓
                スキーマ情報を取得
                      ↓
                SQLを生成

n8n(およびLangChainなどのフレームワーク)では、Toolの実行結果はAIエージェントのコンテキストに追加されます。

例)

コンテキスト内訳 トークン数
システムプロンプト 2000
会話履歴など 3000
Tool結果(RAG) 20000
ユーザー質問 100

繰り返しやりとりしていると、Toolの結果をまたLLMのインプットに利用しているのでさらにトークン量が膨れ上がる といったことがよく起こっていました。

Toolの結果について

Gemini File Search APIのレスポンスには、必要な情報以外にも大量のメタデータが含まれます。

{
  "candidates": [
    {
      "content": { "parts": [{ "text": "..." }] },
      "groundingMetadata": {
        "groundingChunks": [
          { "retrievedContext": { "text": "大量のドキュメント..." } },
          { "retrievedContext": { "text": "大量のドキュメント..." } },
          // さらに続く...
        ],
        "groundingSupports": [...]
      }
    }
  ],
  "usageMetadata": {...}
}

このレスポンス全体がToolの結果としてAIに渡されていたのです。

改善後の設計:

[ユーザー質問] → [Gemini File Search] → [圧縮] → [AIエージェント]

RAGをToolから外し、ワークフローで事前に実行 → 圧縮するように変更しました。

スクリーンショット 2025-12-17 9.53.43

ToolではなくHTTP Requestノードで直接APIを呼び出します。

返ってきた結果から必要な情報だけを抽出しました(コードノードを使用)。

const items = $input.all();

// 全アイテムからテキストを抽出
let allContent = [];

for (const item of items) {
  const response = item.json;
  const candidates = response.candidates || [];

  for (const candidate of candidates) {
    const parts = candidate.content?.parts || [];
    for (const part of parts) {
      if (part.text) {
        allContent.push(part.text);
      }
    }
  }

  // groundingChunksからも抽出(ある場合)
  const groundingMeta = response.candidates?.[0]?.groundingMetadata;
  if (groundingMeta?.groundingChunks) {
    for (const chunk of groundingMeta.groundingChunks) {
      const text = chunk.retrievedContext?.text;
      if (text) {
        allContent.push(text);
      }
    }
  }
}

// 重複を除去
const uniqueContent = [...new Set(allContent)];

// 結合して圧縮
let combined = uniqueContent.join('\n\n---\n\n');

// 長すぎる場合は切り詰め
const MAX_CHARS = 4000;
if (combined.length > MAX_CHARS) {
  combined = combined.substring(0, MAX_CHARS) + '\n\n...(以下省略)';
}

return {
  json: {
    schemaContext: combined.trim(),
    sourceCount: items.length,
    extractedChunks: uniqueContent.length,
    estimatedTokens: Math.ceil(combined.length * 1.5)
  }
};

AIエージェントのプロンプトで圧縮済みのコンテキストを参照します。

## 入力情報
- 質問: {{ $('When chat message received').item.json.chatInput }}
- スキーマ情報: {{ $json.schemaContext }}

同じ質問をした結果、大幅に消費トークンを削減できるようになりました。

例)

スクリーンショット 2025-12-17 10.11.42

Toolにすべきかどうかの判断

Toolに向いているのは以下のようなもの。

  • 結果が小さい(数百トークン以下)
  • 呼ぶかどうかAIに判断させたい
  • 結果をそのまま使える(加工不要)
  • 複数のToolから選択させたい

逆にToolに向いていないのは、

  • 結果が大きい(1,000トークン超)
  • 毎回必ず実行する
  • 結果を圧縮・加工したい
  • エラー時の複雑なハンドリングが必要

今回のRAG検索では、
結果が大きい、毎回実行、圧縮必要 ということになったので、AIエージェントの外に出す判断にしました。

まとめ

今回n8nでAIエージェントを作って学べたことは、

全部Toolでやらせようとすると、トークンが爆発し、動作が不安定になってしまう ということでした。

  1. Toolの結果はコンテキストに入る

    • Toolが返した内容はそのままLLMのコンテキストに追加される
    • 大きな結果を返すToolはトークンを圧迫する
  2. RAGは事前処理で圧縮すべき

    • 検索結果をそのまま渡さない
    • 必要な情報だけ抽出・圧縮してから渡す
  3. Toolは万能ではない

    • 「AIが判断して呼ぶ」ケースに限定する
    • 毎回実行する処理はワークフローで

ユーザー入力 -> 前処理 (検索・加工) -> AI Agent (判断・選択) -> 後処理 (実行) というワークフローにして無駄なトークンを使わず、動作が高速になりました。

今後の改善点

  • スキーマ情報のキャッシュ(同じテーブルは再検索しない)
  • クエリパターンごとのテンプレート化
  • システムプロンプトを軽量にする

参考

この記事をシェアする

FacebookHatena blogX