その他

【MCP】賢すぎずバカすぎない、ほどよいチャットbot

その他
この記事は約17分で読めます。

はじめに

みなさんこんにちは。株式会社インプルの岩崎です。

今回は、普段何気なく使うチャットボットに、ほどよいLLMの機能を追加してみて、性能を少し上げてみた、という、久しぶりのアイデア寄りなお話です。

チャットbotとは

チャットボット(Chatbot)とは、「チャット(会話)」と「ロボット」を組み合わせた言葉です。

AI(LLM)の普及により、近年ではみなさんの生活にあたりまえになってきたのではないでしょうか?chatGPTやClaude、Geminiなど、チャットbotとは言わずとも、チャット型のツールで様々な検索をする機会が増えてきました。

AIが登場する以前にも、機会的にAというワードが含まれたらAに関する内容を返す、というような企業のHPで質問をするチャットbotもあったと思います。それとAIが一緒かどうか、、、の話題も少し気になりますが、それはまた次回の話にしましょう笑。今回は、その企業のHPで使ったことのあるチャットbotについて深掘りしてみます。

企業のHPで使われるような、チャットbotのデメリット

企業のチャットbotは、ユーザーの質問したいことを自由に記述して質問できる仕組みとして、とてもユーザーフレンドリーな仕組みとして注目されていたと思います。

ですが、そんなチャットbotは、最大のデメリットとして、あらかじめ登録したキーワードや質問パターン以外は「理解できませんでした」と返すしかありませんでした。また、その回答も、「もし〇〇と言ったら△△を返す」という分岐を人手で作り込む必要があり、品目が増えるたびにシナリオの修正が必要でした。

さらに、多言語対応ができなかったり、正しく文章を入れないと思わぬ回答が返ってきたりと、様々なデメリットを抱えていたのでした。

AI(LLM)の登場により・・・

そんなチャットbotにも、お役目終了を告げるかのように、AIの登場で一気に立場がなくなります。先ほどのデメリットをほぼ全て解消してしまうような、そんなツールの登場だったからです。

AIであれば、どんな文章でも深く理解してくれますし(ほぼ。)、その質問に対して回答するだけでなく、回答以上のおせっかいもしてくれる。しかも、他言語にも対応するから、AIで全て対応できちゃうよね、と思われる世の中になっているかもしれません。

チャットbotをほどよくする

さて、上記の話を聞くと、チャットbotなんてAIで十分だし、程よくするなんてどういうことだ?と言われるかもしれません。しかし、スーパー万能なAIでさえ、デメリットはあるのです。

AI(LLM)のデメリット

本格的なAIが登場して、2年程度経ったでしょうか。登場当時は賢そうなAIも、時間が立つにつれて徐々にデメリットが見えてきます。

まず、AIはハルシネーションという嘘をつくときがあります。
AIは学習データをもとに「それらしい答え」を生成するため、実際のルールと異なる回答を自信満々に返すことがあります。しかし、その自信満々の嘘は、企業のHPだったり、行政・自治体のような正確性が求められる用途では致命的になるのです。

また、回答するデータはAIが都度調査して回答するものの、情報の鮮度や精度でまだ課題があります。
これまでのチャットbotであれば、回答の精度は管理者側である程度制御することができましたし、鮮度も新鮮な状態のデータを常に届けることができていました。

さらに、特大の致命傷として、コストの問題があります。
賢いAIを使うには、それなりのコストを支払う必要があります。しかし、チャットbotで判断するような内容は、最近の高性能なAIモデルには場外ホームラン並にオーバースペックで、その割にコストを食うという、打撃不振に陥った高年棒の4番バッター並みです。

程よくするスパイス(技術)たち

さて、そんな一長一短なAIですが、これを程よくする技術もあります。

