继续浏览精彩内容
慕课网APP
程序员的梦工厂
打开
继续
感谢您的支持,我会继续努力的
赞赏金额会直接到老师账户
将二维码发送给自己后长按识别
微信支付
支付宝支付

构建用于复杂问题研究的多代理RAG工具 — 基于LangGraph的RAG研究多代理系统

ibeautiful
关注TA
已关注
手记 528
粉丝 108
获赞 529

图片来自:https://infohub.delltechnologies.com/p/the-rise-of-agentic-rag-systems/(该术语如有特定含义,请参考括号内解释)

  1. ❓简介 — Naive vs Agentic RAG[^1]
  2. 🧠 项目概览
  3. 📊 结果
  4. 🔚 尾声

[^1]: Naive vs Agentic RAG 是指两种不同的检索和生成方法。

在这篇文章中,我们介绍了一个实用项目,该项目使用LangGraph开发了一个RAG研究多代理工具。该工具旨在解决需要复杂的问题,并需要多个来源迭代步骤才能最终得出的答案复杂问题。它使用一种混合搜索技术Cohere重排序步骤来检索文档的过程,并且还包含一种自我纠正机制,包括幻觉检查过程,以提高响应的可靠性,非常适合用于企业应用。

Github仓库在这里:here

1. 引言 — 天真 vs 代理式 RAG(RAG)

对于这个项目来说,简单的RAG方法以下几点不足以满足需求:具体来说,

  • 无法理解复杂查询:无法将复杂查询分解为多个可管理的子步骤,而是仅在一个层次上处理查询,没有分析每个步骤并得出统一结论。
  • 缺乏幻觉处理或错误处理:简单的RAG管道没有响应验证步骤和处理幻觉的机制,因此无法通过生成新响应来纠正错误。
  • 缺乏动态工具使用:简单的RAG系统不支持根据工作流程条件使用工具、调用外部API或与数据库交互。

因此开发了一个多代理RAG研究系统来解决所有这些问题。事实上,这种基于代理的框架能够实现:

-

  • 路由和使用工具路由代理可以对用户的查询进行分类,并将其定向到相应的节点或工具。这使得可以基于上下文做出决策,例如确定文档是否需要进行全文总结,是否需要更详细的信息,或者问题是否超出了范围。
  • 规划子步骤:复杂的查询通常需要分解为更小、更可管理的步骤。从查询出发,可以生成一系列步骤以执行,以便在探索查询的不同方面的同时得出结论。例如,如果查询需要在文档的两个不同部分之间进行比较,基于代理的方法能够识别这种比较的需求,分别检索这两个来源,并在最终响应中将它们合并成一个比较分析。
  • 反思和错误校正:除了生成简单的响应,基于代理的方法还允许添加一个验证步骤,以解决潜在的幻觉、错误或无法准确回答用户查询的问题。这也使得与人类在环自我纠正机制集成成为可能,将人工输入纳入自动化流程。这种功能使得基于代理的RAG系统成为企业应用中的强大且可靠的解决方案。
  • 共享全局状态:代理工作流程共享全局状态,简化了跨多个步骤的状态管理。这种共享状态对于多代理过程各阶段的一致性维护至关重要。
2 项目简介

代理型RAG图

步骤如下:

  1. 分析并路由查询(自适应RAG): 用户的查询被分类并路由到相应的节点。从那里,系统可以继续到下一步(生成“研究计划”),请求更多信息来自用户,或者如果查询超出范围,则立即回复
  2. 研究计划生成: 系统生成一个逐步的研究计划,具体步骤数量取决于请求的复杂性。然后返回一份所需的具体步骤列表,以解决用户的问题。
  3. 研究子图: 对于研究计划生成中每个步骤,都会调用一个子图。具体来说,子图开始通过LLM生成两个查询。接下来,系统通过使用集成检索器(使用相似度搜索、BM25和MMR)检索与这些生成查询相关的文档。随后进行重新排序步骤,使用基于Cohere的上下文压缩技术,最终产生所有步骤中最重要的_k_个相关文档及其相关评分。
  4. 生成步骤: 根据获取的相关文档,工具通过LLM生成答案。
  5. 幻觉检查(人机协同的自修正RAG): 在此反馈步骤中,系统分析生成的答案,以确定其是否得到提供的上下文的支持,并涵盖了所有方面。如果检查失败,图形工作流将会中断,并提示用户生成修订答案或结束流程。

为了创建向量存储,我们使用了DoclingLangChain进行基于段落的分块,并使用ChromaDB构建了向量数据库。

建立向量数据库
文档解析

对于具有复杂结构的PDF文件,包括具有复杂页面布局的表格,选择用于解析的工具非常重要。许多库在处理具有复杂页面布局或复杂表格结构的PDF文件时不够精确,尤其是处理具有复杂表格结构的PDF文件时。

为了解决这个问题,使用了开源库Docling。它实现了简单高效的文档解析,并支持将文档导出为所需的格式,包括多种常用的文档格式,如PDF、DOCX、PPTX、XLSX、图片、HTML、AsciiDoc或Markdown。Docling能全面理解PDF文档的内容,包括表格结构、阅读顺序和页面布局。此外,它还支持对扫描PDF的OCR识别。

