MAF 入门(7):在 MAF 中使用 RAG

写在前面

前面六篇,我们的 Agent 要么靠自己的"记忆力"回答问题,要么靠调工具去查接口。但真实业务里有一类问题很常见------公司的退货政策是什么?金卡会员有什么权益? 这些知识既不在大模型的训练数据里,也不适合写成一个个 Function Tool。

这时候就该 RAG 出场了。

RAG(Retrieval-Augmented Generation) ,翻译过来就是"检索增强生成"。说白了就一句话:先帮大模型查资料,再让它基于查到的内容回答。跟你考试的时候翻书再答题是一个道理。

这篇我们来看看,在 MAF 里怎么用几行代码把 RAG 跑起来。


一、先搞懂 RAG 到底在干什么

别被术语唬住。RAG 的流程其实非常简单:

text 复制代码
用户提问:"签收 5 天了还能退货吗?"
    │
    ▼
① 把问题变成向量(一串数字)
    │
    ▼
② 去向量库里找最相似的文档片段
    │
    ▼
③ 找到相关段落:"签收后 7 天内可申请退货退款......"
    │
    ▼
④ 把问题 + 检索到的段落一起喂给大模型
    │
    ▼
⑤ 大模型基于检索内容回答,并引用来源

这里面有两个关键角色:

Embedding 模型 :负责把文字变成向量。你可以理解为"翻译官"------把人类语言翻译成数学空间里的坐标。语义相近的文字,在这个空间里就挨得近。Demo 里用的是阿里百炼的 text-embedding-v3,输出 1024 维向量。

向量数据库 :存储文档向量、做相似度检索的仓库。Demo 用的是 InMemoryVectorStore(内存版,重启就丢),生产环境一般换成分布式向量数据库。


二、Demo 场景

我们的 Demo 模拟了一个电商客服场景:公司有三篇内部政策文档(退货退款、配送运费、会员权益),用户用自然语言提问,Agent 从知识库里检索相关内容后回答,并标注引用来源。

三篇文档很短,直接看就能理解:

文档 内容摘要
退货退款政策 签收 7 天内可退,退款 3 个工作日到账
配送运费说明 满 99 包邮,大件送货上门可预约
会员权益说明 银卡 98 折,金卡 95 折 + 免邮券 + 专属客服

运行后 Agent 会依次回答三个问题,比如"签收 5 天了还能退货吗"------知识库命中退货政策,Agent 据此回答"可以,7 天内都行"。


三、关键代码解析

整个 RAG Demo 涉及两个文件,我们一块一块来看。

3.1 知识库:CompanyKnowledge.cs

这个文件做三件事:定义向量库的表结构、灌入示例文档、提供检索接口。

第一步:定义"表结构"

csharp 复制代码
var definition = new VectorStoreCollectionDefinition
{
    Properties =
    [
        new VectorStoreKeyProperty(nameof(KnowledgeRecord.Id), typeof(string)),
        new VectorStoreDataProperty(nameof(KnowledgeRecord.SourceName), typeof(string)),
        new VectorStoreDataProperty(nameof(KnowledgeRecord.SourceLink), typeof(string)),
        new VectorStoreDataProperty(nameof(KnowledgeRecord.Text), typeof(string)),
        new VectorStoreVectorProperty(nameof(KnowledgeRecord.Embedding), typeof(string), dimensions),
    ]
};

你可以把向量库想象成一张数据库表。这里有主键 Id、几个普通字段(文档名、链接、正文),以及一个向量字段 Embeddingdimensions 是向量的维度,和 Embedding 模型对应------text-embedding-v3 输出 1024 维,这里就传 1024。

第二步:灌入文档

csharp 复制代码
public async Task SeedAsync(CancellationToken cancellationToken = default)
{
    await _collection.EnsureCollectionExistsAsync(cancellationToken);

    foreach ((string id, string name, string link, string text) in SampleDocuments())
    {
        await _collection.UpsertAsync(new KnowledgeRecord
        {
            Id = id,
            SourceName = name,
            SourceLink = link,
            Text = text,
            // 填文本即可;VectorStore 会用 EmbeddingGenerator 自动转向量
            Embedding = text,
        }, cancellationToken);
    }
}

注意一个细节:Embedding 字段我们填的是原始文本,不是向量数字。这是因为 InMemoryVectorStore 发现你配了 EmbeddingGenerator 后,会自动帮你把文本调 Embedding 模型转成向量。你只管塞文本就行。

