
目录什么是向量存储Agent Framework 的向量存储抽象ChatHistoryMemoryProvider创建向量存储向量维度创建 InMemoryVectorStore配置存储范围和搜索范围跨会话记忆是如何产生的示例运行效果如下总结大语言模型本身不会主动记住之前的对话。聊天应用(ChatGPT等)之所以能够表现出连续的对话能力是因为应用会在每次调用模型时将当前会话中的部分或全部历史消息重新放入请求上下文。模型重新看到这些历史消息后才会表现得像是“记得”之前说过的话。在Agent Framework中会话状态通常与 AgentSession 关联。当创建一个新的AgentSession时新会话默认不会包含旧 Session 中的聊天历史。如果希望 Agent 在新会话中仍然能够找回用户过去说过的信息就需要将历史消息保存到外部存储中并在后续对话中重新检索。ChatHistoryMemoryProvider 正是基于这种机制实现跨会话记忆的它将聊天消息写入向量存储并根据当前问题检索语义相关的历史消息再将这些消息补充到当前上下文中。在正式介绍 ChatHistoryMemoryProvider 之前我们先了解它所依赖的向量存储。什么是向量存储向量存储Vector Store是一种能够保存向量数据并执行相似度搜索的存储系统。在大语言模型应用中一段文本通常会先通过 Embedding 模型转换为一组数字也就是向量。例如我是一名教育工作者 ↓ Embedding 模型 ↓ [0.12, -0.37, 0.81, ...]这些向量可以反映文本在语义上的特征。当用户之后输入我是一名老师系统也会将这句话转换成向量然后在向量存储中查找语义上最相似的历史消息。即使两句话没有使用完全相同的关键词只要语义接近也有可能被检索出来。因此向量存储非常适合以下场景检索增强生成也就是 RAG保存 Agent 的长期记忆搜索相关的聊天历史语义搜索混合搜索。ChatHistoryMemoryProvider 正是使用向量存储保存和检索聊天消息。Agent Framework 的向量存储抽象不同向量数据库提供的接口并不相同。Qdrant、Redis、Postgres、Azure AI Search 和 MongoDB 都有自己的客户端和搜索方式。为了统一不同向量存储的使用方式Agent Framework 依赖Microsoft.Extensions.VectorData.Abstractions包。这个包提供了一套用于在 .NET 中操作向量存储的统一抽象。应用可以面向这套抽象编写代码而不必直接依赖某一种数据库。底层向量存储发生变化时上层组件通常不需要进行大范围修改。可以简单理解为Agent Framework 调用的是Microsoft.Extensions.VectorData.Abstractions定义的接口具体连接器再把这些调用转换成对应数据库能够识别的请求。如果Microsoft.Extensions.VectorData.Abstractions提供的是统一接口那么每一种向量存储都需要提供相应的连接器来实现这些接口。连接器也可以理解为适配器它负责把抽象层中的写入、删除和搜索操作映射到底层数据库的 API。Agent Framework 支持的向量存储实现如下。表格中的 Implementation 表示支持的向量数据库或存储方案C# 表示是否提供 C# 连接器Uses officially supported SDK 表示连接器底层是否使用数据库厂商正式支持的 SDKMaintainer / Vendor 则表示这个连接器由谁维护。ImplementationC#Uses officially supported SDKMaintainer / VendorAzure AI Search✅✅MicrosoftCosmos DB MongoDB (vCore)✅✅MicrosoftCosmos DB No SQL✅✅MicrosoftCouchbase✅✅CouchbaseElasticsearch✅✅ElasticIn-Memory✅N/AMicrosoftMongoDB✅✅MicrosoftNeon Serverless PostgresUse Postgres Connector✅MicrosoftOracle✅✅OraclePinecone✅❌MicrosoftPostgres✅✅MicrosoftQdrant✅✅MicrosoftRedis✅✅MicrosoftSQL Server✅✅MicrosoftSQLite✅✅MicrosoftVolatile (In-Memory)Deprecateduse In-MemoryN/AMicrosoftWeaviate✅✅Microsoft这里需要注意Maintainer / Vendor 表示的是连接器的维护方并不是数据库本身的开发者。以 Qdrant 为例ImplementationC#Uses officially supported SDKMaintainer / VendorQdrant✅✅MicrosoftQdrant 数据库、数据库接口以及官方 SDK 仍然由 Qdrant 提供。Microsoft 负责的是 Qdrant 与 Microsoft.Extensions.VectorData 之间的集成也就是实现并维护对应的连接器。Elasticsearch 的情况有所不同它的连接器由 Elastic 自己维护因此表格中的 Maintainer / Vendor 显示为 Elastic。不同连接器的维护方和底层 SDK 支持情况可能不同。在生产环境中使用之前还需要根据具体数据库的文档确认版本兼容性、许可证和技术支持情况。关于 .NET 向量存储的更多内容可以参考微软官方文档https://learn.microsoft.com/en-us/dotnet/ai/vector-stores/overview实际上我们在去年讲解Semantic Kernel 的讲解过相关的内容Semantic Kernel向量存储数据架构Semantic Kernel与Postgres向量存储SemanticKernel中如何生成文本向量(Embedding)Semantic Kernel使用连接器进行向量搜索Semantic Kernel连接器中混合搜索ChatHistoryMemoryProvider了解向量存储之后再来看 ChatHistoryMemoryProvider 就比较容易了。ChatHistoryMemoryProvider 是一个 AIContextProvider。它会将聊天历史存储到向量存储中并在后续对话中检索相关消息用来补充当前请求的上下文。它与普通聊天历史最大的区别是不会简单地把所有历史消息重新发送给模型而是根据当前输入从过去的消息中找出语义上相关的内容。该提供器以两个阶段运行存储(Storage)在每次 agent 调用之后新的请求和响应消息会被存储到向量存储中并根据它们的内容生成 embedding。检索(Retrieve)在每次调用之前或通过函数调用按需进行该提供器会在向量存储中搜索与当前用户输入语义相似的消息并将它们作为上下文注入。存储的消息可以根据标识符来划分作用域。比如应用(application)、代理(agent)、用户(user)、会话(session)进行作用域划分从而可以对哪些历史被存储和可搜索进行细粒度控制。创建向量存储我们在下面示例中使用的是InMemoryVectorStore向量则由 Foundry 项目中部署的 Embedding 模型生成。这里需要提前准备一个能够将文本转换为向量的 Embedding 模型。常见的云端 Embedding 模型如下厂商模型名称说明OpenAItext-embedding-3-small、text-embedding-3-largeOpenAI 提供的文本 Embedding 模型阿里云 Model Studiotext-embedding-v4阿里云当前提供的文本向量模型之一Google Geminigemini-embedding-2Google 提供的多模态 Embedding 模型Cohereembed-v4.0支持文本、图像以及图文混合内容Voyage AIvoyage-4、voyage-4-lite、voyage-code-3其中voyage-code-3主要面向代码检索场景除了通过云端 API 使用 Embedding 模型也可以选择在本地部署开放权重模型例如 Qwen3-Embedding 系列。三种 Qwen3-Embedding 模型的特点可以简单理解为模型特点Qwen3-Embedding-0.6B模型体积较小推理速度较快适合开发测试和资源有限的环境Qwen3-Embedding-4B在检索效果和资源消耗之间相对均衡Qwen3-Embedding-8B模型规模更大通常能够提供更好的检索效果但需要更多显存和计算资源需要注意0.6B、4B和8B表示的是模型的参数规模并不是最终生成的向量维度。写入向量存储时使用的vectorDimensions仍然需要与所选 Embedding 模型实际输出的向量维度保持一致。云端模型通常接入更简单不需要自行维护推理服务本地部署则可以让数据保留在自己的环境中但需要自行准备计算资源并负责模型服务的部署和维护。实际使用时可以根据语言支持、向量维度、检索效果、调用成本和部署条件选择合适的 Embedding 模型。向量维度可以先从二维向量理解“维度”这个概念。例如[0.12, -0.37]这是一个二维向量因为它包含两个数值。可以把它想象成二维坐标系中的一个点0.12 是第一个方向上的坐标-0.37 是第二个方向上的坐标。如果向量中有三个数值[0.12, -0.37, 0.81]它就是三维向量可以把它理解成三维空间中的一个点。按照同样的方式下面这个向量包含四个数值因此是四维向量[0.12, -0.37, 0.81, 0.25]二维和三维向量可以在坐标系中直观地表示出来但 Embedding 模型生成的向量通常包含数百个甚至数千个数值已经无法直接用图形展示。例如一个模型输出 1536 维向量意味着每段文本都会被转换成由 1536 个数值组成的向量[0.12, -0.37, 0.81, ..., 0.25]其中每个数值都可以看作文本在某个向量方向上的坐标。这些值通常是正数、负数或者接近零的小数由 Embedding 模型根据文本内容计算得出。需要注意的是某一个数值通常没有能够单独解释的固定含义。不能简单地认为第一维表示“主题”、第二维表示“情感”或者某个数值越大就代表某种语义越强。文本的语义是由整个向量中的所有维度共同表达的。当两段文本的意思比较接近时Embedding 模型通常会让它们在高维向量空间中的位置也比较接近。系统再通过余弦相似度、点积或欧氏距离等方式比较两个向量从而找到语义相关的内容。向量维度还会影响存储空间和检索计算量。维度越高每条记录需要保存的数值越多计算向量相似度时需要处理的数据也越多。不过向量维度越高并不代表检索效果一定越好。实际效果主要取决于 Embedding 模型本身的训练质量以及该模型是否适合当前的语言和业务场景。注意向量维度不是随意设置的它由所使用的 Embedding 模型及其输出配置决定。不同模型生成的向量维度可能不同部分模型还允许在调用时指定输出维度。向量存储中配置的维度必须与模型实际生成的向量维度保持一致否则可能无法创建集合或写入数据。同一个集合中的向量通常也必须具有相同的维度。创建 InMemoryVectorStoreVectorStore vectorStore new InMemoryVectorStore( new InMemoryVectorStoreOptions { EmbeddingGenerator aiProjectClient .GetProjectOpenAIClient() .GetEmbeddingClient(embeddingDeploymentName) .AsIEmbeddingGenerator() });EmbeddingGenerator 负责将聊天消息转换成向量InMemoryVectorStore 则负责在内存中保存并搜索这些向量。这里使用 In-Memory 主要是为了简化示例。应用进程退出后保存在内存中的数据也会消失因此它只能说明 ChatHistoryMemoryProvider 是如何工作的不能提供生产环境所需要的持久化能力。在实际项目中可以根据需要替换为 Qdrant、Postgres、Redis、Azure AI Search 或其他支持持久化的向量存储实现。配置存储范围和搜索范围创建向量存储后就可以配置 ChatHistoryMemoryProvider。AIProjectClient aiProjectClient new(new Uri(endpoint), new DefaultAzureCredential()); // 创建一个向量存储来存储聊天消息。 // 为了演示目的我们使用内存中的向量存储。 // 将其替换为您选择的可以长期保存聊天历史记录的向量存储实现。 VectorStore vectorStore new InMemoryVectorStore(new InMemoryVectorStoreOptions() { EmbeddingGenerator aiProjectClient .GetProjectOpenAIClient() .GetEmbeddingClient(embeddingDeploymentName) .AsIEmbeddingGenerator() }); var memoryOptions new ChatHistoryMemoryProviderOptions { MaxResults 5, // 仅用于本地调试。 // 开启后日志中会显示用户 ID、搜索文本和检索结果。 EnableSensitiveTelemetryData true, ContextPrompt 下面是从该用户过去的对话中检索到的信息。 回答当前问题时请优先使用其中明确表达的用户偏好 }; // 创建代理并添加 ChatHistoryMemoryProvider 以将聊天消息存储在向量存储中。 AIAgent agent aiProjectClient .AsAIAgent(new ChatClientAgentOptions { ChatOptions new() { ModelId deploymentName, Instructions 你是一个擅长讲笑话的助手。 }, Name Joker, AIContextProviders [new ChatHistoryMemoryProvider( vectorStore, collectionName: chathistory, vectorDimensions: 3072, // 回调以配置 ChatHistoryMemoryProvider 的初始状态。 // ChatHistoryMemoryProvider 将其状态存储在 AgentSession 中并且每当ChatHistoryMemoryProvider // 无法在会话中找到现有状态时将调用此回调 // 通常是在与新会话首次使用时。 session new ChatHistoryMemoryProvider.State( // 配置聊天消息将被存储的范围值。 // 在这种情况下我们使用固定的用户ID和每个新会话的唯一会话ID。 storageScope: new() { UserId UID1, SessionId Guid.NewGuid().ToString() }, // 配置将用于搜索相关先前消息的范围。 // 在这种情况下我们正在搜索该用户在所有会话中的任何消息。 searchScope: new() { UserId UID1 }), options: memoryOptions, loggerFactory: loggerFactory )] });这个示例中collectionName指定聊天历史保存到名为chathistory的集合中。vectorDimensions 表示 Embedding 向量的维度这个值需要与所使用的Embedding 模型保持一致。更值得关注的是 storageScope 和 searchScope。storageScope 决定消息以什么范围写入。示例同时保存了 UserId 和 SessionId因此每条消息不仅能够知道属于哪个用户也能够知道来自哪一次会话。storageScope: new() { UserId UID1, SessionId Guid.NewGuid().ToString() }searchScope 决定后续查询时允许搜索哪些消息。这里仅指定了 UserId没有指定 SessionId。searchScope: new() { UserId UID1 }这意味着消息在写入时仍然按照不同 Session 进行区分但在搜索时只要消息属于同一个用户就可以被检索出来。换句话说写入时记录的是“这个用户在哪次会话中说了什么”查询时关心的则是“这个用户以前说过什么”。这也是跨会话记忆能够产生的关键。跨会话记忆是如何产生的首先创建第一个会话AgentSession session await agent.CreateSessionAsync();用户在这个会话中告诉 Agent自己喜欢发生在茶馆里的笑话await agent.RunAsync( 我喜欢程序员的笑话给我讲一个。, session);调用完成后用户请求和 Agent 的响应会被写入向量存储并标记为属于用户 UID1。随后创建一个全新的会话AgentSession session2 await agent.CreateSessionAsync();新的 AgentSession 本身并不包含第一个会话中的聊天历史。用户在新会话中提出await agent.RunAsync( 给我讲一个我喜欢的笑话。, session2);这句话没有直接提到程序员但 ChatHistoryMemoryProvider 会根据当前请求执行语义检索。由于搜索范围只限定了 UserId它可以搜索用户 UID1 在其他 Session 中留下的消息。此前的这条消息我喜欢程序员的笑话给我讲一个。模型得到用户过去的偏好后就有机会继续讲一个发生在茶馆里的笑话。这里并不是新创建的 AgentSession 自动继承了旧会话而是 ChatHistoryMemoryProvider 从外部向量存储中找回了相关消息。示例运行效果如下总结ChatHistoryMemoryProvider 的作用是把聊天历史保存到向量存储中并在后续请求中找回与当前问题相关的消息。它依赖 Microsoft.Extensions.VectorData.Abstractions 提供的统一向量存储接口因此底层可以使用 InMemoryVectorStore也可以替换成 Qdrant、Postgres、Redis、Azure AI Search 等持久化实现。在配置过程中storageScope 决定消息如何保存searchScope 决定消息如何被搜索。当写入范围包含 SessionId而搜索范围只限定 UserId 时Agent 就可以在保留会话边界的同时检索同一个用户在其他会话中留下的信息。源代码地址https://github.com/bingbing-gui/dotnet-agent-playbook/tree/master/src/ai-agent/Agent-Framework/07-StorageConversations/01-ChatHistoryMemory引入地址