Gemini 3.0 や ChatGPT 5.2 といった最新のクローズドモデルは、性能が年々向上し、多くのタスクで高い精度を発揮している。一方で、オープンウェイトの LLM も、商用 API には及ばないまでも、特定用途では十分に実用的な水準に達しつつある。とくに、データを外部に送らずにローカルだけで動かせる点は、セキュリティやコンプライアンスを重視する現場では大きなメリットになる。

ローカルで LLM を動かす方法として、Ollama は導入が簡単で、開発・検証用途でよく使われる。Meta の Llama 系モデルとの相性も良く、公式やコミュニティのモデルをそのまま利用しやすい。ただし、70B クラスなど大規模な Llama 系は個人環境ではメモリ負荷が重く、8B クラスの Llama 3 系は日本語タスクでまだ改善の余地がある。そこで本記事では、比較的軽量でありながら日本語能力が強化された Llama 3.1 Swallow に注目する。

また、LLM に外部ツール(計算・検索・API など)を安全に渡して使わせるための仕組みとして、MCP(Model Context Protocol) が普及しつつある。MCP は 2024 年 11 月に Anthropic が発表したオープンなプロトコルで、LLM とツール群の接続方法を標準化する。Ollama のようなローカル LLM と MCP を組み合わせることで、ツール呼び出し付きのエージェントをローカル環境で構築できる。

本記事では、Ollama・Llama 3.1 Swallow・MCP・LangChain を組み合わせて、ローカルでツール呼び出し可能なエージェントを動かす手順をまとめる。LangChain は 1 系以降で API が大きく変わっており、古いサンプルではそのまま動かないことが多いため、現行の API に沿った形で説明する。


環境

  • Ubuntu (on WSL)
  • GPU: NVIDIA RTX 4060Ti 16GB

Function calling(Tool calling)に対応しているモデルの調べ方

Ollama では、LLM がツールを呼び出す機能は Tool calling として提供されている。対応モデルは Models - Ollama のページで「tools」でフィルタして確認できる。Llama 3.1 Swallow も Tool 対応モデルとして公開 されているため、本構成で利用できる。

実装

Ollama

Ollama では ollama pull <モデル名> で公式・コミュニティのモデルをそのまま取得できる。今回は Hugging FaceQ5_K_M 量子化 GGUF をダウンロードし、Modelfile でベースに指定したうえで ollama create Llama-3.1-Swallow-8B -f Modelfile を実行してカスタムモデルを作成した。作成したモデル名(ここでは Llama-3.1-Swallow-8B)は、後述の LangChain 側の model 指定と一致させる。

FastMCP

ここでは、FastMCP を使って足し算ツール・リソース・プロンプトを公開する簡単な MCP サーバーを実装する。

import argparse
import logging

from mcp.server.fastmcp import FastMCP

# Create an MCP server
mcp = FastMCP("Demo")


# Add an addition tool
@mcp.tool()
def add(a: int, b: int) -> int:
    """Add two numbers"""
    return a + b


# Add a dynamic greeting resource
@mcp.resource("greeting://{name}")
def get_greeting(name: str) -> str:
    """Get a personalized greeting"""
    return f"Hello, {name}!"


# Add a prompt
@mcp.prompt()
def greet_user(name: str, style: str = "friendly") -> str:
    """Generate a greeting prompt"""
    styles = {
        "friendly": "Please write a warm, friendly greeting",
        "formal": "Please write a formal, professional greeting",
        "casual": "Please write a casual, relaxed greeting",
    }

    return f"{styles.get(style, styles['friendly'])} for someone named {name}."


def parse_args() -> argparse.Namespace:
    parser = argparse.ArgumentParser(description="Run the FastMCP demo server")
    parser.add_argument(
        "--transport",
        default="stdio",
        help="Transport to use for MCP (default: stdio)",
    )
    # keep host/port optional for transports that may need them
    parser.add_argument(
        "--host", default=None, help="Host to bind (if applicable)"
    )
    parser.add_argument(
        "--port", type=int, default=None, help="Port to bind (if applicable)"
    )
    parser.add_argument("--log-level", default="INFO", help="Logging level")
    return parser.parse_args()