まず、AIを「ちょうどいい」にするために、ローカルLLMという技術があります。
これは、インターネットを介さず自身のPCやサーバー上で動作する大規模言語モデルです。通常のAIは、モデルを提供している会社のサーバーにアクセスして、とてつもないスペックのモデルが動作し、返答をします。オーバースペックの原因は、そのモデルにアクセスすることです。
それが、お手元のPCで動くサイズになるので、当然スペックも落ちますが、それがいいのです。今回のチャットbotのようなタスクは、ローカルLLMで十分だったりもします。また、コストも自分のPC上でうごかすので、基本的にかかりません。そのため、何度リクエストされても、問題ないのです。

次に、MCPという技術があります。
Anthropicが発表した、AI(LLM)と外部ツールやデータソース(データベース、SaaS、APIなど)を標準化された手順で接続するオープン規格です。過去に記事でも紹介していますので、よければぜひご覧ください。

これを使えば、データの鮮度を維持しながら、回答を出すことができます。つまり、ほどよい知能を加えて、データの参照元だけ準備しておけば、ほどよいチャットbotができるというわけです。

ほどよいチャットbotを作ってみる(MCP部分だけ)

さて、それではこの仕組みがちゃんとできるのか、作ってみましょう。
今回は、まずはMCP部分だけがしっかりとできるかを試してみたいので、AI(LLM)は普通にClaudeを使います笑。ローカルLLMは、モデルの設定は簡単なものの、MCPの設定周りをまだしたことがないので、また次回のテーマにしてみようと思います!

チャットbotの内容

今回は、なんとも懐かしいネタですが、私が1年前に入社したばかりに投稿した、ゴミ出しについてです笑。

実は1年前にも同じようなネタでチャットbotを作っていたのですが、当時はMCPではなくRAGで作っていました。ちょうど、この記事を書いたあたりでMCPが爆速的に普及し始めたスタートの頃だったと覚えています。。。

さて、RAGとMCPの差については、下記の表の通り。そのため、今回は1年前のチャットbotに対して、実質的な性能アップをさせているわけです笑

RAGMCP
データの渡し方関連文書をプロンプトに詰めるツールを呼び出して構造化データを取得
ハルシネーションリスク低減できるが残るツールの返り値に限定できるので低い
データ更新再インデックスが必要ファイル更新だけでOK
向いている用途大量の非構造化文書の検索ルール・テーブルなど構造化データの参照

今回のファイル構成

今回は、下記のファイル構成でMCPの部分を再現します。プログラムはclaudeに作ってもらいました!

gomibunbetsu-mcp/
├── package.json
├── src/
│   ├── index.js
│   └── data.js

index.jsでMCPの受付部分を担います。
AIの質問に対して、どのようなツールを使うか判断し、結果を返す窓口です。MCPプロトコルの通信処理もここが担っています。そのため、上と下のサーバー周りのプログラムがここでは重要で、真ん中のToolに関するプログラムは、通信させる内容に応じて変わってくる部分になります。

サーバー初期化でサーバーを立ち上げ、通信を開始し使用できるツールをAIに伝えます。伝えた後は待機し、ユーザーから質問が来た時にAIがどのToolを使うべきか判断し、ツールを実行することでMCPサーバーがdeta.jsを検索して結果を返答、AIの返答へつながる、という流れです。

// ──────────────────────────────────────────
// MCPサーバーの初期化
// ──────────────────────────────────────────
const server = new McpServer({
  name: "gomibunbetsu-mcp",
  version: "1.0.0",
});


// ──────────────────────────────────────────
// Tool 1: ゴミの分別方法を検索する
// ──────────────────────────────────────────
server.tool(
  "search_gomi",
  "ゴミの品目名からごみの分別方法・収集日・注意事項を調べる",
  {
    query: z.string().describe("調べたいゴミの品目名(例:「ペットボトル」「電池」「ソファ」)"),
  },
  async ({ query }) => {
    const results = searchGomi(query);

    if (results.length === 0) {
      return {
        content: [
          {
            type: "text",
            text: `「${query}」に一致する分別情報が見つかりませんでした。\n` +
                  `品目名を変えて再度お試しいただくか、お住まいの自治体窓口にお問い合わせください。`,
          },
        ],
      };
    }

    const lines = results.map(item => {
      const noteText = item.notes ? `\n   ⚠ ${item.notes}` : "";
      return `■ ${item.keyword}\n` +
             `   分類: ${item.category}\n` +
             `   収集日: ${item.schedule}${noteText}`;
    });

    return {
      content: [
        {
          type: "text",
          text: `「${query}」の検索結果(${results.length}件):\n\n` + lines.join("\n\n"),
        },
      ],
    };
  }
);