PDF 查看

PDF中的文本随后被转换成Markdown格式,这样做是必要的,因为接下来的分段处理是基于段落的结构。

    从docling.document_converter模块导入DocumentConverter  

    logger.info('开始文档处理')  
    converter = DocumentConverter()  
    markdown_document = converter.convert(source).document.export_to_markdown()

提取的文本结构类似于下面的图片。PDF 和表格解析工具从PDF和表格中提取了保留了原始格式的文本,如下面的图片所示。

根据这些标题,并使用MarkdownHeaderTextSplitter,输出文本随后被分割成多个部分,最终生成了一个包含332个LangChain 文档的列表。

从langchain_text_splitters导入MarkdownHeaderTextSplitter模块

headers_to_split_on = [  
    ("#", "Header 1"),  
    ("##", "Header 2")  
]  

markdown_splitter = MarkdownHeaderTextSplitter(headers_to_split_on)  
文档列表 = markdown_splitter.split_text(这个Markdown文档)  
文档列表。
    # 输出示例  
    [Document(metadata={'Header 2': '我们首席可持续发展官及学习与可持续发展资深副总裁的一封信'}, page_content="……"),  
    ...]  

    # docs_list 的长度:  
    332
构建向量数据库

我们构建了一个向量数据库来存储句子的向量嵌入,并通过该数据库进行搜索查询。在这种情况下,我们使用Chroma,并在本地的‘db_vector’目录中持久化存储数据库。

    from langchain_community.vectorstores import Chroma  
    from langchain_openai import OpenAIEmbeddings  

    embd = OpenAIEmbeddings()  

    vectorstore_from_documents = Chroma.from_documents(  
        documents=docs_list,  
        collection_name="rag-chroma-google-v1",  
        embedding=embd,  
        persist_directory='db_vector'  
    )
主图搭建

所实现的系统包含两个流程图。

  • 研究员图 作为 子图,负责生成不同查询 ,这些查询将用于从向量数据库中检索并重新排序前k个文档。
  • 主图 ,包含主要的工作流程,如分析用户查询、生成完成任务所需步骤、生成响应,以及通过人机交互机制检查错误。
主要图表结构

LangGraph 预览功能

LangGraph 的一个核心概念是 状态。每个图执行时都会创建一个状态,状态在节点执行时在它们之间传递,每个节点在执行后会用返回值更新状态。

让我们从构建图状态开始这个项目吧。为了做到这一点,我们定义了两个类:

  • Router: 将用户的查询分类为“more-info”(更多信息)、“environmental”(环境相关)或 “general”(通用)三类之一的结果。
  • GradeHallucination: 包含一个二进制评分,用于判断响应中是否存在幻觉。
from pydantic import BaseModel, Field

class Router(TypedDict):
    """对用户查询进行分类。"""

    logic: str
    type: Literal["more-info", "environmental", "general"]

class GradeHallucinations(BaseModel):
    """生成答案中幻觉存在的二元评分。"""

    binary_score: str = Field(
        description="答案基于事实,'1'代表基于事实,'0'代表不基于事实"
    )

这些定义的状态是:

  • 输入状态: 包含用户与代理之间交换的所有消息列表。
  • 代理状态: 包含Router对用户查询的分类,研究计划中需要执行的步骤列表,代理可以参考的检索文档列表,以及二进制评分Gradehallucination(幻觉评分)。
    from dataclasses import dataclass, field  
    from typing import Annotated, Literal, TypedDict  
    from langchain_core.documents import Document  
    from langchain_core.messages import AnyMessage  
    from langgraph.graph import add_messages  
    from utils.utils import reduce_docs  

    @dataclass(kw_only=True)  
    class InputState:  
        """表示代理的输入状态。  

        该类定义了输入状态的结构,包括用户和代理之间交换的消息。它提供了一个对完整状态的受限版本,对外界而言,这比内部维护的状态提供了更狭窄的接口。  
        """  

        messages: Annotated[list[AnyMessage], add_messages]  

        """消息跟踪代理的主要执行状态。  

        通常会积累一个 Human/AI/Human/AI 消息模式。  

        返回值:  
            包含来自 `right` 的消息合并到 `left` 的新消息列表。  
            如果 `right` 中的消息 ID 与 `left` 中的某消息相同,则将用 `right` 中的消息替换 `left` 中的相应消息。"""  

    # 主代理状态  
    @dataclass(kw_only=True)  
    class AgentState(InputState):  
        """表示检索图和代理的状态。"""  

        router: Router = field(default_factory=lambda: Router(type="general", logic=""))  
        """用于路由器对用户的查询进行分类。"""  
        steps: list[str] = field(default_factory=lambda: ["研究计划中各步骤的列表。"])  
        """研究计划中各步骤的列表。"""  
        documents: Annotated[list[Document], reduce_docs] = field(default_factory=list)  
        """由检索器填充,这是一个代理可以引用的文档列表。"""  
        hallucination: GradeHallucinations = field(default_factory=lambda: GradeHallucinations(binary_score="0"))
