iT邦幫忙

3

模型當初階客服助理- MCP和LLM搭配實作與外部註冊服務工具

  • 分享至 

  • xImage
  •  

https://ithelp.ithome.com.tw/upload/images/20250616/20144614UKxPsiv4Nh.jpg

當 LLM 可以快速協助開發API 後,接著又想打它的主意:幫忙AI化系統。早期是逐步將繁雜的系統"API化",但現在可能要朝系統"AI化"發展了。

這邊系統AI化最直覺的方法,就是讓模型辨認,使用者下了什麼樣的需求,去協助幫忙溝通與執行系統。
而能夠讓LLM隨時替換模型,又能溝通系統,MCP似乎可以在這個時間點辦到。MCP可以到https://modelcontextprotocol.io/introduction 查看,有將做法與套件公佈出來。
這邊預想實行以下構想:
https://ithelp.ithome.com.tw/upload/images/20250616/20144614g3c54967IH.jpg

MCP中可以將這些服務註冊到工具集合中。
但要如何實作出隨時抽換服務? 可以透過外部組件與讀檔註冊方式實行,概念類似抽換類別模組不重啟服務外部擴充功能研究範例。也就是讀設定檔來逐一註冊組件內的工具。

先準備二個 LLM 助 手,這邊是使用 python 撰寫的 FastAPI 。

第一個助手是幫使用者辨識執行已註冊的系統溝通工具:

from google import genai
from google.genai import types
from io import BytesIO

@app.post("/CallLLM/")
async def CallLLM(data: Item = Body(...,media_type = "application/json")):

    client = genai.Client(api_key="AIhbiexKpggpWVmG8Zjs5QdtnQx8rpwhSac7X2o")
    MODEL_ID = "gemini-2.0-flash"
    user_req =  data.msg  
    proj_name = "DigitalDevProj"

    system_prompt = f"成為一個專業的程式開發者,且精通json格式。 \
        我需要你的協助產生json字串, 只需要依照使用者需求內容來產生json字串。其它以外的資訊請排除掉。"

    prompt = f' 使用者需求: {user_req}  。 ' + r'目前提供現有json物件陣列: \
    [ \
    {"title":"Add","description":"加上二個數字, 對應a是第一個數字, b是第二個數字。","type":"object","properties":{"a":{"type":"integer"},"b":{"type":"integer"}},"required":["a","b"]} ,   \
    {"title":"substract","description":"substract two numbers,parameter a is first integer, than b is second integer。","type":"object","properties":{"a":{"type":"integer"},"b":{"type":"integer"}},"required":["a","b"]}, \
    {"title":"InvoceApplicate","description":"財會發票申請,參數說明:  \
    ItemName為商品名稱,型態為字串。 ItemPrice為商品價格,型態為整數。 Applicants為申請者,型態為字串。 InvoiceDate為發票開立日期,型態為字串。 dep為申請單位,型態為字串。" ,"type":"object", \
        "properties":{"ItemName":{"type":"string"},"ItemPrice":{"type":"integer"},"Applicants":{"type":"string"},"InvoiceDate":{"type":"string"}, "dep":{"type":"string"} },   \
    "required":["a","b"] }  \
    ] \
    ,請由使用者需求,分析對應至現有json陣列中的哪一個物件,關鍵對應是依照其中description的值。回傳找出對應的json物件,此物件包含成員為 title、description、type、properties、required,並將此物件的properties中的成員之值替換成使用者需求所給的值。 \
    如果沒有對應到,則回傳現有json物件陣列中第一個物件,並將並將所有成員的值變成空字串。  '

    response = client.models.generate_content(
        model=MODEL_ID,
        contents=[prompt],
        config=types.GenerateContentConfig(
            system_instruction=system_prompt
        )
    )

    return response.text

注意到提示給 LLM 的敘述中,有"提供現有json物件陣列",這個是代表"掃出"所註冊的執行工具所"組出"的,讓LLM可以從中(限制只能從中)找出對應的執行工具,這邊為了展示方便直接兜出來放到API中展示,之後實作MCP會看到。實際上可以在MCP服務起來後,掃註冊的所有工具服務,存到DB或其它地方,再讓FastAPI底層去讀出來讓LLM模型知道。其組成結構欄位解析如下:

