💸

LangGraph×MCPでAIエージェント開発が…始まらなかった💦(無料モデルの場合

に公開

はじめに

最近はMCPサーバー作ったり、動かしたりばっかりしていたので「今度はMCPで呼び出す側だ!」と思いたち、MCPホスト兼AIエージェントをLangGraphで実装してみました。

開発のハードル下げるため、LLMはTool対応の無料モデル(DeepSeek、Llama等)縛りでやってみましたが、期待する結果が得られず、最終的には低価格有料モデル使うことになりました。

(興味深いことに、Clineでは同じ無料モデル&プロンプトでタスク完了できています。これは純粋にAIエージェントの差異ということであり、とても興味深いテーマですが、それはまた別の機会に。)

本記事では、AIエージェントの実装手順に加え、主要な無料・低価格モデルにおける動作検証結果を解説します。

この記事で学べること

  • 🤖 LangGraphのReActエージェントの実装方法
  • 🔧 MCPを使った外部ツール統合の仕組み
  • ⚠️ モデル毎の実際の動作検証結果
  • 💡 OpenRouterを使った複数LLMの使い分け
  • 🐛 よくあるエラーとその対処法

完成イメージ

🤖 ユーザー: 37.42515666343711, -5.8736564243940395 の天気を調べて、LINEメッセージを送信してください

🔄 エージェントが思考中...

🧠 実行計画:
   タスク: 座標(37.425, -5.874)の天気取得 → LINE送信
   必要なツール: getWeatherByCoordinates, push_text_message
   実行順序: 1) 天気取得 2) LINE送信

🚀 ステップ 1/2: 天気情報取得
🔍 ツール呼び出し: getWeatherByCoordinates(lat=37.425, lon=-5.874)
✅ 結果: {"都市":"San Pablo","気温":"20.53°C","天気":"曇りがち","湿度":"67%","風速":"4.12 m/s"}

🚀 ステップ 2/2: LINE送信
🔍 ツール呼び出し: push_text_message
🔄 メッセージ生成: San Pabloの天気情報を整形
✅ 結果: {"sentMessages":[{"id":"563892..."}]}

🤖 アシスタント: San Pabloの天気情報を取得し、LINEで送信しました!
   📍 場所: San Pablo
   🌡️ 気温: 20.53°C
   ☁️ 天気: 曇りがち
   💧 湿度: 67%
   💨 風速: 4.12 m/s
   📱 LINE送信: 完了 (ID: 563892...)

構成と前提

システム構成図

使用技術

Python 依存パッケージ

  • requirements.txtにはLangGraph, LangChain, OpenRouter, dotenv, etc. を記述

実装手順

Step 1: 環境設定

まず必要な環境変数を.envファイルに設定します:

# OpenRouter設定
OPENROUTER_API_KEY="your-api-key"
OPENROUTER_MODEL="qwen/qwen3-30b-a3b"  # ツール呼び出し対応モデルを指定
# OPENROUTER_MODEL="meta-llama/llama-4-maverick:free"
# OPENROUTER_MODEL="deepseek/deepseek-chat-v3-0324:free"

# 天気API
WEATHER_API_KEY="your-openweathermap-key"

# MCPサーバーパス(絶対パスで指定)
MCP_WEATHER_BOT_PATH="/path/to/weather-mcp-sandbox/dist/index.js"
MCP_LINE_BOT_PATH="/path/to/line-bot-mcp-server/dist/index.js"

# LINE Bot設定(LINE機能を使う場合)
LINE_CHANNEL_ACCESS_TOKEN="your-token"
LINE_DESTINATION_USER_ID="test-user-id"

Step 2: MCPサーバーのビルド

MCPサーバーをNode.jsでビルドします:

# weather-mcp-sandboxのビルド
cd /path/to/weather-mcp-sandbox
npm install
npm run build

# line-bot-mcp-serverのビルド
cd /path/to/line-bot-mcp-server
npm install
npm run build

Step 3: MCPクライアントの初期化

複数のMCPサーバーを管理するクライアントを作成します:

from langchain_mcp_adapters import MultiServerMCPClient
import os
from dotenv import load_dotenv

