DLAI-LangGraph-智能体笔记-全-

DLAI LangGraph 智能体笔记(全)

001:引言 🚀

在本节课中,我们将要学习什么是AI智能体以及LangGraph如何帮助我们构建它们。我们将了解智能体工作流的核心概念,并与传统的单次生成式AI应用进行对比。


智能体工作流概述

传统的语言模型应用通常是一次性生成完整内容。例如,你给出一个提示,模型就从头到尾写出一篇文章。然而,人类的工作方式并非如此。我们更擅长通过迭代协作来完成任务。

想象一下,我们几个人要合作写一篇论文。这个过程可能如下:

  1. 我先进行规划,制定论文的初步大纲。
  2. 接着,有人进行研究,运行查询并收集相关资料。
  3. 然后,由我来撰写初稿
  4. 之后,其他人会审阅初稿,提出建设性意见。
  5. 最后,根据反馈进行修改,并可能再次进行研究。

这种循环往复、各司其职的工作方式,就是智能体工作流的一个生动例子。它通过多个步骤的迭代,最终产出一个更高质量的工作成果。


智能体工作流的关键设计模式

基于上述例子,我们可以总结出构建智能体工作流的几个核心设计模式。以下是这些模式的简要介绍:

  • 规划:思考需要采取的步骤,例如制定大纲和后续行动计划。
  • 工具使用:了解有哪些工具可用以及如何使用它们,例如搜索工具。
  • 反思:指迭代改进结果的过程,可能涉及多个语言模型进行评审并提出有用建议,以驱动编辑循环。
  • 多智能体协作:你可以将每个参与者视为扮演特定角色的语言模型,每个角色都有独特的提示词,在此过程中发挥独特作用。
  • 记忆:跟踪多个步骤中的进展和结果。

这些能力中,一部分与语言模型本身相关(如用于工具调用的函数调用功能),但许多能力实际上是由它们所运行的框架来实现的。


LangChain与LangGraph的演进

LangChain框架早已支持上述的许多元素,例如多种形式的记忆、支持函数调用的语言模型以及工具执行。然而,为了更好地支持上述的循环图式智能体工作流,LangChain近期扩展并推出了LangGraph

LangGraph能够直接支持构建此类智能体。目前已有多种智能体范式得到了更好的支持,例如:

  • ReAct智能体:一种早期的智能体构建范式,ReAct代表“推理与行动”。
  • Self-Refine示例:实现了上文提到的迭代优化过程。
  • AI Codium示例:通过流程工程创建代码智能体。

所有这些智能体的行为都由一个循环图来定义。


本课程内容预告

在上一节我们介绍了智能体工作流的基本概念,本节中我们来看看本课程将具体涵盖哪些内容。

在本课程中,你将:

  1. 从零开始,仅使用一个语言模型和Python构建一个智能体。
  2. 学习LangGraph的组件,并通过直接使用这些组件重建同一个智能体来加深理解。
  3. 鉴于搜索工具是许多智能体应用的重要组成部分,你将学习智能体搜索的功能以及如何使用它。
  4. 探索构建智能体时的两项重要能力:
    • 接收人工输入:允许你在关键时刻引导智能体。
    • 持久化:存储当前状态信息的能力,以便日后可以返回。这对于调试智能体和将其投入生产环境都非常有用。
  5. 使用LangGraph构建一个项目(具体内容将在最后一课揭晓)。
  6. 在课程结束时,了解这项技术的一些酷炫应用和未来发展方向。

本节课中,我们一起学习了智能体工作流的核心思想及其相较于传统单次生成模式的优势,并预览了使用LangGraph构建智能体的课程路线图。接下来,让我们进入下一课,开始动手实践。

002:2. 从零构建简单的ReAct智能体 🧠

概述

在本节课中,我们将从零开始构建一个智能体。你将看到,虽然智能体可以执行相当复杂的任务,但一个基础的智能体实际上并不难构建。在构建过程中,注意区分哪些工作由大语言模型(LLM)完成,哪些由围绕LLM的代码(我们称之为运行时)管理,这将非常有帮助。

构建基础智能体类

首先,我们需要导入所有必要的库并初始化语言模型。我们将使用OpenAI的模型。

import openai

# 初始化OpenAI客户端
client = openai.OpenAI()

测试模型以确保其正常工作。

response = client.chat.completions.create(
    model="gpt-4",
    messages=[{"role": "user", "content": "hello world"}],
    temperature=0
)
print(response.choices[0].message.content)

接下来,我们创建一个基础的智能体类。这个类将由一个系统消息参数化,并维护一个消息列表来记录交互历史。

class Agent:
    def __init__(self, system_message=""):
        self.system_message = system_message
        self.messages = []
        if self.system_message:
            self.messages.append({"role": "system", "content": self.system_message})

    def __call__(self, message):
        self.messages.append({"role": "user", "content": message})
        result = self._execute()
        self.messages.append({"role": "assistant", "content": result})
        return result

    def _execute(self):
        response = client.chat.completions.create(
            model="gpt-4",
            messages=self.messages,
            temperature=0
        )
        return response.choices[0].message.content

实现ReAct模式智能体

上一节我们介绍了基础的智能体框架,本节中我们来看看如何实现ReAct模式。ReAct代表“推理(Reasoning)”加“行动(Acting)”。在这个模式中,LLM首先思考要做什么,然后决定采取什么行动。该行动在环境中执行,并返回一个观察结果。LLM根据这个观察结果重复这个过程,直到决定任务完成。

首先,我们需要一个特定的系统提示词来指导智能体遵循ReAct循环。

REACT_PROMPT = """
你将以一个循环运行:思考 -> 行动 -> 暂停 -> 观察。
当你完成循环后,输出一个答案。
使用“思考:”来描述你对被问到问题的想法。
使用“行动:”来运行一个你可用的行动。
然后返回“暂停”。
之后,“观察:”将用于表示运行那些行动的结果。

你可用的行动是:
1. `calculate`:计算一个数学表达式。输入:一个字符串形式的数学表达式。
2. `average_dog_weight`:获取一个犬种的平均体重。输入:犬种名称。

示例:
问题:一个玩具贵宾犬有多重?
思考:我应该使用`average_dog_weight`工具查找玩具贵宾犬的体重。
行动:average_dog_weight toy poodle
暂停
观察:7
思考:现在我有了答案。
答案:一个玩具贵宾犬重7磅。
"""

现在,我们需要提供上面提到的两个工具函数。

def calculate(expression):
    """计算一个数学表达式。"""
    return str(eval(expression))

def average_dog_weight(breed):
    """获取犬种的平均体重(模拟数据)。"""
    weights = {
        "scottish terrier": "20",
        "border collie": "37",
        "toy poodle": "7"
    }
    return weights.get(breed.lower(), "未知品种")

# 创建工具字典
tools = {
    "calculate": calculate,
    "average_dog_weight": average_dog_weight
}

手动运行智能体

让我们初始化智能体并尝试提出第一个问题。

agent = Agent(system_message=REACT_PROMPT)
response1 = agent("一个玩具贵宾犬有多重?")
print(response1)

输出可能类似于:

