使用微软的 GraphRAG + AutoGen + Ollama + Chainlit 来打造一个完全本地且免费的多代理RAG超级机器人

2,252 阅读11分钟

微软整合了 GraphRAG、AutoGen、Ollama 和 Chainlit 技术,打造出强大的多智能体 RAG(检索增强生成)超级机器人,无需依赖在线 LLM,即可进行复杂、基于情境的响应和本地数据处理。

Microsoft GraphRAG + AutoGen + Ollama + Chainlit 组合利用结构化知识图谱来增强对数据的理解和综合,代表了人工智能的重大进步。这款 Superbot 利用 Ollama 和 LM Studio 的本地 LLM 进行经济高效、安全的数据处理,避免了在线模型带来的隐私风险和费用。该系统旨在处理详细查询、生成和执行代码、创建科学报告和执行数据分析,并借助 Chainlit 提供的用户友好界面。该应用程序已通过技术文档和材料数据表进行了测试,展示了其在科学和工程领域的潜力。开发是在 Linux 环境中使用 WSL 进行的,并提供了设置必要软件和硬件配置的指南。

集成和关键组件的图形摘要

与依赖向量相似性搜索的传统 RAG 方法不同,GraphRAG 从原始文本构建结构化知识图谱,捕获实体、关系和关键声明。这可以增强 LLM 理解和合成复杂数据集的能力,从而产生更准确、更符合上下文的响应。将 GraphRAG 的检索增强生成 (RAG) 优势与 AutoGen AI 代理的对话和面向任务的功能相结合,可以打造出强大的 AI 助手,能够高效处理详细查询、生成和执行代码、创建多页科学报告以及进行数据分析。此外,离线本地 LLM(例如来自 Ollama 或 LM Studio 的 LLM)与 GraphRAG 和 AutoGen 结合使用,可确保经济高效且安全的数据处理。本地 LLM 消除了与在线 LLM 相关的高成本和隐私风险,将敏感数据保留在组织内部并降低运营费用。

