昨天我們學會了如何使用 VectorStore
來製作 RAG 的美味湯底,並手動進行資料注入與搜尋。不過,有沒有覺得這個過程還是有點「手動」呢?想像一下,你是一位餐廳經理,你希望你的 AI 服務生能夠根據客人的需求,自己決定要去「內部知識庫」還是「外部搜尋引擎」找資料,而不是每次都由你來下達指令。
這就是我們今天要探討的重點:Text Search 抽象化。Semantic Kernel 為我們提供了一個更高層次的抽象介面 ITextSearch
,它就像一個萬能的「食材供應商」介面。無論你的食材是來自於內部的 VectorStore
,還是來自於外部的 Bing 搜尋,我們都可以用同一個介面來操作。
ITextSearch
服務回想一下我們昨天的 VectorStore
範例,它其實已經具備了搜尋的能力。Semantic Kernel 讓我們可以非常簡單地將它包裝成一個 ITextSearch
服務。
C#
using Microsoft.SemanticKernel.Text;
using Microsoft.SemanticKernel.Connectors.OpenAI; // 或其他 AI 服務
using Microsoft.SemanticKernel.Memory;
// 1. 建立一個 InMemoryVectorStore
var EmbeddingGenerator = new OpenAIClient(config["OpenAI:ApiKey"]!)
.GetEmbeddingClient("text-embedding-ada-002")
.AsIEmbeddingGenerator();
var vectorStore = new InMemoryVectorStore(new() { EmbeddingGenerator = EmbeddingGenerator });
var collection = vectorStore.GetCollection<string, Recipe>("Recipe");
var recipes = new List<Recipe>
{
new() { Name = "麻婆豆腐", Cuisine = "川菜", Type = "主菜", Description = "麻、辣、燙、嫩、酥、香,這六個字完美地詮釋了麻婆豆腐的精髓。一道極具代表性的川菜,吃起來溫暖又過癮。" },
new() { Name = "宮保雞丁", Cuisine = "川菜", Type = "主菜", Description = "以雞丁、乾辣椒、花生米、花椒粒等材料烹炒而成,鹹甜微辣,醬汁濃郁,是道經典的家常菜。" },
new() { Name = "三杯雞", Cuisine = "台菜", Type = "主菜", Description = "以一杯麻油、一杯醬油、一杯米酒烹調的台式經典名菜,香氣四溢,配飯一流。" },
};
await collection.EnsureCollectionExistsAsync();
await collection.UpsertAsync(recipes);
Console.WriteLine("食譜搜尋服務已準備就緒!");
ITextSearch recipeTextSearch = new VectorStoreTextSearch<Recipe>(collection);
現在,我們的 recipeTextSearch
就可以用 ITextSearch
這個統一的介面來呼叫,無論底層是 InMemory
還是 Qdrant
,對上層程式碼來說都毫無差別。
如果我們需要最新的時事新聞或外部資訊,光靠內部知識庫是不夠的。這時,我們可以請出外部的「食材供應商」——例如 Bing 搜尋引擎。
Semantic Kernel 也為我們提供了 Bing 的 ITextSearch
實作。
C#
using Microsoft.SemanticKernel.Plugins.Core; // 包含 Bing 搜尋 Plugin
// 1. 建立 Bing 搜尋服務,需要設定你的 Bing Search API key
// (注意:請將你的 API key 存放在安全的地方,例如 User Secrets)
var textSearch = new BingTextSearch(apiKey: "<Your Bing API Key>");
Console.WriteLine("Bing 搜尋服務已準備就緒!");
透過這種方式,我們就有了兩種不同來源的 ITextSearch
服務,一個是我們的內部食譜資料庫,一個是外部的 Bing 搜尋。
現在是整個流程中最神奇的部分!我們將這兩種 ITextSearch
服務包裝成一個 Plugin,並讓 AI 服務生可以透過 Function Calling 來自主決定要使用哪個服務。
public class SearchPlugin(ITextSearch recipeSearch, ITextSearch bingSearch)
{
[KernelFunction]
[Description("搜尋內部食譜資料庫。")]
public async IAsyncEnumerable<string> SearchRecipesAsync(
[Description("要搜尋的食譜關鍵字。")] string query)
{
await foreach (var result in recipeSearch.SearchAsync(query, 5))
{
yield return $"食譜: {result.Text}";
}
}
[KernelFunction]
[Description("使用 Bing 搜尋引擎進行網路搜尋。")]
public async IAsyncEnumerable<string> SearchWebAsync(
[Description("要搜尋的網路關鍵字。")] string query)
{
await foreach (var result in bingSearch.SearchAsync(query, 5))
{
yield return $"網頁搜尋結果: {result.Text}";
}
}
}
kernel.Plugins.AddFromObject(new SearchPlugin(recipeTextSearch, textSearch));
var executionSettings = new OpenAIPromptExecutionSettings
{
FunctionChoiceBehavior = FunctionChoiceBehavior.Auto()
};
Console.WriteLine("客人點餐:請推薦一道暖心又過癮的菜色。");
var result1 = await kernel.InvokePromptAsync("請推薦一道暖心又過癮的菜色。", new(executionSettings));
Console.WriteLine($"\nAI 服務生回應:\n{result1}");
Console.WriteLine("------------------------------------------");
// 測試外部搜尋
Console.WriteLine("客人點餐:請問 2022 年世界盃足球賽冠軍是誰?");
var result2 = await kernel.InvokePromptAsync("請問 2022 年世界盃足球賽冠軍是誰?", new(executionSettings));
Console.WriteLine($"\nAI 服務生回應:\n{result2}");
Console.WriteLine("------------------------------------------");
當客人問:「如何做一道三杯雞?」時,AI 會根據 Description
判斷,這是一個內部知識庫就能回答的問題,因此會自動呼叫 SearchRecipesAsync
。
但如果客人問:「請問 2024 年的世界盃足球賽冠軍是誰?」AI 則會判斷這個問題需要最新的外部資訊,因此會自主地呼叫 SearchWebAsync
。
透過 ITextSearch
介面的抽象化和 Function Calling
的整合,我們讓 AI 不再是被動的,而是能夠根據情境主動地選擇合適的工具來解決問題。這就好像我們為 AI 服務生賦予了「自主採購」的能力,大大提升了它的工作效率和精準度!
明天,我們將把眼界放得更寬,探索 AI 如何處理文字之外的資訊,也就是多模態 (Multi-modal) 輸入,敬請期待!