思考:我应该使用`average_dog_weight`工具查找玩具贵宾犬的体重。
行动:average_dog_weight toy poodle
暂停

根据输出,我们需要执行average_dog_weight行动。

# 解析行动和输入
# 假设我们从响应中解析出行动是“average_dog_weight”,输入是“toy poodle”
action = "average_dog_weight"
action_input = "toy poodle"
observation = tools[action](action_input)
print(f"观察:{observation}")

然后,我们将观察结果格式化为下一个提示,传递给智能体。

next_prompt = f"观察:{observation}"
response2 = agent(next_prompt)
print(response2)

这次,智能体应该输出最终答案。

自动化ReAct循环

手动执行每个步骤很繁琐。让我们将整个过程自动化到一个循环中。

首先,我们需要一个正则表达式来解析LLM的响应,判断是要求取行动还是输出最终答案。

import re

# 用于解析“行动:<工具名> <输入>”模式的正则表达式
action_re = re.compile(r'行动:(\w+)\s*(.*)')

接下来,我们创建一个query函数来运行自动化的ReAct循环。

def query(question, max_turns=10):
    """
    使用ReAct模式自动查询智能体。
    :param question: 用户的问题
    :param max_turns: 最大循环次数,防止无限循环
    :return: 最终答案
    """
    agent = Agent(system_message=REACT_PROMPT)
    next_prompt = question
    counter = 0

    while counter < max_turns:
        counter += 1
        print(f"\n--- 第 {counter} 轮 ---")
        response = agent(next_prompt)
        print(f"智能体响应:{response}")

        # 检查响应是否以“答案:”开头,表示结束
        if response.strip().startswith("答案:"):
            print("任务完成。")
            return response

        # 否则,尝试解析行动
        match = action_re.search(response)
        if match:
            action_name = match.group(1)
            action_input = match.group(2).strip()
            print(f"解析到行动:{action_name}, 输入:{action_input}")

            if action_name not in tools:
                raise ValueError(f"未知行动:{action_name}")

            # 执行行动
            observation = tools[action_name](action_input)
            print(f"观察结果:{observation}")
            # 为下一轮创建提示
            next_prompt = f"观察:{observation}"
        else:
            # 如果没有解析到行动,也没有答案,可能出错了
            print("警告:无法解析响应中的行动。")
            next_prompt = "请根据之前的步骤继续思考或给出答案。"

    print(f"达到最大轮数({max_turns}),未完成。")
    return "无法在限制内得出结论。"

现在,让我们用更复杂的问题测试这个自动化智能体。

result = query("我有一只边境牧羊犬和一只苏格兰梗犬。它们的总体重是多少?")
print(f"\n最终结果:{result}")

总结

本节课中我们一起学习了如何从零开始构建一个ReAct模式的智能体。我们首先创建了一个基础的智能体类,然后设计了引导其进行“思考-行动-观察”循环的系统提示词。我们实现了两个简单的工具函数,并演示了如何手动执行循环步骤。最后,我们通过编写一个query函数,利用正则表达式解析和循环逻辑,将整个过程自动化。这个例子展示了仅使用原始LLM API和一些Python代码就能创建一个功能性的智能体。在下一课中,我们将使用LangGraph框架来实现这个相同的智能体。

003:LangGraph核心组件 🧩

在本节课中,我们将学习如何使用LangGraph的核心组件来构建一个智能体。我们将从零开始,逐步介绍提示模板、工具、状态管理以及如何用LangGraph编排控制流。


在上一课中,我们从头构建了一个智能体。现在,让我们使用LangGraph来实现这个智能体,并在此过程中介绍它的一些核心组件和功能。

回顾上节课内容

首先,我们来分解上节课所做的工作。

  1. 我们接收了一条用户消息。
  2. 我们有一个很长的系统提示。
  3. 我们使用这个提示调用了大语言模型。
  4. 模型输出类似“思考”和“行动”的内容。
  5. 基于输出,我们做出决策:要么返回最终答案,要么调用工具。
  6. 我们将所有这些逻辑放入一个大的循环函数中。
  7. 我们有两个可调用的工具:计算平均体重和搜索。
  8. 如果调用了工具,我们会得到一个观察结果,然后将其作为新消息放回提示中,继续循环。

接下来,我们将这些步骤映射到LangChain的组件中。

LangChain核心组件

提示模板

提示模板允许我们创建可复用的提示。这意味着我们可以创建一个带有格式化变量的字符串模板,并根据用户内容以不同方式填充这些变量。

例如,我们可以创建一个类似上节课使用的智能体提示模板。在LangChain Hub中可以看到许多社区贡献的提示模板示例。

工具

工具是智能体可以调用的函数。例如,我们将使用一个名为Tavily的搜索工具。它可以从langchain-community包中导入,该包包含数百个其他工具。

控制流与LangGraph

我们编写的应用程序中,大部分代码是用于管理循环的控制流。LangGraph可以帮助我们描述和编排这种控制流。具体来说,它允许我们创建循环图,这正是我们所需要的。

LangGraph还内置了持久化功能,这对于同时处理多个对话或记住之前的迭代和操作非常有用。这种持久化也支持了很棒的“人在回路”功能。

许多学术论文中的智能体图表都可以表示为图,正是这种认识促使我们创建了LangGraph。它是LangChain的扩展,专门用于智能体和多智能体工作流。关键在于,它允许非常精细的控制流,图中的每个箭头都精确地指明了从一个状态到下一个状态的路径。这种可控性对于创建高性能的智能体至关重要。

LangGraph核心概念

LangGraph有三个核心概念:节点、边和条件边。

  • 节点:代表智能体或函数。
  • :连接这些节点。
  • 条件边:当需要决定下一步应该前往哪个节点时使用。

让我们看一个例子,创建一个与上节课函数等效的LangGraph:

  1. 我们有一个智能体节点,代表调用大语言模型。
  2. 然后有一个条件边,它接收大语言模型的调用结果,并决定下一步做什么。
  3. 其中一条边可以是行动边,它调用一个函数节点,并自动循环回智能体节点。
  4. 有一个入口点,即开始的地方。
  5. 还有一个结束节点,是智能体之后可以采取的另一个行动。

理解状态

使用LangGraph时,最重要的是理解随时间跟踪的状态,通常称为智能体状态。这个状态在图的所有部分(每个节点、每条边)都可以访问。它是图本地的,并且可以存储在持久化层中,这意味着你可以在以后的任何时间点恢复该状态。

以下是两个状态示例:

  1. 简单状态:只包含一个消息列表。messages变量被注解为operator.add,这意味着当用新消息更新状态时,不会覆盖现有消息,而是将它们添加到该状态中。
  2. 复杂状态:可能包含inputchat_historyagent_outcomeintermediate_steps。其中intermediate_steps被注解为operator.add,因为我们需要在智能体执行过程中不断添加新的行动和观察记录。

代码实现

有了高层次的理解,让我们深入代码。

首先,加载所需的环境变量(如OpenAI API密钥),然后导入必要的模块来创建工具、智能体状态和LangGraph本身。

以下是需要导入的关键组件:

