Память в Semantic Kernel: vector data, embeddings, RAG-плагин
«Память» в SK, это векторное хранилище + операции эмбеддинга, обёрнутые в стандартные интерфейсы. С 2024 устаревший ISemanticTextMemory (в Microsoft.SemanticKernel.Memory) заменён на новую абстракцию Microsoft.Extensions.VectorData, отдельный NuGet, который SK использует как один из клиентов. Этот пакет реально применим и без SK, в любом .NET-приложении.
В этом уроке: как объявить векторный record, какие vector store connectors доступны, как собрать RAG-плагин для использования внутри SK-агента.
Уровни абстракции
┌─────────────────────────────────────────────────┐
│ Semantic Kernel (Kernel, Plugins, Functions) │
└─────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────┐
│ Microsoft.Extensions.VectorData │ ← унифицированное API
│ IVectorStore, IVectorStoreRecordCollection<T> │
└─────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────┐
│ Connectors: Qdrant, AzureAISearch, Pinecone, │
│ Postgres (pgvector), Redis, Mongo, Chroma, │
│ Weaviate, Milvus, SQLite, InMemory, ... │
└─────────────────────────────────────────────────┘
│
▼
Реальная векторная БД
Microsoft.Extensions.AI отдельным треком даёт IEmbeddingGenerator<TInput, TEmbedding>, единый интерфейс для генерации эмбеддингов от любого провайдера.
Объявление record для индексации
Все vector store connectors используют единый POCO-формат: класс с атрибутами на свойствах.
public class DocumentChunk
{
[VectorStoreRecordKey]
public Guid Id { get; init; }
[VectorStoreRecordData(IsFilterable = true)]
public string DocumentId { get; init; } = "";
[VectorStoreRecordData(IsFilterable = true)]
public string SourceUrl { get; init; } = "";
[VectorStoreRecordData(IsFilterable = true)]
public DateTime IndexedAt { get; init; }
[VectorStoreRecordData(IsFullTextSearchable = true)]
public string Text { get; init; } = "";
[VectorStoreRecordVector(Dimensions: 3072,
DistanceFunction = DistanceFunction.CosineSimilarity,
IndexKind = IndexKind.Hnsw)]
public ReadOnlyMemory<float> Embedding { get; init; }
}
Атрибуты:
[VectorStoreRecordKey], primary key (один на record).[VectorStoreRecordData], обычные поля;IsFilterableдля фильтров в search-запросе,IsFullTextSearchableдля гибридного BM25.[VectorStoreRecordVector], векторное поле; задаём размерность, distance function и index kind (HNSW / Flat / IVF).
Один record может иметь несколько векторных полей (например, dense + sparse + image-embedding), каждое со своим именем.
Vector store connectors на 2026
| Connector | NuGet | Hybrid (dense+sparse) | Multi-vector | Filter expressions |
|---|---|---|---|---|
AzureAISearch |
Microsoft.SemanticKernel.Connectors.AzureAISearch |
✅ BM25+HNSW | ✅ | OData |
Qdrant |
Microsoft.SemanticKernel.Connectors.Qdrant |
✅ | ✅ | rich payload filters |
Postgres (pgvector) |
Microsoft.SemanticKernel.Connectors.Postgres |
✅ tsvector | , | SQL where |
Pinecone |
Microsoft.SemanticKernel.Connectors.Pinecone |
✅ (sparse) | , | metadata |
Weaviate |
Microsoft.SemanticKernel.Connectors.Weaviate |
✅ | ✅ | GraphQL |
Redis |
Microsoft.SemanticKernel.Connectors.Redis |
✅ | , | RediSearch |
MongoDB Atlas |
Microsoft.SemanticKernel.Connectors.MongoDB |
✅ | , | Mongo query |
Chroma |
Microsoft.SemanticKernel.Connectors.Chroma |
, | , | basic |
Milvus |
Microsoft.SemanticKernel.Connectors.Milvus |
, | , | expressions |
SQLite (sqlite-vec) |
Microsoft.SemanticKernel.Connectors.Sqlite |
, | , | SQL |
InMemory |
Microsoft.SemanticKernel.Connectors.InMemory |
, | , | LINQ-like |
| Cosmos DB (NoSQL/Mongo) | `…Connectors.AzureCosmosDB(NoSQL | MongoDB)` | ✅ | , |
Запись и поиск, короткий пример
// 1. Подключение к Qdrant
var vectorStore = new QdrantVectorStore(
new QdrantClient("localhost", 6334));
// 2. Получить collection
var collection = vectorStore.GetCollection<Guid, DocumentChunk>("docs");
await collection.CreateCollectionIfNotExistsAsync();
// 3. Эмбеддер
var embedder = kernel.GetRequiredService<IEmbeddingGenerator<string, Embedding<float>>>();
// 4. Запись
var chunks = new List<DocumentChunk>();
foreach (var (text, sourceUrl) in chunkedDocs)
{
var emb = await embedder.GenerateAsync(text);
chunks.Add(new DocumentChunk {
Id = Guid.NewGuid(),
Text = text,
SourceUrl = sourceUrl,
IndexedAt = DateTime.UtcNow,
DocumentId = sourceUrl,
Embedding = emb.Vector,
});
}
await collection.UpsertBatchAsync(chunks);
// 5. Поиск
var queryEmb = await embedder.GenerateAsync("Какие правила по командировкам?");
var results = await collection.VectorizedSearchAsync(
queryEmb.Vector,
new() {
Top = 10,
Filter = new VectorSearchFilter()
.EqualTo(nameof(DocumentChunk.DocumentId), "regulations.pdf"),
});
await foreach (var hit in results.Results)
Console.WriteLine($"score={hit.Score} :: {hit.Record.Text[..200]}");
TextSearch, стандартизированная RAG-абстракция
Поверх vector store SK даёт высокоуровневую абстракцию ITextSearch, которая объединяет vector search, web search (Bing, Google, Brave) и custom источники в один интерфейс. На её основе строится KernelPlugin для RAG.
ITextSearch textSearch = new VectorStoreTextSearch<DocumentChunk>(
vectorSearchable: collection,
embeddingGenerator: embedder);
// Регистрируем как plugin
KernelPlugin searchPlugin = textSearch.CreateWithGetTextSearchResults("KbSearch");
kernel.Plugins.Add(searchPlugin);
// Теперь LLM может вызывать KbSearch.GetTextSearchResults(query) сам
var settings = new OpenAIPromptExecutionSettings {
FunctionChoiceBehavior = FunctionChoiceBehavior.Auto()
};
var answer = await kernel.InvokePromptAsync(
"Когда нужно подавать заявление на командировку?",
new(settings));
LLM сам решит вызвать KbSearch, получит топ-K чанков, и сформулирует ответ с цитатами.
Альтернатива, Bing/Google/Brave web search в виде того же ITextSearch:
ITextSearch bing = new BingTextSearch(apiKey);
kernel.Plugins.Add(bing.CreateWithGetTextSearchResults("Web"));
Можно зарегистрировать несколько TextSearch (внутренняя KB + web + историчческие тикеты) и LLM сам выберет, куда идти.
Гибридный поиск (dense + sparse)
Большинство современных connectors (Azure AI Search, Qdrant, Weaviate, Pinecone, Postgres) поддерживают hybrid search через RRF. SK даёт интерфейс IKeywordHybridSearch:
var hybrid = (IKeywordHybridSearch<DocumentChunk>)collection;
var results = await hybrid.HybridSearchAsync(
queryEmb.Vector,
keywords: ["командировка", "регламент"],
options: new() { Top = 10 });
Прирост качества vs чистого dense, типично +10–25% к recall@10 (см. курс по RAG).
Embedding generators
IEmbeddingGenerator<TInput, TEmbedding>, стандартный интерфейс из Microsoft.Extensions.AI. Реализации:
| Provider | Модели | NuGet |
|---|---|---|
| Azure OpenAI / OpenAI | text-embedding-3-large (3072), -3-small (1536), -ada-002 (1536) |
Microsoft.SemanticKernel.Connectors.OpenAI |
| Mistral | mistral-embed (1024) |
Microsoft.SemanticKernel.Connectors.MistralAI |
| HuggingFace | любая HF embedding | Microsoft.SemanticKernel.Connectors.HuggingFace |
| Ollama (local) | nomic-embed-text, mxbai-embed-large, bge-m3 |
Microsoft.SemanticKernel.Connectors.Ollama |
| ONNX (in-process) | bge, e5, mini-LM, кастомные | Microsoft.SemanticKernel.Connectors.Onnx |
text-embedding-005 (768) |
Microsoft.SemanticKernel.Connectors.Google |
Для Voyage AI, Cohere, Jina, Nomic, на 2026 нет first-party connectors; либо обернуть HTTP-вызов в свой IEmbeddingGenerator, либо использовать через community-пакеты.
Долгосрочная vs краткосрочная память
В SK обе хранятся одинаково, это просто данные в разных местах:
Краткосрочная (диалог): ChatHistory объект, передаётся в каждый вызов LLM.
- Строим агента-ассистента: при превышении лимита токенов делаем summarization предыдущих сообщений (есть встроенный
ChatHistorySummarizationReducer). - Хранится в Redis / Cosmos DB / Postgres по
sessionId.
Долгосрочная (knowledge): vector store + plain BLOB-storage оригинала.
- Индексируется async через batch jobs (Hangfire, Azure Functions, Aspire jobs).
- При обновлении исходника, найти по
DocumentIdи переиндексировать.
Шаблон «user memory» (LLM сам пишет в долгосрочную память факты про пользователя) реализуется как IAutoFunctionInvocationFilter, который перехватывает вызовы условного Memory.RememberFact(...) и пишет в коллекцию с фильтром по userId.
Полный пример: RAG-плагин для агента
public sealed class HrKnowledgePlugin
{
private readonly IVectorStoreRecordCollection<Guid, DocumentChunk> collection;
private readonly IEmbeddingGenerator<string, Embedding<float>> embedder;
public HrKnowledgePlugin(
IVectorStoreRecordCollection<Guid, DocumentChunk> collection,
IEmbeddingGenerator<string, Embedding<float>> embedder)
{
this.collection = collection;
this.embedder = embedder;
}
[KernelFunction("search_hr")]
[Description("Найти релевантные фрагменты из HR-документов компании по запросу пользователя")]
public async Task<List<SearchHit>> SearchAsync(
[Description("Что искать на естественном языке")] string query,
[Description("Сколько результатов вернуть, по умолчанию 5")] int topK = 5)
{
var emb = await embedder.GenerateAsync(query);
var results = await collection.VectorizedSearchAsync(
emb.Vector,
new VectorSearchOptions { Top = topK });
var hits = new List<SearchHit>();
await foreach (var r in results.Results)
hits.Add(new SearchHit(r.Record.SourceUrl, r.Record.Text, r.Score ?? 0));
return hits;
}
}
public record SearchHit(string Source, string Text, double Score);
Регистрация:
builder.Services.AddSingleton<HrKnowledgePlugin>();
builder.Plugins.AddFromType<HrKnowledgePlugin>();
С FunctionChoiceBehavior.Auto() агент сам решит вызвать HrKnowledgePlugin.search_hr при HR-вопросе, получит чанки, и сформирует ответ.
Главное
Память в SK, это vector store + embedding через единый pluggable интерфейс. Реализации меняются конфигом без изменений бизнес-кода (Postgres → Qdrant → Pinecone). RAG-сценарий собирается из 50–100 строк: TextSearch + плагин + регистрация.
Параллельно живёт более низкий слой Microsoft.Extensions.VectorData, на котором SK работает и который применим в любом .NET-приложении, даже без SK.
В следующем уроке, как LLM сам решает, какие функции и в каком порядке вызывать: эволюция планировщиков, native function calling, multi-agent, Process Framework.