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

从基础到高级:探索LangGraph

慕桂英3389331
关注TA
已关注
手记 327
粉丝 43
获赞 187
构建包含人机交互的单个和多个代理工作流

图片由 DALL-E 3 创建

LangChain 是构建由大型语言模型驱动的应用程序的领先框架之一。借助 LangChain 表达语言(LCEL),定义和执行分步操作序列(也称为链)变得更加简单。从技术角度来说,LangChain 允许我们创建 DAG(有向无环图)。

随着大型语言模型(LLM)应用,特别是LLM代理的发展,我们已经开始将LLM不仅用于执行,还用于推理引擎。这种转变引入了经常涉及重复(循环)和复杂条件的交互。在这种情况下,LCEL不足以应对,因此LangChain实现了一个新的模块 — LangGraph

LangGraph(从名称可以猜到)将所有交互建模为循环图。这些图支持开发具有多个循环和if语句的高级工作流和交互,使其成为创建单个代理和多代理工作流的得力工具。

在本文中,我将探讨 LangGraph 的关键功能和能力,包括多代理应用。我们将构建一个能够回答不同类型问题的系统,并深入探讨如何实现人机交互的设置。

上一篇文章中,我们尝试使用了CrewAI,另一个流行的多代理系统框架。然而,LangGraph采取了不同的方法。虽然CrewAI是一个具有许多预定义功能和即用组件的高级框架,但LangGraph则在较低的层级上运行,提供了广泛的自定义和控制选项。

有了那个介绍,让我们深入了解一下 LangGraph 的基本概念。

LangGraph 基础

LangGraph 是 LangChain 生态系统的一部分,所以我们将继续使用诸如提示模板、工具等众所周知的概念。然而,LangGraph 带来了许多额外的概念。 让我们来讨论一下这些概念。

LangGraph 用于定义循环图。图包含以下元素:

  • 节点代表实际的操作,可以是LLM、代理或函数。此外,一个特殊的END节点标记执行的结束。
  • 边连接节点并确定图的执行流程。有简单的边,它们只是将一个节点连接到另一个节点,还有条件边,它们包含if语句和额外的逻辑。

另一个重要的概念是图的状态。状态作为图各组件间协作的基础元素。它代表了图在执行过程中任一部分(无论是节点还是边)都可以访问和修改的一个快照,以获取或更新信息。

此外,状态在持久化中起着关键作用。它在每一步后自动保存,允许你在任何时刻暂停和恢复执行。此功能支持开发更复杂的应用程序,例如需要错误修正或包含人机交互的应用程序。

单个代理工作流
从头开始构建代理

让我们从简单开始,尝试使用 LangGraph 来处理一个基本用例——一个带有工具的代理。

我将尝试构建与我们在之前的文章中使用CrewAI创建的应用程序类似的项目。然后,我们将能够比较这两个框架。在这个例子中,让我们创建一个可以根据数据库中的表自动生成文档的应用程序。这可以在为我们数据源创建文档时节省我们大量的时间。

像往常一样,我们将从定义代理的工具开始。由于在这个示例中我将使用 ClickHouse 数据库,所以我定义了一个执行任何查询的函数。如果你愿意,你可以使用不同的数据库,因为我们不会依赖任何特定于数据库的功能。

    CH_HOST = 'http://localhost:8123' # 默认地址   
    import requests  

    def get_clickhouse_data(query, host = CH_HOST, connection_timeout = 1500):  
      r = requests.post(host, params = {'query': query},   
        timeout = connection_timeout)  
      if r.status_code == 200:  
          return r.text  
      else:   
          return '数据库返回了以下错误:\n' + r.text

确保LLM工具可靠且能够处理错误很重要。如果数据库返回错误,我会将此反馈提供给LLM,而不是抛出异常并停止执行。然后,LLM代理有机会修复错误并再次调用该函数。

让我们定义一个名为 execute_sql 的工具,该工具可以执行任何SQL查询。我们使用 pydantic 来指定工具的结构,确保LLM代理拥有使用该工具所需的所有信息。

    from langchain_core.tools import tool  
    from pydantic.v1 import BaseModel, Field  
    from typing import Optional  

    class SQLQuery(BaseModel):  
      query: str = Field(description="要执行的SQL查询")  

    @tool(args_schema = SQLQuery)  
    def execute_sql(query: str) -> str:  
      """返回SQL查询执行的结果"""  
      return get_clickhouse_data(query)

我们可以打印创建的工具的参数,看看传递给LLM的信息是什么。

    print(f'''  
    name: {execute_sql.name}  
    description: {execute_sql.description}  
    arguments: {execute_sql.args}  
    ''')  

    # name: execute_sql  
    # description: 返回SQL查询执行的结果  
    # arguments: {'query': {'title': 'Query', 'description':   
    #   '要执行的SQL查询', 'type': 'string'}}