//註冊工具物件類別
public class ToolDescription
{
    //工具名稱
    public string title { get; set; }
    
    //工具描述
    public string description { get; set; }
    
    //工具型態
    public string type { get; set; }
    
    //工具使用參數
    public Dictionary<string,object> properties { get; set; }
}

第二個助手是當執行發生問題後,跟使用者說明清楚要執行的目標有需要什麼內容,使用者可以較清楚知道缺了什麼。類似客服:

from google import genai
from google.genai import types
from io import BytesIO

@app.post("/CallLLM_CustomerService/")
async def CallLLM_CustomerService(data: Item = Body(...,media_type = "application/json")):

    client = genai.Client(api_key="AIhbiexKpggpWVmG8Zjs5QdtnQx8rpwhSac7X2o")
    MODEL_ID = "gemini-2.0-flash"
    
    system_prompt = f"成為一個專業的程式開發者,且精通json格式。 \
          我需要你的協助和人類溝通,說明json的內容。其它以外的資訊請排除掉。"

    prompt =  r'目前提供 json 物件陣列: \
    [ \
    {"title":"InvoceApplicate","description":"財會發票申請,參數說明:  \
    ItemName為商品名稱,型態為字串。 ItemPrice為商品價格,型態為整數。 Applicants為申請者,型態為字串。 InvoiceDate為發票開立日期,型態為字串。 dep為申請單位,型態為字串。" ,"type":"object", \
        "properties":{"ItemName":{"type":"string"},"ItemPrice":{"type":"integer"},"Applicants":{"type":"string"},"InvoiceDate":{"type":"string"}, "dep":{"type":"string"} },   \
    "required":["ItemName","ItemPrice","Applicants","InvoiceDate","dep"] }  \
    ] \
    ,請以客服的口吻,和人類描述此json主要功能與需要的參數,參數的key值不要直接描述出來,且開頭請以這段話開始: 因為執行出了一點問題,未完成任務,這邊跟您說明此功能的資訊。 '

    response = client.models.generate_content(
        model=MODEL_ID,
        contents=[prompt],
        config=types.GenerateContentConfig(
            system_instruction=system_prompt
        )
    )

    return response.text

注意到這邊也為了展示方便,把工具物件json直接卡進去API函式中,實際上是傳進去在餵給LLM,一樣是展示清楚用。
即以下這json物件是剛剛判斷取得的工具描述內容,再餵進去客服API的物件內容:

{"title":"InvoceApplicate","description":"財會發票申請,參數說明:  \
    ItemName為商品名稱,型態為字串。 ItemPrice為商品價格,型態為整數。 Applicants為申請者,型態為字串。 InvoiceDate為發票開立日期,型態為字串。 dep為申請單位,型態為字串。" ,"type":"object", \
        "properties":{"ItemName":{"type":"string"},"ItemPrice":{"type":"integer"},"Applicants":{"type":"string"},"InvoiceDate":{"type":"string"}, "dep":{"type":"string"} },   \
    "required":["ItemName","ItemPrice","Applicants","InvoiceDate","dep"] }  

準備好LMM後,開始實行MCP。
這邊使用微軟開發的MCP套件,語言是C#。
第一步是建立 MCP Server(當然之後可以建立多個不同用途的 MCP Server!)

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using ModelContextProtocol.Server;
using System.ComponentModel;
using System.Reflection;

var builder = Host.CreateApplicationBuilder(args);
builder.Logging.AddConsole(consoleLogOptions =&gt;
{
    // Configure all logs to go to stderr
    consoleLogOptions.LogToStandardErrorThreshold = LogLevel.Trace;
});

builder.Services
    .AddMcpServer()
    .WithStdioServerTransport()
    .WithToolsFromAssembly()
    .WithToolsFromAssembly(
               Assembly.LoadFile(@"D:\NetMCP\DLL_Extension\DLL_Extension-X\DLL_Extension\bin\Debug\DLL_Extension.dll")
            ); 

await builder.Build().RunAsync();


/// &lt;summary&gt;
/// 測試開通用途-執行加法-寫在建立MCP Server組件內
/// &lt;/summary&gt;
[McpServerToolType]
public  static partial class CalculatorTool
{
    [McpServerTool, Description("add two number, parameter a is first integer, than b is second integer。") ]
    public static string Add(int a, int b) =&gt; $"add {a + b}";
}

注意其中的這段:

.WithToolsFromAssembly(
               Assembly.LoadFile(@"D:\NetMCP\DLL_Extension\DLL_Extension-X\DLL_Extension\bin\Debug\DLL_Extension.dll")
            )

有看到這段讀取嗎? 這就是給機會去讀取外部組件,達成外部設定透過讀取組件來註冊,可以改成讀設定檔取出路徑逐一讀取註冊。如果可以透過設定檔如json檔案,之後上版註冊就相對快與方便一些。

接著此展示於外部組件註冊了二個服務,一個是測試是否開通的功能,一個是正式要執行的服務 "發票申請表單填寫-起流程單"。
外部組件註冊服務工具:

using ModelContextProtocol.Server;
using System.ComponentModel;

/// &lt;summary&gt;
/// 測試開通用途-執行減法
/// &lt;/summary&gt;
[McpServerToolType]
public static class CalculatorTool_EX
{
    [McpServerTool, Description("substract two numbers。parameter a is first integer, than b is second integer。")]
    public static string substract(int a, int b) =&gt; $"substract {a - b}"; 
}