第一步:分析并处理请求

analyze_and_route_query 函数返回并更新状态 AgentState 中的路由变量 routerroute_query 函数根据之前的查询分类来决定后续步骤。

具体来说,这一步使用一个Router对象更新状态,该对象的type变量包含以下值之一:"more-info""environmental",或"general"。根据这一信息,工作流将被导向到适当的节点(其中一个节点为create_research_planask_for_more_inforespond_to_general_query)。

    async def analyze_and_route_query(  
        state: AgentState, *, config: RunnableConfig  
    ) -> dict[str, Router]:  
        """分析用户的查询并决定如何在对话流程中对查询进行路由。  

        该函数使用语言模型来分类用户的查询,并决定如何在对话流程中对其进行路由。  

        参数:  
            state (AgentState): 代理的当前状态,包括对话历史。  
            config (RunnableConfig): 用于查询分析的模型配置。  

        返回:  
            dict[str, Router]: 包含 'router' 键及其分类结果的字典。  
        """  
        model = ChatOpenAI(model=GPT_4o, temperature=TEMPERATURE, streaming=True)  
        messages = [  
            {"role": "system", "content": ROUTER_SYSTEM_PROMPT}  
        ] + state.messages  
        logging.info('---分析和路由查询---')  
        response = cast(  
            Router, await model.with_structured_output(Router).ainvoke(messages)  
        )  
        return {"router": response}  

    def route_query(  
        state: AgentState,  
    ) -> Literal["create_research_plan", "ask_for_more_info", "respond_to_general_query"]:  
        """根据查询分类确定下一步。  

        参数:  
            state (AgentState): 包含路由器分类的代理当前状态。  

        返回:  
            Literal["create_research_plan", "ask_for_more_info", "respond_to_general_query"]: 下一步。  

        抛出:  
            ValueError: 如果遇到未知的路由器类型,将抛出异常。
        """  
        _type = state.router["type"]  
        if _type == "environmental":  
            return "create_research_plan"  
        elif _type == "more-info":  
            return "ask_for_more_info"  
        elif _type == "general":  
            return "respond_to_general_query"  
        else:  
            raise ValueError(f"未知的路由器类型:{_type}")

回答这个问题的示例输出:“获取2019年都柏林的PUE值”:

{
  "logic": "这是一个关于2019年都柏林数据中心环境效率的具体问题,与环境报告有关。",
  "type": "环保"
}
步骤 1.1 超出范围 / 需要更多信息说明

我们接着定义了函数 ask_for_more_inforespond_to_general_query,这两个函数会直接通过调用大语言模型(LLM)来生成回复:前者会在当路由器判断用户需要提供更多信息时被执行,而后者则用于生成对与我们主题无关的一般查询的回复。在这种情况下,需要将生成的回复添加到消息列表中,并更新状态中的 messages 变量。

    async def ask_for_more_info(  
        state: AgentState, *, config: RunnableConfig  
    ) -> dict[str, list[BaseMessage]]:  
        """生成一个请求用户提供更多信息的回复。

        当路由认为需要更多用户信息时,会调用此节点。

        参数:
            state (AgentState): 代理的当前状态,包括对话历史和路由逻辑。
            config (RunnableConfig): 用于生成回复的模型配置。

        返回:
            dict[str, list[BaseMessage]]: 包含回复的字典,键为 'messages'。
        """  
        model = ChatOpenAI(model=GPT_4o_MINI, temperature=TEMPERATURE, streaming=True)  
        system_prompt = MORE_INFO_SYSTEM_PROMPT.format(  
            logic=state.router["logic"]  
        )  
        messages = [{"role": "system", "content": system_prompt}] + state.messages  
        response = await model.ainvoke(messages)  
        return {"messages": [response]}  

    async def respond_to_general_query(  
        state: AgentState, *, config: RunnableConfig  
    ) -> dict[str, list[BaseMessage]]:  
        """生成一个关于非环境相关的一般性查询的回复。

        当路由将查询归类为一般问题时,会调用此节点。

        参数:
            state (AgentState): 代理的当前状态,包括对话历史和路由逻辑。
            config (RunnableConfig): 用于生成回复的模型配置。

        返回:
            dict[str, list[BaseMessage]]: 包含回复的字典,键为 'messages'。
        """  
        model = ChatOpenAI(model=GPT_4o_MINI, temperature=TEMPERATURE, streaming=True)  
        system_prompt = GENERAL_SYSTEM_PROMPT.format(  
            logic=state.router["logic"]  
        )  
        logging.info("---生成回复---")  
        messages = [{"role": "system", "content": system_prompt}] + state.messages  
        response = await model.ainvoke(messages)  
        return {"messages": [response]}

对于问题“Altamura的天气怎么样?”的示例回答:

    {  
      "logic":"阿塔玛拉那边天气怎么样?",  
      "type":"general"  
    }
    # ---响应生成---
    "感谢你的提问,不过关于天气的信息我帮不上忙。我的专长是环境方面的报告。如果你有关于环境报告的问题,请尽管问我,我会很高兴帮助你。"