一切看起来都很好。我们已经设置了必要的工具,现在可以继续定义一个LLM代理了。正如我们上面讨论的,LangGraph中代理的基石是它的状态,这使得信息可以在我们图的不同部分之间共享。

我们的当前示例相对简单。因此,我们只需要存储消息的历史记录。让我们定义代理的状态。

    #  imports that are useful  
    from langgraph.graph import StateGraph, END  
    from typing import TypedDict, Annotated  
    import operator  
    from langchain_core.messages import AnyMessage, SystemMessage, HumanMessage, ToolMessage  

    # defining agent state  
    class AgentState(TypedDict):  
       messages: Annotated[list[AnyMessage], operator.add]

我们在 AgentState 中定义了一个参数 messages,它是一个 AnyMessage 类的对象列表。此外,我们用 operator.add(归约器)对其进行了注解。这个注解确保每次节点返回一条消息时,该消息会被追加到状态中的现有列表中。如果没有这个操作符,每次新的消息将会替换掉之前的值,而不是被添加到列表中。

下一步是定义代理本身。让我们从 __init__ 函数开始。我们将为代理指定三个参数:模型、工具列表和系统提示。

    class SQLAgent:  
      # 初始化对象  
      def __init__(self, model, tools, system_prompt = ""):  
        self.system_prompt = system_prompt  

        # 使用状态初始化图  
        graph = StateGraph(AgentState)  

        # 添加节点   
        graph.add_node("llm", self.call_llm)  
        graph.add_node("function", self.execute_function)  
        graph.add_conditional_edges(  
            "llm",  
            self.exists_function_calling,  
            {True: "function", False: END}  
        )  
        graph.add_edge("function", "llm")  

        # 设置起点  
        graph.set_entry_point("llm")  

        self.graph = graph.compile()  
        self.tools = {t.name: t for t in tools}  
        self.model = model.bind_tools(tools)

在初始化函数中,我们概述了图的结构,其中包括两个节点:llmaction。节点是实际的操作,因此我们为它们关联了函数。我们稍后会定义这些函数。

此外,我们还有一条条件边来决定是否需要执行函数或生成最终答案。对于这条边,我们需要指定前一个节点(在我们的情况下是 llm),一个决定下一步的函数,以及根据该函数输出映射的后续步骤(格式为字典)。如果 exists_function_calling 返回 True,则我们继续执行函数节点。否则,执行将在特殊的 END 节点结束,这标志着过程的结束。

我们在 functionllm 之间添加了一条边。它只是链接了这两个步骤,并且会在没有任何条件的情况下执行。

在主要结构定义完成后,现在是时候创建上述所有函数了。第一个函数是 call_llm。此函数将执行 LLM 并返回结果。

代理的状态将会自动传递给函数,因此我们可以从中使用保存的系统提示和模型。

    class SQLAgent:  
      <...>  

      def call_llm(self, state: AgentState):  
        messages = state['messages']  
        # 如果定义了系统提示,则添加系统提示  
        if self.system_prompt:  
            messages = [SystemMessage(content=self.system_prompt)] + messages  

        # 调用LLM  
        message = self.model.invoke(messages)  

        return {'messages': [message]}

因此,我们的函数返回一个字典,该字典将用于更新代理的状态。由于我们使用了 operator.add 作为状态的归约器,返回的消息将被追加到存储在状态中的消息列表中。

下一个我们需要的函数是 execute_function,它将运行我们的工具。如果LLM代理决定调用一个工具,我们将在message.tool_calls参数中看到它。

    class SQLAgent:  
      <...>    

      def execute_function(self, state: AgentState):  
        tool_calls = state['messages'][-1].tool_calls  

        results = []  
        for tool in tool_calls:  
          # 检查工具名称是否正确  
          if not t['name'] in self.tools:  
            # 返回错误给代理  
            result = "Error: There's no such tool, please, try again"   
          else:  
            # 从工具获取结果  
            result = self.tools[t['name']].invoke(t['args'])  

          results.append(  
            ToolMessage(  
              tool_call_id=t['id'],   
              name=t['name'],   
              content=str(result)  
            )  
        )  
        return {'messages': results}

在这个函数中,我们遍历LLM返回的工具调用,要么调用这些工具,要么返回错误消息。最后,我们的函数返回一个包含单个键 messages 的字典,该字典将用于更新图的状态。

只剩下最后一个函数了——定义是否需要执行工具或提供最终结果的条件边函数。这个函数非常简单。我们只需要检查最后的消息是否包含任何工具调用。

    class SQLAgent:  
      <...>    

      def exists_function_calling(self, state: AgentState):  
        result = state['messages'][-1]  
        return len(result.tool_calls) > 0