if __name__ == "__main__":
    args = parse_args()

    # configure basic logging
    logging.basicConfig(
        level=getattr(logging, args.log_level.upper(), logging.INFO),
        format="%(asctime)s %(levelname)s %(message)s",
    )
    logging.info(
        "Starting MCP server (transport=%s, host=%s, port=%s)",
        args.transport,
        args.host,
        args.port,
    )

    # Call run with available args; only pass host/port if provided
    run_kwargs = {"transport": args.transport}
    if args.host is not None:
        run_kwargs["host"] = args.host
    if args.port is not None:
        run_kwargs["port"] = args.port

    mcp.run(**run_kwargs)

LangChain

上記の MCP サーバーを子プロセスとして起動し、stdio で接続する。LangChain の create_agentlangchain_mcp_adaptersload_mcp_tools で MCP のツールを取得し、Ollama の Llama 3.1 Swallow と組み合わせてエージェントを動かす。

import asyncio
import sys
from pathlib import Path

from langchain.agents import create_agent
from langchain_core.messages import HumanMessage
from langchain_mcp_adapters.tools import load_mcp_tools
from langchain_ollama import ChatOllama
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client


async def main():
    try:
        # モデルを初期化
        model = ChatOllama(
            model="Llama-3.1-Swallow-8B:latest",
            base_url="http://localhost:11434",  # 省略可
            temperature=0.7,
        )

        # server.py の絶対パスを取得
        script_dir = Path(__file__).parent.resolve()
        server_script = script_dir / "server.py"

        if not server_script.exists():
            print(f"エラー: {server_script} が見つかりません。")
            sys.exit(1)

        # server.py スクリプトを実行するためのパラメータを設定
        server_params = StdioServerParameters(
            command="python3",  # 実行するコマンド
            args=[str(server_script)],  # コマンドライン引数(スクリプトパス)
        )

        print("MCPサーバーに接続中...")
        # stdio_client コンテキストマネージャを使用して接続を確立
        async with stdio_client(server_params) as (read, write):
            # read/writeストリームを使用してClientSessionを作成
            # session_kwargs で ClientSession の挙動をカスタマイズ可能
            async with ClientSession(read, write) as session:
                print("セッションを初期化中...")
                # サーバーとのハンドシェイクを実行
                await session.initialize()
                print("セッションが初期化されました。")

                print("MCPツールをロード中...")
                # MCPツールを取得し、LangChainツールに変換
                # この関数が内部で session.list_tools() を呼び出し、
                # 返された MCPTool オブジェクトを convert_mcp_tool_to_langchain_tool で変換
                tools = await load_mcp_tools(session)
                print(f"ロードされたツール: {[tool.name for tool in tools]}")

                # create_agentでエージェントを作成
                print("エージェントを作成中...")
                agent = create_agent(
                    model=model,
                    tools=tools,
                    system_prompt="あなたは役立つアシスタントです。利用可能なツールを使って質問に答えてください。",
                )

                # エージェントを実行
                print("エージェントを呼び出し中...")
                question = "3 + 5 は?"
                result = await agent.ainvoke(
                    {"messages": [HumanMessage(content=question)]}
                )

                print("\n=== エージェントの応答 ===")
                # エージェントの戻り値は {"messages": [...]} の形式
                # 最後のメッセージがエージェントの応答
                if "messages" in result and result["messages"]:
                    print(result["messages"][-1].content)
                else:
                    print(result)

    except Exception as e:
        print(f"エラーが発生しました: {e}", file=sys.stderr)
        import traceback

        traceback.print_exc()
        sys.exit(1)


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

本記事で使用したサンプルコードは GitHub の mcp-tutorial リポジトリ にコミットしている。Ollama の起動、MCP サーバー用の仮想環境、および LangChain 用の環境を用意したうえで実行すれば、同じ構成を再現できる。

おわりに

Ollama と Llama 3.1 Swallow、MCP、LangChain を組み合わせれば、データを外に送らずに、ツールを呼べるエージェントをローカルで動かせる。MCP のおかげでツールの追加や別サーバーの接続もやりやすいので、気になる方はぜひ試してみてほしい。