from langgraph.graph import StateGraph, END
from typing import TypedDict, Annotated, Sequence
import operator
from langchain_core.messages import BaseMessage, HumanMessage, AIMessage, SystemMessage
from langchain_openai import ChatOpenAI
from langchain_community.tools.tavily_search import TavilySearchResults
  • StateGraphEND 用于构建图。
  • TypedDictAnnotated 用于构造智能体状态。
  • BaseMessage 及其子类用于表示不同类型的消息。
  • ChatOpenAI 是LangChain对OpenAI API的包装,提供了一个标准接口。
  • TavilySearchResults 是我们将要使用的搜索工具。

创建工具和状态

我们初始化Tavily搜索工具,并设置最大返回结果数为2。工具有一个特定的名称,大语言模型将使用这个名称来调用它。

接着,我们创建智能体状态,它只是一个被注解的消息列表,我们会随时间向其中添加消息。

构建智能体类

我们将创建一个智能体类,它需要三个函数:一个用于调用OpenAI,一个用于检查是否存在要执行的动作,另一个用于执行该动作。这些将作为类的方法。

智能体类需要三个参数:要使用的模型、要调用的工具列表和系统消息。

我们首先初始化一个带有智能体状态的StateGraph。此时图是空的,没有任何节点或边。

我们知道需要创建三个函数(作为节点和边),所以先规划一下:

  1. 添加一个名为llm的节点,用于执行大语言模型调用。
  2. 添加一个名为action的节点,用于执行工具调用。
  3. 添加一个条件边,在llm节点之后检查是否存在动作。如果有,则前往action节点;如果没有,则前往END节点并结束。
  4. 添加一条从action节点回到llm节点的常规边。
  5. 设置图的入口点为llm节点。
  6. 完成所有设置后,编译图,将其转换为一个可运行的LangChain对象。

我们还将工具名称映射到工具本身,并调用模型的bind_tools方法,让模型知道它可以调用这些工具。

实现节点和边函数

现在,我们需要在智能体类上实现三个方法:

  1. call_open_ai (LLM节点):从状态中获取消息列表,添加系统消息,调用模型,并返回包含模型响应消息的字典。
  2. take_action (行动节点):从状态中获取最后一条消息(其中应包含工具调用信息),循环遍历每个工具调用,查找对应的工具并调用它,然后将结果作为工具消息返回。
  3. should_continue (条件边):检查最后一条消息中是否存在工具调用。如果有,返回True(前往行动节点);如果没有,返回False(前往结束节点)。

将这些方法分别设置为对应节点和边的函数。

使用智能体

创建好智能体后,我们可以使用它了。首先,我们可以可视化刚刚创建的图,这可以通过调用get_graph().draw_png()自动完成。

现在,让我们调用智能体。我们需要创建一个代表用户消息的HumanMessage,并将其放入一个消息列表中,因为智能体状态期望的messages属性是一个消息列表。

然后,我们可以使用agent_graph.invoke()调用智能体,并传入初始状态。

运行示例

让我们尝试几个问题:

  1. “旧金山天气如何?”:智能体会调用Tavily搜索工具获取天气信息,然后返回结果。
  2. “旧金山和洛杉矶的天气如何?”:智能体会并行调用两次搜索工具(分别查询两个城市),然后综合信息返回答案。这展示了并行工具调用的能力。
  3. “2024年超级碗冠军是谁?那个州的GDP是多少?”:智能体会先搜索超级碗冠军,得到结果(堪萨斯城酋长队,位于密苏里州)后,再搜索密苏里州的GDP。这展示了顺序工具调用,因为第二个查询依赖于第一个查询的结果。

通过这些例子,我们看到了如何将原始的大语言模型调用和Python代码,转换成一个能够回答复杂问题的真实智能体,其背后使用了Tavily搜索API。


本节课中,我们一起学习了LangGraph的核心组件,包括提示模板、工具、状态管理以及如何用节点、边和条件边来构建和控制智能体的工作流。我们还通过代码实现了一个功能完整的智能体,并看到了它处理串行和并行任务的能力。

在下一课中,我们将更深入地探索LangGraph的其他功能。

004:4.智能体搜索

概述

在本节课中,我们将要学习智能体搜索。我们将探讨智能体搜索与标准搜索有何不同,以及如何使用它。让我们开始吧。


智能体搜索的作用

在深入探讨智能体搜索的具体功能之前,让我们先理解一个智能体可能会如何使用它。

在零样本学习中,智能体会接收一个提示,并根据其模型的静态权重生成答案。尽管这种方法已被证明非常强大,但这个过程存在许多局限性。

首先,我们周围的数据是动态变化的。例如,智能体无法询问关于昨晚比赛的讨论。
其次,在许多应用场景中,我们希望知道结果中所提供信息的来源。这可以减少幻觉并平滑人机交互的摩擦。

观察幻灯片,我们可以看到提示被智能体接收,然后智能体决定调用搜索工具。接着,找到的信息被返回给智能体。


智能体搜索的内部机制

现在,让我展示其内部发生了什么。这是一个非常基础的搜索工具实现示例。让我们逐步分析。

如果智能体决定向搜索工具发送查询,第一步将是理解问题,并在需要时将其分解为子问题。这是一个重要的步骤,因为它可以处理复杂的查询。

然后,对于每个子查询,搜索工具必须从多个集成中选择最佳来源。例如,如果智能体询问“旧金山的天气如何”,搜索工具应使用天气API以获得最佳结果。

找到正确的来源后,工作并未结束。搜索工具随后必须仅提取与子查询相关的信息。一个基础的实现可以通过对来源进行分块,并运行快速的向量搜索来检索最相关的块来完成。

从每个来源检索数据后,搜索工具将对结果进行评分,并过滤掉相关性较低的信息。


实践测试

好的,让我们来测试一下。

首先,导入一些库并建立与搜索工具的初始连接。这里我们从环境变量中加载了Tavily API密钥。然后我们创建Tavily客户端,它是从tavily库中导入的。

# 示例代码:导入库和建立连接
import os
from tavily import TavilyClient

# 从环境变量加载API密钥
api_key = os.getenv('TAVILY_API_KEY')
# 创建Tavily客户端
client = TavilyClient(api_key=api_key)

建立初始连接后,让我们测试一下。这里我将运行一个搜索,询问关于新的黑曜石GPT的信息,看看答案是什么。

# 示例搜索查询
response = client.search(query="最新的黑曜石GPT是什么?")
print(response)

好的,正如你所见,这是一个相当简单但非常准确的答案。


标准搜索与智能体搜索的对比

接下来,让我们做一个简单的例子,看看常规搜索工具和智能体搜索工具之间的区别。我将创建一个关于特定地点天气的简单查询。你可以随意将地点更改为你的位置。我将以旧金山为例。

查询是:“旧金山现在的天气怎么样?我今天应该去那里旅行吗?”

现在,让我们尝试用常规搜索来处理。这里我将导入DuckDuckGo搜索,尝试运行常规搜索并获取可能引导我找到答案的链接。

# 示例:使用常规搜索(如DuckDuckGo)获取链接
# 注意:此处为概念性代码,实际库可能不同
from duckduckgo_search import DDGS

with DDGS() as ddgs:
    results = [r for r in ddgs.text("旧金山 当前 天气", max_results=5)]
    for result in results:
        print(result['href'])