是时候创建一个代理和为其配置的LLM模型了。我将使用新的OpenAI GPT 4o mini模型(文档),因为它比GPT 3.5更便宜且性能更好。

    import os  

    # 设置凭证  
    os.environ["OPENAI_MODEL_NAME"]='gpt-4o-mini'    
    os.environ["OPENAI_API_KEY"] = '<your_api_key>'  

    # 系统提示  
    prompt = '''您是一位精通SQL和数据分析的高级专家。   
    因此,您可以帮助团队收集所需的数据以支持他们的决策。   
    您非常准确,并考虑到数据中的所有细微差别。  
    您的目标是为数据库中的表提供详细的文档,以帮助用户。'''  

    model = ChatOpenAI(model="gpt-4o-mini")  
    doc_agent = SQLAgent(model, [execute_sql], system=prompt)

LangGraph 提供了一个非常方便的功能来可视化图形。要使用它,您需要安装 pygraphviz

这对使用 M1/M2 芯片的 Mac 来说有点棘手,所以这里有一个技巧给你(来源):

    ! brew install graphviz  
    ! python3 -m pip install -U --no-cache-dir  \  
        --config-settings="--global-option=build_ext" \  
        --config-settings="--global-option=-I$(brew --prefix graphviz)/include/" \  
        --config-settings="--global-option=-L$(brew --prefix graphviz)/lib/" \  
        pygraphviz

安装完成后,这是我们得到的图。

    from IPython.display import Image  
    Image(doc_agent.graph.get_graph().draw_png())

如你所见,我们的图中存在环。使用 LCEL 实现这样的功能会相当具有挑战性。

最后,是时候执行我们的代理了。我们需要传递一组初始消息,将我们的问题作为 HumanMessage 传递。

    messages = [HumanMessage(content="我们在ecommerce_db.users表中有哪些信息?")]  
    result = doc_agent.graph.invoke({"messages": messages})

result 变量中,我们可以观察到执行过程中生成的所有消息。该过程按预期工作:

  • 代理决定调用带有查询 describe ecommerce.db_users 的函数。
  • LLM 然后处理了来自工具的信息,并提供了用户友好的回答。
    result['messages']  

    # [  
    #   HumanMessage(content='我们在ecommerce_db.users表中有哪些信息?'),   
    #   AIMessage(content='', tool_calls=[{'name': 'execute_sql', 'args': {'query': 'DESCRIBE ecommerce_db.users;'}, 'id': 'call_qZbDU9Coa2tMjUARcX36h0ax', 'type': 'tool_call'}]),   
    #   ToolMessage(content='user_id\tUInt64\t\t\t\t\t\ncountry\tString\t\t\t\t\t\nis_active\tUInt8\t\t\t\t\t\nage\tUInt64\t\t\t\t\t\n', name='execute_sql', tool_call_id='call_qZbDU9Coa2tMjUARcX36h0ax'),   
    #   AIMessage(content='`ecommerce_db.users`表包含以下列: <...>')  
    # ]

这里就是最终结果了。看起来相当不错。

    print(result['messages'][-1].content)  

    # `ecommerce_db.users` 表包含以下列:  
    # 1. **user_id**: `UInt64` - 每个用户的唯一标识符。  
    # 2. **country**: `String` - 用户所在的国家。  
    # 3. **is_active**: `UInt8` - 表示用户是否活跃(1 表示活跃,0 表示不活跃)。  
    # 4. **age**: `UInt64` - 用户的年龄。
使用预构建代理

我们已经学会了从零开始构建代理。然而,对于像这样的简单任务,我们可以利用 LangGraph 内置的功能。

我们可以使用一个预构建的ReAct代理来获得类似的结果:一个能够与工具协同工作的代理。

    from langgraph.prebuilt import create_react_agent  
    prebuilt_doc_agent = create_react_agent(model, [execute_sql],  
      state_modifier=system_prompt)

这是我们之前构建的同一个代理。我们稍后会尝试使用它,但在那之前,我们需要了解另外两个重要的概念:持久化和流式传输。

持久化和流式传输

持久性是指在不同交互中保持上下文的能力。这对于代理型用例来说至关重要,因为在这些用例中,应用程序可以从用户那里获取额外的输入。

LangGraph 在每一步后自动保存状态,允许你暂停或恢复执行。这种能力支持实现高级业务逻辑,例如错误恢复或人机交互。

添加持久化最简单的方式是使用内存中的SQLite数据库。

    from langgraph.checkpoint.sqlite import SqliteSaver  
    memory = SqliteSaver.from_conn_string(":memory:")

对于现成的代理,我们可以在创建代理时传递内存作为参数。

    prebuilt_doc_agent = create_react_agent(model, [execute_sql],   
      checkpointer=memory)

如果你正在使用自定义代理,那么在编译图时需要传递内存作为检查点。

    class SQLAgent:  
      def __init__(self, model, tools, system_prompt = ""):  
        <...>  
        self.graph = graph.compile(checkpointer=memory)  
        <...>

让我们执行这个代理并探索 LangGraph 的另一个特性:流处理。通过流处理,我们可以在流中以单独事件的形式接收每个执行步骤的结果。这个特性对于需要同时处理多个对话(或线程)的生产应用来说至关重要。

