在构建复杂的 AI 应用时,我们常常需要将任务分解为多个独立的、可控的模块。传统的单体模型往往难以胜任这种精细化的需求。本文将深入探讨如何使用 LangGraph 这一强大的框架,结合 Groq 提供的 LLaMA 3 大模型,以及 LangChain 工具,构建一个高效且可扩展的 多 Agent 助手。我们将从 LangGraph 的核心概念入手,逐步解析其组件,并通过代码示例,展示如何一步步构建一个能够处理数学计算、网络搜索和礼貌道别的 多 Agent 助手

LangGraph:构建对话蓝图的核心思想

LangGraph 的核心思想是将复杂的对话流程拆解为一系列独立的节点 (Nodes),每个节点负责执行特定的任务。我们可以将其比作一个对话蓝图,其中每个转折点(节点)都有明确的目的,而节点之间的流动方向则由用户的输入或模型的预测来决定。不同于编写单一冗长的逻辑链,LangGraph 允许我们将系统分解为多个独立的模块,并负责管理状态和决策流程。这种模块化的设计使得开发、调试和扩展 AI 助手变得更加容易。 例如,一个客户服务机器人可以被分解为以下几个节点:

  • 问候节点: 负责与用户建立初步联系,询问其需求。
  • 意图识别节点: 利用 LLaMA 3 等模型识别用户的意图,例如“查询订单状态”、“修改收货地址”等。
  • 订单查询节点: 调用数据库 API 查询用户的订单信息。
  • 地址修改节点: 调用地址修改 API 修改用户的收货地址。
  • 结束节点: 结束对话,感谢用户的支持。

通过 LangGraph,我们可以轻松地将这些节点连接起来,构建一个完整的客户服务流程。

关键概念解析:状态、节点、边和监督器

要理解 LangGraph 的工作原理,需要掌握以下几个关键概念:

  • 状态 (State): 可以将状态想象成一个背包,它存储着对话过程中产生的所有信息,例如用户的消息、助手的回复、决策标志等。每个 LangGraph 中的节点都会接收这个背包,进行处理(例如添加新消息),然后将更新后的状态传递给下一个节点。 状态的管理对于构建复杂的对话流程至关重要,它确保了每个节点都能访问到必要的信息,从而做出正确的决策。例如,在一个电商助手中,状态可以包含用户的购物车信息、浏览历史、收货地址等。

  • 节点 (Node): 一个节点就是一个简单的函数。它接收状态作为输入,执行特定的任务,然后返回更新后的状态。 节点是 LangGraph 中最基本的单元,每个节点都应该专注于完成一个明确的任务,例如数学计算、网络搜索或数据库查询。例如,文章中提供的 calculator 函数就是一个节点,它接收包含数学表达式的状态,计算结果并返回包含结果的状态。

  • 边 (Edge): 边连接两个节点,定义了节点之间的流动方向。边可以是简单的,例如总是从节点 A 流向节点 B;也可以是条件性的,根据状态的值来决定下一个节点。 条件边允许我们构建智能的路由逻辑,让系统根据用户的输入或模型的预测,动态地选择下一个执行的节点。例如,在一个医疗助手中,可以根据用户的症状描述,选择不同的诊断节点。

  • 监督器 (Supervisor) / 路由器 (Router): 监督器是 AI 助手的“大脑”。它分析用户的输入,并对其进行分类,例如:这是一个数学问题?一个一般性查询?还是仅仅是用户在说“谢谢”? 基于分类结果,监督器选择下一个执行的节点。 在文章的例子中,监督器利用 LLaMA 3 模型对用户的输入进行分类,并根据分类结果选择 math_node、search_node 或 farewell 节点。 监督器的准确性直接影响到 AI 助手的性能,因此需要精心设计其提示语和分类逻辑。

  • 入口 (Entry) 和出口 (Exit): 每个图都需要一个起始点(称为入口节点)。并且每个流程在某个时候结束。 LangGraph 提供了一个 END 标记来在路径完成后干净地完成。

优势:模块化、可扩展性、透明性和可控性