load_dotenv()

async def setup_mcp_client():
    """複数のMCPサーバーを初期化し、利用可能なツールを取得"""
    client_config = {}
    
    # 天気情報サーバーの追加
    weather_path = os.getenv("MCP_WEATHER_BOT_PATH")
    if weather_path and os.path.exists(weather_path):
        client_config["weather-bot"] = {
            "command": "node",
            "args": [weather_path],
            "transport": "stdio",
            "env": {**os.environ, "WEATHER_API_KEY": os.getenv("WEATHER_API_KEY")}
        }
    
    # LINE Botサーバーの追加
    line_path = os.getenv("MCP_LINE_BOT_PATH")
    if line_path and os.path.exists(line_path):
        client_config["line-bot"] = {
            "command": "node",
            "args": [line_path],
            "transport": "stdio",
            "env": {
                **os.environ,
                "CHANNEL_ACCESS_TOKEN": os.getenv("LINE_CHANNEL_ACCESS_TOKEN"),
                "DESTINATION_USER_ID": os.getenv("LINE_DESTINATION_USER_ID")
            }
        }
    
    # MCPクライアントの初期化
    client = MultiServerMCPClient(client_config)
    tools = await client.get_tools()
    
    print(f"\u2705 利用可能なツール数: {len(tools)}")
    for tool in tools:
        print(f"  - {tool.name}: {tool.description}")
    
    return client, tools

Step 4: LangGraphエージェントの作成

ReActパターンでツールを使うエージェントを構築します:

from langgraph.prebuilt import create_react_agent
from langchain_core.messages import HumanMessage
from langchain_openai import ChatOpenAI

async def create_agent_with_tools(tools):
    """ツールを使えるReActエージェントを作成"""
    
    # OpenRouter経由でLLMを初期化
    llm = ChatOpenAI(
        base_url="https://openrouter.ai/api/v1",
        api_key=os.getenv("OPENROUTER_API_KEY"),
        model=os.getenv("OPENROUTER_MODEL"),
        temperature=0.7
    )
    
    # システムプロンプト
    system_prompt = """
    あなたは親切な日本語アシスタントです。
    ユーザーの要求に応じて、利用可能なツールを使ってタスクを実行してください。
    ツールの結果をもとに、わかりやすく回答してください。
    """
    
    # エージェントの作成
    agent = create_react_agent(
        llm=llm,
        tools=tools,
        state_modifier=system_prompt
    )
    
    return agent

Step 5: エラーハンドリングの実装

ツール呼び出し非対応モデルのエラーを適切に処理します:

async def chat_with_agent(agent, user_input):
    """エージェントとの対話を実行(エラーハンドリング付き)"""
    try:
        # メッセージを作成
        messages = [{"messages": [HumanMessage(content=user_input)]}]
        
        # エージェントを実行
        result = await agent.ainvoke(messages[0])
        
        # 結果を表示
        if hasattr(result, 'content'):
            return result.content
        else:
            return result['messages'][-1].content
            
    except Exception as e:
        error_msg = str(e)
        
        if "function definitions do not address" in error_msg:
            return """
❌ エラー: ツール呼び出しが正常に動作しませんでした
💡 対処方法: 以下の動作確認済みモデルを試してください:
  - qwen/qwen3-30b-a3b ($0.005/1M tokens)
  - google/gemini-pro (無料枠あり)
"""
        else:
            return f"エラーが発生しました: {error_msg}"

Step 6: メイン実行ループ

全てを統合したメイン関数を作成します:

import asyncio
import readline  # コマンド履歴機能用