LangGraph 不仅支持事件流处理,还支持 token 级别的流处理。我目前想到的 token 流处理的一个应用场景是实时逐字显示答案(类似于 ChatGPT 的实现)。

让我们尝试使用流式传输与我们新的预构建代理。我还将使用 pretty_print 函数来美化消息,使结果更易读。

    # 定义线程  
    thread = {"configurable": {"thread_id": "1"}}  
    messages = [HumanMessage(content="我们在ecommerce_db.users表中有哪些信息?")]  

    for event in prebuilt_doc_agent.stream({"messages": messages}, thread):  
        for v in event.values():  
            v['messages'][-1].pretty_print()  

    # ================================== AI 消息 ==================================  
    # 工具调用:  
    # execute_sql (call_YieWiChbFuOlxBg8G1jDJitR)  
    # 调用ID: call_YieWiChbFuOlxBg8G1jDJitR  
    #   参数:  
    #     query: SELECT * FROM ecommerce_db.users LIMIT 1;  
    # ================================= 工具消息 =================================  
    # 名称: execute_sql  
    # 1000001 United Kingdom 0 70  
    #   
    # ================================== AI 消息 ==================================  
    #   
    # `ecommerce_db.users` 表至少包含以下用户信息:  
    #   
    # - **用户ID** (例如,`1000001`)  
    # - **国家** (例如,`United Kingdom`)  
    # - **某个数值** (例如,`0`)  
    # - **另一个数值** (例如,`70`)  
    #   
    # 从检索到的单行数据中无法明确这些数值的具体含义和额外的列信息。  
    # 您是否需要更多详细信息或更广泛的查询?

有趣的是,代理未能提供足够好的结果。由于代理没有查询表结构,它难以猜测所有列的含义。我们可以通过在同一线程中使用跟进问题来改进结果。


    followup_messages = [HumanMessage(content="我想知道列名和数据类型。也许你可以通过数据库的 describe 命令查询。")]  

    for event in prebuilt_doc_agent.stream({"messages": followup_messages}, thread):  
        for v in event.values():  
            v['messages'][-1].pretty_print()  

    # ================================== AI 消息 ==================================  
    # 工具调用:  
    #   execute_sql (call_sQKRWtG6aEB38rtOpZszxTVs)  
    # 调用 ID: call_sQKRWtG6aEB38rtOpZszxTVs  
    #   参数:  
    #     query: DESCRIBE ecommerce_db.users;  
    # ================================= 工具消息 =================================  
    # 名称: execute_sql  
    #   
    # user_id UInt64       
    # country String       
    # is_active UInt8       
    # age UInt64       
    #   
    # ================================== AI 消息 ==================================  
    #   
    # 表 `ecommerce_db.users` 有以下列及其数据类型:  
    #   
    # | 列名 | 数据类型 |  
    # |-------------|-----------|  
    # | user_id     | UInt64    |  
    # | country     | String    |  
    # | is_active   | UInt8     |  
    # | age         | UInt64    |  
    #   
    # 如果你需要进一步的信息或帮助,请随时询问!

这次我们从代理那里得到了完整的答案。因为我们提供了相同的对话线程,代理能够从之前的讨论中获取上下文。这就是持久性工作的原理。

让我们尝试更换线程并询问同样的跟进问题。

    new_thread = {"configurable": {"thread_id": "42"}}  
    followup_messages = [HumanMessage(content="我想知道列名和列类型。也许你可以通过 describe 查看数据库中的信息。")]  

    for event in prebuilt_doc_agent.stream({"messages": followup_messages}, new_thread):  
        for v in event.values():  
            v['messages'][-1].pretty_print()  

    # ================================== AI 消息 ==================================  
    # 工具调用:  
    #   execute_sql (call_LrmsOGzzusaLEZLP9hGTBGgo)  
    # 调用 ID: call_LrmsOGzzusaLEZLP9hGTBGgo  
    #   参数:  
    #     query: DESCRIBE your_table_name;  
    # ================================= 工具消息 =================================  
    # 名称: execute_sql  
    #   
    # 数据库返回了以下错误:  
    # 代码: 60. DB::Exception: 表 default.your_table_name 不存在。 (UNKNOWN_TABLE) (版本 23.12.1.414 (官方构建))  
    #   
    # ================================== AI 消息 ==================================  
    #   
    # 看来数据库中不存在表 `your_table_name`。  
    # 你可以提供你想要描述的实际表名吗?

代理缺乏回答我们问题所需的内容并不令人惊讶。线程的设计目的是隔离不同的对话,确保每个线程都有自己的上下文。

在实际应用中,管理内存非常重要。对话可能会变得很长,最终每次传递整个对话历史给LLM将不再实际。因此,修剪或过滤消息是值得的。我们在这里不会深入细节,但你可以在LangGraph 文档中找到相关指导。另一种压缩对话历史的方法是使用摘要功能(示例)。