好的,正如你所见,我们确实得到了结果,但这并不是智能体需要的东西。现在,我们需要从这些结果中获取一些答案。让我们来做这件事。

我们将创建一个函数,从第一个URL中抓取数据。我们将使用Beautiful Soup来提取HTML。

# 示例:使用Beautiful Soup抓取网页内容
import requests
from bs4 import BeautifulSoup

def scrape_url(url):
    response = requests.get(url)
    soup = BeautifulSoup(response.content, 'html.parser')
    # 提取文本,这里是一个简化示例
    text = soup.get_text()
    return text

# 假设我们获取了第一个链接
first_link = results[0]['href']
scraped_content = scrape_url(first_link)
print(scraped_content[:500]) # 打印前500个字符

正如你所见,输出内容很多。如果你想,可以继续向下滚动。但让我们尝试清理它。

为了清理输出,我将使用一些解析方法。我将提取标题和一些内容,将其剥离,并使用join来获取文本。

# 示例:清理抓取的文本
def clean_scraped_text(text):
    # 这里是一个简化的清理过程,实际中可能需要更复杂的处理
    lines = text.split('\n')
    cleaned_lines = [line.strip() for line in lines if line.strip()]
    cleaned_text = '\n'.join(cleaned_lines[:50]) # 取前50行作为示例
    return cleaned_text

cleaned_output = clean_scraped_text(scraped_content)
print(cleaned_output)

正如你所见,输出好多了,但仍然不够简洁。


使用智能体搜索工具

在看到常规搜索的结果后,让我们尝试使用智能体搜索工具来运行相同的查询。我们将调用Tavily来获取结果。

# 使用Tavily进行智能体搜索
agentic_response = client.search(query="旧金山现在的天气怎么样?我今天应该去那里旅行吗?")
print(agentic_response)

正如你所见,我们得到了一个简单的JSON,其中包含大量关于旧金山天气的信息。

让我们解析并高亮显示JSON,以便能清楚地看到它。

# 示例:解析和格式化JSON响应
import json

# 假设agentic_response是一个字典或JSON字符串
if isinstance(agentic_response, str):
    data = json.loads(agentic_response)
else:
    data = agentic_response

# 漂亮地打印JSON
formatted_json = json.dumps(data, indent=2, ensure_ascii=False)
print(formatted_json)

正如你所见,这不是作为人类我想看到的答案,但这正是智能体想要的结构化数据。


人类需求与智能体需求的差异

我将加载一个来自Google搜索的示例,以便我们看到差异。

在这里,我得到了作为人类我 exactly 想要的东西:一张漂亮的图片显示温度、湿度、风速,但没有不必要的数据。这正是人类需求和智能体需求之间的区别。


总结

本节课中,我们一起学习了智能体搜索。我们介绍了智能体搜索的基本概念,探讨了它与标准搜索的区别,并通过代码示例演示了其内部工作流程和实际应用。智能体搜索旨在为AI代理提供结构化、精确且来源可追溯的信息,这与人类用户通常需要的简洁、直观的答案形成了对比。

在下一节课中,Harrison将讨论持久化和流式处理。

005:持久化与流式处理

在本节课中,我们将学习如何为运行时间较长的智能体任务实现两个关键功能:持久化流式处理。持久化允许我们保存智能体在特定时间点的状态,以便后续恢复。流式处理则能让我们实时了解智能体正在执行的操作。这两个功能对于构建生产级应用至关重要。

持久化:保存智能体状态

上一节我们介绍了如何构建一个基础的智能体。本节中,我们来看看如何为其添加持久化能力,使其能够记住对话历史并在后续交互中恢复状态。

为了实现持久化,LangGraph引入了检查点的概念。检查点会在每个节点执行后保存状态。我们将使用一个简单的SQLite检查点存储器。

以下是创建并配置带持久化功能智能体的步骤:

  1. 导入必要的库并设置环境:与之前一样,我们首先导入所需模块并加载环境变量。
  2. 创建工具和智能体状态:定义我们之前用过的Tavily搜索工具和智能体状态结构。
  3. 初始化检查点存储器:我们将使用一个内存中的SQLite检查点存储器。请注意,重启笔记本后内存数据会消失,但在生产环境中可以轻松连接到外部数据库(如PostgreSQL或Redis)。
  4. 将检查点器传递给智能体:在编译智能体图时,将检查点器作为参数传入。

核心的配置代码如下所示:

# 初始化一个内存中的SQLite检查点器
checkpointer = SqliteSaver.from_conn_string(":memory:")

# 在编译智能体图时传入检查点器
agent = create_agent(llm, tools, checkpointer=checkpointer)

流式处理:实时观察智能体行为

配置好持久化后,我们现在可以添加流式处理功能。流式处理主要关注两方面:消息流令牌流。消息流让我们能看到智能体决定采取什么行动(AI消息)以及行动的结果(工具观察消息)。令牌流则允许我们实时看到大语言模型生成的每一个词。

首先,我们实现消息流。

为了使用流式处理并利用持久化的状态,我们需要引入线程配置的概念。线程配置是一个字典,其中包含一个可配置的thread_id键。这允许我们在同一个检查点器中管理多个独立的对话线程,模拟多用户场景。

以下是使用流式处理调用智能体的方法:

# 定义消息和线程配置
messages = {"messages": [HumanMessage(content="旧金山的天气怎么样?")]}
thread_config = {"configurable": {"thread_id": "1"}}  # 为此次对话设置一个线程ID

# 使用stream方法进行调用,并传入线程配置
stream_events = agent.stream(messages, thread_config)

# 遍历并打印流式事件
for event in stream_events:
    # 事件中包含状态更新,我们关注其中的消息
    if "messages" in event:
        print(event["messages"][-1])  # 打印最新的消息

运行上述代码,我们将看到一系列消息被流式输出:首先是AI决定调用天气工具的消息,然后是工具返回的天气结果消息,最后是AI总结的最终答案消息。

实践:多轮对话与线程隔离

现在,让我们利用持久化进行多轮对话。我们可以在同一个线程ID下提出后续问题。

# 继续线程ID为“1”的对话
follow_up_messages = {"messages": [HumanMessage(content="那洛杉矶呢?")]}
for event in agent.stream(follow_up_messages, {"configurable": {"thread_id": "1"}}):
    if "messages" in event:
        print(event["messages"][-1])

智能体会利用之前的对话历史,理解“洛杉矶”指的是天气,并给出回答。这证明了持久化的有效性。

为了展示线程ID的重要性,我们尝试使用一个新的线程ID提问:

# 使用新的线程ID“2”开启一个全新对话
new_thread_messages = {"messages": [HumanMessage(content="哪个更暖和?")]}
for event in agent.stream(new_thread_messages, {"configurable": {"thread_id": "2"}}):
    if "messages" in event:
        print(event["messages"][-1])

由于线程ID“2”没有历史记录,智能体会感到困惑,并要求用户明确比较对象。这清晰地说明了线程ID如何隔离不同的对话会话。

进阶:流式处理令牌

除了消息,我们还可以实时流式输出大语言模型生成的每一个令牌(词)。这需要使用异步方法和异步检查点器。