async def main():
    """メイン実行関数"""
    print("🤖 LangGraph MCP AIアシスタントを起動中...")
    
    # MCPクライアントとツールを初期化
    client, tools = await setup_mcp_client()
    
    if not tools:
        print("⚠️ 警告: 利用可能なツールがありません")
        return
    
    # エージェントを作成
    agent = await create_agent_with_tools(tools)
    
    print("\n🎆 アシスタントが準備完了しました!")
    print("💬 'exit' または 'quit' で終了します\n")
    
    # 対話ループ
    while True:
        try:
            # ユーザー入力を取得
            user_input = input("🤖 ユーザー: ")
            
            # 終了コマンドのチェック
            if user_input.lower() in ['exit', 'quit', '終了']:
                print("👋 さようなら!")
                break
            
            # 空入力のスキップ
            if not user_input.strip():
                continue
            
            # スピナーを表示
            print("\n🔄 エージェントが思考中...\n")
            
            # エージェントに問い合わせ
            response = await chat_with_agent(agent, user_input)
            
            # 応答を表示
            print(f"\n🤖 アシスタント: {response}\n")
            
        except KeyboardInterrupt:
            print("\n🛑 中断されました")
            break
        except Exception as e:
            print(f"\n❌ エラー: {e}\n")

if __name__ == "__main__":
    asyncio.run(main())

モデルごとの挙動比較

以下は、同じプロンプトを各モデルで実行した結果の比較です。

検証に使用したプロンプト:

37.42515666343711,-5.8736564243940395 の天気を調べて、LINEメッセージを送信してください。私への送信前確認は不要です。

このプロンプトは、以下の2つのツールを連鎖的に使用する必要があるため、モデルの能力を測るのに最適です:

  1. getWeatherByCoordinates - 緯度経度から天気情報を取得
  2. push_text_message - 取得した天気をLINEで送信

想定される正しい実行フロー:

  1. 緯度経度(37.425..., -5.873...)の天気情報を取得
  2. 取得した天気情報(都市名、気温、天気、湿度、風速)をテキスト化
  3. そのテキストをLINEメッセージとして送信
  4. 送信完了をユーザーに報告

実際に検証したモデル比較

OpenRouterで無料かつツール呼び出し対応と表示されているモデルから選択して検証しました(検索結果)。

無料モデル (OpenRouter) - ツール対応と表示されているが実際は...

モデル名 公式ツール対応 実際の挙動
meta-llama/llama-4-maverick:free ❌ Tool名は認識するが、実際のツール実行には至らず、単にツール名を文字列として返すだけ。
deepseek/deepseek-chat-v3-0324:free ⚠️ ツール連鎖に一部成功することもあるが、同条件でも失敗が多く、再現性がない。
mistralai/devstral-small:free ❌ ツール定義を理解できず、エラーメッセージを返す。
mistralai/mistral-small-3.1-24b-instruct:free ❌ ツール呼び出し機能が正常に動作しない。

結論: OpenRouterで「ツール対応」と表示されていても、無料モデルでは実用的な動作は期待できない。

有料モデル (OpenRouter)

モデル名 ツール呼び出し 料金 挙動
qwen/qwen3-30b-a3b $0.005/1M tokens Tool名の解析・呼び出し、戻り値の連携、LINEへの送信まで全て正常完了。常に成功。最もコスパが良い。

無料モデルで観察された現象

OpenRouterで「ツール対応」と表示されている無料モデルでも、実際の検証では以下の現象が観察されました:

  1. ツール名の認識はできるが実行されない: モデルはツール定義を理解し、適切なツール名を応答に含めるが、実際のツール実行フローに入らない
  2. 不安定な動作: 同じプロンプトでも成功・失敗が不規則に発生し、再現性がない
  3. 処理の途中停止: ツール呼び出しの途中でタイムアウトや無応答が発生
  4. エラーメッセージの不明確さ: 具体的な失敗理由が分からないエラーが返される

観察されたエラーパターン

パターン1: ツール定義理解不可エラー

Your current function definitions do not address this task adequately. 
Please refine them to better handle the user's request.

パターン2: 処理途中での停止

⠹ AIが考え中...

(この状態でタイムアウトまで応答なし)

パターン3: ツール名のみ文字列として返却

[getWeatherByCoordinates(lat=43.89327...), push_text_message(message="...")]

(実際のツール実行には至らず)

実際の動作比較

🔴 無料モデル(llama-4-maverick:free)の実行結果

You: 43.89327583467077, 134.08130339555856 の天気を調べて、LINEメッセージを送信してください。

