微信扫码
添加专属顾问
 
                        我要投稿
探索MCP架构下的Agentic RAG系统实战案例,深入了解MCP与RAG的融合魅力。 核心内容: 1. MCP与Agentic RAG的融合思考 2. MCP标准下的Agentic RAG架构设计 3. MCP Server与客户端Agent的实现细节
 
                                思考:MCP与Agentic RAG的融
RAG是一种借助外部知识来给LLM提供上下文的AI应用范式。从这个角度来说,RAG与MCP有着相似的意义:给大模型补充上下文,以增强其能力。只是MCP以提供外部工具为主,而RAG则是以注入参考知识为主。这就像一个考试的学生,MCP给你提供计算器,而RAG则是给你一本书。
当然,两者的重点并不一样,MCP强调的是提供工具的方式(集成标准);而RAG则是需要你实现的完整应用。所以两者并不冲突,完全可以用MCP的方法来集成一个RAG应用。
特别是在Agentic RAG系统(如下图)中,由于通常涉及到多个RAG查询管道与Agent的融合,这就与MCP的思想非常契合:
一个针对大量不同文档的问答Agent,这些问答有事实性问题也有摘要性问题,更有跨越多个文档的融合问题,甚至需要搜索引擎来补充信息。
MCP标准下的Agentic RAG架构
在MCP架构下,无论是SSE还是stdio模式,都是Client/Server模式。你必须在开始之前清晰的设计好MCP Server与Client应用的分工及交互。比如:
服务端提供的工具,包括功能边界、输入输出
服务功能粒度不能太大(丧失模块化)也不能太小(复杂化)
缓存与持久化设计:毕竟RAG是数据密集型应用
客户端Agent设计:模型、工作流、与服务端的交互等
如果是多用户环境:要考虑只是文档与索引的隔离
【总体思想】
我们基于如下的总体架构来实现:
MCP Server:RAG管道的核心
【工具(Tools)】
create_vector_index:输入文档、索引名与参数,完成解析与索引创建。
query_document:查询事实问题的RAG管道,输入索引名与查询问题。
get_document_summary:查询总结性问题的RAG管道,输入文件和查询问题。
list_indies等:辅助工具,包括一个自己实现的Web搜索工具。
需要说明,在这里的设计中,不同的RAG管道查询的工具是一样的,但参数(索引名,依赖于Agent推理)不同。一个是推理工具,一个推理参数,效果一致。
【缓存机制】
服务端要对文档解析(含分割)与索引创建的信息进行缓存(持久化存储),以防止可能的重复解析与索引创建,提高性能。
文档节点缓存:缓存文档解析分拆后的结果,确保文档解析过一次后,只要内容与参数(如chunk_size)不变,就不会被重新解析。
文档缓存的唯一名称是文档内容hash值+解析参数的联合。比如:
“questions.csv_f4056ac836fc06bb5f96ed233d9e2b63_500_50”
索引信息缓存:缓存已经创建过的索引信息,防止重复嵌入及向量库访问,避免不必要的模型调用成本。
索引缓存的唯一名称是每个文档关联的唯一索引名称。比如:
“questions_for_customerservice”
以下情况下会导致索引被重建:
客户端强制要求重建
索引信息缓存不存在
文档节点缓存不存在
这样的缓存管理方式,可以增加处理的灵活性与健壮性。如:
更改文档内容或解析参数,即时文档名与索引名不变:仍然会触发索引重建。
文档内容与参数不变,但修改索引名:会创建新索引,但不会重新解析文档。
【工具实现:create_vector_index】
这是服务端两个重要工具之一,核心代码如下,请参考注释理解:
.....
@app.tool()
asyncdefcreate_vector_index(
    ctx: Context, 
    file_path: str, 
    index_name: str, 
    chunk_size: int = 500, 
    chunk_overlap: int = 50, 
    force_recreate: bool = False
) -> str:
    """创建或加载文档向量索引(使用缓存的节点)
    
    Args:
        ctx: 上下文对象
        file_path: 文档文件路径
        index_name: 索引名称
        chunk_size: 文本块大小
        chunk_overlap: 文本块重叠大小
        force_recreate: 是否强制重新创建索引
    
    Returns:
        操作结果描述
    """
    #用来判断索引是否存在
    storage_path = f"{storage_dir}/{index_name}"
    
    try:
        # 获取Chroma客户端
        chroma = ctx.request_context.lifespan_context.chroma
        
        # 获取节点缓存路径(文档内容hash_chunksize_chunovlerlap)
        cache_path = get_cache_path(file_path, chunk_size, chunk_overlap)
        
        # 确定是否需要重建索引:强制 or 索引不存在 or 文档有变
        need_recreate = (
            force_recreate or
            not os.path.exists(storage_path) or
            not os.path.exists(cache_path)
        )
            
        if os.path.exists(storage_path) andnot need_recreate:
            returnf"索引 {index_name} 已存在且参数未变化,无需创建"
        
        # 如果需要重新创建,首先尝试删除现有的索引向量库
        try:
            chroma.delete_collection(name=index_name)
        except Exception as e:
            logger.warning(f"删除集合时出错 (可能是首次创建): {e}")
            
        # 创建新的向量库
        collection = chroma.get_or_create_collection(name=index_name)
        vector_store = ChromaVectorStore(chroma_collection=collection)
        
       # 加载与拆分文档 
        nodes = await load_and_split_document(ctx, file_path, chunk_size, chunk_overlap) 
        logger.info(f"加载了 {len(nodes)} 个节点")
        
        # 创建向量索引
        storage_context = StorageContext.from_defaults(vector_store=vector_store)
        vector_index = VectorStoreIndex(nodes, storage_context=storage_context, embed_model=embedded_model)
        
        # 缓存索引信息,这样下次不会重建
        vector_index.storage_context.persist(persist_dir=storage_path)
        returnf"成功创建索引: {index_name}, 包含 {len(nodes)} 个节点"
        
    except Exception as e:
......【工具实现:query_document】
这是客户端调用的主要工具。其输入是索引名与查询问题。借助索引缓存,可以快速加载并执行RAG查询。这里不再展示完整处理过程:
@app.tool()
async  def query_document(
    ctx: Context, 
    index_name: str, 
    query: str,
    similarity_top_k: int = 5
) -> str:
    """从文档中查询事实性信息,用于回答具体的细节问题
    
    Args:
        ctx: 上下文对象
        index_name: 索引名称
        query: 查询文本
        similarity_top_k: 返回的相似节点数量
    
    Returns:
        查询结果
    """
......按类似方法,再创建一个用于回答总结性问题的工具(利用LlamaIndex的SummaryIndex类型索引),此处不在赘述。
MCP客户端:实现Agent(基于LangGraph)
客户端的工作流程如下:
客户端的几个设计重点简单说明如下:
【配置文件】
客户端有两个重要的配置信息,分别用于MCP Server与知识文档的配置。
{
  "servers": {
    "rag_server": {
      "transport": "sse",
      "url": "http://localhost:5050/sse",
      "allowed_tools": ["load_and_split_document", "create_vector_index", "get_document_summary", "query_document"]
    },
     
    ...其他server...
}doc_config.json:
{
    "data/c-rag.pdf": {
        "description": "c-rag技术论文,可以回答c-rag有关问题",
        "index_name": "c-rag",
        "chunk_size": 500,
        "chunk_overlap": 50
    },
    "data/questions.csv": {
        "description": "税务问题数据集,包含常见税务咨询问题和答案",
        "index_name": "tax-questions",
        "chunk_size": 500,
        "chunk_overlap": 50
    },
    ....其他需要索引和查询的文档.....
}【主程序】
客户端主程序流程非常简单,基于一个封装的MCP客户端与AgenticRAG类型:
......
        client = MultiServerMCPClient.from_config('mcp_config.json')
        asyncwith client as mcp_client: 
        
            logger.info(f"已连接到MCP服务器: {', '.join(mcp_client.get_connected_servers())}")
            
            # 创建智能体
            rag = AgenticRAGLangGraph(client=mcp_client, doc_config=doc_config)
            # 创建向量索引,自动排重
            await rag.process_files()
            
            # 构建智能体
            await rag.build_agent()
            
            # 交互式对话
            await rag.chat_repl()......
    async def build_agent(self) -> None:
        # 获取服务端提供的工具列表
        mcp_tools = await self.client.get_tools_for_langgraph()
        ...略:配置文件生成doc_info....
        # 使用LangGraph创建ReAct智能体
        self.agent = create_react_agent(
            model=llm,
            tools=mcp_tools,
            prompt=SYSTEM_PROMPT.format(
              doc_info_str=doc_info_str,
              current_time=datetime.now().strftime('%Y-%m-%d %H:%M:%S')),
        )
        
        logger.info("===== 智能体构建完成 =====")篇幅原因,一些细节部分不在这做详细展示。如果有疑问,欢迎后台交流。
端到端效果演示
现在让我们来测试下这个的“MCP化”的Agentic RAG应用的运行效果。按照如下步骤来进行:
1. 启动MCP RAG-Server。这里用更复杂的SSE模式(暂时未支持文档上传,所以只能本机启动):
启动时会自动提取并展示服务端的工具清单。
2. 准备客户端知识文档与配置文件。将需要索引和查询的文档放在应用的data/目录,配置好mcp_config与doc_config。不做任何其他处理。直接启动客户端应用:
python rag_agent_langgraph.py
* 观察首次运行的跟踪信息(如下图),这里的过程是:
连接RAG-Server与初始化
调用服务端工具创建向量索引。由于是首次访问,服务端没有索引缓存,所以会逐个对文件解析与创建向量索引
最后会加载Server端工具,创建LangGraph的Agent
* 现在退出程序,再次启动客户端,观察输出(如下图),可以看到由于索引已经创建,所以会显示“无需创建”。
3. 交互式测试
进入交互式测试环节(图中的服务端信息是通过MCP接口推送到客户端的远程日志,方便观察服务端的工作状态):
1. 关联两个文档信息的查询
由于提供的文档有北京和上海的城市信息介绍,所以看到这个问题调用了北京和上海的RAG管道查询,还自作主张的调用了搜索引擎做补充,然后输出答案:
2. 查询知识库答案,并要求和网络搜索结果核对。
日志显示,Agent先用本地向量索引查询,然后通过搜索引擎对比,非常准确。
3. 总结性问题测试。
日志显示,这里未加载向量索引,而是由工具加载这个文档的节点,并生成文档摘要后返回(SummaryIndex的效率不太高,有待优化)
4. 最后一个很有意思的测试。
由于我们把创建索引的过程“工具”化了,所以甚至可以用自然语言来管理索引。比如这里我要求把csv文档的索引重建,智能体准确的推理出工具及参数,并重建了csv文档索引(实际应用要考虑安全性):
以上展示了一个基于MCP架构的Agentic RAG系统的实现。总结这种架构下的一些明显的变化:
end
福利时间
为了帮助LLM开发人员更系统性与更深
RAG是一种借助外部知识来给LLM提供上下文的AI应用范式。从这个角度来说,RAG与MCP有着相似的意义:给大模型补充上下文,以增强其能力。只是MCP以提供外部工具为主,而RAG则是以注入参考知识为主。这就像一个考试的学生,MCP给你提供计算器,而RAG则是给你一本书。
当然,两者的重点并不一样,MCP强调的是提供工具的方式(集成标准);而RAG则是需要你实现的完整应用。所以两者并不冲突,完全可以用MCP的方法来集成一个RAG应用。
特别是在Agentic RAG系统(如下图)中,由于通常涉及到多个RAG查询管道与Agent的融合,这就与MCP的思想非常契合:
一个针对大量不同文档的问答Agent,这些问答有事实性问题也有摘要性问题,更有跨越多个文档的融合问题,甚至需要搜索引擎来补充信息。
MCP标准下的Agentic RAG架构
在MCP架构下,无论是SSE还是stdio模式,都是Client/Server模式。你必须在开始之前清晰的设计好MCP Server与Client应用的分工及交互。比如:
服务端提供的工具,包括功能边界、输入输出
服务功能粒度不能太大(丧失模块化)也不能太小(复杂化)
缓存与持久化设计:毕竟RAG是数据密集型应用
客户端Agent设计:模型、工作流、与服务端的交互等
如果是多用户环境:要考虑只是文档与索引的隔离
【总体思想】
我们基于如下的总体架构来实现:
MCP Server:RAG管道的核心
【工具(Tools)】
create_vector_index:输入文档、索引名与参数,完成解析与索引创建。
query_document:查询事实问题的RAG管道,输入索引名与查询问题。
get_document_summary:查询总结性问题的RAG管道,输入文件和查询问题。
list_indies等:辅助工具,包括一个自己实现的Web搜索工具。
需要说明,在这里的设计中,不同的RAG管道查询的工具是一样的,但参数(索引名,依赖于Agent推理)不同。一个是推理工具,一个推理参数,效果一致。
【缓存机制】
服务端要对文档解析(含分割)与索引创建的信息进行缓存(持久化存储),以防止可能的重复解析与索引创建,提高性能。
文档节点缓存:缓存文档解析分拆后的结果,确保文档解析过一次后,只要内容与参数(如chunk_size)不变,就不会被重新解析。
文档缓存的唯一名称是文档内容hash值+解析参数的联合。比如:
“questions.csv_f4056ac836fc06bb5f96ed233d9e2b63_500_50”
索引信息缓存:缓存已经创建过的索引信息,防止重复嵌入及向量库访问,避免不必要的模型调用成本。
索引缓存的唯一名称是每个文档关联的唯一索引名称。比如:
“questions_for_customerservice”
以下情况下会导致索引被重建:
客户端强制要求重建
索引信息缓存不存在
文档节点缓存不存在
这样的缓存管理方式,可以增加处理的灵活性与健壮性。如:
更改文档内容或解析参数,即时文档名与索引名不变:仍然会触发索引重建。
文档内容与参数不变,但修改索引名:会创建新索引,但不会重新解析文档。
【工具实现:create_vector_index】
这是服务端两个重要工具之一,核心代码如下,请参考注释理解:
.....
@app.tool()
asyncdefcreate_vector_index(
    ctx: Context, 
    file_path: str, 
    index_name: str, 
    chunk_size: int = 500, 
    chunk_overlap: int = 50, 
    force_recreate: bool = False
) -> str:
    """创建或加载文档向量索引(使用缓存的节点)
    
    Args:
        ctx: 上下文对象
        file_path: 文档文件路径
        index_name: 索引名称
        chunk_size: 文本块大小
        chunk_overlap: 文本块重叠大小
        force_recreate: 是否强制重新创建索引
    
    Returns:
        操作结果描述
    """
    #用来判断索引是否存在
    storage_path = f"{storage_dir}/{index_name}"
    
    try:
        # 获取Chroma客户端
        chroma = ctx.request_context.lifespan_context.chroma
        
        # 获取节点缓存路径(文档内容hash_chunksize_chunovlerlap)
        cache_path = get_cache_path(file_path, chunk_size, chunk_overlap)
        
        # 确定是否需要重建索引:强制 or 索引不存在 or 文档有变
        need_recreate = (
            force_recreate or
            not os.path.exists(storage_path) or
            not os.path.exists(cache_path)
        )
            
        if os.path.exists(storage_path) andnot need_recreate:
            returnf"索引 {index_name} 已存在且参数未变化,无需创建"
        
        # 如果需要重新创建,首先尝试删除现有的索引向量库
        try:
            chroma.delete_collection(name=index_name)
        except Exception as e:
            logger.warning(f"删除集合时出错 (可能是首次创建): {e}")
            
        # 创建新的向量库
        collection = chroma.get_or_create_collection(name=index_name)
        vector_store = ChromaVectorStore(chroma_collection=collection)
        
       # 加载与拆分文档 
        nodes = await load_and_split_document(ctx, file_path, chunk_size, chunk_overlap) 
        logger.info(f"加载了 {len(nodes)} 个节点")
        
        # 创建向量索引
        storage_context = StorageContext.from_defaults(vector_store=vector_store)
        vector_index = VectorStoreIndex(nodes, storage_context=storage_context, embed_model=embedded_model)
        
        # 缓存索引信息,这样下次不会重建
        vector_index.storage_context.persist(persist_dir=storage_path)
        returnf"成功创建索引: {index_name}, 包含 {len(nodes)} 个节点"
        
    except Exception as e:
......【工具实现:query_document】
这是客户端调用的主要工具。其输入是索引名与查询问题。借助索引缓存,可以快速加载并执行RAG查询。这里不再展示完整处理过程:
@app.tool()
async  def query_document(
    ctx: Context, 
    index_name: str, 
    query: str,
    similarity_top_k: int = 5
) -> str:
    """从文档中查询事实性信息,用于回答具体的细节问题
    
    Args:
        ctx: 上下文对象
        index_name: 索引名称
        query: 查询文本
        similarity_top_k: 返回的相似节点数量
    
    Returns:
        查询结果
    """
......按类似方法,再创建一个用于回答总结性问题的工具(利用LlamaIndex的SummaryIndex类型索引),此处不在赘述。
MCP客户端:实现Agent(基于LangGraph)
客户端的工作流程如下:
客户端的几个设计重点简单说明如下:
【配置文件】
客户端有两个重要的配置信息,分别用于MCP Server与知识文档的配置。
{
  "servers": {
    "rag_server": {
      "transport": "sse",
      "url": "http://localhost:5050/sse",
      "allowed_tools": ["load_and_split_document", "create_vector_index", "get_document_summary", "query_document"]
    },
     
    ...其他server...
}doc_config.json:
{
    "data/c-rag.pdf": {
        "description": "c-rag技术论文,可以回答c-rag有关问题",
        "index_name": "c-rag",
        "chunk_size": 500,
        "chunk_overlap": 50
    },
    "data/questions.csv": {
        "description": "税务问题数据集,包含常见税务咨询问题和答案",
        "index_name": "tax-questions",
        "chunk_size": 500,
        "chunk_overlap": 50
    },
    ....其他需要索引和查询的文档.....
}【主程序】
客户端主程序流程非常简单,基于一个封装的MCP客户端与AgenticRAG类型:
......
        client = MultiServerMCPClient.from_config('mcp_config.json')
        asyncwith client as mcp_client: 
        
            logger.info(f"已连接到MCP服务器: {', '.join(mcp_client.get_connected_servers())}")
            
            # 创建智能体
            rag = AgenticRAGLangGraph(client=mcp_client, doc_config=doc_config)
            # 创建向量索引,自动排重
            await rag.process_files()
            
            # 构建智能体
            await rag.build_agent()
            
            # 交互式对话
            await rag.chat_repl()......
    async def build_agent(self) -> None:
        # 获取服务端提供的工具列表
        mcp_tools = await self.client.get_tools_for_langgraph()
        ...略:配置文件生成doc_info....
        # 使用LangGraph创建ReAct智能体
        self.agent = create_react_agent(
            model=llm,
            tools=mcp_tools,
            prompt=SYSTEM_PROMPT.format(
              doc_info_str=doc_info_str,
              current_time=datetime.now().strftime('%Y-%m-%d %H:%M:%S')),
        )
        
        logger.info("===== 智能体构建完成 =====")篇幅原因,一些细节部分不在这做详细展示。如果有疑问,欢迎后台交流。
端到端效果演示
现在让我们来测试下这个的“MCP化”的Agentic RAG应用的运行效果。按照如下步骤来进行:
1. 启动MCP RAG-Server。这里用更复杂的SSE模式(暂时未支持文档上传,所以只能本机启动):
启动时会自动提取并展示服务端的工具清单。
2. 准备客户端知识文档与配置文件。将需要索引和查询的文档放在应用的data/目录,配置好mcp_config与doc_config。不做任何其他处理。直接启动客户端应用:
python rag_agent_langgraph.py
* 观察首次运行的跟踪信息(如下图),这里的过程是:
连接RAG-Server与初始化
调用服务端工具创建向量索引。由于是首次访问,服务端没有索引缓存,所以会逐个对文件解析与创建向量索引
最后会加载Server端工具,创建LangGraph的Agent
* 现在退出程序,再次启动客户端,观察输出(如下图),可以看到由于索引已经创建,所以会显示“无需创建”。
3. 交互式测试
进入交互式测试环节(图中的服务端信息是通过MCP接口推送到客户端的远程日志,方便观察服务端的工作状态):
1. 关联两个文档信息的查询
由于提供的文档有北京和上海的城市信息介绍,所以看到这个问题调用了北京和上海的RAG管道查询,还自作主张的调用了搜索引擎做补充,然后输出答案:
2. 查询知识库答案,并要求和网络搜索结果核对。
日志显示,Agent先用本地向量索引查询,然后通过搜索引擎对比,非常准确。
3. 总结性问题测试。
日志显示,这里未加载向量索引,而是由工具加载这个文档的节点,并生成文档摘要后返回(SummaryIndex的效率不太高,有待优化)
4. 最后一个很有意思的测试。
由于我们把创建索引的过程“工具”化了,所以甚至可以用自然语言来管理索引。比如这里我要求把csv文档的索引重建,智能体准确的推理出工具及参数,并重建了csv文档索引(实际应用要考虑安全性):
以上展示了一个基于MCP架构的Agentic RAG系统的实现。总结这种架构下的一些明显的变化:
end
53AI,企业落地大模型首选服务商
产品:场景落地咨询+大模型应用平台+行业解决方案
承诺:免费POC验证,效果达标后再合作。零风险落地应用大模型,已交付160+中大型企业
 
            2025-09-15
2025-09-02
2025-08-05
2025-08-18
2025-08-25
2025-08-25
2025-08-25
2025-09-03
2025-08-20
2025-09-08
2025-10-04
2025-09-30
2025-09-10
2025-09-10
2025-09-03
2025-08-28
2025-08-25
2025-08-20