我们已经学会了如何使用 LangGraph 构建单个代理的系统。下一步是将多个代理结合到一个应用程序中。

多代理系统

作为一个多代理工作流的例子,我想要构建一个能够处理来自不同领域的提问的应用程序。我们将拥有一组专家代理,每个代理专注于不同类型的问题,还有一个路由器代理,它将找到最适合处理每个查询的专家。这样的应用程序有许多潜在的应用场景:从自动化客户支持到回答内部聊天中同事的问题。

首先,我们需要创建代理状态——帮助代理共同解决问题的信息。我将使用以下字段:

  • question — 初始客户请求;
  • question_type — 定义哪个代理将处理请求的类别;
  • answer — 对问题的建议回答;
  • feedback — 一个将来收集反馈的字段。
    class MultiAgentState(TypedDict):  
        question: str  
        question_type: str  
        answer: str  
        feedback: str

我没有使用任何减少器,所以我们的状态将只存储每个字段的最新版本。

然后,让我们创建一个路由器节点。它将是一个简单的LLM模型,用于定义问题的类别(数据库、LangChain或一般问题)。

    question_category_prompt = '''您是一位高级分析支持专家。您的任务是分类传入的问题。   
    根据您的回答,问题将被路由到正确的团队,因此您的任务对我们团队至关重要。   
    有3种可能的问题类型:   
    - DATABASE - 与我们的数据库(表或字段)相关的问题  
    - LANGCHAIN - 与LangGraph或LangChain库相关的问题  
    - GENERAL - 一般问题  
    在输出中仅返回一个单词(DATABASE、LANGCHAIN 或 GENERAL)。  
    '''  

    def router_node(state: MultiAgentState):  
      messages = [  
        SystemMessage(content=question_category_prompt),   
        HumanMessage(content=state['question'])  
      ]  
      model = ChatOpenAI(model="gpt-4o-mini")  
      response = model.invoke(messages)  
      return {"question_type": response.content}

现在我们有了第一个节点——路由器,让我们构建一个简单的图来测试工作流。

    memory = SqliteSaver.from_conn_string(":memory:")

    builder = StateGraph(MultiAgentState)
    builder.add_node("router", router_node)

    builder.set_entry_point("router")
    builder.add_edge('router', END)

    graph = builder.compile(checkpointer=memory)

让我们通过不同类型的问题来测试我们的工作流,看看它在实际操作中的表现。这将帮助我们评估路由器代理是否正确地将问题分配给适当的专家代理。

    thread = {"configurable": {"thread_id": "1"}}  
    for s in graph.stream({  
        'question': "LangChain 是否支持 Ollama?",  
    }, thread):  
        print(s)  

    # {'router': {'question_type': 'LANGCHAIN'}}  

    thread = {"configurable": {"thread_id": "2"}}  
    for s in graph.stream({  
        'question': "我们在 ecommerce_db.users 表中有哪些信息?",  
    }, thread):  
        print(s)  
    # {'router': {'question_type': 'DATABASE'}}  

    thread = {"configurable": {"thread_id": "3"}}  
    for s in graph.stream({  
        'question': "你好吗?",  
    }, thread):  
        print(s)  

    # {'router': {'question_type': 'GENERAL'}}

它运行得很好。我建议你逐步构建复杂的图,并独立测试每一步。采用这种方法,你可以确保每一步都能按预期工作,并且可以节省大量的调试时间。

接下来,让我们为我们的专家代理创建节点。我们将使用带有之前构建的SQL工具的ReAct代理作为数据库代理。

    # 数据库专家  
    sql_expert_system_prompt = '''  
    你是一位SQL专家,可以帮助团队获取所需的数据来支持他们的决策。  
    你非常准确,并考虑到了数据中的所有细微差别。  
    在回答问题之前,你需要使用SQL来获取数据。  
    '''  

    def sql_expert_node(state: MultiAgentState):  
        model = ChatOpenAI(model="gpt-4o-mini")  
        sql_agent = create_react_agent(model, [execute_sql],  
            state_modifier = sql_expert_system_prompt)  
        messages = [HumanMessage(content=state['question'])]  
        result = sql_agent.invoke({"messages": messages})  
        return {'answer': result['messages'][-1].content}  

对于与LangChain相关的问题,我们将使用ReAct代理。为了使代理能够回答关于该库的问题,我们将为其配备一个搜索引擎工具。我选择了Tavily来实现这一目的,因为它提供了针对LLM应用优化的搜索结果。

