五一期间,小编尝试用MCP架构从零实现一个完整的Agentic RAG系统,以演示MCP与RAG、Agent的一些有趣融合,在此与大家一起分享。内容涵盖:- MCP服务端:实现RAG-Server(LLamaIndex)
- MCP客户端:实现Agent(LangGraph)
RAG是一种借助外部知识来给LLM提供上下文的AI应用范式。从这个角度来说,RAG与MCP有着相似的意义:给大模型补充上下文,以增强其能力。只是MCP以提供外部工具为主,而RAG则是以注入参考知识为主。这就像一个考试的学生,MCP给你提供计算器,而RAG则是给你一本书。 当然,两者的重点并不一样,MCP强调的是提供工具的方式(集成标准);而RAG则是需要你实现的完整应用。所以两者并不冲突,完全可以用MCP的方法来集成一个RAG应用。 特别是在Agentic RAG系统(如下图)中,由于通常涉及到多个RAG查询管道与Agent的融合,这就与MCP的思想非常契合: 一个针对大量不同文档的问答Agent,这些问答有事实性问题也有摘要性问题,更有跨越多个文档的融合问题,甚至需要搜索引擎来补充信息。 现在我们来用MCP的标准设计并完整的实现这个场景。在MCP架构下,无论是SSE还是stdio模式,都是Client/Server模式。你必须在开始之前清晰的设计好MCP Server与Client应用的分工及交互。比如:
【总体思想】 我们基于如下的总体架构来实现:  ![]() ![]() ![]() ![]() ![]()
- 在MCP Server上提供RAG管道构建与查询的工具;在客户端创建使用这些工具的AI Agent,提供查询任务规划与执行能力
- MCP Server借助LlamaIndex实现RAG管道;在客户端借助LangGraph实现Agent:让每个‘人’干更擅长的事。
MCP Server是RAG功能实现的位置。我们对MCP Server拆解设计如下:【工具(Tools)】 create_vector_index:输入文档、索引名与参数,完成解析与索引创建。
query_document:查询事实问题的RAG管道,输入索引名与查询问题。
get_document_summary:查询总结性问题的RAG管道,输入文件和查询问题。 list_indies等:辅助工具,包括一个自己实现的Web搜索工具。
需要说明,在这里的设计中,不同的RAG管道查询的工具是一样的,但参数(索引名,依赖于Agent推理)不同。一个是推理工具,一个推理参数,效果一致。 【缓存机制】 服务端要对文档解析(含分割)与索引创建的信息进行缓存(持久化存储),以防止可能的重复解析与索引创建,提高性能。 文档缓存的唯一名称是文档内容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_recreateor notos.path.exists(storage_path)or notos.path.exists(cache_path) ) ifos.path.exists(storage_path)andnotneed_recreate: returnf"索引{index_name}已存在且参数未变化,无需创建" # 如果需要重新创建,首先尝试删除现有的索引向量库 try: chroma.delete_collection(name=index_name) exceptExceptionase: logger.warning(f"删除集合时出错 (可能是首次创建):{e}") # 创建新的向量库 collection = chroma.get_or_create_collection(name=index_name) vector_store = ChromaVectorStore(chroma_collection=collection) # 加载与拆分文档 nodes =awaitload_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)}个节点" exceptExceptionase: ......
【工具实现:query_document】 这是客户端调用的主要工具。其输入是索引名与查询问题。借助索引缓存,可以快速加载并执行RAG查询。这里不再展示完整处理过程: @app.tool() async defquery_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与知识文档的配置。 配置MCP Servers的信息,支持多Server连接、工具加载与过滤(这是一个在langgraph-mcp-adapers基础上扩展的版本)。比如:{ "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: 配置需要索引和查询的全部文档信息。这些信息还会在查询时被注入Agent提示词,用来推理工具的使用参数:{ "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') asyncwithclientasmcp_client: logger.info(f"已连接到MCP服务器:{', '.join(mcp_client.get_connected_servers())}") # 创建智能体 rag = AgenticRAGLangGraph(client=mcp_client, doc_config=doc_config)
# 创建向量索引,自动排重 awaitrag.process_files() # 构建智能体 awaitrag.build_agent() # 交互式对话 awaitrag.chat_repl() 注意到这里的关键步骤是build_agent,会借助LangGraph预置的create_react_agent快速创建Agent。如果你需要精细化的控制,也可以自定义Graph:...... asyncdefbuild_agent(self)->None:
# 获取服务端提供的工具列表 mcp_tools =awaitself.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。不做任何其他处理。直接启动客户端应用: pythonrag_agent_langgraph.py * 观察首次运行的跟踪信息(如下图),这里的过程是: * 现在退出程序,再次启动客户端,观察输出(如下图),可以看到由于索引已经创建,所以会显示“无需创建”。 3. 交互式测试 进入交互式测试环节(图中的服务端信息是通过MCP接口推送到客户端的远程日志,方便观察服务端的工作状态): 1. 关联两个文档信息的查询 由于提供的文档有北京和上海的城市信息介绍,所以看到这个问题调用了北京和上海的RAG管道查询,还自作主张的调用了搜索引擎做补充,然后输出答案: 2. 查询知识库答案,并要求和网络搜索结果核对。 日志显示,Agent先用本地向量索引查询,然后通过搜索引擎对比,非常准确。 3. 总结性问题测试。 日志显示,这里未加载向量索引,而是由工具加载这个文档的节点,并生成文档摘要后返回(SummaryIndex的效率不太高,有待优化) 4. 最后一个很有意思的测试。 由于我们把创建索引的过程“工具”化了,所以甚至可以用自然语言来管理索引。比如这里我要求把csv文档的索引重建,智能体准确的推理出工具及参数,并重建了csv文档索引(实际应用要考虑安全性): 以上展示了一个基于MCP架构的Agentic RAG系统的实现。总结这种架构下的一些明显的变化: - MCP要求对整个系统做模块化与松耦合的重新设计,这会带来一系列工程上的好处。比如分工与效率提升、可维护性、独立扩展、部署更灵活等。
- MCP不依赖于某个技术堆栈。因此技术选择上更灵活,比如服务端用LlamaIndex框架,而客户端则用LangGraph;甚至可以用不同的语言。
- MCP实现了基于标准的模块间互操作。这有助于资源共享,减少重复开发,比如其他人可以基于你的RAG Server构建Agent,而无需了解RAG的具体实现。
当然,本文应用还只是基本能力的演示,实际还有大量优化空间。比如服务端的并行处理(大规模文档)、索引进度报告、多模态解析等,后续我们将不断完善并分享。ingFang SC';font-weight: bold;color: rgb(255, 255, 255);line-height: 22px;letter-spacing: 1px;">福利时间为了帮助LLM开发人员更系统性与更深 RAG是一种借助外部知识来给LLM提供上下文的AI应用范式。从这个角度来说,RAG与MCP有着相似的意义:给大模型补充上下文,以增强其能力。只是MCP以提供外部工具为主,而RAG则是以注入参考知识为主。这就像一个考试的学生,MCP给你提供计算器,而RAG则是给你一本书。 当然,两者的重点并不一样,MCP强调的是提供工具的方式(集成标准);而RAG则是需要你实现的完整应用。所以两者并不冲突,完全可以用MCP的方法来集成一个RAG应用。 特别是在Agentic RAG系统(如下图)中,由于通常涉及到多个RAG查询管道与Agent的融合,这就与MCP的思想非常契合: ingFang SC"; color: rgb(51, 51, 51); line-height: 20px; letter-spacing: 1px;">一个针对大量不同文档的问答Agent,这些问答有事实性问题也有摘要性问题,更有跨越多个文档的融合问题,甚至需要搜索引擎来补充信息。现在我们来用MCP的标准设计并完整的实现这个场景。ingFang SC"; color: rgb(255, 255, 255); line-height: 42px; letter-spacing: 1px; text-shadow: rgb(255, 216, 56) 2px -2px 0px; -webkit-text-stroke: 2px rgb(0, 0, 0);">02ingFang SC"; font-weight: bold; color: rgb(51, 51, 51); line-height: 22px; letter-spacing: 1px;">MCP标准下的Agentic RAG架构在MCP架构下,无论是SSE还是stdio模式,都是Client/Server模式。你必须在开始之前清晰的设计好MCP Server与Client应用的分工及交互。比如:
【总体思想】 我们基于如下的总体架构来实现:      
- 在MCP Server上提供RAG管道构建与查询的工具;在客户端创建使用这些工具的AI Agent,提供查询任务规划与执行能力
- MCP Server借助LlamaIndex实现RAG管道;在客户端借助LangGraph实现Agent:让每个‘人’干更擅长的事。
ingFang SC"; color: rgb(255, 255, 255); line-height: 42px; letter-spacing: 1px; text-shadow: rgb(255, 216, 56) 2px -2px 0px; -webkit-text-stroke: 2px rgb(0, 0, 0);">03ingFang SC"; font-weight: bold; color: rgb(51, 51, 51); line-height: 22px; letter-spacing: 1px;">MCP Server:RAG管道的核心MCP Server是RAG功能实现的位置。我们对MCP Server拆解设计如下:【工具(Tools)】 create_vector_index:输入文档、索引名与参数,完成解析与索引创建。
query_document:查询事实问题的RAG管道,输入索引名与查询问题。
get_document_summary:查询总结性问题的RAG管道,输入文件和查询问题。 list_indies等:辅助工具,包括一个自己实现的Web搜索工具。
需要说明,在这里的设计中,不同的RAG管道查询的工具是一样的,但参数(索引名,依赖于Agent推理)不同。一个是推理工具,一个推理参数,效果一致。 【缓存机制】 服务端要对文档解析(含分割)与索引创建的信息进行缓存(持久化存储),以防止可能的重复解析与索引创建,提高性能。 文档缓存的唯一名称是文档内容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_recreateor notos.path.exists(storage_path)or notos.path.exists(cache_path) ) ifos.path.exists(storage_path)andnotneed_recreate: returnf"索引{index_name}已存在且参数未变化,无需创建" # 如果需要重新创建,首先尝试删除现有的索引向量库 try: chroma.delete_collection(name=index_name) exceptExceptionase: logger.warning(f"删除集合时出错 (可能是首次创建):{e}") # 创建新的向量库 collection = chroma.get_or_create_collection(name=index_name) vector_store = ChromaVectorStore(chroma_collection=collection) # 加载与拆分文档 nodes =awaitload_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)}个节点" exceptExceptionase: ......
【工具实现:query_document】 这是客户端调用的主要工具。其输入是索引名与查询问题。借助索引缓存,可以快速加载并执行RAG查询。这里不再展示完整处理过程: @app.tool() async defquery_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类型索引),此处不在赘述。 ingFang SC"; color: rgb(255, 255, 255); line-height: 42px; letter-spacing: 1px; text-shadow: rgb(255, 216, 56) 2px -2px 0px; -webkit-text-stroke: 2px rgb(0, 0, 0);">04ingFang SC"; font-weight: bold; color: rgb(51, 51, 51); line-height: 22px; letter-spacing: 1px;">MCP客户端:实现Agent(基于LangGraph)客户端的工作流程如下: 客户端的几个设计重点简单说明如下: 【配置文件】 客户端有两个重要的配置信息,分别用于MCP Server与知识文档的配置。 配置MCP Servers的信息,支持多Server连接、工具加载与过滤(这是一个在langgraph-mcp-adapers基础上扩展的版本)。比如:{ "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: 配置需要索引和查询的全部文档信息。这些信息还会在查询时被注入Agent提示词,用来推理工具的使用参数:{ "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') asyncwithclientasmcp_client: logger.info(f"已连接到MCP服务器:{', '.join(mcp_client.get_connected_servers())}") # 创建智能体 rag = AgenticRAGLangGraph(client=mcp_client, doc_config=doc_config)
# 创建向量索引,自动排重 awaitrag.process_files() # 构建智能体 awaitrag.build_agent() # 交互式对话 awaitrag.chat_repl() 注意到这里的关键步骤是build_agent,会借助LangGraph预置的create_react_agent快速创建Agent。如果你需要精细化的控制,也可以自定义Graph:...... asyncdefbuild_agent(self)->None:
# 获取服务端提供的工具列表 mcp_tools =awaitself.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("===== 智能体构建完成 =====")
篇幅原因,一些细节部分不在这做详细展示。如果有疑问,欢迎后台交流。 ingFang SC"; color: rgb(255, 255, 255); line-height: 42px; letter-spacing: 1px; text-shadow: rgb(255, 216, 56) 2px -2px 0px; -webkit-text-stroke: 2px rgb(0, 0, 0);">05ingFang SC"; font-weight: bold; color: rgb(51, 51, 51); line-height: 22px; letter-spacing: 1px;">端到端效果演示现在让我们来测试下这个的“MCP化”的Agentic RAG应用的运行效果。按照如下步骤来进行: 1. 启动MCP RAG-Server。这里用更复杂的SSE模式(暂时未支持文档上传,所以只能本机启动): 启动时会自动提取并展示服务端的工具清单。 2. 准备客户端知识文档与配置文件。将需要索引和查询的文档放在应用的data/目录,配置好mcp_config与doc_config。不做任何其他处理。直接启动客户端应用: pythonrag_agent_langgraph.py * 观察首次运行的跟踪信息(如下图),这里的过程是: * 现在退出程序,再次启动客户端,观察输出(如下图),可以看到由于索引已经创建,所以会显示“无需创建”。 3. 交互式测试 进入交互式测试环节(图中的服务端信息是通过MCP接口推送到客户端的远程日志,方便观察服务端的工作状态): 1. 关联两个文档信息的查询 由于提供的文档有北京和上海的城市信息介绍,所以看到这个问题调用了北京和上海的RAG管道查询,还自作主张的调用了搜索引擎做补充,然后输出答案: 2. 查询知识库答案,并要求和网络搜索结果核对。 日志显示,Agent先用本地向量索引查询,然后通过搜索引擎对比,非常准确。 3. 总结性问题测试。 日志显示,这里未加载向量索引,而是由工具加载这个文档的节点,并生成文档摘要后返回(SummaryIndex的效率不太高,有待优化) 4. 最后一个很有意思的测试。 由于我们把创建索引的过程“工具”化了,所以甚至可以用自然语言来管理索引。比如这里我要求把csv文档的索引重建,智能体准确的推理出工具及参数,并重建了csv文档索引(实际应用要考虑安全性): 以上展示了一个基于MCP架构的Agentic RAG系统的实现。总结这种架构下的一些明显的变化: - MCP要求对整个系统做模块化与松耦合的重新设计,这会带来一系列工程上的好处。比如分工与效率提升、可维护性、独立扩展、部署更灵活等。
- MCP不依赖于某个技术堆栈。因此技术选择上更灵活,比如服务端用LlamaIndex框架,而客户端则用LangGraph;甚至可以用不同的语言。
- MCP实现了基于标准的模块间互操作。这有助于资源共享,减少重复开发,比如其他人可以基于你的RAG Server构建Agent,而无需了解RAG的具体实现。
当然,本文应用还只是基本能力的演示,实际还有大量优化空间。比如服务端的并行处理(大规模文档)、索引进度报告、多模态解析等,后续我们将不断完善并分享。end![]() |