LangGraph 的设计带来了诸多优势:

  • 模块化 (Modularity): 每个函数(节点)都专注于完成一个简单的任务,降低了代码的复杂度,提高了可维护性。
  • 可扩展性 (Scalability): 可以轻松地添加新的查询类型或行为,而无需修改现有的代码。 例如,我们可以轻松地添加一个新的节点来处理用户上传的图片,或者添加一个新的节点来提供个性化的推荐。
  • 透明性 (Transparency): 可以可视化和调试流程,更好地理解系统的行为。 LangGraph 提供了可视化工具,可以清晰地展示节点之间的连接关系和状态的流动方向,帮助开发者快速定位问题。
  • 可控性 (Control): 可以显式地定义逻辑在何处发生,而不是依赖于一个模型来神奇地完成所有事情。 这种控制力使得开发者可以更好地掌握系统的行为,并进行精细化的调整。

代码实践:构建一个多 Agent 助手

接下来,我们将通过代码示例,展示如何使用 LangGraphLLaMA 3LangChain 工具构建一个 多 Agent 助手

步骤 1:导入必要的库

import os
import operator
import re
from typing import TypedDict, Literal, Annotated
import langgraph
from langgraph.graph import StateGraph, END
from langchain_groq import ChatGroq
from langchain_core.messages import HumanMessage, AIMessage, SystemMessage
from langchain.tools import tool
from langchain_community.tools.tavily_search import TavilySearchResults
from pydantic import BaseModel

这段代码导入了构建 多 Agent 助手 所需的各种库,包括:

  • os: 用于访问环境变量,例如 API 密钥。
  • re: 用于使用正则表达式处理数学表达式。
  • typing: 用于定义 LangGraph 状态的严格模式及其合并行为。
  • langgraph: 提供定义图(节点 + 流程)的工具。
  • langchain_groq: 允许调用 Groq 托管的模型,例如 LLaMA 3
  • langchain_core.messages: 用于模拟对话过程中交换的消息。
  • langchain.tools.tool: 用于将任何自定义函数注册为 LangChain 工具。
  • langchain_community.tools.tavily_search: 用于使用 Tavily API 获取网络搜索结果的内置 LangChain 工具。
  • pydantic: 用于定义具有模型输出验证的模式(例如来自路由器的结构化 JSON)。

步骤 2:定义共享状态

class AgentState(TypedDict):
    messages: Annotated[list, operator.add]
    step: str

这段代码定义了在节点之间传递的共享状态。messages 存储消息列表(例如聊天记录)。Annotated[list, operator.add] 告诉 LangGraph 如何在需要时组合消息(通过附加)。step 存储一个字符串,例如 “math_node” 或 “search_node”,告诉图接下来应该执行哪个节点。

步骤 3:加载 LLaMA 3 模型

groq_model = ChatGroq(
    model="llama3-70b-8192",
    temperature=0.2,
    api_key=os.getenv("GROQ_API_KEY")
)

这段代码初始化一个 Groq 客户端,用于与 LLaMA 3 模型进行通信。temperature 参数设置为较低的值,以确保确定性行为(这对于路由非常有用)。它使用存储在环境变量中的 API 密钥。

步骤 4:定义工具

@tool
def calculator(expression: str) -> str:
    """Calculates a simple math expression."""
    try:
        expression = expression.strip()
        expression = expression.replace(' ', '')
        numbers = re.findall(r'\d+', expression)
        numbers = [int(number) for number in numbers]
        operator_match = re.search(r'[+\-*\/]', expression)
        operator = operator_match.group(0) if operator_match else None
        if not operator:
            return "Error: No operator found in the expression."
        if operator == '+':
            result = numbers[0] + numbers[1]
        elif operator == '-':
            result = numbers[0] - numbers[1]
        elif operator == '*':
            result = numbers[0] * numbers[1]
        elif operator == '/':
            if numbers[1] == 0:
                return "Error: Division by zero."
            result = numbers[0] / numbers[1]
        return str(result)
    except Exception as e:
        return f"Error: {str(e)}"


try:
    tavily_search = TavilySearchResults(max_results=2)
    TAVILY_AVAILABLE = True
except:
    TAVILY_AVAILABLE = False

@tool
def fallback_search(query: str) -> str:
    """A fallback search tool when Tavily API key is not configured."""
    return f"I would search for '{query}' but Tavily API key is not configured."

这段代码定义了两个工具:calculator 用于计算数学表达式,tavily_search 用于进行网络搜索。如果 Tavily API 密钥未配置,则使用 fallback_search 作为替代方案。