// ──────────────────────────────────────────
// Tool 2: カテゴリ一覧を取得する
// ──────────────────────────────────────────
server.tool(
  "list_categories",
  "ゴミの分別カテゴリ一覧を取得する(例:燃えるゴミ、資源ゴミ等)",
  {}, // 引数なし
  async () => {
    const categories = listCategories();
    return {
      content: [
        {
          type: "text",
          text: "ゴミ分別カテゴリ一覧:\n\n" + categories.map(c => `• ${c}`).join("\n"),
        },
      ],
    };
  }
);


// ──────────────────────────────────────────
// Tool 3: カテゴリで絞り込む
// ──────────────────────────────────────────
server.tool(
  "get_gomi_by_category",
  "指定したカテゴリのゴミ品目を一覧表示する(例:「燃えるゴミ」「粗大ゴミ」)",
  {
    category: z.string().describe("カテゴリ名(例:「燃えるゴミ」「資源ゴミ(紙類)」)"),
  },
  async ({ category }) => {
    const results = gomiData.filter(item =>
      item.category.includes(category)
    );

    if (results.length === 0) {
      return {
        content: [
          {
            type: "text",
            text: `カテゴリ「${category}」は見つかりませんでした。\n` +
                  `list_categories ツールで正しいカテゴリ名を確認してください。`,
          },
        ],
      };
    }

    // 最初のアイテムからスケジュールを取得(同カテゴリは基本同じ収集日)
    const schedule = results[0].schedule;
    const items = results.map(item => {
      const noteText = item.notes ? ` (${item.notes})` : "";
      return `• ${item.keyword}${noteText}`;
    });

    return {
      content: [
        {
          type: "text",
          text: `【${category}】\n` +
                `収集日: ${schedule}\n\n` +
                `対象品目:\n` + items.join("\n"),
        },
      ],
    };
  }
);


// ──────────────────────────────────────────
// サーバー起動
// ──────────────────────────────────────────
async function main() {
  const transport = new StdioServerTransport();
  await server.connect(transport);
  // ⚠ console.log はNG(stdoutを汚染する)。必ずconsole.errorを使う
  console.error("✅ gomibunbetsu-mcp サーバー起動中 (stdio)");
}

main().catch((err) => {
  console.error("❌ 起動エラー:", err);
  process.exit(1);
});

data.jsはその名の通り、データ部分です。
ゴミ分別のルールデータと、それを検索する関数が入っています。index.jsから「ペットボトルで検索して」と頼まれたら結果を返すだけで、通信のことは一切知りません。下記のような形で、データと検索プログラム、返答の仕組みだけが作られています。役割は非常にシンプルですね。

export const gomiData = [
  // ────────────── 燃えるゴミ ──────────────
  { keyword: "生ゴミ",     category: "燃えるゴミ",   schedule: "月・木曜日",   notes: "水を切ってから出してください" },
  { keyword: "野菜くず",   category: "燃えるゴミ",   schedule: "月・木曜日",   notes: "水を切ってから出してください" },
// ────────────── 燃えないゴミ ──────────────
  { keyword: "ガラス",     category: "燃えないゴミ", schedule: "第2・第4水曜日", notes: "割れたガラスは紙に包んで「危険」と表示" },
  { keyword: "陶器",       category: "燃えないゴミ", schedule: "第2・第4水曜日", notes: "" },
// ────────────── 資源ゴミ ──────────────
  { keyword: "新聞紙",     category: "資源ゴミ(紙類)", schedule: "火曜日",     notes: "ひもで十字に縛って出してください" },
  { keyword: "段ボール",   category: "資源ゴミ(紙類)", schedule: "火曜日",     notes: "折りたたんでひもで縛ってください" },
];
/**
 * キーワードでゴミデータを検索する
 * @param {string} query - 検索クエリ
 * @returns {Array} マッチしたゴミデータの配列
 */
