iT邦幫忙

2025 iThome 鐵人賽

DAY 8
0
生成式 AI

AI 產品與架構設計之旅:從 0 到 1,再到 Day 2系列 第 8

Day 8: 從嘴砲王到行動派 - MCP 工具整合讓 ChatBot 不只純聊天

  • 分享至 

  • xImage
  •  

嗨大家,我是 Debuguy。

前面七天我們建了一個能多人協作的 Slack ChatBot 架構,但說實話,雖然我們做了一個 ChatBot,但目的上來說不是要做一個只能聊天的機器人。

真正的目標是要讓它能解決實際問題

來看看 Day 7 的版本

一個讓人無奈的對話現場

問題的本質

我們花了這麼多心力讓 LLM 學會「對話」,但 LLM 本質上還是被困在預先訓練的世界裡:

「就像一個超級聰明但被綁在椅子上的人,只能動嘴巴不能動手...」

我們需要的不是會聊天的 Slack Bot,而是能真正解決問題的助手。

MCP 來拯救我們了

什麼是 MCP?

MCP(Model Context Protocol)是讓 LLM 能「動手做事」的標準化協議。簡單說就是給 LLM 裝上各種「外掛工具」的介面。

雖然概念不新,但 MCP 的標準化讓整個生態系變得更有秩序。不管只是 Claude Desktop 還是其他支援 MCP 的平台,都能共用相同的工具。

這裡先以簡單的街上 Playwright MCP 作為範例?

在所有可能的 MCP Server 中,我選 Playwright 有幾個原因:

1. 立竿見影的效果

從「我無法查看網頁」變成能實際執行網頁操作和資訊查詢。

2. 生活化的應用場景

比如查詢颱風假資訊、查看網站狀態等日常工作需求。

3. 除錯友善

開發期間可以用 non-headless 方式啟動 Playwright 提供視覺化的執行過程,出問題時容易診斷。

實戰:把 Playwright 接入我們的架構

Step 1: 建立 MCP 配置

先建立 config/mcp-config.json

{
    "mcpServers": {
        "playwright": {
            "command": "npx",
            "args": [
                "@playwright/mcp",
                "--no-sandbox",
                "--isolated"
                "--browser", 
                "chrome"
            ]
        }
    }
}

Step 2: 在程式中載入 MCP 配置

// src/index.ts
import { readFileSync } from 'fs';
import { join } from 'path';
import { createMcpHost } from '@genkit-ai/mcp';

async function startGenkitFlow() {
  
  // 省略

  // 載入 MCP 配置
  const host = createMcpHost(
    JSON.parse(readFileSync(join(process.cwd(), 'config/mcp-config.json'), 'utf-8'))
  );

  const chatFlow = ai.defineFlow({
      // 省略
    },
    async ({ messages }) => {
      const { newMessages, history } = organizeMessages(messages);
      
      // 取得 MCP 工具和資源
      const tools = await host.getActiveTools(ai);
      const resources = await host.getActiveResources(ai);
      
      return (await ai.prompt('chatbot')({
        botUserId: process.env['SLACK_BOT_USER_ID']!,
        prompt: newMessages,
      }, {
        messages: history,
        tools,        // 注入工具
        resources,    // 注入資源
        toolChoice: 'auto',  // 讓 LLM 自動決定是否使用工具
      })).text;
    }
  );

  // ... rest of the code
}

程式碼的重點有兩個:

  1. 配置外部化:MCP 設定獨立於程式碼,易於管理
  2. 自動工具選擇toolChoice: 'auto' 讓 LLM 根據需求自動決定是否使用工具

實際效果:颱風假查詢範例

讓我們看看實際的效果。

開發體驗的提升

GenKit Developer UI 的工具整合

當 MCP 工具註冊後,Developer UI 可以查看:

  1. 可用工具清單:清楚看到 Playwright 相關的所有工具

完整的 Trace 記錄

每次 MCP 工具使用都會留下詳細記錄:

  • 何時決定使用工具
  • 使用了哪些工具
  • 工具的執行結果
  • LLM 如何解讀結果

小結:從「只會聊天」到「能做實事」

今天我們成功讓 ChatBot 具備了真正的行動能力

技術架構的完善:

  • MCP 標準化整合:使用配置檔案管理工具
  • GenKit 無縫支援:開發和除錯體驗優秀
  • 動態工具載入:靈活的工具管理方式

使用體驗的革命性提升:

  • 從被動到主動:不再說「我無法」,而是直接解決問題
  • 從抽象到具體:提供實際的搜尋結果和詳細資訊
  • 從單一到多元:同時具備對話和行動能力