第二步:制定一个研究计划

如果查询分类返回值为 "environmental",用户的请求与文档内容相关,工作流将到达 create_research_plan 节点,该节点的功能是为回答环境相关查询,逐步创建研究计划。

    async def 创建研究计划(  
        state: AgentState, *, config: RunnableConfig  
    ) -> dict[str, list[str] | str]:  
        """为回答环境相关查询创建一个逐步的研究计划。  

        参数:  
            state (AgentState): 代理当前的状态,包括对话历史。  
            config (RunnableConfig): 用于生成计划的模型配置。  

        返回:  
            dict[str, list[str]]: 包含研究步骤列表的字典。  
        """  

        class Plan(TypedDict):  
            """生成研究计划."""  

            steps: list[str]  

        model = ChatOpenAI(model=GPT_4o_MINI, temperature=TEMPERATURE, streaming=True)  
        messages = [  
            {"role": "system", "content": RESEARCH_PLAN_SYSTEM_PROMPT}  
        ] + state.messages  
        logging.info("---生成计划---")  
        response = cast(Plan, await model.with_structured_output(Plan).ainvoke(messages))  
        return {"steps": response["steps"], "documents": "删除"}

输出示例的响应为问题 “检索都柏林数据中心的2019年PUE值”:

{
  "步骤":  
    ["查询2019年都柏林特定数据中心的PUE(电源使用效益)值,利用统计资料来源。"]  
}

用户的请求只需一步即可得到信息。

第三步:进行研究

首先,这个功能从研究计划中取出第一步,并使用它来进行研究。然后,它调用子图部分researcher_graph,该部分返回一个块列表,我们在接下来的部分会进行探索。最后,我们通过移除刚才执行的步骤来更新状态中的steps变量部分。

    async def conduct_research(state: AgentState) -> dict[str, Any]:  
        """执行研究的第一步。  

        此函数使用第一个研究步骤来开展研究。  

        参数:  
            state (AgentState): 代理的当前状态,包括研究步骤。  

        返回:  
            字典[str, 列表[str]]: 包含'documents'键,值为研究结果的文档列表;  
                                 包含'steps'键,其值为剩余的研究步骤列表。  

        行为:  
            - 调用researcher_graph,使用第一个研究步骤。  
            - 更新状态信息,并移除已完成的步骤。  
        """  
        result = await researcher_graph.ainvoke({"question": state.steps[0]}) # 直接调用researcher_graph  
        docs = result["documents"]  
        step = state.steps[0]  
        logging.info(f"\n共检索到 {len(docs)} 份文档,步骤是: {step}。")  
        return {"documents": result["documents"], "steps": state.steps[1:]}  
步骤 4:建立研究员子图

如上图所示,该图包括一个查询生成步骤,从主图传递的步骤开始,以及一个检索相关片段的步骤。就像我们定义主图的状态一样,让我们继续定义 QueryStateResearcherState 状态。QueryState 是检索文档节点的私人状态,而 ResearcherState 是研究员图的状态。

"""
研究员子图的状态。

此模块定义了在研究员子图中使用的状态结构。
"""

from dataclasses import dataclass, field
from typing import Annotated
from langchain_core.documents import Document
from utils.utils import reduce_docs

@dataclass(kw_only=True)
class QueryState:
    """检索节点在研究员子图中的私有状态。"""
    query: str

@dataclass(kw_only=True)
class ResearcherState:
    """研究员图/代理的状态。"""
    question: str
    """研究计划中的一个步骤,由检索器生成。"""
    queries: list[str] = field(default_factory=list)
    """根据研究员生成的问题生成的搜索查询列表。"""
    documents: Annotated[list[Document], reduce_docs] = field(default_factory=list)
    """这是代理可以引用的文档列表。"""
第4.1步:生成查询.

这一步是基于问题来生成搜索查询。这个功能利用大模型来生成各种搜索查询以帮助回答这个问题。

    async def generate_queries(  
        state: ResearcherState, *, config: RunnableConfig  
    ) -> dict[str, list[str]]:  
        """根据问题生成搜索查询(研究计划中的一个环节)。  

        此函数使用语言模型生成多样化搜索查询,以帮助回答用户的问题。  

        参数:  
            state (ResearcherState): 研究员的当前状态,包括用户的问题。  
            config (RunnableConfig): 用于生成查询的模型配置。  

        返回:  
            dict[str, list[str]]: 包含生成的搜索查询列表的字典,其中键为 'queries'。  
        """  

        class Response(TypedDict):  
            queries: list[str]  

        logger.info("---生成查询---")  
        model = ChatOpenAI(model="gpt-4o-mini-2024-07-18", temperature=0)  
        messages = [  
            {"role": "system", "content": GENERATE_QUERIES_SYSTEM_PROMPT},  
            {"role": "human", "content": state.question},  
        ]  
        response = cast(Response, await model.with_structured_output(Response).ainvoke(messages))  
        queries = response["queries"]  
        queries.append(state.question)  
        logger.info(f"生成的查询为:{queries}")  
        return {"queries": response["queries"]}

