LangChain + MCP + RAG + Ollama:强大的智能代理人工智能的关键(二)

200 阅读6分钟

Server.py

这里有一个由 FastMCP 驱动的网页搜索工具,它整合了使用 Exa 和 Firecrawl API 的实时网络功能,并通过检索增强生成(RAG)技术优化结果相关性。例如,名为 web_search 的 FastMCP 服务支持高级筛选(按域名、关键词和日期),并提供结构化输出,包括标题、URL 和摘要。同时也设计了一个通过 @mcp.tool() 注册的 search_web_tool 函数,它接受查询、执行搜索、提取并验证 URL,然后使用 create_ragsearch_rag 函数进行 RAG 处理以检索语义相关内容。并且还为无结果、无效 URL 和搜索失败等情况添加了健壮的错误处理。

此外, get_web_content_tool 函数用来获取并返回完整网页内容,设置了 15 秒超时,并能优雅地处理各种错误。最后, get_tools() 函数被用来注册所有工具(支持可选的检索器),并将所有内容封装在 main 块中,使用服务器发送事件(SSE)运行 FastMCP 服务器以实现实时工具交互。

mcp = FastMCP(
    name="web_search", 
    version="1.0.0",
    description="Web search capability using Exa API , Firecrawl API  that provides real-time internet search results and use RAG to search for relevant data. Supports both basic and advanced search with filtering options including domain restrictions, text inclusion requirements, and date filtering. Returns formatted results with titles, URLs, publication dates, and content summaries."
)

@mcp.tool()
async def search_web_tool(query: str) -> str:
    logger.info(f"Searching web for query: {query}")
    formatted_results, raw_results = await search.search_web(query)
    
    if not raw_results:
        return "No search results found."
    
    urls = [result.url for result in raw_results if hasattr(result, 'url')]
    if not urls:
        return "No valid URLs found in search results."
        
    vectorstore = await rag.create_rag(urls)
    rag_results = await rag.search_rag(query, vectorstore)
    
    # You can optionally include the formatted search results in the output
    full_results = f"{formatted_results}\n\n### RAG Results:\n\n"
    full_results += '\n---\n'.join(doc.page_content for doc in rag_results)
    
    return full_results

@mcp.tool()
async def get_web_content_tool(url: str) -> str:
    try:
        documents = await asyncio.wait_for(search.get_web_content(url), timeout=15.0)
        if documents:
            return '\n\n'.join([doc.page_content for doc in documents])
        return "Unable to retrieve web content."
    except asyncio.TimeoutError:
        return "Timeout occurred while fetching web content. Please try again later."
    except Exception as e:
        return f"An error occurred while fetching web content: {str(e)}"

Rag.py

这里设计了一个 RAG 流程,通过 Mistral 嵌入或 Ollama 嵌入以及异步内容检索,从 URL 列表创建 FAISS 向量存储。create_rag 函数首先初始化嵌入模型(默认使用所需嵌入,支持自定义 API 密钥和端点),并以 64 个令牌为批次处理输入。

接下来通过异步方式从所有 URL 抓取网页内容以提升速度,并聚合检索到的 Document 对象。使用 RecursiveCharacterTextSplitter 将这些文档分割为重叠块(10,000 字符,500 字符重叠),以保持上下文并适配 LLM 上下文限制。分割后的块会转换为嵌入向量并存储在 FAISS 索引中,以支持快速相似度搜索。我添加了健壮的错误处理机制,用于捕获嵌入、抓取或存储过程中的异常。

此外,这里还创建了 create_rag_from_documents 作为更灵活的替代方案,它接受已抓取的文档,适用于离线或缓存工作流。最后,search_rag 函数基于余弦相似度从 FAISS 中语义检索前 3 个最相关的块,实现高效且有意义的文档级问答。

async def create_rag(links: list[str]) -> FAISS:
    try:
        model_name = os.getenv("MODEL", "text-embedding-ada-002") 
        # 可替换为任何嵌入模型(Ollama 或 MistralAIEmbeddings)
        # embeddings = MistralAIEmbeddings(
        #     model="mistral-embed",
        #     chunk_size=64
        # )
        embeddings = OpenAIEmbeddings(
            model=model_name,
            openai_api_key=os.getenv("OPENAI_API_KEY"),
            openai_api_base=os.getenv("OPENAI_API_BASE"),
            chunk_size=64
        )
        documents = []
        # 使用 asyncio.gather 并行处理所有 URL 请求
        tasks = [search.get_web_content(url) for url in links]
        results = await asyncio.gather(*tasks)
        for result in results:
            documents.extend(result)
        
        # 文本分块处理
        text_splitter = RecursiveCharacterTextSplitter(
            chunk_size=10000,
            chunk_overlap=500,
            length_function=len,
            is_separator_regex=False,
        )
        split_documents = text_splitter.split_documents(documents)
        # print(documents)
        vectorstore = FAISS.from_documents(documents=split_documents, embedding=embeddings)
        return vectorstore
    except Exception as e:
        print(f"Error in create_rag: {str(e)}")
        raise