生产环境里,这一步通常换成"读取 PDF/Word/网页 → 分块 → 批量写入向量库"的流程,后面会展开讲。

第三步:检索

csharp 复制代码
public async Task<List<KnowledgeHit>> SearchAsync(
    string query, int topK = 2, CancellationToken cancellationToken = default)
{
    var hits = new List<KnowledgeHit>();
    await foreach (VectorSearchResult<KnowledgeRecord> result in _collection.SearchAsync(
                       query, topK, cancellationToken: cancellationToken))
    {
        hits.Add(new KnowledgeHit(
            result.Record.SourceName,
            result.Record.SourceLink,
            result.Record.Text,
            result.Score));
    }
    return hits;
}

SearchAsync 接收用户的提问,在向量库里找 topK 条最相似的文档。返回的 Score 是相似度分数,越接近 1 越相关。底层做的事情是:把 query 也转成向量,然后算它和库里每条记录的余弦相似度。

3.2 把 RAG 接入 Agent:

知识库建好了,怎么让 Agent 用上它?MAF 提供了一个叫 TextSearchProvider 的组件,它就是 RAG 和 Agent 之间的桥梁。

第一步:创建向量库 + Embedding

csharp 复制代码
OpenAIClient openAiClient = ChatClientFactory.CreateOpenAiClient(llm);
var vectorStore = new InMemoryVectorStore(new()
{
    EmbeddingGenerator = openAiClient
        .GetEmbeddingClient(llm.EmbeddingModel)
        .AsIEmbeddingGenerator(llm.EmbeddingDimensions),
});

这里通过百炼的 OpenAI 兼容接口创建了一个 Embedding 客户端。InMemoryVectorStore 拿到它之后,就知道怎么把文本变成向量了。

第二步:配置 TextSearchProvider

csharp 复制代码
var textSearchOptions = new TextSearchProviderOptions
{
    SearchTime = TextSearchProviderOptions.TextSearchBehavior.BeforeAIInvoke,
    RecentMessageMemoryLimit = 0,
    CitationsPrompt = "在回答末尾用「引用:」列出文档名称和链接。",
};

SearchTime 控制检索时机,有两种模式:

  • BeforeAIInvoke(默认):每次调大模型之前,自动用用户的问题去检索。简单粗暴,但很可靠。
  • OnDemandFunctionCalling:让大模型自己决定要不要检索。模型觉得需要查资料时,会主动调一个"搜索工具"。更灵活,但对模型能力要求更高。

CitationsPrompt 告诉模型"回答完要列出引用来源",这样用户能看到答案出自哪篇文档。

第三步:挂到 Agent 上

csharp 复制代码
AIAgent agent = chatClient.AsAIAgent(new ChatClientAgentOptions
{
    Name = "ContosoSupport",
    ChatOptions = new ChatOptions
    {
        Instructions = SystemInstructions,
    },
    AIContextProviders =
    [
        new TextSearchProvider(
            (query, ct) => SearchKnowledgeAsync(knowledge, query, ct),
            textSearchOptions),
    ],
});

关键在 AIContextProviders 这个参数。TextSearchProvider 实现了 AIContextProvider 接口,它的作用是:在 Agent 调大模型之前,往上下文里"注入"检索到的内容。

你只需要提供一个搜索回调------"给你一个问题,你去知识库查,返回结果"。剩下的事情(什么时候查、怎么把结果塞进 prompt),TextSearchProvider 全帮你搞定。

第四步:搜索回调的实现

csharp 复制代码
private static async Task<IEnumerable<TextSearchProvider.TextSearchResult>> SearchKnowledgeAsync(
    CompanyKnowledge knowledge, string query, CancellationToken cancellationToken)
{
    List<KnowledgeHit> hits = await knowledge.SearchAsync(query, topK: 2, cancellationToken);
    Console.WriteLine($"  [检索] 「{TrimQuery(query)}」→ 命中 {hits.Count} 条");
    foreach (KnowledgeHit hit in hits)
    {
        Console.WriteLine($"         · {hit.SourceName} (score={hit.Score:F3})");
    }

    return hits.Select(hit => new TextSearchProvider.TextSearchResult
    {
        SourceName = hit.SourceName,
        SourceLink = hit.SourceLink,
        Text = hit.Text,
    });
}