如果你没有账户,你可以免费注册使用 Tavily(每月最多 1K 次请求)。开始使用时,你需要在环境变量中指定 Tavily 的 API 密钥。

    # 搜索专家  
    from langchain_community.tools.tavily_search import TavilySearchResults  
    os.environ["TAVILY_API_KEY"] = 'tvly-...'  
    tavily_tool = TavilySearchResults(max_results=5)  

    search_expert_system_prompt = '''  
    你是一位精通LangChain和其他技术的专家。  
    你的目标是根据搜索结果来回答问题。  
    你不会添加任何自己的内容,只会提供其他来源的信息。  
    '''  

    def search_expert_node(state: MultiAgentState):  
        model = ChatOpenAI(model="gpt-4o-mini")  
        sql_agent = create_react_agent(model, [tavily_tool],  
            state_modifier = search_expert_system_prompt)  
        messages = [HumanMessage(content=state['question'])]  
        result = sql_agent.invoke({"messages": messages})  
        return {'answer': result['messages'][-1].content}

对于一般性的问题,我们将利用一个简单的LLM模型而无需特定的工具。

    # 通用模型  
    general_prompt = '''你是一个友好的助手,你的目标是回答一般性问题。  
    请不要提供未经核实的信息,如果信息不足,请直接说明你不知道。  
    '''  

    def general_assistant_node(state: MultiAgentState):  
        messages = [  
            SystemMessage(content=general_prompt),   
            HumanMessage(content=state['question'])  
        ]  
        model = ChatOpenAI(model="gpt-4o-mini")  
        response = model.invoke(messages)  
        return {"answer": response.content}

最后一个缺失的部分是一个路由的条件函数。这将会非常直接——我们只需要从路由器节点定义的状态中传播问题类型。

    def 路由问题(state: MultiAgentState):  
        return state['question_type']

现在,是时候创建我们的图了。

    builder = StateGraph(MultiAgentState)  
    builder.add_node("router", router_node)  
    builder.add_node('database_expert', sql_expert_node)  
    builder.add_node('langchain_expert', search_expert_node)  
    builder.add_node('general_assistant', general_assistant_node)  
    builder.add_conditional_edges(  
        "router",   
        route_question,  
        {'DATABASE': 'database_expert',   
         'LANGCHAIN': 'langchain_expert',   
         'GENERAL': 'general_assistant'}  
    )  

    builder.set_entry_point("router")  
    builder.add_edge('database_expert', END)  
    builder.add_edge('langchain_expert', END)  
    builder.add_edge('general_assistant', END)  
    graph = builder.compile(checkpointer=memory)

现在,我们可以用几个问题来测试一下这个设置的表现如何。

    thread = {"configurable": {"thread_id": "2"}}  
    results = []  
    for s in graph.stream({  
      'question': "我们在电商数据库的users表中有哪些信息?",  
    }, thread):  
      print(s)  
      results.append(s)  
    print(results[-1]['database_expert']['answer'])  

    # ecommerce_db.users表包含以下列:  
    # 1. **用户ID**:每个用户的唯一标识符。  
    # 2. **国家**:用户所在的国家。  
    # 3. **是否活跃**:一个标志,表示用户是否活跃(1表示活跃,0表示不活跃)。  
    # 4. **年龄**:用户的年龄。  
    # 以下是表中的一些示例条目:  
    #   
    # | 用户ID | 国家        | 是否活跃 | 年龄 |  
    # |---------|----------------|-----------|-----|  
    # | 1000001 | 英国          | 0         | 70  |  
    # | 1000002 | 法国         | 1         | 87  |  
    # | 1000003 | 法国         | 1         | 88  |  
    # | 1000004 | 德国         | 1         | 25  |  
    # | 1000005 | 德国         | 1         | 48  |  
    #   
    # 这提供了表中可用用户数据的概览。

做得好!它对数据库相关的问题给出了一个相关的结果。让我们试着问一下关于LangChain的问题。


    thread = {"configurable": {"thread_id": "42"}}  
    results = []  
    for s in graph.stream({  
        'question': "LangChain 是否支持 Ollama?",  
    }, thread):  
        print(s)  
        results.append(s)  

    print(results[-1]['langchain_expert']['answer'])  

    # 是的,LangChain 支持 Ollama。Ollama 允许你本地运行开源大型语言模型,例如 Llama 2,并且 LangChain 提供了一个灵活的框架将这些模型集成到应用程序中。你可以使用 LangChain 与 Ollama 运行的模型进行交互,并且有特定的包装器和工具可用于此集成。   
    #   
    # 为了获取更多详细信息,你可以访问以下资源:  
    # - [LangChain 和 Ollama 集成](https://js.langchain.com/v0.1/docs/integrations/llms/ollama/)  
    # - [ChatOllama 文档](https://js.langchain.com/v0.2/docs/integrations/chat/ollama/)  
    # - [关于 Ollama 和 LangChain 的 Medium 文章](https://medium.com/@abonia/ollama-and-langchain-run-llms-locally-900931914a46)

太棒了!一切运行良好,很明显Tavily的搜索对于LLM应用非常有效。

添加人机交互

我们已经出色地完成了一个回答问题的工具。然而,在许多情况下,让人类参与批准建议的操作或提供额外反馈是有益的。让我们添加一个步骤,在返回最终结果给用户之前,先收集人类的反馈。