摘自麻省理工学院 Markus Beuhler 的一篇论文(链接在此处

以下是此应用程序的关键组件:

  1. GraphRAG 的知识搜索方法通过函数调用与 AutoGen 代理集成。
  2. GraphRAG(本地和全局搜索)配置为支持来自 Ollama 的本地模型进行推理和嵌入。
  3. AutoGen 已扩展,以支持通过 Lite-LLM 代理服务器从 Ollama 使用非 OpenAI LLM 进行函数调用。
  4. Chainlit UI 用于处理连续对话、多线程和用户输入设置。

通过询问与 ABAQUS(一款 FEA 工程软件)文档以及供应商 Toray 提供的碳纤维技术数据表相关的问题来测试此应用程序。

带有示例查询的主应用程序 UI。最后两个有相同的查询,但第一个是全局搜索,而第二个是本地搜索。

安装模型依赖项并克隆存储库。

安装来自 Ollama 的语言模型以进行推理和嵌入

# Mistral for GraphRAG Inference
ollama pull mistral

# Nomic-Embed-Text for GraphRAG Embedding
ollama pull nomic-embed-text

# LLama3 for Autogen Inference
ollama pull llama3

# Host Ollama on a local server: http://localhost:11434
ollama serve

创建 conda 环境并安装这些依赖项

# Create and activate a conda environment
conda create -n RAG_agents python=3.12
conda activate RAG_agents

# Lite-LLM proxy server for Ollama
pip install 'litellm[proxy]'

# Install Ollama
pip install ollama

# Microsoft AutoGen
pip install pyautogen "pyautogen[retrievechat]" 

# Microsoft GraphRAG
pip install graphrag

# Text-Token Encoder-Decoder
pip install tiktoken

# Chainlit Python application
pip install chainlit

# Clone my Git-hub repository
git clone https://github.com/karthik-codex/autogen_graphRAG.git

# (BONUS) To Convert PDF files to Markdown for GraphRAG 
pip install marker-pdf

# (BONUS) Only if you installed Marker-pdf since it removes GPU CUDA support by default
conda install pytorch torchvision torchaudio pytorch-cuda=12.1 -c pytorch -c nvidia

您将在我的 GitHub 存储库中找到以下文件。

  1. /requirements.txt包含上述所有软件包的列表
  2. /utils/settings.yaml包含使用 Mistral 7B 和 Ollama 的 Nomic-Text-Embedding 进行 GraphRAG 离线嵌入和索引的 LLM 配置。 您将使用此文件替换首次在工作目录中初始化 GraphRAG 时创建的文件。
  3. /utils/chainlit_agents.py包含包括 AutoGen 助手和用户代理的类定义。这允许跟踪多个代理并将其消息显示在 UI 中。(感谢 Chainlit 团队构建模板
  4. /utils/embedding.py包含使用 Ollama 进行本地搜索查询的 GraphRAG 嵌入修改后的嵌入函数。您将使用此文件替换 GraphRAG 包中的文件(更多信息见下文)
  5. utils/openai_embeddings_llm.py包含使用 Ollama 进行 GraphRAG 索引和嵌入的修改后的嵌入函数。您将使用此文件替换 GraphRAG 包中的文件(更多信息见下文)。
  6. /appUI.py包含设置代理、定义 GraphRAG 搜索功能、跟踪和处理消息以及在 Chainlit UI 中显示它们的主要异步函数。
  7. /utils/pdf_to_markdown.py奖励文件包含将 PDF 文件转换为 markdown 文件以供 GraphRAG 提取的功能。

创建 GraphRAG 知识库。

在存储库的根文件夹中初始化 GraphRAG

#make a new folder "input" to place your input files for GraphRAG (.txt or .md)
mkdir -p ./input

# Initialize GraphRAG to create the required files and folders in the root dir
python -m graphrag.index --init  --root .

# Move the settings.yaml file to replace the one created by GraphRAG --init
mv ./utils/settings.yaml ./

配置 GraphRAG 设置以支持来自 Ollama 的本地模型

下面是一段settings.yaml说明 LLM 创建索引和嵌入的配置的代码片段。GraphRAG 需要 32k 上下文长度才能进行索引,因此 Mistral 是所选模型。对于嵌入,选择了 Nomic-embed-text,但您可以尝试使用 Ollama 的其他嵌入。无需设置${GRAPHRAG_API_KEY},因为不需要访问这些本地模型的端点。

encoding_model: cl100k_base
skip_workflows: []
llm:
  api_key: ${GRAPHRAG_API_KEY}
  type: openai_chat # or azure_openai_chat
  model: mistral
  model_supports_json: true
  api_base: http://localhost:11434/v1 
.
.
.
embeddings:
  async_mode: threaded # or asyncio
  llm:
    api_key: ${GRAPHRAG_API_KEY}
    type: openai_embedding # or azure_openai_embedding
    model: nomic_embed_text
    api_base: http://localhost:11434/api 
.
.
.
input:  #Change input file pattern to.md, or .txt
  type: file # or blob
  file_type: text # or csv
  base_dir: "input"
  file_encoding: utf-8
  file_pattern: ".*\.md$"

您可以在根目录中的“input”文件夹中指定包含输入文件的文件夹。文本和 markdown 文件都可以使用。您可以使用将/utils/pdf_to_markdown.pyPDF 转换为 markdown 文件,然后将其放在“input”文件夹中。处理多种文件格式的问题尚未解决,但这是一个可以解决的问题。

在运行 GraphRAG 进行索引、创建嵌入和执行本地查询之前,您必须修改 GraphRAG 包中的 Python 文件openai_embeddings_llm.pyembedding.py。如果没有进行此修改,GraphRAG 在创建嵌入时会抛出错误,因为它不会将“nomic-embed-text”识别为来自 Ollama 的有效嵌入模型。在我的设置中,这些文件位于/home/karthik/miniconda3/envs/RAG_agents/lib/python3.12/site-packages/graphrag/llm/openai/openai_embeddings_llm.py/home/karthik/miniconda3/envs/RAG_agents/lib/python3.12/site-packages/graphrag/query/llm/oai/embedding.py

您可以使用命令来找到这些文件sudo find / -name openai_embeddings_llm.py

创建嵌入和知识图。

最后,我们创建嵌入并使用全局或本地搜索方法测试知识图谱。完成嵌入过程后,您可以在 GraphRAG 工作目录的“output”文件夹中找到输出工件(.parquet 文件)和报告(.json 和 .logs),在本例中,该文件夹是根文件夹。

# Create knowledge graph - this takes some time
python -m graphrag.index --root .

# Test GraphRAG
python -m graphrag.query --root . --method global "<insert your query>"

启动 Lite-LLM 服务器并从终端运行应用程序

下面是在运行应用程序之前初始化服务器的命令。我选择了 Llama3:8b 来测试这个应用程序。如果硬件允许,您可以使用更大的模型。有关 Lite-LLM 的更多信息,请访问此链接。现在您可以从另一个终端运行该应用程序。确保您处于正确的 conda 环境中。

# start server from terminal
litellm --model ollama_chat/llama3

# run app from another terminal
chainlit run appUI.py

appUI.py 的核心组件

导入 Python 库

import autogen
from rich import print
import chainlit as cl
from typing_extensions import Annotated
from chainlit.input_widget import (
   Select, Slider, Switch)
from autogen import AssistantAgent, UserProxyAgent
from utils.chainlit_agents import ChainlitUserProxyAgent, ChainlitAssistantAgent
from graphrag.query.cli import run_global_search, run_local_search

您会注意到从chainlit_agents导入了两个类。这些 AutoGen 代理的包装器类使 Chainlit 能够跟踪其对话并处理终止或其他用户输入。您可以在此处阅读更多相关信息。

配置 AutoGen 代理

AutoGen 代理通过 Lite-LLM 代理服务器利用来自 Ollama 的模型。这是必要的,因为 AutoGen 不支持通过非 OpenAI 推理模型进行函数调用。代理服务器允许使用 Ollama 模型进行函数调用和代码执行。

# LLama3 LLM from Lite-LLM Server for Agents #
llm_config_autogen = {
    "seed": 40,  # change the seed for different trials
    "temperature": 0,
    "config_list": [{"model": "litellm", 
                     "base_url": "http://0.0.0.0:4000/", 
                     'api_key': 'ollama'},
    ],
    "timeout": 60000,
}

在聊天开始时实例化代理并输入用户设置

我创建了三个 Chainlit 小部件(开关、选择和滑块)作为用户设置,以选择 GraphRAG 搜索类型、社区级别和内容生成类型。打开时,开关小部件使用 GraphRAG 本地搜索方法进行查询。内容生成的选择选项包括“优先列表”、“单个段落”、“多个段落”和“多页报告”。滑块小部件使用选项 0、1 和 2 选择社区生成级别。您可以在此处阅读有关 GraphRAG 社区的更多信息。

@cl.on_chat_start
async def on_chat_start():
  try:
    settings = await cl.ChatSettings(
            [      
                Switch(id="Search_type", label="(GraphRAG) Local Search", initial=True),       
                Select(
                    id="Gen_type",
                    label="(GraphRAG) Content Type",
                    values=["prioritized list", "single paragraph", "multiple paragraphs", "multiple-page report"],
                    initial_index=1,
                ),          
                Slider(
                    id="Community",
                    label="(GraphRAG) Community Level",
                    initial=0,
                    min=0,
                    max=2,
                    step=1,
                ),

            ]
        ).send()

    response_type = settings["Gen_type"]
    community = settings["Community"]
    local_search = settings["Search_type"]
    
    cl.user_session.set("Gen_type", response_type)
    cl.user_session.set("Community", community)
    cl.user_session.set("Search_type", local_search)

    retriever   = AssistantAgent(
       name="Retriever", 
       llm_config=llm_config_autogen, 
       system_message="""Only execute the function query_graphRAG to look for context. 
                    Output 'TERMINATE' when an answer has been provided.""",
       max_consecutive_auto_reply=1,
       human_input_mode="NEVER", 
       description="Retriever Agent"
    )

    user_proxy = ChainlitUserProxyAgent(
        name="User_Proxy",
        human_input_mode="ALWAYS",
        llm_config=llm_config_autogen,
        is_termination_msg=lambda x: x.get("content", "").rstrip().endswith("TERMINATE"),
        code_execution_config=False,
        system_message='''A human admin. Interact with the retriever to provide any context''',
        description="User Proxy Agent"
    )
    
    print("Set agents.")

    cl.user_session.set("Query Agent", user_proxy)
    cl.user_session.set("Retriever", retriever)

    msg = cl.Message(content=f"""Hello! What task would you like to get done today?      
                     """, 
                     author="User_Proxy")
    await msg.send()

    print("Message sent.")
    
  except Exception as e:
    print("Error: ", e)
    pass

我选择不使用 Chainlit 包装器类作为检索器助手代理。这样我就可以禁用对检索器输出的跟踪,并直接捕获来自 GraphRAG 函数的响应。原因是当响应通过检索器时,文本会丢失其格式,包括空格和段落缩进。在生成带有主标题和副标题的多页报告时,此问题尤其明显。我可以通过绕过 Chainlit 包装器并直接从 GraphRAG 函数检索输出来保留原始格式。您将在下面看到我是如何实现这一点的。

更新输入设置中的更改

此功能可检测对设置中的选择、开关和滑块小部件所做的任何更改,以便在后续查询中反映这些更改。

@cl.on_settings_update
async def setup_agent(settings):
    response_type = settings["Gen_type"]
    community = settings["Community"]
    local_search = settings["Search_type"]
    cl.user_session.set("Gen_type", response_type)
    cl.user_session.set("Community", community)
    cl.user_session.set("Search_type", local_search)
    print("on_settings_update", settings)

使用来自代理和用户的传入消息更新 UI。

这是应用程序的核心部分,它创建一个与两个代理的群聊,定义一个函数“state_transition”来管理对话序列,以及异步 RAG 查询函数。

您会注意到,INPUT_DIR ,ROOT_DIR, RESPONSE_TYPE, COMMUNTIY基于 bool 参数传递到本地和全局搜索 GraphRAG 查询函数中的参数LOCAL_SEARCHROOT_DIR,设置为’.’— 如果您在不同的目录中初始化了 GraphRAG,请注意这一点。

异步函数“query_graphRAG”调用 GraphRAG 全局或本地搜索方法。您会注意到函数await cl.Message(content=result.response).send()内部的一行async def query_graphRAG直接检索 RAG 查询的输出并保留检索到的内容的文本格式。

@cl.on_message
async def run_conversation(message: cl.Message):
    print("Running conversation")
    CONTEXT = message.content

    MAX_ITER = 10
    INPUT_DIR = None
    ROOT_DIR = '.'
    RESPONSE_TYPE = cl.user_session.get("Gen_type")
    COMMUNITY = cl.user_session.get("Community")
    LOCAL_SEARCH = cl.user_session.get("Search_type")

    print("Setting groupchat")

    retriever   = cl.user_session.get("Retriever")
    user_proxy  = cl.user_session.get("Query Agent")

    def state_transition(last_speaker, groupchat):
        messages = groupchat.messages
        if last_speaker is user_proxy:
            return retriever
        if last_speaker is retriever:
            if messages[-1]["content"].lower() not in ['math_expert','physics_expert']:
                return user_proxy
            else:
                if messages[-1]["content"].lower() == 'math_expert':
                    return user_proxy
                else:
                    return user_proxy
        else:
            pass
            return None

    async def query_graphRAG(
          question: Annotated[str, 'Query string containing information that you want from RAG search']
                          ) -> str:
        if LOCAL_SEARCH:
            result = run_local_search(INPUT_DIR, ROOT_DIR, COMMUNITY ,RESPONSE_TYPE, question)
        else:
            result = run_global_search(INPUT_DIR, ROOT_DIR, COMMUNITY ,RESPONSE_TYPE, question)
        await cl.Message(content=result).send()
        return result

    for caller in [retriever]:
        d_retrieve_content = caller.register_for_llm(
            description="retrieve content for code generation and question answering.", api_style="function"
        )(query_graphRAG)

    for agents in [user_proxy, retriever]:
        agents.register_for_execution()(d_retrieve_content)

    groupchat = autogen.GroupChat(
        agents=[user_proxy, retriever],
        messages=[],
        max_round=MAX_ITER,
        speaker_selection_method=state_transition,
        allow_repeat_speaker=True,
    )
    manager = autogen.GroupChatManager(groupchat=groupchat,
                                       llm_config=llm_config_autogen, 
                                       is_termination_msg=lambda x: x.get("content", "") and x.get("content", "").rstrip().endswith("TERMINATE"),
                                       code_execution_config=False,
                                       )    

# -------------------- Conversation Logic. Edit to change your first message based on the Task you want to get done. ----------------------------- # 
    if len(groupchat.messages) == 0: 
      await cl.make_async(user_proxy.initiate_chat)( manager, message=CONTEXT, )
    elif len(groupchat.messages) < MAX_ITER:
      await cl.make_async(user_proxy.send)( manager, message=CONTEXT, )
    elif len(groupchat.messages) == MAX_ITER:  
      await cl.make_async(user_proxy.send)( manager, message="exit", )

对于此应用程序,我们只需要两个代理。您可以添加/修改代理并配置“state_transition”函数来协调对话中的说话者选择,以实现更复杂的工作流程。

最后

这是我第一次涉足 AI 代理、LLM 和 RAG,过去几周我直接开始创建此实现,绕过了很多基础知识。虽然此实现并不完美,但它是开发更复杂应用程序的绝佳模板。它为集成多个功能和编码代理奠定了坚实的基础,并使您能够构建复杂的工作流程、自定义代理交互并根据需要增强功能。