这个方法做的事情很简单:调 CompanyKnowledge.SearchAsync 查知识库,然后把结果转成 MAF 要求的 TextSearchResult 格式。顺带在控制台打印检索日志,方便调试。

3.3 整体流程串起来

把所有部件拼在一起,一次 RAG 问答的完整链路是这样的:

text 复制代码
用户:"签收 5 天了还能退货吗?"
    │
    ▼
TextSearchProvider 拦截,拿问题去搜知识库
    │
    ▼
CompanyKnowledge.SearchAsync → InMemoryVectorStore.SearchAsync
    │  query 被 Embedding 模型转向量 → 余弦相似度匹配 → 返回 top 2
    │
    ▼
命中:「Contoso 商城退货退款政策」(score=0.87)
    │
    ▼
TextSearchProvider 把检索结果注入上下文
    │
    ▼
大模型收到:System Instructions + 检索到的政策原文 + 用户问题
    │
    ▼
大模型回答:"签收 7 天内可以退货退款,您的情况在期限内......"
           引用:Contoso 商城退货退款政策 https://docs.contoso.com/policies/returns

四、生产环境怎么用

这里用的是内存向量库 + 三篇短文档,跑着玩够了。但真正上生产,你还需要考虑下面这些问题。

4.1 文档分块(Chunking)

Demo 里每篇文档就几行字,整个塞进向量库没问题。但真实文档动辄几千字------一篇产品手册、一份合同、一个 FAQ 列表,你不可能把整篇文档当成一条记录。

为什么要分块? 两个原因。第一,Embedding 模型有输入长度限制(通常 512~8192 个 token),超长文本塞不进去。第二,即使能塞进去,一大段文字转成向量后,语义会被"稀释"------检索效果变差。

怎么分? 常见策略有这几种:

固定长度切分:按字符数或 token 数切,比如每 500 个 token 一块,相邻块之间重叠 50~100 个 token(overlap)。重叠是为了保证语义连贯------一句话被从中间切断的话,前后两块都抓不住完整意思。这是最简单也最常用的方式。

csharp 复制代码
// 伪代码:固定长度 + 重叠切分
public static List<string> ChunkByTokens(string text, int chunkSize = 500, int overlap = 50)
{
    var chunks = new List<string>();
    int start = 0;
    while (start < text.Length)
    {
        int end = Math.Min(start + chunkSize, text.Length);
        chunks.Add(text[start..end]);
        start = end - overlap;  // 回退 overlap 个字符
    }
    return chunks;
}

按段落/标题切分 :如果文档有清晰的结构(Markdown 标题、HTML 的 <h2> 标签、PDF 的章节),可以按结构自然切分。这样每一块的语义更完整。

语义切分:用 Embedding 模型计算相邻句子的相似度,在相似度骤降的地方切开。效果好但成本高,适合对检索质量要求极高的场景。

分块之后,每条记录除了 TextEmbedding,通常还会带上 DocumentId(属于哪篇原文档)和 ChunkIndex(第几块),方便回溯来源。

4.2 向量数据库选型

Demo 用的 InMemoryVectorStore 重启数据就没了。生产环境你需要一个持久化的向量数据库。Semantic Kernel 生态里已经有多款连接器可选:

向量数据库 特点 适用场景
Azure AI Search 微软全托管,支持混合检索 Azure 生态用户
Qdrant 开源,性能优秀,Rust 实现 中小规模,自建部署
Milvus 开源,支持十亿级向量 大规模生产环境
Pinecone 全托管 SaaS 快速上线,不想运维
Redis Stack 在 Redis 上加了向量搜索 已有 Redis 基础设施的团队
Weaviate 开源,内置模块化向量化 需要灵活 schema 的场景

好消息是,Semantic Kernel 的 VectorStore 抽象层做了统一接口。 Demo 里写的 CompanyKnowledge 类,换向量库只需要把 InMemoryVectorStore 换成对应的连接器(比如 QdrantVectorStore),其他代码基本不用动。

csharp 复制代码
// 生产环境示例:换成 Qdrant
var vectorStore = new QdrantVectorStore("http://qdrant-server:6334");
// 其余 CompanyKnowledge 代码不变

4.3 Embedding 模型选择

Embedding 模型直接决定了检索质量。几个主流选择:

国内 :阿里百炼 text-embedding-v3(Demo 用的就是这个,1024 维,中文效果好)、智谱 embedding-3、BGE 系列(开源可本地部署)。