以下是实现令牌流式处理的步骤:

  1. 切换到异步检查点器:例如使用AsyncSqliteSaver
  2. 使用astream_events方法:这是一个异步方法,可以提供更底层的事件流。
  3. 筛选令牌事件:我们监听类型为on_chat_model_stream的事件,这些事件包含了新生成的令牌。

核心代码如下:

import asyncio

# 使用异步检查点器
async_checkpointer = AsyncSqliteSaver.from_conn_string(":memory:")
async_agent = create_agent(llm, tools, checkpointer=async_checkpointer)

# 定义异步函数来流式处理令牌
async def stream_tokens():
    messages = {"messages": [HumanMessage(content="巴黎的天气如何?")]}
    thread_config = {"configurable": {"thread_id": "async_1"}}

    async for event in async_agent.astream_events(messages, thread_config, version="v1"):
        # 筛选出聊天模型生成令牌的事件
        if event["event"] == "on_chat_model_stream":
            # 打印令牌内容,用“|”分隔以便观察
            content = event["data"]["chunk"].content
            if content:
                print(f"|{content}", end="", flush=True)

# 运行异步函数
await stream_tokens()

运行后,你将看到最终答案的令牌被一个一个地实时打印出来,中间由“|”符号分隔。在工具调用阶段可能没有内容流式输出,因为那只是一个函数调用指令。

总结

本节课中我们一起学习了为LangGraph智能体添加持久化流式处理功能。

  • 持久化通过检查点器实现,允许智能体保存状态,支持多轮对话和多个独立对话线程(通过thread_id管理),这是构建复杂生产应用的基础。
  • 流式处理提供了两种观察智能体工作的方式:
    • 消息流:让我们能看到智能体推理、行动和观察的完整步骤。
    • 令牌流:让我们能实时看到大语言模型生成答案的过程。

这些功能不仅提升了用户体验,使得长时间运行的任务可交互、可观察,也为实现“人在回路”等高级交互模式奠定了基础。在下一节课中,我们将具体探讨如何利用这些功能实现人机协同交互。

006:人机交互循环

概述

在本节课中,我们将学习如何在LangGraph智能体中引入“人机交互循环”。这意味着在智能体执行关键操作(如调用工具)之前,可以暂停并等待人工审核或干预。这能让你更好地监控和控制智能体的行为,确保其操作的正确性和安全性。


人机交互循环简介

在许多场景下,你可能希望将人类纳入循环,以监控智能体的行为。使用LangGraph可以轻松实现这一点。

上一节我们介绍了如何构建一个基础的智能体,本节中我们来看看如何为这个智能体添加人工审核的“暂停点”。

设置环境与状态

我们从上节课结束的地方继续。首先,设置环境变量,导入相关库,并配置检查点。

接下来,设置智能体的状态。这里我们需要做一个小修改。在之前的例子中,我们使用 operator.add 来注解消息列表,这会将新消息添加到现有数组中。然而,对于人机交互,我们可能需要替换现有的消息。

为了实现这一点,我们将编写一个自定义的 reducer 函数。该函数会检查新消息的ID,如果发现与已有消息ID相同,则替换它;否则,将其追加到列表末尾。

# 自定义reducer函数示例
def custom_reducer(existing_messages, new_messages):
    # 实现逻辑:根据ID替换或追加消息
    pass

之后,我们创建之前用过的相同工具和智能体。

添加执行前中断

我们将对智能体做一个小修改。在编译图时,除了传入检查点,我们还将传入 interrupts_before=[“action”] 参数。这将在调用“action”节点之前添加一个中断。

“action”节点是调用工具的地方。这样做的目的是,在运行任何工具之前,添加一个需要手动批准的步骤。这对于确保工具正确执行非常有用。

这个中断发生在调用所有工具的“action”节点之前。有时,你可能只想在调用特定工具时中断,这在本课程的其他部分有介绍,建议你稍后查看。

让我们用相同的系统提示、模型和检查点来初始化智能体。

# 编译图并添加中断
graph = graph.compile(checkpointer=checkpointer, interrupts_before=[“action”])

运行与中断演示

我们现在调用智能体,传入一个新的线程ID(例如 thread_id=1),因为这是一个新的会话,它将从头开始。

我们流式返回响应,并在AI消息处停止。这是因为AI消息指示应该调用工具,但我们设置了 interrupts_before 参数,使其在此处暂停。

从这里,我们可以获取此线程的图的当前状态。为此,我们需要传入线程配置。

我们将返回一个配置对象,其中包含几个参数。最重要的一个是这里的消息列表,它代表了图在此时刻的状态。我们还可以看到它有一个 next 参数,指示接下来要调用的节点。这里显示是 action,意味着我们即将调用“action”节点。

为了继续执行,我们可以再次调用 graph.stream(),传入相同的线程配置,并将输入设为 None

这将流式返回结果,我们将看到来自调用工具的工具消息,以及最终的AI消息。注意,在“action”节点和“LLM”节点之间没有中断,因为我们没有在那里添加任何中断。

如果我们现在获取状态,可以看到消息列表包含了完整的消息序列。如果查看 next 参数,会发现它是空的,表示没有其他任务需要执行。

构建交互式循环

为了更有趣,我们可以编写一些代码,在一个小循环中运行此过程,并提示我们输入是否继续。我们将传入一个新的线程ID以重新开始。

以下是实现交互式循环的步骤:

  1. 初始化智能体并设置中断点。
  2. 启动一个循环,处理用户查询。
  3. 当智能体运行到中断点时,暂停并提示用户。
  4. 根据用户输入(例如“是”或“否”)决定是否继续执行工具调用。
  5. 如果继续,则完成剩余步骤;否则,可以修改状态或终止。
# 伪代码示例
while True:
    user_input = input(“请输入您的问题(或输入‘退出’结束): “)
    if user_input.lower() == ‘退出’:
        break
    # 调用智能体,它会自动在工具调用前暂停
    for step in graph.stream({“messages”: [(“human”, user_input)]}, config):
        # 处理并显示每一步的输出
        # 如果遇到中断,则等待用户输入
        if 需要人工批准:
            approval = input(“是否批准执行此操作?(是/否): “)
            if approval == ‘是’:
                # 继续执行
                continue_execution()
            else:
                # 处理不批准的情况,例如修改状态或跳过
                handle_rejection()

这是一个停下来尝试其他输入的好时机。尝试在不同的位置添加中断,看看会发生什么。

深入理解状态与记忆

在进入下一部分之前,让我们更深入地讨论一下状态记忆。

当图执行时,每个状态的快照都存储在内存中。这个快照里有什么?首先是你已经定义的智能体状态,其次还有一些其他有用的信息,例如线程和每个快照的唯一标识符(即 thread_ts)。你可以使用它来访问快照。

有一些命令可以访问内存。你已经见过 get_state,如果你只提供线程ID而不提供唯一标识符,它将返回当前状态。还有 get_state_history,它返回一个遍历所有状态快照的迭代器。你可以使用这个迭代器来获取每个状态的所有唯一标识符。