步骤 5:定义路由器模式和提示语

ROUTER_PROMPT = """Classify the user's input and pick one category:
- If the user asks a math question, select "math_node".
- If the user asks a real-world question, select "search_node".
- If the user says goodbye, select "farewell".
"""

class Router(BaseModel):
    next: Literal["search_node", "math_node", "farewell"]
    reasoning: str

这段代码定义了路由器模式和提示语。ROUTER_PROMPT 用于指导模型对用户的输入进行分类。Router 类定义了模型返回的 JSON 结构,包括 next (下一个执行的节点) 和 reasoning (选择该节点的理由)。

步骤 6:定义节点

def supervisor_node(state: AgentState):
    messages = state["messages"]
    response = groq_model.with_structured_output(Router).invoke({"messages": [
        SystemMessage(content=ROUTER_PROMPT),
        *messages
    ]})
    return {"step": response.next}

def math_node(state: AgentState):
    messages = state["messages"]
    query = messages[-1].content
    result = calculator.invoke({"expression": query})
    response = f"The answer is: {result}"
    return {"messages": [AIMessage(content=response)], "step": "farewell"}

def search_node(state: AgentState):
    messages = state["messages"]
    query = messages[-1].content
    if not TAVILY_AVAILABLE:
        results = fallback_search.invoke({"query": query})
    else:
        results = tavily_search.invoke({"query": query})
    response = f"Here are the search results: {results}"
    return {"messages": [AIMessage(content=response)], "step": "farewell"}

def farewell_node(state: AgentState):
    return {"messages": [AIMessage(content="Goodbye!")]}

这段代码定义了四个节点:supervisor_node (监督器节点),math_node (数学计算节点),search_node (网络搜索节点) 和 farewell_node (道别节点)。每个节点都接收状态作为输入,执行特定的任务,然后返回更新后的状态。

步骤 7:设置 LangGraph 并定义流程

def route_supervisor(state: AgentState):
    return state["step"]

graph = StateGraph(AgentState)
graph.set_entry_point("supervisor")
graph.add_node("supervisor", supervisor_node)
graph.add_node("math_node", math_node)
graph.add_node("search_node", search_node)
graph.add_node("farewell", farewell_node)

graph.add_conditional_edges(
    "supervisor", route_supervisor,
    {
        "math_node": "math_node",
        "search_node": "search_node",
        "farewell": "farewell"
    }
)

graph.add_edge("math_node", "farewell")
graph.add_edge("search_node", "farewell")
graph.add_edge("farewell", END)

app = graph.compile()

这段代码设置 LangGraph 并定义流程。route_supervisor 函数根据状态中的 step 字段选择下一个执行的节点。graph 对象是 LangGraph 的核心,它将所有节点和边连接起来。app 对象是编译后的 LangGraph 应用,可以被调用来执行对话流程。

步骤 8:运行助手

initial_state = {
    "messages": [HumanMessage(content="What's 15*9?")],
    "step": "supervisor"
}

result = app.invoke(initial_state)

这段代码定义了初始状态,并调用 app.invoke() 方法来运行助手。

步骤 9:打印输出

print("\n--- Conversation ---")
for msg in result["messages"]:
    print(f"{msg.__class__.__name__}: {msg.content}")

这段代码打印对话历史记录,以便理解助手的工作过程。

总结:LangGraph 的未来

通过本文的介绍,我们了解了如何使用 LangGraphLLaMA 3LangChain 工具构建一个 多 Agent 助手LangGraph 提供了一种模块化、可扩展和可控的方式来构建复杂的 AI 应用。随着大模型技术的不断发展,LangGraph 将在构建更加智能、更加个性化的 AI 助手方面发挥越来越重要的作用。 我们期待 LangGraph 在未来能够支持更多的特性,例如:

  • 更强大的可视化工具: 提供更丰富的可视化功能,帮助开发者更好地理解和调试复杂的对话流程。
  • 更灵活的路由机制: 支持更复杂的路由规则,例如基于上下文的路由和基于用户角色的路由。
  • 更丰富的节点类型: 提供更多预定义的节点类型,例如数据库查询节点、API 调用节点和自然语言生成节点。

通过不断地完善和发展,LangGraph 有望成为构建下一代 AI 应用的标准框架。