掘金 人工智能 前天 17:18
实战指南:从零构建 MCP 架构下的 Agentic RAG 系统,无第三方MCP Server
index_new5.html
../../../zaker_core/zaker_tpl_static/wap/tpl_guoji1.html

 

本文分享了如何利用MCP架构从零构建一个Agentic RAG系统,重点在于MCP与RAG、Agent的融合。该系统包括MCP服务端(基于LlamaIndex实现RAG管道)和MCP客户端(基于LangGraph实现Agent),通过模块化设计和工具化RAG功能,实现了灵活、可扩展的问答系统。文章详细介绍了系统架构、工具设计、缓存机制、Agent构建以及端到端的效果演示,展示了MCP架构在构建Agentic RAG系统中的优势。

💡 **MCP与RAG的融合**: RAG系统旨在为大模型提供外部知识,类似于MCP提供外部工具。文章提出,可以用MCP的方法来集成RAG应用,特别是在Agentic RAG系统中,MCP的思想与多个RAG查询管道和Agent的融合非常契合。

🛠️ **MCP Server的设计**: MCP Server提供RAG管道构建与查询的工具,包括`create_vector_index`、`query_document`等。为了提高性能,服务端设计了文档解析和索引信息的缓存机制,并详细介绍了`create_vector_index`工具的实现细节。

🤖 **MCP客户端Agent的实现**: 客户端基于LangGraph实现Agent,通过配置文件配置MCP Server和知识文档。文章介绍了客户端的主程序流程、Agent的构建过程,以及如何利用LangGraph的`create_react_agent`快速创建Agent。

⚙️ **端到端效果演示**: 文章通过端到端的效果演示,展示了基于MCP架构的Agentic RAG系统的运行效果。演示了连接RAG-Server、创建向量索引、交互式测试等步骤,验证了系统在处理不同类型查询(事实性问题、总结性问题)时的有效性,并展示了Agent利用自然语言管理索引的能力。

作者:AI大模型应用实践

五一期间,小编尝试用MCP架构从零实现一个完整的Agentic RAG系统,以演示MCP与RAG、Agent的一些有趣融合,在此与大家一起分享。内容涵盖:

01

 思考:MCP与Agentic RAG的融合

RAG是一种借助外部知识来给LLM提供上下文的AI应用范式。从这个角度来说,RAG与MCP有着相似的意义:给大模型补充上下文,以增强其能力。只是MCP以提供外部工具为主,而RAG则是以注入参考知识为主。这就像一个考试的学生,MCP给你提供计算器,而RAG则是给你一本书。

当然,两者的重点并不一样,MCP强调的是提供工具的方式(集成标准);而RAG则是需要你实现的完整应用。所以两者并不冲突,完全可以用MCP的方法来集成一个RAG应用。

特别是在Agentic RAG系统(如下图)中,由于通常涉及到多个RAG查询管道与Agent的融合,这就与MCP的思想非常契合:

假设一个典型的Agentic RAG应用:

一个针对大量不同文档的问答Agent,这些问答有事实性问题也有摘要性问题,更有跨越多个文档的融合问题,甚至需要搜索引擎来补充信息。

现在我们来用MCP的标准设计并完整的实现这个场景。

02

 MCP标准下的Agentic RAG架构

在MCP架构下,无论是SSE还是stdio模式,都是Client/Server模式。你必须在开始之前清晰的设计好MCP Server与Client应用的分工及交互。比如:

【总体思想】

我们基于如下的总体架构来实现:

03

MCP Server:RAG管道的核心

MCP Server是RAG功能实现的位置。我们对MCP Server拆解设计如下:

【工具(Tools)】

需要说明,在这里的设计中,不同的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: int500,     chunk_overlap: int50,     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: int5) -> str:    """从文档中查询事实性信息,用于回答具体的细节问题        Args:        ctx: 上下文对象        index_name: 索引名称        query: 查询文本        similarity_top_k: 返回的相似节点数量        Returns:        查询结果    """......

按类似方法,再创建一个用于回答总结性问题的工具(利用LlamaIndex的SummaryIndex类型索引),此处不在赘述。

04

MCP客户端:实现Agent(基于LangGraph)

客户端的工作流程如下:

客户端的几个设计重点简单说明如下:

【配置文件】

客户端有两个重要的配置信息,分别用于MCP Server与知识文档的配置。

mcp_config.json:配置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')        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()

【创建智能体(build_agent)】注意到这里的关键步骤是build_agent,会借助LangGraph预置的create_react_agent快速创建Agent。如果你需要精细化的控制,也可以自定义Graph:

......    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("===== 智能体构建完成 =====")

篇幅原因,一些细节部分不在这做详细展示。如果有疑问,欢迎后台交流。

05

端到端效果演示

现在让我们来测试下这个的“MCP化”的Agentic RAG应用的运行效果。按照如下步骤来进行:

    启动MCP RAG-Server。这里用更复杂的SSE模式(暂时未支持文档上传,所以只能本机启动):

启动时会自动提取并展示服务端的工具清单。

    准备客户端知识文档与配置文件。将需要索引和查询的文档放在应用的data/目录,配置好mcp_config与doc_config。不做任何其他处理。直接启动客户端应用:
python rag_agent_langgraph.py

    交互式测试

进入交互式测试环节(图中的服务端信息是通过MCP接口推送到客户端的远程日志,方便观察服务端的工作状态):

    关联两个文档信息的查询

由于提供的文档有北京和上海的城市信息介绍,所以看到这个问题调用了北京和上海的RAG管道查询,还自作主张的调用了搜索引擎做补充,然后输出答案:

    查询知识库答案,并要求和网络搜索结果核对。

日志显示,Agent先用本地向量索引查询,然后通过搜索引擎对比,非常准确。

    总结性问题测试。

日志显示,这里未加载向量索引,而是由工具加载这个文档的节点,并生成文档摘要后返回(SummaryIndex的效率不太高,有待优化)

    最后一个很有意思的测试。

由于我们把创建索引的过程“工具”化了,所以甚至可以用自然语言来管理索引。比如这里我要求把csv文档的索引重建,智能体准确的推理出工具及参数,并重建了csv文档索引(实际应用要考虑安全性):


以上展示了一个基于MCP架构的Agentic RAG系统的实现。总结这种架构下的一些明显的变化:

当然,本文应用还只是基本能力的演示,实际还有大量优化空间。比如服务端的并行处理(大规模文档)、索引进度报告、多模态解析等,后续我们将不断完善并分享。

Fish AI Reader

Fish AI Reader

AI辅助创作,多种专业模板,深度分析,高质量内容生成。从观点提取到深度思考,FishAI为您提供全方位的创作支持。新版本引入自定义参数,让您的创作更加个性化和精准。

FishAI

FishAI

鱼阅,AI 时代的下一个智能信息助手,助你摆脱信息焦虑

联系邮箱 441953276@qq.com

相关标签

MCP Agentic RAG LlamaIndex LangGraph
相关文章