📝 メッセージ履歴 (合計: 2件)
   [0] HumanMessage: '43.89327583467077, 134.08130339555856 の天気を調べて...'
   [1] AIMessage: '[getWeatherByCoordinates(lat=43.89327583467077, lon=134.08130339555856), push_text_message(message="The weather is...")]'

🤖: [getWeatherByCoordinates(lat=43.89327583467077, lon=134.08130339555856), push_text_message(message="The weather is...")]

結果: ツール名は認識しているが、実際のツール実行には至らず、単にツール名を文字列として返すだけで終了。

🔴 無料モデル(deepseek-chat-v3-0324:free)の実行結果(失敗例)

You: 37.42515666343711,-5.8736564243940395 の天気を調べて、LINEメッセージを送信してください。
⠹ AIが考え中...

結果: 処理が止まってしまい、タイムアウトまで応答なし。

🟡 無料モデル(deepseek-chat-v3-0324:free)の実行結果(成功例)

You: 37.42515666343711,-5.8736564243940395 の天気を調べて、LINEメッセージを送信してください。

📝 メッセージ履歴 (合計: 6件)
   [0] HumanMessage: '37.42515666343711,-5.8736564243940395 の天気を調べて...'
   [1] AIMessage: ツール呼び出し → ['getWeatherByCoordinates']
   [2] ToolMessage: ツール 'getWeatherByCoordinates' の結果: {"緯度":37.42515666343711,"経度":-5.8736564243940395,"都市":"San Pablo","気温":"24.97°C","天気":"晴天","湿度":"42%","風速":"6.17 m/s"}
   [3] AIMessage: ツール呼び出し → ['push_text_message']
   [4] ToolMessage: ツール 'push_text_message' の結果: {"sentMessages":[{"id":"563926786103574786"...}]}
   [5] AIMessage: 'San Pabloの現在の天気情報をLINEメッセージで送信しました。...'

🤖: San Pabloの現在の天気情報をLINEメッセージで送信しました。

結果: まれに成功することもあるが、同じ条件でも失敗することが多く、再現性がない。

🔵 有料モデル(qwen3-30b-a3b)の実行結果

You: 37.42515666343711,-5.8736564243940395 の天気を調べて、LINEメッセージを送信してください。

📝 メッセージ履歴 (合計: 6件)
   [0] HumanMessage: '37.42515666343711,-5.8736564243940395 の天気を調べて...'
   [1] AIMessage: ツール呼び出し → ['getWeatherByCoordinates']
   [2] ToolMessage: ツール 'getWeatherByCoordinates' の結果: {"緯度":37.42515666343711,"経度":-5.8736564243940395,"都市":"San Pablo","気温":"25.14°C","天気":"晴天","湿度":"39%","風速":"5.14 m/s"}
   [3] AIMessage: ツール呼び出し → ['push_text_message']
   [4] ToolMessage: ツール 'push_text_message' の結果: {"sentMessages":[{"id":"563930671203745878"...}]}
   [5] AIMessage: 'San Pabloの天気情報をLINEメッセージで送信しました。...'

🤖: San Pabloの天気情報をLINEメッセージで送信しました。
天気: 晴天  
気温: 25.14°C  
湿度: 39%  
風速: 5.14 m/s

結果: 安定してツール呼び出しを連鎖させ、天気取得→LINE送信まで完璧に実行。

🔵 (参考情報)Cline+無料モデル(llama-4-maverick:free)の実行結果

実行結果
**User:**

<task>
43.89327583467077, 134.08130339555856 の天気を調べて、LINEメッセージを送信してください。私への送信前確認は不要です。
</task>
<environment_details>
# VSCode Visible Files
logs/langgraph-trial_memo.txt

# VSCode Open Tabs
.env
logs/langgraph-trial_memo.txt
.mcp.json
../../../../../../../../home/ardupilot/.vscode-server/data/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json
.env.example
README.md
chatbot.py
.gitignore
requirements.txt
CLAUDE.md

# Current Time
6/3/2025, 4:18:45 PM (Asia/Tokyo, UTC+9:00)

# Current Working Directory (/mnt/c/Users/hfuji/OneDrive/dev/ai-agent-compe/lang-graph-trial) Files
.env.example
.gitignore
chatbot.py
CLAUDE.md
README.md
requirements.txt
logs/