export function searchGomi(query) {
  const q = query.toLowerCase();
  return gomiData.filter(item =>
    item.keyword.toLowerCase().includes(q) ||
    item.category.toLowerCase().includes(q) ||
    item.notes.toLowerCase().includes(q)
  );
}

/**
 * カテゴリ一覧を返す
 * @returns {string[]} ユニークなカテゴリの配列
 */
export function listCategories() {
  return [...new Set(gomiData.map(item => item.category))];
}

MCPの設定

MCPは本来サーバーに置くものですが、今回はローカル環境で擬似的に再現するので、ローカルに置ければOKです。ローカルで試すには、Claude デスクトップアプリをインストールする必要があります。

Download Claude | Claude by Anthropic
Download Claude apps for Mac, Windows, iOS, and Android. Install extensions for Chrome, Excel, PowerPoint, and Slack.

ダウンロードしたら、下記設定ファイルの場所に移動してみましょう。

OSパス
Mac~/Library/Application Support/Claude/claude_desktop_config.json
Windows%APPDATA%\Claude\claude_desktop_config.json

このJSONファイルを以下の形に編集して、Claude Desktopを再起動するとMCPサーバーが読み込まれます。Windowsのパスは C:\\Users\\yourname\\... とバックスラッシュを2つ重ねる必要があるので、注意してください!

{
  "mcpServers": {
    "gomibunbetsu": {
      "command": "node",
      "args": ["/絶対パス/gomibunbetsu-mcp/src/index.js"]
    }
  }
}

ほどよいチャットbotを動かしてみる

上記設定までできたら、早速動かしてみましょう。

CLIでの動作確認

ちなみに、依存パッケージをインストールした後、CLIで動作確認することができます。

# 下記コマンドを実行
node src/index.js

# この返答が返ってきたらOK
✅ gomibunbetsu-mcp サーバー起動中 (stdio)

デスクトップアプリで質問してみる

それではclaudeで質問してみます。なお、アプリの設定をいじって反映されるのは、再起動後なので、cmd+Qなどで、再起動をお忘れなく!!

今回はペットボトルの捨て方を聞いてみます。deta.jsでは下記の通りになっていますから、コレに関連する回答ができれば正解ですね。

{ keyword: "ペットボトル", category: "資源ゴミ(容器)", schedule: "水曜日",   notes: "キャップ・ラベルを外し、すすいで出してください" },


ストレートに「ペットボトルの捨て方を教えて」と聞いてみると・・・

うーん、claudeが独自の回答をしてしまいました。コレが先ほど言っていた、高性能AIのデメリット、ハルシネーション(してないけど、今回の場合は。)です。

さて、それを解決すべく、「ゴミ分別MCPツールを使って、ペットボトルの捨て方を教えて」と、一言追加してみましょう。すると、、、

ちゃんと動作しました!!
先ほどのdata.js通りに機能していますね。素晴らしいです。ちなみに、応用的な質問をしてみると・・・

ちゃんとツールを使って確認したようですが、回答結果が出なかったので一般的なルール(モデル独自の知識)で回答してくれました。これも、データベースに入れれば、解決する問題ですね。

MCPを使っている証

チャットを確認すると、読み込まれたツール~~の部分があると思います。

コレをクリックすると、どのようなToolが使われたか確認することができ、今回はしっかりとgomibunbetsu MCPを使用できているようです。

おわりに

1年越しのリベンジマッチ、いかがでしたでしょうか!?

しかし、まだローカルLLMの連携はしていませんし、じゃぁデータの管理はどうすればいいのか、という、実際のユースケースも想定できていません。次回はその辺りを探ってみることにしましょう。

ご覧いただきありがとうございました。