async def create_rag_from_documents(documents: list[Document]) -> FAISS:
    """
    直接从文档列表创建 RAG 系统以避免重复网页爬取
    
    Args:
        documents: 已抓取的文档列表
        
    Returns:
        FAISS: 向量存储对象
    """
    try:
        model_name = os.getenv("MODEL")
        embeddings = OpenAIEmbeddings(
            model=model_name,
            openai_api_key=os.getenv("OPENAI_API_KEY"),
            openai_api_base=os.getenv("OPENAI_API_BASE"),
            chunk_size=64
        )
        
        # 文本分块处理
        text_splitter = RecursiveCharacterTextSplitter(
            chunk_size=10000,
            chunk_overlap=500,
            length_function=len,
            is_separator_regex=False,
        )
        split_documents = text_splitter.split_documents(documents)
        
        vectorstore = FAISS.from_documents(documents=split_documents, embedding=embeddings)
        return vectorstore
    except Exception as e:
        print(f"Error in create_rag_from_documents: {str(e)}")
        raise

async def search_rag(query: str, vectorstore: FAISS) -> list[Document]:
    """
    使用查询搜索 RAG 系统
    
    Args:
        query: 搜索查询字符串
        vectorstore: 要搜索的 FAISS 向量存储
        
    Returns:
        list[Document]: 相关文档列表
    """
    return vectorstore.similarity_search(query, k=3)

Search.py

这里开发了一个使用 Exa 和 FireCrawl API 的网页搜索与内容检索系统,用于执行智能网页查询、提取相关信息,并将其格式化以支持检索增强生成(RAG)等下游任务。

首先创建了一个配置字典来管理搜索默认值和常量(如最大重试次数和请求超时时间)。系统核心包含三个主要函数:

  • search_web():向 Exa API 发送查询并返回格式化的 Markdown 摘要和原始结果;

  • format_search_results():将结果按标题、URL、发布日期和摘要整理为清晰的 Markdown 格式;

  • get_web_content():使用 FireCrawl 从 URL 抓取完整页面内容并转换为结构化的 Document 对象。

这里也实现了健壮的错误处理和重试逻辑,以处理不支持的网站和临时故障。整个流程从用户提供的查询开始,从 Exa 检索摘要结果,再通过 FireCrawl 抓取内容,最终返回用户友好的输出和机器可读的文档,适用于聊天机器人和研究助手等应用场景。

# 加载.env变量
load_dotenv(override=True)

# 初始化Exa客户端
exa_api_key = os.getenv("EXA_API_KEY", " ")
exa = Exa(api_key=exa_api_key)
os.environ['FIRECRAWL_API_KEY'] = ''
# 默认搜索配置
websearch_config = {
    "parameters": {
        "default_num_results": 5,
        "include_domains": []
    }
}

# 网页内容抓取常量
MAX_RETRIES = 3
FIRECRAWL_TIMEOUT = 30  # 秒

async def search_web(query: str, num_results: int = None) -> Tuple[str, list]:
    """使用Exa API搜索网页,并返回格式化结果和原始结果。"""
    try:
        search_args = {
            "num_results": num_results or websearch_config["parameters"]["default_num_results"]
        }

        search_results = exa.search_and_contents(
            query,
            summary={"query": "Main points and key takeaways"},
            **search_args
        )

        formatted_results = format_search_results(search_results)
        return formatted_results, search_results.results
    except Exception as e:
        return f"使用Exa搜索时发生错误: {e}", []

def format_search_results(search_results):
    if not search_results.results:
        return "未找到结果。"

    markdown_results = "### 搜索结果:\n\n"
    for idx, result in enumerate(search_results.results, 1):
        title = result.title if hasattr(result, 'title') and result.title else "无标题"
        url = result.url
        published_date = f" (发布时间: {result.published_date})" if hasattr(result, 'published_date') and result.published_date else ""

        markdown_results += f"**{idx}.** [{title}]({url}){published_date}\n"

        if hasattr(result, 'summary') and result.summary:
            markdown_results += f"> **摘要:** {result.summary}\n\n"
        else:
            markdown_results += "\n"

    return markdown_results

