延續昨天的 AI Agent Tools 設計原則,今天將透過實際的程式碼範例深入實作層面,介紹如何使用 Semantic Kernel 撰寫 Tools(工具函式)並將其掛載到 Kernel 中,以實現 AI Agent 中不可或缺的 Function Calling 。
在前面的文章內容裡,大家應該已經理解到 AI Agent 之所以能力強大,很大一部份原因取決於其強大的 Function Calling 功能。它允許 AI 模型呼叫開發者定義的函式,實現與現有程式碼的互動,進而自動化商業流程、產生程式碼片段等多種應用。然而實際 LLM 的 Function Calling 機制通常相當複雜,涉及函式定義的序列化、參數傳遞、錯誤處理等多個環節。這些細節當然開發者可以自行實作,但通常較為繁瑣且容易出錯。而 Semantic Kernel 簡化了 Function Calling 的開發及使用過程,自動處理函式描述和參數向模型的傳遞,以及模型與程式碼之間的雙向溝通。
Semantic Kernel 會把你註冊進 Kernel 的函式(含參數)序列化成 JSON Schema,連同聊天過程送給模型;模型回來的不是話、而是「要呼叫哪個函式+參數」,Semantic Kernel 便會根據這些資訊把 Function 叫起來,並把結果再塞回聊天歷史,直到模型給出最終回答或迭代上限為止,而這個流程可以全自動,也可以半自動(手動調用)。
在 Semantic Kernel 中,最簡單的方式是使用 [KernelFunction]
屬性並使用[Description]
標註所寫的 C# 方法用途與參數描述,這個關鍵的標註會被 Semantic Kernel 用來生成函式的 JSON Schema,並提供給 LLM 模型使用,是一個非常重要的步驟。以下是一個簡單的天氣查詢工具範例:
public class WeatherServicePlugin
{
private static readonly Dictionary<string, int> CityTemperatures = new Dictionary<string, int>
{
{ "Taipei", 28 },
{ "Kaohsiung", 31 },
{ "Taichung", 27 },
{ "Tainan", 30 },
{ "Hsinchu", 26 }
};
[KernelFunction]
[Description("Retrieves the today temperature of the city.")]
public int Get_Today_Temperature(
[Description("The name of the city to get the temperature for.The city names in the weather data are in English")]
string city)
{
if (CityTemperatures.TryGetValue(city, out int temp))
{
return temp;
}
// 模擬未知城市的溫度
return 25;
}
}
這個範例中,請注意到關於 city 參數的描述特別強調「城市名稱是英文」,這是因為模型在理解參數時,會依賴這些描述來決定如何填入參數值,清楚且具體的描述能夠大幅提升模型正確呼叫函式的機率。所以當使用者詢問「台北今天幾度?」時,模型能夠理解並自動將 city 參數設為 "Taipei"。是的,它會自動把「台北」翻譯成「Taipei」,這就是 LLM 的強大之處。
Semantic Kernel 也支援複雜的參數類型,包括陣列或是自定義物件。以下是一個企業人力資源管理系統的範例:
public enum EmployeeLevel
{
Junior,
Senior,
Lead
}
public enum Department
{
Engineering,
Sales,
Marketing
}
public enum SkillCategory
{
Programming,
ProjectManagement,
Communication,
Leadership
}
public class EmployeeSearchCriteria
{
public List<Department> Departments { get; set; } = new();
public List<EmployeeLevel> Levels { get; set; } = new();
public List<string> Skills { get; set; } = new();
public int? MinExperience { get; set; }
public int? MaxExperience { get; set; }
public bool? IsRemoteWorker { get; set; }
}
public class HRManagementPlugin
{
private static readonly List<Employee> _employees = GenerateEmployeeData();
/// <summary>
/// 產生模擬員工資料
/// </summary>
private static List<Employee> GenerateEmployeeData()
{
var random = new Random(42); // 固定種子以獲得一致的結果
var employees = new List<Employee>();
var names = new[] { "王小明", "李小華", "張大偉", "陳美玲", "林志強", "黃淑芬", "吳建國", "劉雅婷",
"蔡政宏", "鄭惠美", "楊承翰", "許雅雯", "馬志豪", "周美惠", "趙家豪", "錢雅萍",
"孫建華", "李志明", "王美玲", "陳大偉", "林小華", "張雅婷", "吳志強", "劉美惠",
"蔡建國", "鄭志豪", "楊雅雯", "許承翰", "馬美玲", "周志強" };
var departments = Enum.GetValues<Department>();
var levels = Enum.GetValues<EmployeeLevel>();
var skills = Enum.GetValues<SkillCategory>();
for (int i = 0; i < 30; i++)
{
var employeeSkills = new List<SkillCategory>();
var skillCount = random.Next(1, 4); // 每人1-3項技能
for (int j = 0; j < skillCount; j++)
{
var skill = skills[random.Next(skills.Length)];
if (!employeeSkills.Contains(skill))
employeeSkills.Add(skill);
}
employees.Add(new Employee
{
Id = $"EMP{(i + 1):D3}",
Name = names[i],
Department = departments[random.Next(departments.Length)],
Level = levels[random.Next(levels.Length)],
Skills = employeeSkills,
ExperienceYears = random.Next(1, 16), // 1-15年經驗
IsRemoteWorker = random.NextDouble() > 0.7, // 30%機率為遠端工作者
JoinDate = DateTime.Now.AddDays(-random.Next(365 * 10)) // 過去10年內加入
});
}
return employees;
}
[KernelFunction]
[Description("根據複雜條件搜尋員工,支援多維度篩選")]
public async Task<string> SearchEmployees(
[Description("要搜尋的部門陣列")] Department[]? departments = null,
[Description("員工職級陣列")] EmployeeLevel[]? levels = null,
[Description("必備技能陣列")] SkillCategory[]? requiredSkills = null,
[Description("最少工作經驗年數")] int minExperience = 0,
[Description("最多工作經驗年數,不限制則輸入-1")] int maxExperience = -1,
[Description("是否限定遠端工作者,true=僅遠端,false=僅非遠端,空白=不限制")] string remoteOnly = "")
{
// 處理陣列參數並轉換為 List
var departmentList = departments?.ToList() ?? new List<Department>();
var levelList = levels?.ToList() ?? new List<EmployeeLevel>();
var skillList = requiredSkills?.ToList() ?? new List<SkillCategory>();
bool? isRemoteWorker = null;
if (!string.IsNullOrWhiteSpace(remoteOnly))
{
if (bool.TryParse(remoteOnly, out var remote))
isRemoteWorker = remote;
}
// 模擬搜尋邏輯的延遲
await Task.Delay(100);
// 實際執行員工搜尋
var query = _employees.AsQueryable();
// 部門篩選
if (departmentList.Any())
{
query = query.Where(e => departmentList.Contains(e.Department));
}
// 職級篩選
if (levelList.Any())
{
query = query.Where(e => levelList.Contains(e.Level));
}
// 技能篩選(員工必須具備所有要求的技能)
if (skillList.Any())
{
query = query.Where(e => skillList.All(skill => e.Skills.Contains(skill)));
}
// 經驗年數篩選
query = query.Where(e => e.ExperienceYears >= minExperience);
if (maxExperience != -1)
{
query = query.Where(e => e.ExperienceYears <= maxExperience);
}
// 遠端工作篩選
if (isRemoteWorker.HasValue)
{
query = query.Where(e => e.IsRemoteWorker == isRemoteWorker.Value);
}
var matchedEmployees = query.ToList();
var searchCriteria = new EmployeeSearchCriteria
{
Departments = departmentList,
Levels = levelList,
Skills = skillList.Select(s => s.ToString()).ToList(),
MinExperience = minExperience,
MaxExperience = maxExperience == -1 ? null : maxExperience,
IsRemoteWorker = isRemoteWorker
};
// 產生搜尋結果
var searchResults = new
{
criteria = searchCriteria,
totalFound = matchedEmployees.Count,
matchedEmployees = matchedEmployees.Select(e => new
{
id = e.Id,
name = e.Name,
department = e.Department.ToString(),
level = e.Level.ToString(),
skills = e.Skills.Select(s => s.ToString()).ToArray(),
experienceYears = e.ExperienceYears,
isRemoteWorker = e.IsRemoteWorker,
joinDate = e.JoinDate.ToString("yyyy-MM-dd")
}).ToArray(),
searchTimestamp = DateTime.UtcNow
};
return System.Text.Json.JsonSerializer.Serialize(searchResults, new System.Text.Json.JsonSerializerOptions
{
WriteIndented = true,
Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping
});
}
[KernelFunction]
[Description("查看所有員工的基本資訊")]
public async Task<string> GetAllEmployees()
{
await Task.Delay(50);
var allEmployeesInfo = new
{
totalEmployees = _employees.Count,
employees = _employees.Select(e => new
{
id = e.Id,
name = e.Name,
department = e.Department.ToString(),
level = e.Level.ToString(),
skills = e.Skills.Select(s => s.ToString()).ToArray(),
experienceYears = e.ExperienceYears,
isRemoteWorker = e.IsRemoteWorker,
joinDate = e.JoinDate.ToString("yyyy-MM-dd")
}).ToArray(),
timestamp = DateTime.UtcNow
};
return System.Text.Json.JsonSerializer.Serialize(allEmployeesInfo, new System.Text.Json.JsonSerializerOptions
{
WriteIndented = true,
Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping
});
}
}
Semantic Kernel 完全支援非同步方法操作:
public class WeatherServicePlugin
{
private static readonly Dictionary<string, int> CityTemperatures = new Dictionary<string, int>
{
{ "Taipei", 28 },
{ "Kaohsiung", 31 },
{ "Taichung", 27 },
{ "Tainan", 30 },
{ "Hsinchu", 26 }
};
[KernelFunction]
[Description("Retrieves the today temperature of the city.")]
public async Task<int> Get_Today_Temperature(
[Description("The name of the city to get the temperature for.The city names in the weather data are in English")]
string city)
{
// 模擬非同步操作,例如從 API 獲取天氣資料
await Task.Delay(500); // 模擬 500ms 的網路延遲
if (CityTemperatures.TryGetValue(city, out int temp))
{
return temp;
}
// 模擬未知城市的溫度
return 25;
}
}
將撰寫好的 Tools 掛載到 Kernel 非常簡單,可以使用 AddFromType<T>
方法,這裡要特別說明的是,Semantic Kernel 支援為每個 Plugin 指定 Plugin Name,類似於命名空間(namespace)的概念,這樣可以幫助模型識別不同類型的工具,避免命名衝突並提升工具的可發現性。另外特別要提醒的是,僅掛載必要的 Plugin,避免讓模型面對過多選擇,這會增加模型選錯工具的風險。
// 建立 Kernel 並掛載 Plugin
Kernel kernel = Kernel.CreateBuilder()
.AddOpenAIChatCompletion(
apiKey: Config.OpenAI_ApiKey,
modelId: Config.ModelId)
.Build();
// 從類型掛載 Plugin,並指定 Plugin Name
kernel.Plugins.AddFromType<WeatherServicePlugin>("WeatherService");
kernel.Plugins.AddFromType<HRManagementPlugin>("HRManagement");
根據 Anthropic 的建議,當一個 Agent 掛載所有工具時,容易造成 LLM 的選擇困難,進而增加選錯工具的風險。所以比較好的做法是,應該根據 Agent 的角色來選擇合適的工具:
public static class PluginManager
{
public static void ConfigureForCustomerService(Kernel kernel)
{
// 客服專員只需要客戶相關工具集
kernel.Plugins.AddFromType<CustomerServicePlugin>("CustomerService");
kernel.Plugins.AddFromType<OrderManagementPlugin>("OrderManagement");
}
public static void ConfigureForHRService(Kernel kernel)
{
// 人力資源服務工具集
kernel.Plugins.AddFromType<HRManagementPlugin>("HRManagement");
}
public static void ConfigureForSalesAgent(Kernel kernel)
{
// 銷售代表需要銷售相關工具集
kernel.Plugins.AddFromType<CustomerServicePlugin>("CustomerService");
kernel.Plugins.AddFromType<SalesPlugin>("Sales");
kernel.Plugins.AddFromType<ProductCatalogPlugin>("ProductCatalog");
}
}
// 使用方式
Kernel customerServiceKernel = Kernel.CreateBuilder()
.AddOpenAIChatCompletion(apiKey: apiKey, modelId: modelId)
.Build();
PluginManager.ConfigureForCustomerService(customerServiceKernel);
把上述的概念整合起來,成為一個完整 Agent 服務範例:
/// <summary>
/// AI Agent 管理器
/// </summary>
public static class AgentManager
{
/// <summary>
/// 啟動客戶服務助手
/// </summary>
/// <returns></returns>
public static async Task RunCustomerServiceAgent()
{
var agent = new CustomerServiceAgent();
await agent.ProcessCustomerQueryAsync();
}
/// <summary>
/// 啟動天氣服務助手
/// </summary>
/// <returns></returns>
public static async Task RunWeatherServiceAgent()
{
var agent = new WeatherServiceAgent();
await agent.ProcessWeatherQueryAsync();
}
/// <summary>
/// 啟動人力資源助手
/// </summary>
/// <returns></returns>
public static async Task RunHRManagementAgent()
{
var agent = new HRManagementAgent();
await agent.ProcessHRQueryAsync();
}
/// <summary>
/// 啟動訂單管理助手
/// </summary>
/// <returns></returns>
public static async Task RunOrderManagementAgent()
{
var agent = new OrderManagementAgent();
await agent.ProcessOrderQueryAsync();
}
}
/// <summary>
/// Plugin 管理器,用於配置不同 AI Agent 所需的工具集
/// </summary>
public static class PluginManager
{
public static void ConfigureForCustomerService(Kernel kernel)
{
// 客服專員只需要客戶相關工具集
kernel.Plugins.AddFromType<CustomerServicePlugin>("CustomerService");
kernel.Plugins.AddFromType<OrderManagementPlugin>("OrderManagement");
}
public static void ConfigureForWeatherService(Kernel kernel)
{
// 天氣服務相關工具集
kernel.Plugins.AddFromType<WeatherServicePlugin>("WeatherService");
}
public static void ConfigureForHRService(Kernel kernel)
{
// 人力資源服務工具集
kernel.Plugins.AddFromType<HRManagementPlugin>("HRManagement");
}
public static void ConfigureForOrderManagement(Kernel kernel)
{
// 訂單管理專用工具集
kernel.Plugins.AddFromType<OrderManagementPlugin>("OrderManagement");
}
}
/// <summary>
/// 訂單管理插件,提供查詢訂單狀態和處理退換貨申請的功能
/// </summary>
public class OrderManagementPlugin
{
// 模擬的訂單資料(訂單編號 -> 訂單資訊)
private readonly Dictionary<string, object> _orders = new Dictionary<string, object>
{
{ "A001", new { orderId = "A001", status = "已出貨", customerName = "王小明", amount = 1500 } },
{ "A002", new { orderId = "A002", status = "處理中", customerName = "李小華", amount = 2300 } },
{ "A003", new { orderId = "A003", status = "已取消", customerName = "張小美", amount = 980 } },
{ "A004", new { orderId = "A004", status = "已完成", customerName = "陳大明", amount = 3200 } }
};
// 查詢訂單狀態的方法
[KernelFunction]
[Description("Retrieves the order status by order ID.")]
public string GetOrderStatus(
[Description("The ID of the order to retrieve the status for.")]
string orderId)
{
if (string.IsNullOrWhiteSpace(orderId))
{
return "訂單編號不可為空";
}
if (_orders.TryGetValue(orderId, out var order))
{
return System.Text.Json.JsonSerializer.Serialize(order);
}
else
{
return "查無此訂單";
}
}
[KernelFunction]
[Description("處理退換貨申請")]
public string ProcessRefundRequest(
[Description("訂單編號")] string orderId,
[Description("退換貨原因")] string reason)
{
if (string.IsNullOrWhiteSpace(orderId) || string.IsNullOrWhiteSpace(reason))
{
return "訂單編號和退換貨原因不可為空";
}
if (_orders.TryGetValue(orderId, out var order))
{
var refundRequest = new
{
refundId = $"REF-{System.DateTime.Now:yyyyMMddHHmmss}",
orderId = orderId,
reason = reason,
status = "已受理",
requestTime = System.DateTime.Now
};
return $"退換貨申請已受理:{System.Text.Json.JsonSerializer.Serialize(refundRequest)}";
}
else
{
return "查無此訂單,無法處理退換貨申請";
}
}
}
/// <summary>
/// 天氣服務插件,提供查詢今日天氣的功能
/// </summary>
public class WeatherServicePlugin
{
private static readonly Dictionary<string, int> CityTemperatures = new Dictionary<string, int>
{
{ "Taipei", 28 },
{ "Kaohsiung", 31 },
{ "Taichung", 27 },
{ "Tainan", 30 },
{ "Hsinchu", 26 }
};
[KernelFunction]
[Description("Retrieves the today temperature of the city.")]
public async Task<int> Get_Today_Temperature(
[Description("The name of the city to get the temperature for.The city names in the weather data are in English")]
string city)
{
// 模擬非同步操作,例如從 API 獲取天氣資料
await Task.Delay(500); // 模擬 500ms 的網路延遲
if (CityTemperatures.TryGetValue(city, out int temp))
{
return temp;
}
// 模擬未知城市的溫度
return 25;
}
}
Console.Clear();
Console.WriteLine("=== Day 5: Semantic Kernel AI Agent 系統 ===");
Console.WriteLine("請選擇要執行的服務:");
Console.WriteLine("1. 客戶服務助手 - 客戶資訊查詢、訂單處理、退換貨");
Console.WriteLine("2. 天氣服務助手 - 台灣各城市天氣查詢");
Console.WriteLine("3. 人力資源助手 - 員工搜尋、HR管理建議");
Console.WriteLine("4. 訂單管理助手 - 專業訂單查詢與處理");
Console.WriteLine("5. 退出程式");
Console.Write("請輸入選項 (1-5): ");
var choice = Console.ReadLine()?.Trim();
switch (choice)
{
case "1":
Console.WriteLine("\n啟動客戶服務助手...");
await AgentManager.RunCustomerServiceAgent();
break;
case "2":
Console.WriteLine("\n啟動天氣服務助手...");
await AgentManager.RunWeatherServiceAgent();
break;
case "3":
Console.WriteLine("\n啟動人力資源助手...");
await AgentManager.RunHRManagementAgent();
break;
case "4":
Console.WriteLine("\n啟動訂單管理助手...");
await AgentManager.RunOrderManagementAgent();
break;
case "5":
Console.WriteLine("\n謝謝使用 AI Agent 系統,再見!");
return;
case "":
Console.WriteLine("\n⚠️ 請輸入有效的選項數字。");
break;
default:
Console.WriteLine($"\n⚠️ 無效的選項:'{choice}'");
Console.WriteLine("請輸入 1-5 之間的數字。");
break;
}
實際看一下運行結果
=== Day 5: Semantic Kernel AI Agent 系統 ===
請選擇要執行的服務:
1. 客戶服務助手 - 客戶資訊查詢、訂單處理、退換貨
2. 天氣服務助手 - 台灣各城市天氣查詢
3. 人力資源助手 - 員工搜尋、HR管理建議
4. 訂單管理助手 - 專業訂單查詢與處理
5. 退出程式
請輸入選項 (1-5): 1
啟動客戶服務助手...
=== 客戶服務助手 ===
您好!我是您的客戶服務助手,可以幫您查詢客戶資訊、訂單狀態和處理退換貨。
輸入 'exit' 結束對話。
您 > 查一下訂單A003的情況
助手 > 訂單A003的情況如下:
- 訂單狀態:已取消
- 訂購人:張小美
- 訂單金額:980元
如果您需要了解更多細節或有其他需求,請隨時告訴我。
您 > A001訂單要退貨
助手 > 請問您申請退貨的原因是什麼呢?這有助於我們為您加快處理並提供更完善的服務。
您 > 品質不良
助手 > 您的A001訂單退貨申請(原因:品質不良)已成功受理,目前狀態為「已受理」。我們將盡快處理您的退貨需求,有最新進度會再通知您。
=== Day 5: Semantic Kernel AI Agent 系統 ===
請選擇要執行的服務:
1. 客戶服務助手 - 客戶資訊查詢、訂單處理、退換貨
2. 天氣服務助手 - 台灣各城市天氣查詢
3. 人力資源助手 - 員工搜尋、HR管理建議
4. 訂單管理助手 - 專業訂單查詢與處理
5. 退出程式
請輸入選項 (1-5): 3
啟動人力資源助手...
=== 人力資源管理助手 ===
您好!我是您的HR助手,可以幫您搜尋符合條件的員工和提供人力資源建議。
輸入 'exit' 結束對話。
您 > 工程部資深人員
HR助手 > 在工程部(Engineering)裡,目前有以下資深人員(Senior):
1. 李小華
- 技能:Programming、Project Management、Communication
- 工作經驗:4年
- 是否遠端:否
2. 李志明
- 技能:Programming、Leadership
- 工作經驗:10年
- 是否遠端:否
如需進一步查看他們的詳細資料或進行後續行動,請隨時告知!
您 > 找有專案管理技能的
HR助手 > 以下是具備專案管理(Project Management)技能的員工:
1. 李小華(工程部|資深|4年經驗|非遠端)
2. 張大偉(工程部|主管|13年經驗|非遠端)
3. 劉雅婷(行銷部|初級|7年經驗|非遠端)
4. 蔡政宏(工程部|主管|1年經驗|非遠端)
5. 楊承翰(行銷部|資深|7年經驗|非遠端)
6. 馬志豪(工程部|主管|3年經驗|非遠端)
7. 趙家豪(行銷部|初級|4年經驗|非遠端)
8. 孫建華(行銷部|主管|1年經驗|非遠端)
9. 吳志強(銷售部|初級|14年經驗|遠端)
10. 劉美惠(銷售部|資深|15年經驗|遠端)
11. 蔡建國(銷售部|主管|4年經驗|遠端)
12. 鄭志豪(工程部|主管|10年經驗|非遠端)
基於 Anthropic 的建議和 Semantic Kernel 的特性,以下是 Tools 開發的建議實務做法:
透過今天的詳細介紹,深入了解如何使用 Semantic Kernel 撰寫和掛載 Tools。從基礎的函式標註到複雜參數類型的處理,再到根據 Agent 角色選擇合適的工具集管理,這些技巧有助於打造更強大及更可靠的 AI Agent。
明天將會探討 Function Choice Behavior 的詳細配置,看如何精確控制 AI Agent 的函式選擇和呼叫行為。"自動"就意味著會有出錯的風險,了解如何調整這些行為將有助於提升 Agent 的穩定性和降低風險。