大家好,欢迎来到 MAF 教学系列的第四期。

今天我们来聊聊一个非常实用的话题:如何给 AI 大模型外挂一个“知识库”,让它回答问题时不再“一本正经地胡说八道”, 也就是所谓的大模型幻觉。

熟悉LLM的朋友都知道,模型“幻觉”一直是个让人头疼的问题。你问它一个事实性问题,它可能凭借训练数据里的模糊记忆给你编造一段听起来合理但完全错误的答案,这是从llm原理上就难以避免的问题,当然,就算人也是会有幻觉的,这有办法避免吗?

对于人来说,我们想要避免幻觉,最简单的方法就是去搜索引擎中去查找“可靠”的资料,或者去翻阅我们以前看过的“可靠”的书籍,这样我们便可以引用“可靠的”信源,来正确的回答问题,对LLM也是同理

于是,“检索增强生成”(RAG,Retrieval-Augmented Generation)技术应运而生。

RAG 原理:先查资料,再将资料整合成人话给我们看

就和我们人类避免幻觉的办法一模一样 

当你向 AI 提问时,系统会先拦截你的问题。

系统拿着你的问题,去一个指定的“知识源”(比如你的本地文档、数据库、或者网页)里进行检索,寻找最相关的信息片段。

如果找到了相关内容,系统会把这些资料和你的原始问题一并打包,喂给 AI 模型。

AI 模型依据这些确切的资料来组织语言,生成最终的回答。

这样一来,AI 的回答就被牢牢地“锚定”在了我们提供的信息源上,大大降低了它天马行空产生幻觉的可能性。这对于构建企业知识库、文档问答助手等场景来说,是一项基石技术。

那么,在 Microsoft.Agents.Framework (MAF) 中,我们该如何实现 RAG 呢?

快速上手:模拟一个文本搜索

MAF 为我们提供了 TextSearchProvider,我们只需要定义一个配置和一个搜索适配器(Adapter)即可。

首先,声明一个 TextSearchProviderOptions,并设置 SearchTime 为 BeforeAIInvoke,这表示在每次 AI 被调用之前,都会先执行我们的搜索逻辑。

TextSearchProviderOptions providerOptions = new TextSearchProviderOptions()
{
    SearchTime = TextSearchProviderOptions.TextSearchBehavior.BeforeAIInvoke
};

接下来,我们定义核心的 SearchAdapter 函数。在这个简单的例子里,我们不连接任何外部数据库,而是模拟一个搜索行为。当检测到用户问题中包含“4月21日”时,我们就返回一段预先写好的“资料”。

static Task<IEnumerable<TextSearchProvider.TextSearchResult>> SearchAdapter(string query, CancellationToken cancellationToken)
{
    List<TextSearchProvider.TextSearchResult> results = new();
    if (query.Contains("4月21日"))
    {
        results.Add(new()
        {
            SourceName = "4月21号事件",
            SourceLink = "none",
            Text = "无聊的年即将发布MAF的RAG相关的视频."
        });
    }
    return Task.FromResult<IEnumerable<TextSearchProvider.TextSearchResult>>(results);
} 

最后,我们创建 AIAgent 时,将这个 SearchAdapter 包装成 TextSearchProvider 放入 AIContextProviders 列表里。同时,我们也可以在 Instructions 中加一句提示,告诉 AI 优先使用数据源。

AIAgent ai = new AzureOpenAIClient(/* ... */)
        .GetChatClient("gpt-4o")
        .AsAIAgent(new ChatClientAgentOptions
        {
            ChatOptions = new() { Instructions = "你是一个充满宇宙诗意的AI助手... 当数据源可用时,请优先使用数据源中的信息来回答问题,并且在回答的末尾注明信息来源的名称." },
            AIContextProviders = [new TextSearchProvider(SearchAdapter, providerOptions)],
        });
 
 
 Console.WriteLine(await ai.RunAsync("4月21日发生了什么?"));

运行这段代码,AI 就会准确地告诉你:“4月21日,无聊的年即将发布MAF的RAG相关的视频。” 这便是 RAG 消除幻觉的最基本形态。当然,这只是一个“玩具级”的演示,在实际应用中,我们往往会依赖更强大的向量数据库。

进阶实战:集成向量数据库

向量数据库是实现 RAG 的更专业的方案。它能将文本转换为“向量”(一串代表语义的数字),并通过计算向量间的相似度,来找到语义最相关的内容,而不仅仅是关键词匹配。

MAF 生态中的 SemanticKernel 提供了大量的连接器,方便我们接入各种向量数据库。今天我们选用轻量级的文件数据库 SqliteVec 作为演示。

第一步:准备数据模型和存储

我们需要定义一个文档“块”(Chunk)的模型,用于存储分割后的文本片段及其向量。

然后,创建向量存储实例。这里我们需要一个 IEmbeddingGenerator 来将文本转换为向量,我们使用 Azure OpenAI 的 text-embedding-3-large 模型。