最简单的方法是添加两个额外的节点:

  • 一个 human 节点用于收集反馈,
  • 一个 editor 节点用于重新审视答案,考虑反馈。

让我们创建这些节点:

  • Human节点: 这将是一个虚拟节点,不会执行任何操作。
  • Editor节点: 这将是一个LLM模型,它接收所有相关信息(客户问题、草稿答案和提供的反馈),并修订最终答案。
    def human_feedback_node(state: MultiAgentState):  
        pass  

    editor_prompt = '''你是一名编辑,你的目标是根据客户的反馈提供最终答案。  
    你不会添加任何自己的信息。请使用友好且专业的语气。  
    在输出中,请提供给客户的最终答案,不要添加额外的评论。  
    以下是所需的所有信息。  

    客户的问题:  
    ----  
    {question}  
    ----  
    草稿答案:  
    ----  
    {answer}  
    ----  
    反馈:  
    ----  
    {feedback}  
    ----  
    '''  

    def editor_node(state: MultiAgentState):  
      messages = [  
        SystemMessage(content=editor_prompt.format(question = state['question'], answer = state['answer'], feedback = state['feedback']))  
      ]  
      model = ChatOpenAI(model="gpt-4o-mini")  
      response = model.invoke(messages)  
      return {"answer": response.content}  

让我们将这些节点添加到我们的图中。此外,我们还需要在human节点之前引入一个中断,以确保流程暂停以等待人类反馈。

    builder = StateGraph(MultiAgentState)  
    builder.add_node("router", router_node)  
    builder.add_node('database_expert', sql_expert_node)  
    builder.add_node('langchain_expert', search_expert_node)  
    builder.add_node('general_assistant', general_assistant_node)  
    builder.add_node('human', human_feedback_node)  
    builder.add_node('editor', editor_node)  

    builder.add_conditional_edges(  
      "router",   
      route_question,  
      {'DATABASE': 'database_expert',   
      'LANGCHAIN': 'langchain_expert',   
      'GENERAL': 'general_assistant'}  
    )  

    builder.set_entry_point("router")  

    builder.add_edge('database_expert', 'human')  
    builder.add_edge('langchain_expert', 'human')  
    builder.add_edge('general_assistant', 'human')  
    builder.add_edge('human', 'editor')  
    builder.add_edge('editor', END)  
    graph = builder.compile(checkpointer=memory, interrupt_before = ['human'])

现在,当我们运行图时,执行将在人类节点之前停止。

    thread = {"configurable": {"thread_id": "2"}}  

    for event in graph.stream({  
        'question': "ecommerce_db.users 表中的字段类型有哪些?",  
    }, thread):  
        print(event)  

    # {'question_type': 'DATABASE', 'question': 'ecommerce_db.users 表中的字段类型有哪些?'}  
    # {'router': {'question_type': 'DATABASE'}}  
    # {'database_expert': {'answer': '`ecommerce_db.users` 表包含以下字段:\n\n1. **user_id**: UInt64\n2. **country**: String\n3. **is_active**: UInt8\n4. **age**: UInt64'}}

让我们获取客户输入,并用反馈更新状态。

    user_input = input("答案中需要修改什么?")  
    # 答案中需要修改什么?   
    # 看起来很棒。你能让它更友好一些吗?  

    graph.update_state(thread, {"反馈": user_input}, as_node="human")

我们可以检查状态以确认反馈已填充,并且序列中的下一个节点是 editor

    print(graph.get_state(thread).values['反馈'])  
    # 看起来很棒。能否让它更友好一些呢?  

    print(graph.get_state(thread).next)  
    # ('编辑器',)

我们可以继续执行。将 None 作为输入传入会从暂停的位置恢复进程。

    for event in graph.stream(None, thread, stream_mode="values"):  
      print(event)  

    print(event['answer'])  

    # 瑞典语!`ecommerce_db.users` 表有以下字段:  
    # 1. **user_id**: UInt64  
    # 2. **country**: String  
    # 3. **is_active**: UInt8  
    # 4. **age**: UInt64  
    # 祝你有美好的一天!

编辑采纳了我们的反馈,在最终消息中添加了一些礼貌用语。这真是一个很棒的结果!

我们可以通过为我们的编辑器配备Human工具来以更代理的方式实现人机交互。