你能用它做什么?这里有一个例子:给定线程标识符(更确切地说是那个唯一的 thread_ts),你可以访问第一个状态 state1,并在 invoke 命令中使用它。这将使用 state1 作为图的当前状态或起始点。这实际上是“时间旅行”。相反,如果没有那个 thread_ts,只传入线程ID,将使用线程的当前状态作为起始点。

你也可以使用那个唯一标识符来访问特定状态。然后,你可以修改那个状态。接着,你可以使用 update_state 来更新状态,并将其存储回内存中当前状态的位置。之后,如果你运行 streaminvoke,它将使用新的、修改后的状态作为起始点。

好的,你将在接下来的部分看到这些例子,让我们继续。

修改状态示例

现在让我们展示一个修改状态的例子。让我们启动一个新线程,询问“洛杉矶的天气怎么样”。

在这个线程的这一点上,我们有两条消息:人类消息和AI消息。AI消息指示搜索“洛杉矶的当前天气”。但让我们修改一下。假设我们实际上想问的是路易斯安那州的天气,而不是洛杉矶。那么,我们如何修改以纠正智能体的行为呢?

首先,让我们将图的当前状态保存到一个名为 current_values 的变量中。这个状态中的最后一条消息是AI消息,它指示搜索特定的搜索词,即“洛杉矶的当前天气”。我们可以进一步深入查看与此消息关联的工具调用列表。

现在更新这些工具调用。为此,我们首先获取与工具调用关联的ID。然后,我们将 tool_calls 属性更新为一个列表。这个列表有一个元素,是一个字典。它有一个工具调用,调用的是 tavily_search_results_json 工具,和之前一样,但参数不同。这次,查询是“路易斯安那州的当前天气”。

在我们对图调用 update_state 之前,这实际上不会做任何事情。我们将传入线程配置,这样我们就知道正在操作哪个线程,并且我们将传入我们想要覆盖的新值。

如果我们现在获取图的当前状态,可以看到搜索词已经变成了“路易斯安那州的当前天气”。

如果我们从这里继续,可以看到它用“路易斯安那州的当前天气”调用Tavily,得到一些响应,然后相应地回复:路易斯安那州当前的天气是晴朗的。

状态历史与时间旅行

我们现在已经展示了如何修改图的状态以控制智能体的行为。需要注意的重要一点是,我们实际上保留了所有这些状态的运行列表。因此,当我们修改状态时,实际上是创建了一个新状态,它成为了新的当前状态。然后,每次我们用节点的结果更新它时,实际上是一个接一个地创建新状态。

这非常好,因为它实际上允许我们回到过去,访问之前的状态,我们称之为“时间旅行”。

为了做到这一点,我们可以在图上调用 get_state_history,再次传入线程ID。然后,我们将开始随时间构建这个状态列表。

如果我们获取这个列表中的最后一个状态,可以看到它实际上是最早的一个。这是它查找洛杉矶当前天气时的状态。这是基于第一次语言模型调用所做的原始状态更新。如果我们想回到过去,从这个它正在寻找洛杉矶的检查点恢复,我们完全可以做到。

为此,我们只需再次调用 graph.stream(),传入 None,但请注意,我们现在传入的是 config,其中 config 包含 “replay” 参数,指定了我们想要从哪个状态恢复。config 只是告诉我们正在从这个状态恢复的配置参数。

因此,如果我们运行这个,将看到它开始搜索洛杉矶的当前天气,从Tavily获取结果,然后生成响应,这就是我们的最终答案。

在历史状态上进行编辑

我们还可以做的一件事是回到过去,然后从那里进行编辑。

这里我们有这个 config,其中 replay 指向它正在查找洛杉矶当前天气的状态。所以我们可以做和之前一样的事情来修改这个状态。

因此,我们将修改它以执行“洛杉矶的当前天气 AccuWeather”,假设我们想要来自AccuWeather的响应。这里与之前不同的原因是,我们是回到过去然后进行编辑,而不是像之前那样从最近的状态进行编辑。

我们可以更新这个 replay 状态,然后返回到这个分支状态,我们已经通过这个修改分支出去了。

如果我们现在在这个分支状态上调用 graph.stream(),传入 None,可以看到它正在查找AccuWeather并从那里获取结果,我们得到一个新的答案,然后从AI那里得到响应。

手动添加消息到状态

我们还可以做的另一件事是在任何给定的时间点向状态添加消息。

这里我们有这个 config,我们已经将其修改为包含“洛杉矶的当前天气 AccuWeather”。现在假设我们不想实际调用Tavily,而是想模拟一个响应。我们可以通过向状态追加一个新消息来实现。

我们将获取我们应该进行的工具调用的ID。然后,我们将创建这个状态更新,它是一个消息列表,这次是一条新消息。它有一个工具调用ID,工具名称是“TavilySearch”,内容是“54摄氏度”。所以这是一条新消息。因此,当我们在图中更新这个状态时,它不会替换现有消息,而是会将其追加到消息列表中。

我们现在要更新图的状态。但是,因为我们实际上是在添加并假装一个动作已经发生,而不是修改现有状态,所以我们需要做一件额外的事情。我们需要在这里添加 as_node=“action” 参数。这基本上是在说,我们正在做的状态更新不仅仅是一个修改,我们实际上是在模拟“action”节点进行这个更新。这之所以相关,是因为在我们添加这条消息之前,图的当前状态即将进入“action”节点。但在我们添加这条消息之后,我们不希望它再进入“action”节点了。所以,我们基本上是在说,当我们更新这个状态时,我们表现得好像我们就是“action”节点。

如果我们现在在这个新配置上调用 stream(),可以看到它不再执行动作了。相反,它只是调用模型并用这条AI消息响应:“洛杉矶的当前天气是54摄氏度”。这就是我们假装工具已经返回的内容。

总结

本节课中,我们一起学习了如何在LangGraph智能体中实现复杂的人机交互循环模式。

你已经学会了如何在一个节点执行前添加中断,这允许人类批准或拒绝特定的操作。你还展示了如何回到过去,以及如何修改状态(无论是当前状态还是过去的状态)。此外,你还展示了如何手动更新状态,这允许你手动给智能体提供调用工具的结果,而不是实际调用工具本身。

所有这些“人在循环中”的模式都有助于促进你与智能体的交互。它们让你对智能体的行为有更多的控制权,允许你回到之前的时间点,并通过编辑来纠正它的行为。

到目前为止,我们一直在使用一个相当简单的智能体:它有一个LLM调用,一个提示,并且状态很简单,只是一个消息列表。在本课程的下一个也是最后一个例子中,我们将创建一个更复杂的智能体,它由多个LLM调用组成,并拥有一个相当复杂的状态。

007:7.第六课 论文写作器 📝

概述

在本节课中,我们将构建一个功能更全面的项目——一个AI论文写作器。这个智能体将遵循“规划-研究-生成-反思”的循环流程,自动撰写并迭代改进一篇论文。


项目架构与流程

上一节我们介绍了基础智能体的构建,本节中我们来看看一个更复杂的、具备迭代能力的写作智能体。