对于问题“查询2019年都柏林数据中心的PUE值”的回答示例:

    {  
      "queries":[  
        "查找2019年都柏林数据中心的PUE(电源使用效率)数值,使用统计资料来源。",
        "2019年都柏林数据中心的PUE效率数值",
        "2019年都柏林数据中心的电源使用效率统计"
      ]  
    }

一旦查询生成了,我们可以使用之前定义的持久数据库来定义向量存储库。

# 初始化 Chroma 向量存储实例
def _setup_vectorstore() -> Chroma:  
    """  
    初始化并返回 Chroma 向量存储实例。  
    """  
    embeddings = OpenAIEmbeddings() # 向量嵌入
    return Chroma(  
        collection_name=VECTORSTORE_COLLECTION, # 向量集合名称
        embedding_function=embeddings, # 向量嵌入函数
        persist_directory=VECTORSTORE_DIRECTORY # 持久化目录
    )

在RAG系统中,最关键的部分是文档检索过程。 因此,人们非常重视所采用的技术:具体来说,选择了集成检索器(Ensemble Retriever)作为混合搜索(Hybrid Search),以及使用Cohere进行重排序。

混合搜索技术是“基于关键词的”搜索和“基于向量的”搜索的结合。它具有执行关键词搜索和利用嵌入进行语义搜索的优势。联合检索算法是一种旨在通过结合多个独立检索器的优势来增强信息检索性能的检索算法。这种技术称为“联合检索”,采用互反排名融合技术对不同检索器的结果进行重新排序和合并,从而提供比单一检索器更准确和相关的结果。

    # 创建一些基础的检索器  
    retriever_bm25 = BM25Retriever.from_documents(documents, search_kwargs={"k": TOP_K})  # 创建BM25检索器
    retriever_vanilla = vectorstore.as_retriever(search_type="similarity", search_kwargs={"k": TOP_K})  # 创建相似度检索器
    retriever_mmr = vectorstore.as_retriever(search_type="mmr", search_kwargs={"k": TOP_K})  # 创建MMR检索器

    ensemble_retriever = EnsembleRetriever(  
            retrievers=[retriever_vanilla, retriever_mmr, retriever_bm25],  
            weights=ENSEMBLE_WEIGHTS,  # 集成检索器的权重
        )  # 集成多个检索器作为ensemble检索器

重新排序 是一种可以用来提升 RAG 管道性能的技术。RAG 是“Retrieval-Augmented Generation”的简称。它是一种非常强大的方法,可以显著提升搜索系统的性能。简而言之,重新排序会根据查询和响应给出一个 相关性分数。这样,可以使用任何搜索系统找到一些可能包含查询答案的文档,然后通过重排序端点对它们进行排序。

但是,为什么我们需要重新排序?