让我们调整一下编辑器。我稍微修改了一下提示,并将工具添加到了代理中。

    from langchain_community.tools import HumanInputRun  
    human_tool = HumanInputRun()  

    editor_agent_prompt = '''你是编辑,你的目标是根据初始问题为客户提供最终答案。  
    如果你需要任何澄清或需要反馈,请使用human。在最终答案之前,总是先向human寻求反馈。  
    你不会添加任何自己的信息。你使用友好和专业的语气。  
    在输出中,请提供给客户的最终答案,不要添加额外的评论。  
    以下是你需要的所有信息。  

    客户的问题:  
    ----  
    {question}  
    ----  
    草稿答案:  
    ----  
    {answer}  
    ----  
    '''  

    model = ChatOpenAI(model="gpt-4o-mini")  
    editor_agent = create_react_agent(model, [human_tool])  
    messages = [SystemMessage(content=editor_agent_prompt.format(question = state['question'], answer = state['answer']))]  
    editor_result = editor_agent.invoke({"messages": messages})  

    # 草稿答案是否完整且准确地回答了客户关于ecommerce_db.users表字段类型的问题?  
    # 是的,但能否让它更友好一些。  

    print(editor_result['messages'][-1].content)  
    # ecommerce_db.users表包含以下字段:  
    # 1. **user_id**: UInt64  
    # 2. **country**: String  
    # 3. **is_active**: UInt8  
    # 4. **age**: UInt64  
    #   
    # 如果你有任何其他问题,请随时提问!

所以,编辑向人类提出了一个问题:“草稿中的回答是否完整且准确地回答了客户关于ecommerce_db.users表字段类型的问题?”。在收到反馈后,编辑改进了回答,使其更易于用户理解。

让我们更新我们的主图,用新的代理替换原来的两个单独节点。通过这种方法,我们就不再需要中断了。

    def editor_agent_node(state: MultiAgentState):  
      model = ChatOpenAI(model="gpt-4o-mini")  
      editor_agent = create_react_agent(model, [human_tool])  
      messages = [SystemMessage(content=editor_agent_prompt.format(question = state['question'], answer = state['answer']))]  
      result = editor_agent.invoke({"messages": messages})  
      return {'answer': result['messages'][-1].content}  

    builder = StateGraph(MultiAgentState)  
    builder.add_node("router", router_node)  
    builder.add_node('database_expert', sql_expert_node)  
    builder.add_node('langchain_expert', search_expert_node)  
    builder.add_node('general_assistant', general_assistant_node)  
    builder.add_node('editor', editor_agent_node)  

    builder.add_conditional_edges(  
      "router",   
      route_question,  
      {'DATABASE': 'database_expert',   
       'LANGCHAIN': 'langchain_expert',   
       'GENERAL': 'general_assistant'}  
    )  

    builder.set_entry_point("router")  

    builder.add_edge('database_expert', 'editor')  
    builder.add_edge('langchain_expert', 'editor')  
    builder.add_edge('general_assistant', 'editor')  
    builder.add_edge('editor', END)  

    graph = builder.compile(checkpointer=memory)  

    thread = {"configurable": {"thread_id": "42"}}  
    results = []  

    for event in graph.stream({  
      'question': "电商数据库ecommerce_db.users表中的字段类型有哪些?",  
    }, thread):  
      print(event)  
      results.append(event)

这个图的工作方式与之前的图类似。我个人更喜欢这种方法,因为它利用了工具,使解决方案更加灵活。例如,代理可以多次与人类互动,并根据需要完善问题。

就这样。我们建立了一个多代理系统,能够回答不同领域的提问并考虑人类的反馈。

你可以在 GitHub 上找到完整的代码。

概要

在本文中,我们探讨了 LangGraph 库及其在构建单个和多代理工作流中的应用。我们已经考察了它的各种能力,现在是时候总结它的优缺点了。此外,将 LangGraph 与我们在 我之前的博客文章 中讨论的 CrewAI 进行比较也将是有用的。

总体而言,我发现LangGraph是一个构建复杂LLM应用的强大框架:

  • LangGraph 是一个低级框架,提供了广泛的自定义选项,允许你构建你需要的任何东西。
  • 由于 LangGraph 是基于 LangChain 构建的,因此它可以无缝地集成到其生态系统中,使其轻松利用现有的工具和组件。

然而,LangGraph 在某些方面仍有改进的空间:

  • LangGraph 的灵活性伴随着较高的入门门槛。虽然你可以在 15-30 分钟内理解 CrewAI 的概念,但要熟悉并掌握 LangGraph 则需要一些时间。
  • LangGraph 提供了更高的控制级别,但缺少 CrewAI 的一些酷炫的预构建功能,例如 协作 或即用型 RAG 工具。
  • LangGraph 并不像 CrewAI 那样强制执行最佳实践(例如角色扮演或护栏),因此可能会导致较差的结果。

我认为CrewAI对于新手和常见用例来说是一个更好的框架,因为它能帮助你快速获得好的结果,并提供指导以防止犯错。

如果你想要构建一个高级应用并需要更多控制,那么LangGraph就是你的选择。请记住,你需要投入时间来学习LangGraph,并且需要对最终解决方案负全责,因为该框架不会提供指导来帮助你避免常见错误。

非常感谢您阅读这篇文章。希望这篇文章对您有所启发。如果您有任何后续问题或评论,请在评论区留言。

参考资料

本文受到来自 DeepLearning.AI 的 “LangGraph 中的 AI 代理” 短课程的启发。

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