# Context Window Usage
0 / 1,048.576K tokens used (0%)

# Current Mode
ACT MODE
</environment_details>

---

**Assistant:**

<thinking>
The task requires checking the weather at the given coordinates (43.89327583467077, 134.08130339555856) and sending a LINE message. To accomplish this, I will first use the `weather-bot` MCP server to get the weather information for these coordinates. Then, I will use the `line-bot` MCP server to send the weather information as a LINE message.

First, I need to use the `getWeatherByCoordinates` tool from the `weather-bot` MCP server to retrieve the weather information for the given coordinates.

The required parameters for `getWeatherByCoordinates` are `lat` and `lon`, which correspond to the latitude and longitude of the location.

Let's prepare the arguments for the `getWeatherByCoordinates` tool:
- `lat`: 43.89327583467077
- `lon`: 134.08130339555856

</thinking>

<use_mcp_tool>
<server_name>weather-bot</server_name>
<tool_name>getWeatherByCoordinates</tool_name>
<arguments>
{
  "lat": 43.89327583467077,
  "lon": 134.08130339555856
}
</arguments>
</use_mcp_tool>

---

**User:**

[use_mcp_tool for 'weather-bot'] Result:
{"緯度":43.89327583467077,"経度":134.08130339555856,"都市":"Chuguyevka","気温":"7.13°C","天気":"適度な雨","湿度":"97%","風速":"1.96 m/s"}
<environment_details>
# VSCode Visible Files
logs/langgraph-trial_memo.txt

# VSCode Open Tabs
.env
logs/langgraph-trial_memo.txt
.mcp.json
../../../../../../../../home/ardupilot/.vscode-server/data/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json
.env.example
README.md
chatbot.py
.gitignore
requirements.txt
CLAUDE.md

# Current Time
6/3/2025, 4:18:53 PM (Asia/Tokyo, UTC+9:00)

# Context Window Usage
12,914 / 1,048.576K tokens used (1%)

# Current Mode
ACT MODE
</environment_details>

---

**Assistant:**

<thinking>
I have successfully retrieved the weather information for the given coordinates. Now, I will prepare a LINE message with the weather details and use the `line-bot` MCP server to send it.

The weather information is as follows:
- Location: Chuguyevka
- Temperature: 7.13°C
- Weather: 適度な雨 (Moderate rain)
- Humidity: 97%
- Wind speed: 1.96 m/s

I will now use the `push_text_message` tool from the `line-bot` MCP server to send this information as a LINE message.

Let's prepare the arguments for the `push_text_message` tool:
- `message.text`: Weather information in text format

The message will be: "Chuguyevkaの天気: 7.13°C, 適度な雨, 湿度: 97%, 風速: 1.96 m/s"

</thinking>

<use_mcp_tool>
<server_name>line-bot</server_name>
<tool_name>push_text_message</tool_name>
<arguments>
{
  "message": {
    "text": "Chuguyevkaの天気: 7.13°C, 適度な雨, 湿度: 97%, 風速: 1.96 m/s"
  }
}
</arguments>
</use_mcp_tool>

---

**User:**

[use_mcp_tool for 'line-bot'] Result:
{"sentMessages":[{"id":"563888661054881861","quoteToken":"UPBFUrH0-ssnfHcwfHXM31njNwPUC65YvvdFL4XcpE9DrsRD8Ek60i_6MDuL6_J8e5-MKdFzNZoHRDo0Qa2mjxknbjO7oKTqg-cDLVupt29aiEsBvSQNAlxGLZNbWCg9lHPAeett3YBB41Mkh-5IHg"}]}
<environment_details>
# VSCode Visible Files
logs/langgraph-trial_memo.txt

# VSCode Open Tabs
.env
logs/langgraph-trial_memo.txt
.mcp.json
../../../../../../../../home/ardupilot/.vscode-server/data/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json
.env.example
README.md
chatbot.py
.gitignore
requirements.txt
CLAUDE.md

# Current Time
6/3/2025, 4:18:57 PM (Asia/Tokyo, UTC+9:00)

# Context Window Usage
13,432 / 1,048.576K tokens used (1%)