整个论文写作器的流程可以分解为以下几个步骤:

  1. 规划:根据用户任务生成论文大纲。
  2. 研究:根据大纲,调用Tavili搜索工具获取相关文档。
  3. 生成:基于研究内容和规划,撰写论文初稿。
  4. 判断:检查修订次数是否达到上限,决定是结束还是进入反思循环。
  5. 反思:对当前论文草稿进行批判性分析,生成改进建议。
  6. 二次研究:根据反思建议,再次进行搜索以获取补充资料。
  7. 循环:带着新的研究资料,返回“生成”步骤,开始新一轮的撰写与修订。

这个循环将持续进行,直到满足预设的修订次数上限为止。


代码实现详解

1. 定义智能体状态

首先,我们需要定义一个更复杂的状态结构来跟踪整个写作过程。

from typing import List, TypedDict
from langgraph.graph import StateGraph

class AgentState(TypedDict):
    task: str           # 用户输入的写作主题
    plan: str           # 生成的论文大纲
    draft: str          # 当前的论文草稿
    critique: str       # 对草稿的批判性意见
    content: List[str]  # 研究获取的文档内容列表
    revision_number: int # 当前的修订次数
    max_revisions: int  # 允许的最大修订次数

2. 创建提示词

以下是控制不同环节智能体行为的提示词。

规划提示词:指导LLM生成论文大纲。

你是一位专业的论文规划师。请根据用户提供的主题,生成一份详细、结构清晰的论文大纲。大纲应包含引言、主体段落和结论。

写作提示词:指导LLM基于研究和规划进行写作。

你是一位专业的作家。请根据以下研究资料和论文规划,撰写一篇结构完整、论据充分的论文。
研究资料:{content}
论文规划:{plan}

反思提示词:指导LLM对草稿进行批判性评估。

你是一位严格的编辑。请仔细审阅以下论文草稿,指出其在逻辑、论据、结构和语言上的不足之处,并提供具体的改进建议。
论文草稿:{draft}

研究提示词(规划后):指导LLM根据大纲生成搜索查询。

基于以下论文大纲,生成一系列用于搜索相关资料的查询关键词。
大纲:{plan}

研究提示词(反思后):指导LLM根据批判意见生成新的搜索查询。

基于以下对论文草稿的批判意见,生成一系列用于搜索补充资料的查询关键词,以解决指出的问题。
批判意见:{critique}

3. 构建功能节点

接下来,我们创建实现每个步骤的功能节点。

规划节点:接收任务,生成论文计划。

def planning_node(state: AgentState):
    messages = [
        SystemMessage(content=planning_prompt),
        HumanMessage(content=state[“task”])
    ]
    response = model.invoke(messages)
    return {“plan”: response.content}

研究节点(规划后):根据计划生成查询并获取资料。

def research_plan_node(state: AgentState):
    # 生成搜索查询
    queries = model.with_structured_output(Queries).invoke([
        SystemMessage(content=research_planning_prompt),
        HumanMessage(content=state[“task”])
    ])
    # 执行搜索并累积结果
    current_content = state.get(“content”, [])
    for query in queries.list:
        results = tavili_client.search(query)
        current_content.extend(results)
    return {“content”: current_content}

生成节点:结合研究资料和计划,撰写论文草稿。

def generate_node(state: AgentState):
    # 准备输入
    formatted_content = “\n”.join(state[“content”])
    user_message = f”任务:{state[‘task’]}\n计划:{state[‘plan’]}”
    # 调用模型生成草稿
    messages = [
        SystemMessage(content=writer_prompt.format(content=formatted_content)),
        HumanMessage(content=user_message)
    ]
    response = model.invoke(messages)
    # 更新状态
    new_revision = state[“revision_number”] + 1
    return {“draft”: response.content, “revision_number”: new_revision}

反思节点:对当前草稿进行评估,生成批判意见。

def reflect_node(state: AgentState):
    messages = [
        SystemMessage(content=reflection_prompt),
        HumanMessage(content=state[“draft”])
    ]
    response = model.invoke(messages)
    return {“critique”: response.content}

研究节点(反思后):根据批判意见进行补充研究。

def research_critique_node(state: AgentState):
    # 基于批判生成新的查询
    queries = model.with_structured_output(Queries).invoke([
        SystemMessage(content=research_critique_prompt),
        HumanMessage(content=state[“critique”])
    ])
    # 执行搜索并更新内容
    current_content = state.get(“content”, [])
    for query in queries.list:
        results = tavili_client.search(query)
        current_content.extend(results)
    return {“content”: current_content}

4. 定义条件边

我们需要一个逻辑来决定是结束流程还是继续反思循环。

def should_continue(state: AgentState) -> str:
    if state[“revision_number”] >= state[“max_revisions”]:
        return “end”
    else:
        return “reflect”

5. 组装成图

最后,将所有节点和边组合成一个完整的工作流图。

# 初始化图
workflow = StateGraph(AgentState)

