Semantic Kernel 入門系列:🥑Memory記憶體

2023-04-14 06:00:45

瞭解的運作原理之後,就可以開始使用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 了。


參考資料:

  1. https://learn.microsoft.com/en-us/semantic-kernel/concepts-sk/memories
  2. https://github.com/microsoft/semantic-kernel/blob/main/samples/notebooks/dotnet/6-memory-and-embeddings.ipynb
  3. https://github.com/johnmaeda/SK-Recipes/blob/main/e4-memories/notebook.ipynb
  4. https://learn.microsoft.com/en-us/semantic-kernel/concepts-ai/vectordb