當 LLM 可以快速協助開發API 後,接著又想打它的主意:幫忙AI化系統。早期是逐步將繁雜的系統"API化",但現在可能要朝系統"AI化"發展了。
這邊系統AI化最直覺的方法,就是讓模型辨認,使用者下了什麼樣的需求,去協助幫忙溝通與執行系統。
而能夠讓LLM隨時替換模型,又能溝通系統,MCP似乎可以在這個時間點辦到。MCP可以到https://modelcontextprotocol.io/introduction 查看,有將做法與套件公佈出來。
這邊預想實行以下構想:
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 =>
{
// 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();
/// <summary>
/// 測試開通用途-執行加法-寫在建立MCP Server組件內
/// </summary>
[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) => $"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;
/// <summary>
/// 測試開通用途-執行減法
/// </summary>
[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) => $"substract {a - b}";
}
/// <summary>
/// 發票申請表單填寫-起流程單
/// </summary>
[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)
=> 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<ToolDescription>(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 => 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"
這是做開通測試的功能。有找到已註冊的服務工具並實行此工具"減法":
(2)訊息 userMessage = "幫我送發票申請,商品名稱為手機,價格36000,申請者為喵喵有限公司,發票日期為2025-05-08,申請單位為開發部"
這是找到已註冊的服務工具"發票申請"並執行送出發票申請單,起流程。
(3)訊息 userMessage = "幫我送請假單,公假,日期是今天,時間下午13:00到17:00"
因為沒有註冊請假申請表單送出的服務,自然就是找不到的狀況。
(4)訊息 userMessage = "幫我送發 票申請,商 品 名稱為手機,申請者為喵喵 有 限,發 票日期為2025-05-08,申請單位為開 發 部"
忘了輸入價 格,不能順利申請。因為價 格為必要欄位。
當這個狀況,有找出註冊的服務,但使用者給的資訊不足,就需要將此服務需要的內容"轉述"給使用者知道。
所以要呼叫第二個助手客服工程師幫忙解析工具服務。
根據此狀況執行設定好的客服助手 LLM 後,會回傳:
因為執行出了一點問題,未完成任務,這邊跟您說明此功能的資訊。
這個功能是關於「財會發票申請」。您需要提供一些資訊來完成申請,這些資訊包括:
* **商品名稱**:您要申請發票的品名稱。
* **商品價格**:這個商品的價格是多少。
* **申請者**:是誰提出這個發票申請。
* **發票開立日期**:發票是什麼時候開立的。
* **申請單位**:哪個單位提出這個申請。
請確保您提供了以上所有資訊,才能順利完成發票申請。
藉由上述簡單的展示,未來可以將這MCP模組化、套件化或元件化,讓它更彈性的重複利用,只需要專注在教更多模型助手協助,讓功能更加完整。
另外,使用者訊息歷程是另外一個問題,怎麼去標註是連續同一個任務,還是穿插的任務,這部分需要再規劃,或替訊息標註flag,或設置有效指令連續範圍,或再請另一個"LLM助手"來幫忙整理記憶,都需要規劃與測試。
資安與權限部分,是展示未提到的,這部分也要額外注意。