/// &lt;summary&gt;
/// 發票申請表單填寫-起流程單
/// &lt;/summary&gt;
[McpServerToolType]
public static class InvoiceModule_EX
{
    [McpServerTool, Description(@"發票申請,參數說明: (1) ItemName為商品名稱,型態為字串。 
                                                     (2) ItemPrice為商品價格,型態為整數。
                                                     (3) Applicants為申請者,型態為字串。
                                                     (4) InvoiceDate為發票開立日期,型態為字串。
                                                     (5) dep為申請單位,型態為字串。")]
    public static string InvoceApplicate(string ItemName, int ItemPrice, string Applicants, string InvoiceDate,string dep) 
                                        =&gt; Invoce.ExecApplicate( ItemName,  ItemPrice, Applicants, InvoiceDate); 
}

建好 MCP Server 後,繼續建立接收使用者訊息並讓 LLM 和 MCP 溝通的 Client 部分:
MCP Client

using ModelContextProtocol.Client;
using Newtonsoft.Json;
using ModelContextProtocol.Protocol;

var clientTransport = new StdioClientTransport(new()
{
    Name = "ConnetServer",
    Command = @"dotnet",
    Arguments = ["run", "--project", "D:\\NetMCP\\ConsoleMCP_Server\\ConsoleMCP.csproj"],
});

var userMessage = string.Empty; //準備接收的訊息會指派到這


string retResponse = string.Empty;
string apiUrl = "http://localhost:5005/CallLLM/";  //FastAPI:第一個助手幫使用者辨識執行已註冊的系統溝通工具
using (var client = new HttpClient())
{
    var jsonContent = "{ \"msg\" : \"" + userMessage + "\" }";  //JsonConvert.SerializeObject(data);
    var stringContent = new StringContent(jsonContent, System.Text.Encoding.UTF8, "application/json");

    // 發出POST請求
    var response = await client.PostAsync(apiUrl, stringContent);


    if (response.IsSuccessStatusCode)
    {
        // 取得內容
        string responseContent = await response.Content.ReadAsStringAsync();
        Console.WriteLine(responseContent);
        responseContent = responseContent.Replace("\"```json", "").Replace("```\\n\"", "").Replace("\\n", "").Replace("\\", "");

        try
        {
            ToolDescription ret = JsonConvert.DeserializeObject&lt;ToolDescription&gt;(responseContent);

            if (ret != null && ret.title != null && ret.properties != null)
            {
                var result = ((CallToolResponse)await mcpClient.CallToolAsync(
                        ret.title,
                        ret.properties,
                        cancellationToken: CancellationToken.None
                    )
                  );
                Console.WriteLine(result.Content.First(c =&gt; c.Type == "text").Text);
            }
        }
        catch(Exception ex)
        {
            Console.WriteLine(ex.Message.ToString()); 
            //使用者看不懂錯誤訊息,要呼叫第二個助手客服工程師幫忙解析工具服務
        }   
    }
    else
    {
        // 處理錯誤情況
        Console.WriteLine($"Error: {response.StatusCode}");
    }

而將已註冊的服務掃出來,準備要讓 LLM 知道的訊息,可以利用以下來組出轉換,再存到DB或其他檔案中供FastAPI那邊讀取,這邊不實作:

public class ToolDescription
{
    public string title { get; set; }
    public string description { get; set; }
    public string type { get; set; }
    public Dictionary<string,object> properties { get; set; }
}

async Task<List<ToolDescription>> GetMcpTools()
{
    Console.WriteLine("Listing tools");
    var tools = await mcpClient.ListToolsAsync();

    List<ToolDefinition> toolDefinitions = new List<ToolDefinition>();

    foreach (var tool in tools)
    {
        Console.WriteLine($"Connected to server with tools: {tool.Name}");
        Console.WriteLine($"Tool description: {tool.Description}");
        Console.WriteLine($"Tool parameters: {tool.JsonSchema}");

        toolDefinitions.Add(new toolDefinitions(){
                                 title = tool.Name, 
                                 description = tool.description,
                                 properties=ConverToDict(tool.JsonSchema)
                            });
    }

    return toolDefinitions;
}

開始Demo訊息:

(1)訊息 userMessage = "2 減 3"
這是做開通測試的功能。有找到已註冊的服務工具並實行此工具"減法":
https://ithelp.ithome.com.tw/upload/images/20250616/20144614AVGkGVEsZE.jpg

(2)訊息 userMessage = "幫我送發票申請,商品名稱為手機,價格36000,申請者為喵喵有限公司,發票日期為2025-05-08,申請單位為開發部"
這是找到已註冊的服務工具"發票申請"並執行送出發票申請單,起流程。
https://ithelp.ithome.com.tw/upload/images/20250616/20144614UMidWbNXub.jpg

(3)訊息 userMessage = "幫我送請假單,公假,日期是今天,時間下午13:00到17:00"
因為沒有註冊請假申請表單送出的服務,自然就是找不到的狀況。
https://ithelp.ithome.com.tw/upload/images/20250616/20144614tkdCDyWZRf.jpg

(4)訊息 userMessage = "幫我送發 票申請,商 品 名稱為手機,申請者為喵喵 有 限,發 票日期為2025-05-08,申請單位為開 發 部"
忘了輸入價 格,不能順利申請。因為價 格為必要欄位。
https://ithelp.ithome.com.tw/upload/images/20250616/20144614oSkIYJmy1T.jpg
當這個狀況,有找出註冊的服務,但使用者給的資訊不足,就需要將此服務需要的內容"轉述"給使用者知道。
所以要呼叫第二個助手客服工程師幫忙解析工具服務。

根據此狀況執行設定好的客服助手 LLM 後,會回傳:

因為執行出了一點問題,未完成任務,這邊跟您說明此功能的資訊。

這個功能是關於「財會發票申請」。您需要提供一些資訊來完成申請,這些資訊包括:

*   **商品名稱**:您要申請發票的品名稱。
*   **商品價格**:這個商品的價格是多少。
*   **申請者**:是誰提出這個發票申請。
*   **發票開立日期**:發票是什麼時候開立的。
*   **申請單位**:哪個單位提出這個申請。

請確保您提供了以上所有資訊,才能順利完成發票申請。

藉由上述簡單的展示,未來可以將這MCP模組化、套件化或元件化,讓它更彈性的重複利用,只需要專注在教更多模型助手協助,讓功能更加完整。

另外,使用者訊息歷程是另外一個問題,怎麼去標註是連續同一個任務,還是穿插的任務,這部分需要再規劃,或替訊息標註flag,或設置有效指令連續範圍,或再請另一個"LLM助手"來幫忙整理記憶,都需要規劃與測試。

資安與權限部分,是展示未提到的,這部分也要額外注意。


圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言