![](https://github.com/OpenDocCN/dsai-notes-pt1-zh/raw/master/docs/dlai-lnggph-aiagt/img/b9aa3130604dcbcb03d9630d24407b8d_20.png)

# 添加所有节点
workflow.add_node(“plan”, planning_node)
workflow.add_node(“research_plan”, research_plan_node)
workflow.add_node(“generate”, generate_node)
workflow.add_node(“reflect”, reflect_node)
workflow.add_node(“research_critique”, research_critique_node)

# 设置入口点
workflow.set_entry_point(“plan”)

# 添加普通边
workflow.add_edge(“plan”, “research_plan”)
workflow.add_edge(“research_plan”, “generate”)
workflow.add_edge(“reflect”, “research_critique”)
workflow.add_edge(“research_critique”, “generate”)

# 添加条件边(在生成节点之后)
workflow.add_conditional_edges(
    “generate”,
    should_continue,
    {
        “end”: END,
        “reflect”: “reflect”
    }
)

# 编译图
app = workflow.compile()

运行与测试

编译完成后,我们可以运行这个写作智能体。例如,让它撰写一篇关于“LangChain和LangSmith区别”的短文,并设置最大修订次数为2。

inputs = {
    “task”: “LangChain和LangSmith的主要区别是什么?”,
    “revision_number”: 1,
    “max_revisions”: 2
}
for output in app.stream(inputs):
    print(output) # 可以观察到规划、研究、生成、反思等各个步骤的输出

运行过程将清晰展示智能体如何:

  1. 生成初步大纲。
  2. 进行第一轮研究并获取资料。
  3. 撰写第一版草稿。
  4. 对草稿进行批判,提出改进建议(如“需要更深入的优势劣势分析”)。
  5. 根据批判进行第二轮研究。
  6. 结合新资料撰写第二版(最终版)草稿。

最终,我们得到一篇经过研究和迭代改进的、结构清晰的对比文章。


总结

本节课中我们一起学习并构建了一个功能完整的AI论文写作智能体。我们掌握了如何:

  • 设计一个包含规划、研究、生成、反思循环的复杂智能体工作流。
  • 使用 TypedDict 定义和管理包含多个键的复杂智能体状态。
  • 为工作流中不同的环节(规划、写作、批判)编写针对性的提示词
  • 利用 with_structured_output 确保语言模型输出结构化的数据(如查询列表)。
  • 在图中使用 add_conditional_edges 来实现基于条件(如修订次数)的流程分支控制。
  • 通过可视化工具观察智能体的执行步骤和状态变化。

这个项目展示了LangGraph在构建多步骤、可迭代、具备自我改进能力的高级智能体方面的强大灵活性。你可以尝试修改提示词、调整循环逻辑或集成不同的工具,来打造属于你自己的专属写作助手或研究助理。

008:学习资源 📚

在本节课中,我们将一起了解如何获取更多信息,以继续你在智能体领域的探索之旅。我们已经介绍了许多基础知识,现在来看看有哪些优秀的资源可以帮助你深入学习。

LangChain生态系统概览

上一节我们介绍了LangGraph的基础概念,本节中我们来看看整个LangChain生态系统的资源分布。LangChain文档是一个极佳的起点。

该文档提供了LangChain生态系统中所有包和服务的高层次概述。以下是其核心组成部分:

  • LangChain核心包:包含构建链、智能体和检索系统的基础模块。
  • LangChain社区包:包含由社区贡献的集成,例如我们课程中使用的Tavily搜索工具。
  • 合作伙伴包:如langchain-openai,是与特定供应商深度集成的独立包。文档中列出了大量其他集成可供探索。

LangChain高阶工具与平台

了解了生态结构后,我们来看看能加速开发的具体工具。LangChain本身提供了构建智能体、链和各种检索策略的高阶入口。

如果你希望快速开始,LangChain还提供了大量模板,可以通过LangServe轻松部署。LangServe是一个将你的LangChain应用转化为Web服务器的简便方式。

在整个开发过程中,LangSmith可以提供有力支持。从项目初期开始,LangSmith就能协助调试。它还能用于生产环境监控,并且包含我们之前见过的Playground功能。

代码库与模板资源

除了官方文档和平台,代码库是获取实战资源的好地方。LangChain的GitHub仓库包含了许多优质资源。

以下是该仓库中的主要资源类型:

  • 实用指南:包含大量入门教程和示例代码。
  • 项目模板:提供多种可直接使用的项目模板。

当然,LangGraph的GitHub仓库包含了关于LangGraph的深入文档,涵盖了我们课程所讲的所有内容。这里有优秀的参考文档、教程和分步指南。

扩展学习课程

我们已经介绍了静态资源,动态的学习课程也是很好的补充。DeepLearning.AI此前开设的LangChain相关课程也值得高度推荐。

特别是《LangChain中的函数、工具与智能体》这门课,它是本课程非常好的先导学习材料。

提示词灵感库

最后,我们来回顾一个能激发创造力的资源。我们之前已经见过Prompt Hub,这里再次强调。

Prompt Hub是一个获取灵感的绝佳场所,你可以看到其他提示词专家正在做什么。


本节课中我们一起学习了继续探索LangGraph和智能体开发的各种资源路径,包括官方文档、开发工具、代码库、扩展课程以及灵感社区。利用这些资源,你可以更深入地理解和构建复杂的智能体应用。

009:总结与展望

在本节课中,我们将总结整个课程的核心内容,并展望几种更复杂的智能体架构范式。这些架构展示了如何超越基础的循环结构,构建更强大、更灵活的智能体系统。

恭喜你,现在你已经掌握了如何构建自己的智能体。

基于今天所学的知识,你可以构建简单或相当复杂的智能体。但在结束之前,我想介绍一些我们今天未能构建,但你应该了解的智能体流程架构。

多智能体架构

上一节我们介绍了基础的智能体循环,本节中我们来看看更复杂的协作模式。第一种架构是多智能体架构。

我们在写作智能体的例子中对此略有涉及,但这里将使其更具体。多智能体架构是指多个不同的智能体在同一个共享状态上工作。

以下是其关键特征:

  • 这些智能体可以像写作智能体中的那样,仅由一个提示词和一个语言模型构成。
  • 它们也可以拥有不同的、可供调用的工具。即,一个智能体可以是“提示词 + 语言模型 + 工具”的组合。
  • 每个智能体内部实际上可以拥有自己的循环。

这里的核心要点,以及它与我们将要看到的下一种架构的区别在于,所有智能体都在同一个共享状态上工作,它们将这个状态从一个智能体传递到下一个智能体。

监督者智能体架构

让我们将其与监督者智能体架构进行对比。这里我们有一个监督者,它负责调用若干个子智能体。

监督者将决定传递给这些子智能体的输入是什么。而这些子智能体内部可以拥有各自不同的状态,它们本身可以是一个图。

因此,这里不一定存在“共享状态”的概念。除此之外,它与多智能体框架非常相似,但它特别强调存在一个负责路由和协调其他智能体的监督者。

当你能够使用一个非常强大的语言模型作为监督者时,这种架构通常很有效,因为执行这种监督和规划工作需要很高的智能。

流程工程

最近频繁出现的一个术语是“流程工程”。

这个概念源自一篇Alpha Codium论文,他们在其中实现了最先进的编码性能,并且采用了一种类似这样的、非常图形化的解决方案。观察这个架构,你可以看到它基本上是一个流水线,但在几个关键节点上实际上存在循环。

例如,在初始代码解决方案处有循环,在迭代公共测试时有循环,在迭代AI测试时也有循环。这是一个有趣的图结构,因为在到达某个点之前,信息流是非常定向的,然后在这些点处进行迭代。

这是一个为特定编码问题量身定制的架构,但“流程工程”的概念具有更广泛的适用性。它通常指的是为你的智能体思考和行动设计恰当的信息流。

规划与执行流程

顺着这个思路,一个常见的范式是采用“规划与执行”风格的流程。

首先,你在前期执行一个明确的规划步骤,然后开始执行该计划。

其流程可能如下:

  1. 规划:为一个子智能体制定几个需要执行的步骤。
  2. 执行:子智能体执行第一步,然后返回结果。
  3. 评估与调整:你可能根据结果更新计划,也可能不更新。这里存在一些不同的变体。
  4. 迭代:然后它继续执行下一步,完成后返回,直到完成整个计划。
  5. 最终检查:你可能会检查计划是否成功完成,或者是否需要重新规划。

如果一切顺利,最终将结果返回给用户。

树搜索架构

最后我想提及的是一篇非常有趣的论文,名为《Lagu Agent Tese》。

它基本上是在可能的行动状态空间上进行树搜索。

其流程如下:

  1. 生成一个行动。
  2. 对该行动进行反思。
  3. 基于该行动向下探索,生成一些其他子行动。
  4. 对这些子行动进行反思。
  5. 在所有这些反思过程中,它可以基本上决定想要跳回到行动状态树中的哪个位置。

因此,它可以反向传播并更新父节点,以携带更多信息,这可能会从先前的状态为未来的方向提供参考。对于这种架构,你可以真正理解为什么状态持久化非常重要,因为你需要能够回溯到之前的状态。

总结

本节课中我们一起学习了多种构建复杂智能体流程的新兴范式。以上只是一些用于创建更复杂智能体流程的新兴范式示例。

LangGraph的目标是高度可控,并允许你创建这些循环或非循环的流程。这种程度的可控性正是它与其他框架的区别所在。我们认为这对于创建真正有效的智能体至关重要,因此我们对它的未来发展感到非常兴奋。

😊

posted @ 2026-03-26 08:11  绝不原创的飞龙  阅读(9)  评论(0)    收藏  举报