「現在我的 Bot 終於不再是只會聊天了!」

明天我們要來解決另一個重要的使用者體驗問題:如何讓 AI 的思考過程變得透明?

當 AI 在執行複雜任務時,使用者往往不知道它在做什麼,這種「黑盒子」的等待體驗很糟糕,尤其有時候要等到結果需要三十秒以上的時候,會不知道 application 是不是壞了!明天我們就要來著手解決這個問題。


完整的原始碼在這裡,包含 MCP 配置和 Playwright 工具整合!


One More Thing

如果框架不支援 MCP 怎麼辦?

我踩過的坑:Google 套件的 Deprecated 悲劇

在這個專案的早期,我使用的是 @google/generative-ai 套件,結果開發到一半發現這個套件被 Deprecated 了!

「當下的心情就像打開冰箱拿飲料想暢飲一番才發現過期了一樣...」

而且更慘的是,這個舊套件根本不支援 MCP。
但當時對我來說不是大問題,因為 MCP 的本質就是 Function Calling 的標準化協議

MCP = Function Calling + 標準化定義

MCP 實際上定義了三個核心概念:

  1. 資源 (Resources):伺服器可以暴露的資料,如檔案、資料庫記錄、API 回應等
  2. 工具 (Tools):伺服器提供給模型使用的功能,如執行查詢、建立檔案、呼叫 API 等
  3. 提示詞 (Prompts):可重複使用的提示詞模板,可以包含參數和上下文

既然本質上是 Function Calling,我們完全可以自己實作轉換!

自救方案:MCP 到 Function Calling 的轉換

我們可以使用 @modelcontextprotocol/sdk 來連接 MCP Server,然後自己轉換成對應框架的 Function Calling 格式。

以下程式碼皆由 AI 生成,各位客官就當虛擬碼看看吧

import { Client } from '@modelcontextprotocol/sdk/client/index.js';

async function createMcpTools() {
  // 1. 連接到 MCP Server
  const client = new Client({
    name: 'my-slack-bot',
    version: '1.0.0',
  });

  await client.connect(playwrightMcpServer);

  // 2. 取得 MCP Server 的工具清單
  const { tools } = await client.listTools();

  // 3. 轉換成 Function Calling 格式
  const functionDeclarations = tools.map(tool => ({
    name: tool.name,
    description: tool.description,
    parameters: tool.inputSchema,  // MCP 的 JSON Schema 格式
  }));

  return {
    tools: functionDeclarations,
    executeFunction: async (name: string, args: any) => {
      // 4. 實際執行 MCP 工具
      const result = await client.callTool({
        name,
        arguments: args,
      });
      return result.content;
    }
  };
}

實際使用(以舊版 Google SDK 為例):

// 這是請 AI 寫的範例,但概念上是這樣沒錯
import { GoogleGenerativeAI } from '@google/generative-ai';

const genAI = new GoogleGenerativeAI(process.env.GEMINI_API_KEY!);

async function chatWithMcp(userMessage: string) {
  const { tools, executeFunction } = await createMcpTools();
  
  // 建立支援 Function Calling 的模型
  const model = genAI.getGenerativeModel({
    model: 'gemini-2.5-flash-lite',
    tools: [{ functionDeclarations: tools }],  // 注入轉換後的工具
  });

  const chat = model.startChat();
  let result = await chat.sendMessage(userMessage);
  
  // 處理 Function Calling
  let response = result.response;
  if (response.functionCalls()) {
    const functionCalls = response.functionCalls();
    
    for (const call of functionCalls) {
      // 執行 MCP 工具
      const functionResult = await executeFunction(call.name, call.args);
      
      // 把結果回傳給 LLM
      result = await chat.sendMessage([{
        functionResponse: {
          name: call.name,
          response: { content: functionResult }
        }
      }]);
    }
  }

  return result.response.text();
}

--

AI 的發展變化很快,目前這個想法以及專案也還在實驗中。但也許透過這個過程大家可以有一些經驗和想法互相交流,歡迎大家追蹤這個系列。

也歡迎追蹤我的 Threads @debuguy.dev


上一篇
Day 7: 從 N-1 到 N-N 當協作成為必然結果 bot 也會玩狼人殺 !?
下一篇
Day 9: 讓 AI 思考過程透明化 - 從黑盒子到玻璃屋
系列文
AI 產品與架構設計之旅:從 0 到 1,再到 Day 29
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言