「能做事」的 AI Agent,而不只是聊天視窗時,很快就會遇到一個架構問題:AI 推理、工具呼叫、業務邏輯,這三件事該怎麼分層?
用 ASP.NET Core 打造 Semantic Kernel + MCP 的 AI Agent 協調層
前言
當團隊決定把 LLM 真正落地成「能做事」的 AI Agent,而不只是聊天視窗時,很快就會遇到一個架構問題:AI 推理、工具呼叫、業務邏輯,這三件事該怎麼分層?
Microsoft 的 Semantic Kernel(SK) 擅長「協調(orchestration)」——管理 Prompt、Function、對話狀態;而 Model Context Protocol(MCP) 擅長「工具連接(tool connectivity)」——用一套標準協定讓任何 LLM 都能發現、呼叫外部工具,不必為每個 AI 框架各寫一套 function calling 格式。
這篇文章會用一個範例,示範如何在 ASP.NET Core 專案中,用一個「薄 Controller(thin controller)」把這兩者串起來,做成一個可以被前端或其他系統呼叫的 AI Agent 入口。
一、整體架構
使用者請求
│
▼
ASP.NET Core Controller(協調入口)
│ 1. 驗證請求
│ 2. 查詢/識別使用者
│ 3. 呼叫 MCP Tool
▼
IMcpClientService(抽象化的 MCP Client)
│
▼
MCP Server(暴露一個或多個 Tool)
│
▼
Semantic Kernel / LLM 推理
│
▼
結構化回應 → 回傳給 Controller → 回傳給使用者
這裡的關鍵設計原則是:Controller 不做推理、不做複雜業務運算,只負責「驗證 → 轉發 → 整理回應」。所有跟 AI 相關的邏輯,都封裝在 MCP Server 端的 Tool 裡;Controller 只是一個乾淨的協調層。
二、為什麼不直接在 Controller 裡呼叫 Semantic Kernel?
一個常見的新手做法,是直接在 Controller 裡 new 一個 Kernel、掛上 Plugin、呼叫 InvokeAsync。這樣做在 Demo 階段沒問題,但正式環境會遇到幾個問題:
- 難以測試:Controller 直接依賴 SK 的具體實作,單元測試時很難 mock。
- 難以水平擴充:如果之後想把 MCP Server 換成獨立服務、甚至讓多個不同語言的 Client 共用同一組 Tool,Controller 內嵌的呼叫邏輯會綁死在單一專案裡。
- 職責混雜:Controller 同時做驗證、AI 呼叫、資料庫存取,違反單一職責原則。
所以比較穩健的做法,是把 MCP 呼叫包成一個介面:
public interface IMcpClientService
{
Task<ToolResponse> CallToolsAsync(
string toolName,
Dictionary<string, object?> parameters);
Task<IEnumerable<string>> ListToolNamesAsync(string serverUrl);
}
Controller 只依賴這個介面,實作細節(要連哪個 MCP Server、用什麼傳輸協定 stdio/SSE/HTTP)都被隱藏起來,之後要更換或 mock 都很單純。
三、範例 Controller
以下是一個精簡過的協調層 Controller 範例,示範核心流程:
[ApiController]
[Route("api/agent")]
public class AgentRouteController : ControllerBase
{
private readonly IMcpClientService _mcpClient;
private readonly IUserService _userService;
private readonly ILogger<AgentRouteController> _logger;
public AgentRouteController(
IMcpClientService mcpClient,
IUserService userService,
ILogger<AgentRouteController> logger)
{
_mcpClient = mcpClient;
_userService = userService;
_logger = logger;
}
[HttpPost("route")]
public async Task<IActionResult> Route([FromBody] AgentRequest request)
{
// 1. 基本驗證
if (request is null || string.IsNullOrEmpty(request.SessionId))
{
return BadRequest(new { message = "SessionId 不可為空" });
}
// 2. 使用者識別(容錯:允許用 Email 或帳號查詢,不分大小寫)
var user = await _userService.FindUserAsync(request.UserIdentifier);
if (user is null)
{
return NotFound(new { message = "使用者不存在" });
}
// 3. 呼叫 MCP Tool
try
{
var result = await _mcpClient.CallToolsAsync(
"RouteAgentTool",
new Dictionary<string, object?>
{
["userId"] = user.Id,
["prompt"] = request.Prompt,
["sessionId"] = request.SessionId
});
if (string.IsNullOrEmpty(result.Error) && !string.IsNullOrEmpty(result.RawResult))
{
return Ok(new { status = 200, response = result.RawResult });
}
return NotFound(new { status = 404, response = result.Error ?? "無有效回應" });
}
catch (Exception ex)
{
_logger.LogError(ex, "呼叫 MCP Tool 失敗");
return StatusCode(500, new { message = "AI Agent 服務暫時無法回應" });
}
}
}
四、幾個實務上值得注意的設計細節
1. 使用者識別要做「容錯查詢」
企業系統裡,同一個使用者常常有多種識別方式(Email、帳號、員編)。與其要求前端一定要傳對欄位,不如在後端做一層容錯:先用主要欄位查,查不到再用次要欄位(不分大小寫)比對一次。這能大幅降低串接失敗率,代價是多一次查詢,通常可以接受。
2. 回應「有效性」不要只靠字串比對
如果 AI 回應「這個問題我無法回答」之類的文字,很容易讓人直覺地寫:
bool isValid = !result.RawResult.Contains("無法回答");
這樣做短期有效,但只要 Prompt 或模型輸出的措辞一改,判斷就會失靈。比較穩健的做法是:
- 讓 MCP Tool/Prompt 端直接回傳結構化欄位(例如
"status": "unsupported"),而不是靠自然語言文字判斷; - 或至少把這些「魔法字串」抽成設定檔/常數集中管理,方便日後調整。
3. 用 Session 維持多輪對話狀態
AI Agent 很少是一問一答就結束,通常需要維持上下文(例如使用者上一輪問的是什麼類別的問題)。常見做法是:
- 呼叫 Agent 前,先透過一個
GetOrCreateSession端點,取得/建立SessionId; - 每次呼叫都帶上
SessionId,並在 Server 端把最近幾輪的對話記錄存起來(DB 或 Cache 皆可),必要時回查最新一筆記錄的分類/狀態,回傳給前端。
4. 例外處理要分層
MCP 呼叫可能因為「連線失敗」「逾時」「回應格式錯誤」失敗,這三種情況對維運人員的意義完全不同,建議在 catch 區塊至少做基本分類記錄,而不是全部包成同一種 500/404,方便之後查 log 定位問題。
五、什麼時候該用 MCP,什麼時候不需要
MCP 不是萬靈丹,導入前可以先問自己幾個問題:
- 需要多個不同的 AI Client(例如公司內部 Agent、GitHub Copilot、其他廠商的 Copilot)共用同一組工具嗎? 如果是,MCP 的標準化發現機制會很有價值。
- 只是做一個固定資料源的內部小工具? 如果需求單純、只有一個呼叫端,直接用 Semantic Kernel 的原生 Plugin/Function Calling 反而更輕量,不需要多一層協定開銷。
- 對延遲非常敏感嗎? MCP 走 JSON-RPC,會有序列化與往返成本,如果需要毫秒級回應,這層開銷需要納入評估。
六、小結
這個架構的核心精神很簡單:讓 Controller 保持「薄」,把 AI 推理與工具邏輯都交給 MCP Server/Semantic Kernel 處理。這樣做的好處是:
- Controller 容易測試、容易維護;
- MCP Server 可以被多個不同的 Client/團隊重複使用;
- 未來要更換底層 LLM 供應商,或是把 Agent 邏輯搬到獨立服務,都不需要動到最外層的 API 入口。
如果你正在把 LLM 從「聊天視窗」升級成「真正能串接系統的 AI Agent」,這套「Controller + MCP Client 介面 + Semantic Kernel」的分層方式,會是一個蠻穩妥的起手式。