async def get_web_content(url: str) -> List[Document]:
    """获取网页内容并转换为文档列表。"""
    for attempt in range(MAX_RETRIES):
        try:
            # 创建FireCrawlLoader实例
            loader = FireCrawlLoader(
                url=url,
                mode="scrape"
            )
            
            # 使用超时保护
            documents = await asyncio.wait_for(loader.aload(), timeout=FIRECRAWL_TIMEOUT)
            
            # 成功检索文档时返回结果
            if documents and len(documents) > 0:
                return documents
            
            # 未检索到文档但无异常时重试
            print(f"未从{url}检索到文档 (尝试 {attempt + 1}/{MAX_RETRIES})")
            if attempt < MAX_RETRIES - 1:
                await asyncio.sleep(1)  # 重试前等待1秒
                continue
                
        except requests.exceptions.HTTPError as e:
            if "Website Not Supported" in str(e):
                # 创建包含错误信息的最小文档
                print(f"FireCrawl不支持的网站: {url}")
                content = f"无法从{url}检索内容: FireCrawl API不支持该网站。"
                return [Document(page_content=content, metadata={"source": url, "error": "Website not supported"})]
            else:
                print(f"从{url}检索内容时发生HTTP错误: {str(e)} (尝试 {attempt + 1}/{MAX_RETRIES})")
                
            if attempt < MAX_RETRIES - 1:
                await asyncio.sleep(1)
                continue
            raise
        except Exception as e:
            print(f"从{url}检索内容时发生错误: {str(e)} (尝试 {attempt + 1}/{MAX_RETRIES})")
            if attempt < MAX_RETRIES - 1:
                await asyncio.sleep(1)
                continue
            raise
    
    # 所有重试失败时返回空列表
    return []

Agent.py

注意:即使不调用 MCP 也能成功运行 Agent.py,但除非服务器正在运行,否则无法获取数据。

此处创建了一个系统,首先从命令行参数或用户输入中检查搜索查询,然后通过搜索模块启动网页搜索。该脚本设计为处理原始搜索结果、提取 URL,并将其传递给 RAG 模块,后者从网页内容创建向量存储。系统随后对该向量存储执行语义搜索,以找到与原始查询最相关的信息。

代码确保包含适当的错误处理和用户反馈,分别显示传统搜索结果和 AI 增强的 RAG 结果。整个过程使用 asyncio 异步运行,以实现高效的 I/O 操作,并且输出结构清晰区分不同类型的结果,以提供更好的用户体验。

import asyncio
import os
import sys

# 直接导入搜索和RAG模块
import search
import rag

# 配置环境


async def main():    
    # 从命令行或输入获取查询
    if len(sys.argv) > 1:
        query = " ".join(sys.argv[1:])
    else:
        query = input("Enter search query: ")
    
    print(f"Searching for: {query}")
    
    try:
        # 直接调用搜索
        formatted_results, raw_results = await search.search_web(query)
        
        if not raw_results:
            print("No search results found.")
            return
        
        print(f"Found {len(raw_results)} search results")
        
        # 提取URL
        urls = [result.url for result in raw_results if hasattr(result, 'url')]
        if not urls:
            print("No valid URLs found in search results.")
            return
            
        print(f"Processing {len(urls)} URLs")
        
        # 创建RAG
        vectorstore = await rag.create_rag(urls)
        rag_results = await rag.search_rag(query, vectorstore)
        
        # 格式化结果
        print("\n=== Search Results ===")
        print(formatted_results)
        
        print("\n=== RAG Results ===")
        for doc in rag_results:
            print(f"\n---\n{doc.page_content}")
    
    except Exception as e:
        print(f"Error: {e}")
        import traceback
        traceback.print_exc()

if __name__ == "__main__":
    asyncio.run(main())

结论

至此可见,将 RAG 与 MCP 服务器结合使用,通过为智能体提供所需知识(通过检索)和情境感知能力(通过记忆与数据集成),显著提升了 AI 的性能。

这种 AI 驱动系统变得更具自主性和实用性。它可充当研究员、助手或分析师,不仅能随时获取信息,还能理解何时以及如何应用这些信息。