Learnly

Как ИИ помнит информацию: концепция памяти

Память в Semantic Kernel: vector data, embeddings, RAG-плагин

«Память» в SK, это векторное хранилище + операции эмбеддинга, обёрнутые в стандартные интерфейсы. С 2024 устаревший ISemanticTextMemoryMicrosoft.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
Google 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.

AI-тест
1 / 10

Какие процессы становятся более эффективными благодаря памяти ИИ?