国外 :OpenAI text-embedding-3-small(便宜够用)、text-embedding-3-large(精度更高)、Cohere embed-v4

选模型时注意:向量维度要和向量库配置对齐;不同模型的向量维度不同,不能混用同一个集合;中文场景优先选中文语料训练充分的模型。

4.4 检索策略优化

生产环境的检索通常不会像 Demo 这么简单,常见的优化手段包括:

混合检索(Hybrid Search):同时用向量检索和关键词检索(BM25),然后融合排序。向量检索擅长理解语义("退货"能匹配"退款"),关键词检索擅长精确匹配(订单号、产品型号)。两者互补效果最好。

重排序(Reranking):先用向量检索快速捞出 top-20 条候选,再用一个 Cross-Encoder 重排序模型精排,取 top-5 喂给大模型。多一步排序,但准确率提升明显。

查询改写(Query Rewriting):用户的问题有时候很口语化或者很模糊,可以先让大模型把问题改写成更适合检索的形式。比如用户说"我买的东西不想要了",改写成"退货退款政策 时效 条件",检索效果会好很多。

多路召回:同时从多个知识库检索------产品文档、FAQ、历史工单------然后合并去重。适合知识来源分散的企业场景。

4.5 一个典型的生产架构

把上面这些拼在一起,生产环境的 RAG 长这样:

text 复制代码
文档入库流程(离线):
  PDF/Word/网页 → 文档解析 → 分块(Chunking) → Embedding → 写入向量数据库

在线检索流程(实时):
  用户提问 → 查询改写 → 混合检索(向量+关键词) → 重排序 → top-K 片段
       → 拼装 Prompt(System Instructions + 检索结果 + 用户问题)
       → 大模型生成回答 → 返回给用户(附引用来源)

MAF 的 TextSearchProvider 帮你处理的是"在线检索流程"中和 Agent 对接的部分------什么时候检索、怎么把结果注入上下文。至于文档解析、分块、向量库选型、检索策略优化,这些需要你自己搭建。


六、小结

概念 Demo 中的实现 生产建议
向量库 InMemoryVectorStore 换 Qdrant / Milvus / Azure AI Search
Embedding text-embedding-v3 (1024 维) 按语言和数据量选型
文档分块 整篇存储(Demo 文档短) 固定长度 + overlap,或按结构切分
RAG 接入 TextSearchProvider + AIContextProviders 同,这是 MAF 推荐方式
检索模式 BeforeAIInvoke / OnDemand 生产推荐 BeforeAIInvoke,稳定可靠
引用溯源 SourceName + SourceLink 必须保留,用户需要验证答案

RAG 的本质就是"开卷考试"------与其让大模型凭记忆回答(可能编造),不如先帮它查资料,再让它基于查到的内容作答。MAF 把这件事简化成了 TextSearchProvider + 一个搜索回调,你只需要关心"去哪里查",框架帮你搞定"什么时候查"和"怎么喂给模型"。

相关推荐
JuiceFS1 小时前
GPFS、Alluxio、JuiceFS 怎么选?一文看懂架构与适用场景
人工智能·后端
爱吃程序猿的喵1 小时前
LingBot-Map 复现与原理剖析:基于 Geometric Context Transformer 的流式 3D 重建
人工智能·python·深度学习·计算机视觉·3d·transformer
俊哥V1 小时前
每日 AI 研究简报 · 2026-07-23
人工智能·ai
a1117761 小时前
基于PyTorch的动物图像识别系统 开源
人工智能·pytorch·python
Xzaveir_7771 小时前
企业号码负面标记治理:认证、申诉与合规事件的三轨模型
大数据·网络·人工智能·科技·产品经理
武子康1 小时前
1.2GB 离线语音 Agent 真正值得复用的不是 908ms:四阶段职责 + 状态感知 Tool Schema + 可观测时间锚点
人工智能·后端·agent
水如烟1 小时前
孤能子视角:中西医合璧系列·05 收束篇——同一关系场的四次显影:术法道伦理的完整循环
人工智能
Summer-Bright2 小时前
消费者 AI 变现竞争:从“一家独大“到“iOS vs Android“,谁在为 AI 买单?
android·人工智能·ios·ai·自然语言处理·agi
前端Baymax2 小时前
一篇文章讲透Infra:AI数据云计算基础架构分类详解
人工智能