为了应对准确性方面的挑战,我们采用了两阶段检索方法。首先,由集成检索器从大量数据中挑选出一些候选文档。然后,重排序器会根据第一阶段的结果对这些文档进行重新排序。像Cohere Rerank这样的重排序模型,会在给定查询和文档对时输出一个相似度评分,这个评分可以帮助我们重新排列那些最符合搜索查询的文档。在重排序方法中,Cohere Rerank模型因其能显著提升搜索准确性而脱颖而出。它与传统的嵌入模型不同,通过深度学习直接评估每个文档与查询的匹配度。Cohere Rerank在处理查询和文档的同时输出相关性评分,使得文档选择过程更加精确。(完整引用见此链接:https://aws.amazon.com/it/blogs/machine-learning/improve-rag-performance-using-cohere-rerank/

这样的话,检索到的文档会被重新排序和筛选,返回最相关的两条。

    from langchain.retrievers.contextual_compression import ContextualCompressionRetriever  
    from langchain_cohere import CohereRerank  
    from langchain_community.llms import Cohere  

    # 设置 Cohere 重排  
    compressor = CohereRerank(top_n=2, model="rerank-english-v3.0")  

    # 创建压缩检索器  
    compression_retriever = ContextualCompressionRetriever(  
        base_compressor=compressor,  
        base_retriever=ensemble_retriever,  
    )  

    compression_retriever.invoke(  
        "调用压缩检索器以检索2019年都柏林数据中心的PUE效率"  
    )

针对问题的输出示例:“查询2019年都柏林数据中心的PUE效率值”:

[Document(元数据:{'Header 2': '注释', 'relevance_score': 0.27009502}, 页面内容="- 1 此计算基于以下..."),  
 Document(元数据:{'Header 2': '数据中心电网区域 CFE', 'relevance_score': 0.20593424}, 页面内容="2023  \n| 国家...")]
第4.2步:获取和重排序文档功能
     async def retrieve_and_rerank_documents() -> dict[str, list[Document]]:  
        """基于给定查询检索和重排文档。  

        此函数使用检索器来获取和重排给定查询的相关文档。  

        参数:  
            state (QueryState): 包含查询字符串的当前状态。  
            config (RunnableConfig): 用于获取文档的检索器的配置。  

        返回:  
            字典,键为 'documents',值为 Document 类型的列表。  
        """  
        logger.info("----正在检索文档------")  
        logger.info(f"正在检索的查询字符串:{state.query}")  

        response = compression_retriever.invoke(state.query)  

        return {"documents": response}
第四步第三阶段 构建子图阶段
    builder = StateGraph(ResearcherState)  
    builder.add_node(生成查询)  
    builder.add_node(检索并重新排序文档)  
    builder.add_edge(START, "生成查询")  
    builder.add_conditional_edges(  
        "生成查询",  
        并行检索,  # type: ignore  
        path_map=["检索并重新排序文档"],  
    )  
    builder.add_edge("检索并重新排序文档", END)  
    researcher_graph = builder.compile()
第五步:检查结束

使用 conditional_edge,我们用它来构建一个循环,其结束条件由 check_finished 函数决定。该函数检查是否有更多步骤需要处理,这些步骤是由 create_research_plan 节点生成的。一旦所有步骤都已完成,流程将转入 respond 节点。

    def check_finished(state: AgentState) -> Literal["respond", "conduct_research"]:  
        """判断研究流程是否已经结束,或者是否还需要继续进行研究。  

        这个函数会检查研究计划里是否还有未完成的步骤:  
            - 如果有未完成的步骤,则返回到 `conduct_research` 节点  
            - 否则,转向 `respond` 节点  

        参数:  
            state (AgentState): 代理的当前状态,包括剩余的研究步骤。  

        返回:  
            Literal["respond", "conduct_research"]: 根据研究是否完成,返回下一步的行动。  
        """  
        if len(state.steps or []) > 0:  
            return "conduct_research"  
        else:  
            return "respond"
第6步:回应

根据所进行的研究生成用户的查询的最终回复。此功能会根据对话历史和研究代理检索到的文档来给出全面的回答。

    async def respond(  
        state: AgentState, *, config: RunnableConfig  
    ) -> dict[str, list[BaseMessage]]:  
        """根据已经进行的研究,生成对用户查询的最终回复。  

        该函数利用对话历史和检索到的文档生成全面的回答。  

        Args:  
            state (AgentState): 代理的状态,包括检索到的文档和对话历史。  
            config (RunnableConfig): 用于响应的模型配置项。  

        Returns:  
            dict[str, list[BaseMessage]]: 包含回复的 'messages' 键的字典。  
        """  
        print("--- 生成回复的步骤 ---")  
        model = ChatOpenAI(model="gpt-4o-2024-08-06", temperature=0)  
        context = format_docs(state.documents)  
        prompt = RESPONSE_SYSTEM_PROMPT.format(context=context)  
        messages = [{"role": "system", "content": prompt}] + state.messages  
        response = await model.ainvoke(messages)  

        return {"messages": [response]}

第七步:检查异常

这一步验证上一步的回答内容是否被检索文档中的事实支持,并给出一个二进制评分结果。

    async def check_hallucinations(  
        state: AgentState, *, config: RunnableConfig  
    ) -> dict[str, Any]:  
        """分析用户的查询,并检查响应是否基于检索到的文档中的事实集得到支持,提供一个二元评分结果。  

        此函数使用语言模型来分析用户的查询,并给出一个二元评分结果。  

        参数:  
            state (AgentState): 代理当前的状态,包括对话历史。  
            config (RunnableConfig): 用于查询分析的模型设置。  

        返回:  
            dict[str, Router]: 包含 'router' 键及其分类结果的字典。  
        """  

        model = ChatOpenAI(model=GPT_4o_MINI, temperature=TEMPERATURE, streaming=True)  
        system_prompt = CHECK_HALLUCINATIONS 格式化(  
            documents=state.documents,  
            generation=state.messages[-1]  
        )  

        messages = [  
            {"role": "system", "content": system_prompt}  
        ] + state.messages  
        logging.info(\"---检测幻觉---\")  
        response = await model.with_structured_output(GradeHallucinations).ainvoke(messages)  

        返回 {"hallucination": response}
步骤 8:人工审批,人在回路中

如果LLM的回复没有根据设定的事实依据,那么很可能会包含虚构的信息。在这种情况下,对话会被中断,用户可以控制下一步的操作:仅重新尝试最后一步生成,而无需重做整个流程,或者直接结束流程。这种“人在回路”的方法确保用户可以控制流程,并避免意外循环或不期望的操作。

LangGraph 中的 [interrupt](https://langchain-ai.github.io/langgraph/reference/types/#langgraph.types.interrupt)函数 通过在特定节点暂停图,向人展示信息,并在他们提供输入后继续进行,从而支持了人机协作的工作流。此功能对于批准、编辑或收集额外信息等任务非常有用。当与 [Command](https://langchain-ai.github.io/langgraph/reference/types/#langchain.types.Command) 对象一起使用时,该 [interrupt](https://langchain-ai.github.io/langgraph/reference/types/#langgraph.types.interrupt)函数 可以使用人类提供的值继续图。

def 人工确认(
    state: AgentState,
):
    _binary_score = state.幻觉.二进制得分
    if _binary_score == "1":
        return "结束"
    else:
        重试生成 = 中断生成(
        {
            "问题": "这个回答正确吗?",
            "llm_output": state.messages[-1]
        })

        if 重试生成 == "y" or 重试生成.lower() == "yes":
            print("我想继续生成")
            return "回复"
        else:
            return "结束"

4.3 建立主图。

from langgraph.graph import END, START, StateGraph  
from langgraph.checkpoint.memory import MemorySaver  

checkpointer = MemorySaver()  

builder = StateGraph(AgentState, input=InputState)  
builder.add_node(analyze_and_route_query)  # 添加分析并路由查询节点
builder.add_edge(START, "analyze_and_route_query")  
builder.add_conditional_edges("analyze_and_route_query", route_query)  # 添加条件边到路由查询
builder.add_node(create_research_plan)  
builder.add_node(ask_for_more_info)  
builder.add_node(respond_to_general_query)  
builder.add_node(conduct_research)  
builder.add_node("respond", respond)  
builder.add_node(check_hallucinations)  
builder.add_conditional_edges("check_hallucinations", human_approval, {"END": END, "respond": "respond"})  # 条件边检查幻觉,包括人类审批和回应
builder.add_edge("create_research_plan", "conduct_research")  
builder.add_conditional_edges("conduct_research", check_finished)  
builder.add_edge("respond", "check_hallucinations")  

graph = builder.compile(checkpointer=checkpointer)  # 编译图
主函数构建(app.py 文件)

“每个函数都被定义为 async 以启用流处理在生成步骤中。

    从 subgraph.graph_states 导入 ResearcherState  
    从 main_graph.graph_states 导入 AgentState  
    从 utils.utils 导入 config, new_uuid  
    从 subgraph.graph_builder 导入 researcher_graph  
    从 main_graph.graph_builder 导入 InputState, graph  
    从 langgraph.types 导入 Command  
    导入 asyncio  
    导入 uuid  

    thread = {"configurable": {"thread_id": new_uuid()}}  

    异步 def process_query(query):  
        inputState = InputState(messages=query)  

        异步 for c, metadata in graph.astream(input=inputState, stream_mode="messages", config=thread):  
            if c.additional_kwargs.get("tool_calls"):  
                print(c.additional_kwargs.get("tool_calls")[0]["function"].get("arguments"), end="", flush=True)  
            if c.content:  
                time.sleep(0.05)  
                print(c.content, end="", flush=True)  

        if len(graph.get_state(thread)[-1]) > 0:  
            if len(graph.get_state(thread)[-1][0].interrupts) > 0:  
                response = input("\n响应可能包含不确定的信息。需要重新生成吗?如果是,请按'y':")  
                if response.lower() == 'y':  
                    异步 for c, metadata in graph.astream(Command(resume=response), stream_mode="messages", config=thread):  
                        if c.additional_kwargs.get("tool_calls"):  
                            print(c.additional_kwargs.get("tool_calls")[0]["function"].get("arguments"), end="")  
                        if c.content:  
                            time.sleep(0.05)  
                            print(c.content, end="", flush=True)  

    异步 def main():  
        input = builtins.input  
        print("请输入查询,输入'-q'退出:")  
        while True:  
            query = input("> ")  
            if query.strip().lower() == "-q":  
                print("准备退出...")  
                break  
            await process_query(query)  

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

在第一次调用之后,会检查图形状态是否有中断发生。如果有中断发生,可以使用以下命令重新调用该图:

graph调用astream方法,参数包括Command对象(其中resume属性设置为response),stream_mode设为'messages',config设为thread。

就这样,工作流会从被中断的步骤继续进行,而不会重新执行之前的步骤,并使用相同的 thread_id

3. 结果:

以下测试使用了谷歌关于2024年环境战略的年度报告,该报告可在此处免费获取here

实时测试

作为第一个测试步骤,执行了如下查询,从不同表中提取不同的值,结合了多步骤方法的特点,并利用了Docling库的解析特性。

复杂的问题:“检索新加坡第二设施在2019年2022年的数据中心PUE效率值数值。同时,还需检索2023年的亚太地区的平均CFE。”

准确的信息

现场演示

结果完全正确,并且事实核查也顺利通过了。

聊天机器人生成的步骤:

  • “查一下新加坡第二设施在2019年和2022年的PUE值。”
  • “找到2023年亚太地区的平均CFE。”

“- 2019年新加坡第二设施的电力使用效率(PUE)无法提供,因为该年度的数据未提供。不过,2022年的PUE为1.21

2023年,亚太地区的清洁能源(CFE)平均占比为12%。

完整的输出结果
    输入您的查询(输入'-q'以退出):  
    > 检索新加坡第二数据中心在2019年和2022年的PUE效率值。另外检索2023年亚太地区的平均CFE值  
    2025-01-10 20:39:53,381 - INFO - ---分析和路由查询---  
    2025-01-10 20:39:53,381 - INFO - 消息:[HumanMessage(content='检索新加坡第二数据中心在2019年和2022年的PUE效率值。另外检索2023年亚太地区的平均CFE值', additional_kwargs={}, response_metadata={}, id='351a00e9-ecda-49e2-b069-19196348a82a')]  
    {"logic":"检索新加坡第二数据中心在2019年和2022年的PUE效率值。另外检索2023年亚太地区的平均CFE值","type":"环境性能"}  
    2025-01-10 20:39:55,586 - INFO - ---生成计划---  
    {"steps":["查找2019年和2022年新加坡第二数据中心的PUE效率值","查找2023年亚太地区的平均CFE值"]}  
    2025-01-10 20:39:57,323 - INFO - ---生成查询---  
    {"queries":["新加坡第二数据中心2019年的PUE效率值","新加坡第二数据中心2022年的PUE效率值"]}  
    2025-01-10 20:39:58,285 - INFO - 查询:['新加坡第二数据中心2019年的PUE效率值', '新加坡第二数据中心2022年的PUE效率值', '查找2019年和2022年新加坡第二数据中心的PUE效率值']  
    2025-01-10 20:39:58,288 - INFO - ---检索文档---  
    2025-01-10 20:39:58,288 - INFO - 检索查询:新加坡第二数据中心2019年的PUE效率值  
    2025-01-10 20:39:59,568 - INFO - ---检索文档---  
    2025-01-10 20:39:59,568 - INFO - 检索查询:新加坡第二数据中心2022年的PUE效率值  
    2025-01-10 20:40:00,891 - INFO - ---检索文档---  
    2025-01-10 20:40:00,891 - INFO - 检索查询:查找2019年和2022年新加坡第二数据中心的PUE效率值  
    2025-01-10 20:40:01,820 - INFO -  
    为‘查找2019年和2022年新加坡第二数据中心的PUE效率值’这一步骤检索到了4篇文档。  
    2025-01-10 20:40:01,825 - INFO - ---生成查询---  
    {"queries":["2023年亚太地区的平均CFE值","2023年亚太地区的CFE统计数据"]}  
    2025-01-10 20:40:02,778 - INFO - 查询:['2023年亚太地区的平均CFE值', '2023年亚太地区的CFE统计数据', '查找2023年亚太地区的平均CFE值']  
    2025-01-10 20:40:02,780 - INFO - ---检索文档---  
    2025-01-10 20:40:02,780 - INFO - 检索查询:2023年亚太地区的平均CFE值  
    2025-01-10 20:40:03,757 - INFO - ---检索文档---  
    2025-01-10 20:40:03,757 - INFO - 检索查询:2023年亚太地区的CFE统计数据  
    2025-01-10 20:40:04,885 - INFO - ---检索文档---  
    2025-01-10 20:40:04,885 - INFO - 检索查询:查找2023年亚太地区的平均CFE值  
    2025-01-10 20:40:06,526 - INFO -  
    为‘查找2023年亚太地区的平均CFE值’这一步骤检索到了4篇文档。  
    2025-01-10 20:40:06,530 - INFO - ---响应生成步骤---  
    - 新加坡第二数据中心在2019年的PUE效率值无法提供,不过,2022年的PUE效率值是1.21 [e048d08a-4ef6-77b5-20d3-352dcec590b7]。  

    - 2023年亚太地区的平均碳自由能源(CFE)值是12% [9c489d2f-f16f-572b-abed-ee1d5d0ed379]。  
    2025-01-10 20:40:14,918 - INFO - ---检查幻觉---  
    {"binary_score":"1"}> 

现在我们试着用ChatGPT来试试。上传pdf文件到web应用之后,我们做了相同的查询。

如图所示的,ChatGPT 返回的值是错误的,模型出现了错误的幻觉。在这种情况下,如果进行一次幻觉检查,则可以重新生成回复(自我反思型检索与生成,Self-Reflective RAG)。

GPT测试

结论部分
代理RAG:技术挑战及考虑因素

尽管性能有所提升,实施Agentic RAG仍然面临不少难题。

  • 延迟:智能代理之间的交互复杂性增加常常导致响应时间变长。找到速度和准确性的平衡是一个关键挑战。
  • 评估和可观测性:随着智能代理系统的复杂性变得越来越复杂,持续的评估和可观测性变得必要。

总之,代理型检索增强生成模型(Agentic RAG)在人工智能领域取得了重大突破。通过将自主推理和信息检索能力结合,Agentic RAG 确立了新的智能与灵活标准。随着人工智能的持续发展,Agentic RAG 将在各个行业中发挥关键作用,改变我们利用技术的方式。

Github仓库页面点击这里****

参考:

https://github.com/DS4SD/docling

点击查看如何在human-in-the-loop中批准或拒绝

https://www.kaggle.com/code/marcinrutecki/rag-ensemble-retriever-in-langchain

https://sustainability.google/reports/google-2024-environmental-report/

https://python.langchain.com/docs/integrations/retrievers/cohere-reranker/

使用混合搜索和重排序的高级RAG实现

https://infohub.delltechnologies.com/it-it/p/the-rise-of-agentic-rag-systems/
(代理RAG系统的发展)

https://langchain-ai.github.io/langgraph/how-tos/

https://aws.amazon.com/it/blogs/machine-learning/improve-rag-performance-using-cohere-rerank/
(提升RAG性能)

打开App,阅读手记
0人推荐
发表评论
随时随地看视频慕课网APP