VectorStore vectorStore = new SqliteVectorStore("Data Source=videoData.db;", new()
{
    EmbeddingGenerator = azureOpenAIClient.GetEmbeddingClient(embeddingDeploymentName).AsIEmbeddingGenerator()
});
var documentationCollection = vectorStore.GetCollection<Guid, DocumentationChunk>("video_documentation");
// 每次运行都清空并重建,保证环境干净
await documentationCollection.EnsureCollectionDeletedAsync();
await documentationCollection.EnsureCollectionExistsAsync();

第二步:加载并分割文档

我们从一个 Markdown 文件中读取数据(比如我们过往的视频列表),然后将长文本分割成一个个 chunk,并上传到向量数据库中。

// 一个简单的文档分割与上传函数
static async Task UploadDataFromMarkdownFileAsync(string markdownFilePath, string sourceName, VectorStoreCollection<Guid, DocumentationChunk> vectorStoreCollection, int chunkSize, int overlap)
{
    var markdown = await File.ReadAllTextAsync(markdownFilePath);
    var chunks = new List<DocumentationChunk>();
    for (int i = 0; i < markdown.Length; i += chunkSize)
    {
        var chunk = new DocumentationChunk
        {
            Key = Guid.NewGuid(),
            SourceLink = Path.GetFileName(markdownFilePath),
            SourceName = sourceName,
            Text = markdown.Substring(i, Math.Min(chunkSize + overlap, markdown.Length - i))
        };
        chunks.Add(chunk);
    }
    await vectorStoreCollection.UpsertAsync(chunks);
}
// 调用上传
await UploadDataFromMarkdownFileAsync(videoMarkdownPath, "无聊的年视频整理", documentationCollection, 1000, 200);

第三步:实现向量搜索适配器

这次的 SearchAdapter 不再做简单的字符串匹配,而是实实在在地去向量数据库里进行语义搜索。

Func<string, CancellationToken, Task<IEnumerable<TextSearchProvider.TextSearchResult>>> SearchAdapter = async (text, ct) =>
{
    List<TextSearchProvider.TextSearchResult> results = [];
    // 在向量集合中搜索与用户输入 "text" 最相似的前5个结果
    await foreach (var result in documentationCollection.SearchAsync(text, 5, cancellationToken: ct))
    {
        results.Add(new TextSearchProvider.TextSearchResult
        {
            SourceName = result.Record.SourceName,
            SourceLink = result.Record.SourceLink,
            Text = result.Record.Text ?? string.Empty,
            RawRepresentation = result
        });
    }
    return results;
};

第四步:创建 Agent 并发起会话

流程和之前的模拟版本完全一样,只是将 SearchAdapter 替换成了我们的向量搜索版本。

AIAgent ai = new AzureOpenAIClient(/* ... */)
    .GetChatClient("gpt-4o")
    .AsAIAgent(new ChatClientAgentOptions
    {
        ChatOptions = new() { Instructions = "..." },
        AIContextProviders = [new TextSearchProvider(SearchAdapter, textSearchOptions)],
    });
AgentSession session = await ai.CreateSessionAsync();
// 连续提问,验证RAG效果
Console.WriteLine("查询最新的视频");
Console.WriteLine(await ai.RunAsync("最新的视频是哪一个?"));
Console.WriteLine(await ai.RunAsync("播放量最高的视频是哪一个?"));
Console.WriteLine(await ai.RunAsync("Microsoft Agent Framework相关的视频呢?"));
Console.WriteLine(await ai.RunAsync(".NET相关的视频呢?"));

运行代码后,你会发现 AI 能够准确地根据我们上传的视频列表,回答出诸如“最新的视频是关于 MAF”这样的事实性问题,并且每一次回答都有据可循。

小结

今天我们走通了在 MAF 中使用 RAG 技术的全流程。从最初模拟一个简单的文本搜索,到最终集成一个功能完备的向量数据库,核心思路始终清晰:

搜索时机:在 AI 调用之前(BeforeAIInvoke)。

搜索逻辑:无论是简单的 if 判断还是复杂的向量语义搜索,都封装在一个 SearchAdapter 函数里。

注入 AI Agent:将 TextSearchProvider 作为上下文提供者交给 Agent。

RAG 就如同AI的百科全书,你提供给他什么,他就回去搜索什么,自然也就能更好的回答你的问题。MAF 则为这个强大的功能提供了极其简洁、优雅的实现方式。

希望本期内容能对大家有所帮助。本期的代码也将会在后续上传到 GitHub,敬请关注。

学会了吗,学会了

GitHub: 

https://github.com/ssccinng/MAF.Video/tree/master/MAF.RAG.Vector.Videohttps://github.com/ssccinng/MAF.Video/tree/master/MAF.RAG.Vector.Video

Bilibili: @无聊的年

微信公众号: @scixing的炼丹房

【Microsoft Agent Framework】4. 检索增强生成(RAG)_哔哩哔哩_bilibili

视频点击下方查看原文

图片

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