# Current Mode
ACT MODE
</environment_details>

---

**Assistant:**

<thinking>
The LINE message has been successfully sent with the weather information for Chuguyevka. I have completed the task as requested.

Now, I will use the `attempt_completion` tool to present the result of the task to the user.

</thinking>

<attempt_completion>
<result>
The weather information for Chuguyevka (latitude: 43.89327583467077, longitude: 134.08130339555856) has been retrieved and sent via LINE message. The current weather is 7.13°C with moderate rain, humidity at 97%, and wind speed at 1.96 m/s.
</result>
</attempt_completion>


結果: 安定してツール呼び出しを連鎖させ、天気取得→LINE送信まで完璧に実行。

よくあるトラブルと解決方法

🔴 問題1: MCPサーバーが起動しない

症状:

✅ 利用可能なツール数: 0

解決方法:

  1. Node.jsがインストールされているか確認
node --version  # v18以上推奨
  1. MCPサーバーのビルドを確認
cd weather-mcp-sandbox
npm run build
ls dist/index.js  # ファイルが存在するか確認
  1. 環境変数のパスを絶対パスで指定
# ❌ 相対パス
MCP_WEATHER_BOT_PATH=./dist/index.js

# ✅ 絶対パス
MCP_WEATHER_BOT_PATH=/home/user/weather-mcp-sandbox/dist/index.js

🔴 問題2: OpenRouterでAPIキーエラー

症状:

Error code: 401 - {'error': {'message': 'Unauthorized'}}

解決方法:

  1. APIキーの先頭/末尾の空白を確認
  2. OpenRouterダッシュボードでAPIキーを再生成
  3. モデル名が正しいか確認(openai/プレフィックスが必要)

🔴 問題3: 日本語が文字化けする

症状:

🤖: ????????????????????????

解決方法:

# ターミナルのエンコーディングを設定
import sys
sys.stdout.reconfigure(encoding='utf-8')

# または環境変数で設定
export PYTHONIOENCODING=utf-8

🔴 問題4: ツール呼び出しが失敗する

症状:

Your current function definitions do not address this task adequately.

解決方法:

ツール呼び出し対応モデルに切り替える:

# .envファイルを編集
OPENROUTER_MODEL="qwen/qwen3-30b-a3b"  # または他の対応モデル

⚠️ 注意: APIコストの管理

ツール呼び出しは通常の会話より多くのトークンを消費します:

  • 通常の会話: 約100-200トークン
  • ツール呼び出し1回: 約500-1000トークン
  • 複数ツール連携: 約1500-3000トークン

OpenRouterのUsageページで使用量を定期的に確認しましょう。

まとめ

この記事では、LangGraphとMCPを使ったマルチツールAIエージェントの実装方法を解説しました。

重要なポイント

  1. 無料モデルの検証結果: OpenRouterで「ツール対応」と表示されている無料モデルでも、実用的な安定動作は期待できなかった
  2. MCPの利点: 外部APIを簡単にツール化できる
  3. LangGraphの威力: ReActパターンで複雑なタスクも自動化
  4. 低価格有料モデルでの成功: qwen3-30b-a3b($0.005/1M tokens)で安定したツール連鎖が実現

今後やりたいこと

  • Cline vs LangGraph の性能差分析
    同一の無料モデル・プロンプトにおけるタスク実行結果の差異について、以下の観点から原因を究明します:

    • LangChainエコシステムの詳細調査
      LangChainの各機能がAIエージェント実装に与える影響の分析
      代替的なAIエージェント実装手法の学習・比較検証
    • Clineソースコード解析
      Clineの実装における性能向上に寄与する設計パターンの特定
      無料モデルでも高い実行成功率を実現する技術的要因の解明
  • LangGraphベースAIエージェントの性能最適化
    上記分析結果を踏まえ、LangGraphを使用したAIエージェントの実行精度と安定性の向上を図ります。

ソースコード

GitHubリポジトリで公開しています。質問やフィードバックはコメント欄でお待ちしています!

参考資料

タグ

#LangGraph #MCP #OpenRouter #AIエージェント #ツール呼び出し

Discussion