瞭解的運作原理之後,就可以開始使用Semantic Kernel來製作應用了。
Semantic Kernel將embedding的功能封裝到了Memory中,用來儲存上下文資訊,就好像電腦的記憶體一樣,而LLM就像是CPU一樣,我們所需要做的就是從記憶體中取出相關的資訊交給CPU處理就好了。
使用Memory需要註冊 embedding
模型,目前使用的就是 text-embedding-ada-002
。同時需要為Kernel新增MemoryStore,用於儲存更多的資訊,這裡Semantic Kernel提供了一個 VolatileMemoryStore
,就是一個普通的記憶體儲存的MemoryStore。
var kernel = Kernel.Builder.Configure(c =>
{
c.AddOpenAITextCompletionService("openai", "text-davinci-003", Environment.GetEnvironmentVariable("MY_OPEN_AI_API_KEY"));
c.AddOpenAIEmbeddingGenerationService("openai", "text-embedding-ada-002", Environment.GetEnvironmentVariable("MY_OPEN_AI_API_KEY"));
})
.WithMemoryStorage(new VolatileMemoryStore())
.Build();
完成了基礎資訊的註冊後,就可以往Memroy中儲存資訊了。
const string MemoryCollectionName = "aboutMe";
await kernel.Memory.SaveInformationAsync(MemoryCollectionName, id: "info1", text: "My name is Andrea");
await kernel.Memory.SaveInformationAsync(MemoryCollectionName, id: "info2", text: "I currently work as a tourist operator");
await kernel.Memory.SaveInformationAsync(MemoryCollectionName, id: "info3", text: "I currently live in Seattle and have been living there since 2005");
await kernel.Memory.SaveInformationAsync(MemoryCollectionName, id: "info4", text: "I visited France and Italy five times since 2015");
await kernel.Memory.SaveInformationAsync(MemoryCollectionName, id: "info5", text: "My family is from New York");
SaveInformationAsync
會將text的內容通過 embedding
模型轉化為對應的文字向量,存放在的MemoryStore中。其中CollectionName如同資料庫的表名,Id就是Id。
完成資訊的儲存之後,就可以用來語意搜尋了。
直接使用Memory.SearchAsync
方法,指定對應的Collection,同時提供相應的查詢問題,查詢問題也會被轉化為embedding,再在MemoryStore中計算查詢最相似的資訊。
var questions = new[]
{
"what is my name?",
"where do I live?",
"where is my family from?",
"where have I travelled?",
"what do I do for work?",
};
foreach (var q in questions)
{
var response = await kernel.Memory.SearchAsync(MemoryCollectionName, q).FirstOrDefaultAsync();
Console.WriteLine(q + " " + response?.Metadata.Text);
}
// output
/*
what is my name? My name is Andrea
where do I live? I currently live in Seattle and have been living there since 2005
where is my family from? My family is from New York
where have I travelled? I visited France and Italy five times since 2015
what do I do for work? I currently work as a tourist operator
*/
到這個時候,即便不需要進行總結歸納,光是這樣的語意查詢,都會很有價值。
除了新增資訊以外,還可以新增參照,像是非常有用的參考連結之類的。
const string memoryCollectionName = "SKGitHub";
var githubFiles = new Dictionary<string, string>()
{
["https://github.com/microsoft/semantic-kernel/blob/main/README.md"]
= "README: Installation, getting started, and how to contribute",
["https://github.com/microsoft/semantic-kernel/blob/main/samples/notebooks/dotnet/2-running-prompts-from-file.ipynb"]
= "Jupyter notebook describing how to pass prompts from a file to a semantic skill or function",
["https://github.com/microsoft/semantic-kernel/blob/main/samples/notebooks/dotnet/Getting-Started-Notebook.ipynb"]
= "Jupyter notebook describing how to get started with the Semantic Kernel",
["https://github.com/microsoft/semantic-kernel/tree/main/samples/skills/ChatSkill/ChatGPT"]
= "Sample demonstrating how to create a chat skill interfacing with ChatGPT",
["https://github.com/microsoft/semantic-kernel/blob/main/dotnet/src/SemanticKernel/Memory/Volatile/VolatileMemoryStore.cs"]
= "C# class that defines a volatile embedding store",
["https://github.com/microsoft/semantic-kernel/tree/main/samples/dotnet/KernelHttpServer/README.md"]
= "README: How to set up a Semantic Kernel Service API using Azure Function Runtime v4",
["https://github.com/microsoft/semantic-kernel/tree/main/samples/apps/chat-summary-webapp-react/README.md"]
= "README: README associated with a sample starter react-based chat summary webapp",
};
foreach (var entry in githubFiles)
{
await kernel.Memory.SaveReferenceAsync(
collection: memoryCollectionName,
description: entry.Value,
text: entry.Value,
externalId: entry.Key,
externalSourceName: "GitHub"
);
}
同樣的,使用SearchAsync搜尋就行。
string ask = "I love Jupyter notebooks, how should I get started?";
Console.WriteLine("===========================\n" +
"Query: " + ask + "\n");
var memories = kernel.Memory.SearchAsync(memoryCollectionName, ask, limit: 5, minRelevanceScore: 0.77);
var i = 0;
await foreach (MemoryQueryResult memory in memories)
{
Console.WriteLine($"Result {++i}:");
Console.WriteLine(" URL: : " + memory.Metadata.Id);
Console.WriteLine(" Title : " + memory.Metadata.Description);
Console.WriteLine(" ExternalSource: " + memory.Metadata.ExternalSourceName);
Console.WriteLine(" Relevance: " + memory.Relevance);
Console.WriteLine();
}
//output
/*
===========================
Query: I love Jupyter notebooks, how should I get started?
Result 1:
URL: : https://github.com/microsoft/semantic-kernel/blob/main/samples/notebooks/dotnet/Getting-Started-Notebook.ipynb
Title : Jupyter notebook describing how to get started with the Semantic Kernel
ExternalSource: GitHub
Relevance: 0.8677381632778319
Result 2:
URL: : https://github.com/microsoft/semantic-kernel/blob/main/samples/notebooks/dotnet/2-running-prompts-from-file.ipynb
Title : Jupyter notebook describing how to pass prompts from a file to a semantic skill or function
ExternalSource: GitHub
Relevance: 0.8162989178955157
Result 3:
URL: : https://github.com/microsoft/semantic-kernel/blob/main/README.md
Title : README: Installation, getting started, and how to contribute
ExternalSource: GitHub
Relevance: 0.8083238591883483
*/
這裡多使用了兩個引數,一個是limit,用於限制返回資訊的條數,只返回最相似的前幾條資料,另外一個是minRelevanceScore,限制最小的相關度分數,這個取值範圍在0.0 ~ 1.0 之間,1.0意味著完全匹配。
將Memory的儲存、搜尋功能和語意技能相結合,就可以快速的打造一個實用的語意問答的應用了。
只需要將搜尋到的相關資訊內容填充到 prompt中,然後將內容和問題都拋給LLM,就可以等著得到一個滿意的答案了。
const string MemoryCollectionName = "aboutMe";
await kernel.Memory.SaveInformationAsync(MemoryCollectionName, id: "info1", text: "My name is Andrea");
await kernel.Memory.SaveInformationAsync(MemoryCollectionName, id: "info2", text: "I currently work as a tourist operator");
await kernel.Memory.SaveInformationAsync(MemoryCollectionName, id: "info3", text: "I currently live in Seattle and have been living there since 2005");
await kernel.Memory.SaveInformationAsync(MemoryCollectionName, id: "info4", text: "I visited France and Italy five times since 2015");
await kernel.Memory.SaveInformationAsync(MemoryCollectionName, id: "info5", text: "My family is from New York");
var prompt =
"""
It can give explicit instructions or say 'I don't know' if it does not have an answer.
Information about me, from previous conversations:
{{ $fact }}
User: {{ $ask }}
ChatBot:
""";
var skill = kernel.CreateSemanticFunction(prompt);
var ask = "Hello, I think we've met before, remember? my name is...";
var fact = await kernel.Memory.SearchAsync(MemoryCollectionName,ask).FirstOrDefaultAsync();
var context = kernel.CreateNewContext();
context["fact"] = fact?.Metadata?.Text;
context["ask"] = ask;
var resultContext =await skill.InvokeAsync(context);
resultContext.Result.Dump();
//output
/*
Hi there! Yes, I remember you. Your name is Andrea, right?
*/
由於這種場景太常見了,所以Semantic Kernel中直接提供了一個技能TextMemorySkill,通過Function呼叫的方式簡化了搜尋的過程。
// .. SaveInformations
// TextMemorySkill provides the "recall" function
kernel.ImportSkill(new TextMemorySkill());
var prompt =
"""
It can give explicit instructions or say 'I don't know' if it does not have an answer.
Information about me, from previous conversations:
{{ recall $ask }}
User: {{ $ask }}
ChatBot:
""";
var skill = kernel.CreateSemanticFunction(prompt);
var ask = "Hello, I think we've met before, remember? my name is...";
var context = kernel.CreateNewContext();
context["ask"] = ask;
context[TextMemorySkill.CollectionParam] = MemoryCollectionName;
var resultContext =await skill.InvokeAsync(context);
resultContext.Result.Dump();
// output
/*
Hi there! Yes, I remember you. Your name is Andrea, right?
*/
這裡直接使用 recall 方法,將問題傳給了 TextMemorySkill,搜尋對應得到結果,免去了手動搜尋注入得過程。
VolatileMemoryStore
本身也是易丟失的,往往使用到記憶體的場景,其中的資訊都是有可能長期儲存的,起碼並不會即刻過期。那麼將這些資訊的 embedding
能夠長期儲存起來,也是比較划算的事情。畢竟每一次做 embedding的轉化也是需要調介面,需要花錢的。
Semantic Kernel庫中包含了SQLite、Qdrant和CosmosDB的實現,自行擴充套件的話,也只需要實現 IMemoryStore
這個介面就可以了。
至於未來,可能就是專用的 Vector Database
了。
參考資料: