昨天我們成功打造了超強的米其林級湯底——向量儲存庫,但光有湯底還不夠,你總不能讓客人喝清水吧?今天,我們就要學習如何把「新鮮食材」(外部資料)放進湯裡,並在客人點餐時,精準地從湯裡撈出最對味的料。這就是 RAG 應用的第二步:資料注入 (Upsert) 與 向量搜尋 (SearchAsync)。
Upsert 這個詞是 Update 和 Insert 的組合,意思是如果資料存在就更新,不存在就新增。在我們的「湯底」情境中,Upsert 就是將文字資料轉換成向量,然後儲存到向量儲存庫中。
Semantic Kernel 讓這個過程變得非常簡單,你只需要建立一個 MemoryStore
物件,並告訴它要使用的向量儲存庫和嵌入模型 (Embedding Model)。
using Microsoft.SemanticKernel.Memory;
using Microsoft.SemanticKernel.Connectors.OpenAI;
// 假設你已經設定好 Kernel 和 OpenAI 服務
// 這是我們昨天定義的 Recipe 類別
var collection = vectorStore.GetCollection<string, Recipe>("Recipe");
// 建立 collection
await collection.EnsureCollectionExistsAsync();
// 準備我們的「食譜」資料
var recipes = new List<Recipe>
{
new() { Name = "麻婆豆腐", Cuisine = "川菜", Type = "主菜", Description = "麻、辣、燙、嫩、酥、香,這六個字完美地詮釋了麻婆豆腐的精髓。一道極具代表性的川菜,吃起來溫暖又過癮。" },
new() { Name = "宮保雞丁", Cuisine = "川菜", Type = "主菜", Description = "以雞丁、乾辣椒、花生米、花椒粒等材料烹炒而成,鹹甜微辣,醬汁濃郁,是道經典的家常菜。" },
new() { Name = "三杯雞", Cuisine = "台菜", Type = "主菜", Description = "以一杯麻油、一杯醬油、一杯米酒烹調的台式經典名菜,香氣四溢,配飯一流。" },
};
// 進行資料注入,Semantic Kernel 會自動將 Description 轉成向量並存入
await recipeStore.UpsertAsync(recipes);
Console.WriteLine("食譜資料已成功加入湯底!");
這段程式碼中,recipeStore.UpsertAsync(recipes)
就是那句神奇的咒語!它會自動呼叫嵌入模型,將每道食譜的 Description
轉化為向量,然後連同其他屬性一起存到我們的 Qdrant 向量儲存庫中。
當客人問:「有沒有一道暖心又過癮的菜?」時,我們需要精準地從湯底中撈出最相關的食譜。這時,SearchAsync
就派上用場了。它會將你的問題轉換成向量,然後在資料庫中尋找語意上最接近的資料。
// 客人的問題
string question = "有什麼吃起來很溫暖又有點辣的菜?";
// 進行向量搜尋,尋找最相關的 3 個結果
var searchResults = recipeStore.SearchAsync(
searchValue: question,
top: 3 // 只撈取最相關的前 3 個結果
);
Console.WriteLine($"針對您的問題「{question}」,我從食譜資料庫中找到了以下結果:");
// 顯示搜尋結果
await foreach (var result in searchResults)
{
Console.WriteLine($"\n--- 找到的食譜:{result.Record.Name} (相關度:{result.Score}) ---");
Console.WriteLine($"描述:{result.Record.Description}");
}
這個範例中,SearchAsync
會自動處理所有複雜的細節,你只需要提供一個問題和想要撈取的數量。瞧,我們就這麼簡單地實現了語意搜尋!
光靠語意搜尋有時還不夠,如果客人說:「我想找一道台式的、吃起來溫暖的菜」,我們就需要結合過濾 (Filter) 來進一步縮小範圍。
Semantic Kernel 允許你在 SearchAsync
中加入過濾條件。在 Qdrant 中,這就是一個 Filter
物件。
// 客人更精確的問題:我要找台菜,而且吃起來很溫暖
var morePreciseQuestion = "我想找一道溫暖又好吃的台式菜色";
// 向量搜尋後,再進行 LINQ 過濾
var filteredResults = collection.SearchAsync(
searchValue: morePreciseQuestion,
top: 10 // 這次先撈取多一點,再進行過濾
).Where(r => r.Record.Cuisine == "台菜");
Console.WriteLine($"\n針對您的問題「{morePreciseQuestion}」,我從台菜食譜中找到了以下結果:");
await foreach (var result in filteredResults)
{
Console.WriteLine($"\n--- 找到的食譜:{result.Record.Name} (相關度:{result.Score}) ---");
Console.WriteLine($"描述:{result.Record.Description}");
}
透過 filter
參數,我們可以精準地告訴向量儲存庫,我們只對 Cuisine
屬性為 "台菜"
的食譜感興趣。這就像在撈麵時,先用篩子把其他類型的麵條都過濾掉,只留下我們想要的。
今天我們學會了如何讓 AI 擁有外部知識,並能精準地從中檢索。明天,我們會把這個「湯底」的概念再提升一個層次,將 RAG 流程抽象化,讓 AI 能夠自主地進行外部搜尋!