补充:LangGraph

LangGraph 核⼼组件: Graphs, State, Nodes, Edges

LangGraph ⚡ 以图的⽅式构建语⾔代理 ⚡

官⽅⽂档地址:LangGraph是⼀个⽤于构建具有 LLMs 的有状态、多⻆⾊应⽤程序的库,⽤于创建代理和多代理 ⼯作流。与其他 LLM 框架相⽐,它提供了以下核⼼优势:循环、可控性和持久性。 LangGraph 允许您定义涉及循环的流程,这对于⼤多数代理架构⾄关重要。作为⼀种⾮常底层的 框架,它提供了对应⽤程序的流程和状态的精细控制,这对创建可靠的代理⾄关重要。此外, LangGraph 包含内置的持久性,可以实现⾼级的“⼈机交互”和内存功能。 LangGraph 的灵感来⾃ Pregel 和 Apache Beam。公共接⼝借鉴了 NetworkX。 LangGraph 由 LangChain In(LangChain 的创建者)构建,但可以在没有 LangChain 的情况 下使⽤。 循环和分⽀:在您的应⽤程序中实现循环和条件语句。 持久性:在图中的每个步骤之后⾃动保存状态。在任何时候暂停和恢复图执⾏以⽀持错误恢 复、“⼈机交互”⼯作流、时间旅⾏等等。 “⼈机交互”:中断图执⾏以批准或编辑代理计划的下⼀个动作。 流⽀持:在每个节点产⽣输出时流式传输输出(包括令牌流式传输)。 与 LangChain 集成:LangGraph 与 LangChain 和 LangSmith ⽆缝集成(但不需要它 们)。 

 安装

pip install -U langgraph

 示例

LangGraph 的⼀个核⼼概念是状态。每次图执⾏都会创建⼀个状态,该状态在图中的节点执⾏时 传递,每个节点在执⾏后使⽤其返回值更新此内部状态。图更新其内部状态的⽅式由所选图类型或 ⾃定义函数定义。 让我们看⼀个可以使⽤搜索⼯具的简单代理示例。

pip install langchain-openai
setx OPENAI_BASE_URL "https://api.openai.com/v1"
setx OPENAI_API_KEY "sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"

可以选择设置 LangSmith 以实现最佳的可观察性。

setx LANGSMITH_TRACING "true"
setx LANGSMITH_API_KEY "xxxxxxxxxxxxxxxx"
from typing import Literal
from langchain_core.messages import HumanMessage
from langchain_core.tools import tool
from langchain_openai import ChatOpenAI
# pip install -U langgraph
from langgraph.checkpoint.memory import MemorySaver
from langgraph.graph import END, StateGraph, MessagesState
from langgraph.prebuilt import ToolNode

# 定义工具函数,用于代理调用外部工具
@tool
def search(query: str):
    """模拟一个搜索工具"""
    if "上海" in query.lower() or "Shanghai" in query.lower():
        return "现在30度,有雾."
    return "现在是35度,阳光明媚。"


# 将工具函数放入工具列表
tools = [search]

# 创建工具节点
tool_node = ToolNode(tools)

# 1.初始化模型和工具,定义并绑定工具到模型
model = ChatOpenAI(model="gpt-4o", temperature=0).bind_tools(tools)

# 定义函数,决定是否继续执行
def should_continue(state: MessagesState) -> Literal["tools", END]:
    messages = state['messages']
    last_message = messages[-1]
    # 如果LLM调用了工具,则转到“tools”节点
    if last_message.tool_calls:
        return "tools"
    # 否则,停止(回复用户)
    return END


# 定义调用模型的函数
def call_model(state: MessagesState):
    messages = state['messages']
    response = model.invoke(messages)
    # 返回列表,因为这将被添加到现有列表中
    return {"messages": [response]}

# 2.用状态初始化图,定义一个新的状态图
workflow = StateGraph(MessagesState)
# 3.定义图节点,定义我们将循环的两个节点
workflow.add_node("agent", call_model)
workflow.add_node("tools", tool_node)

# 4.定义入口点和图边
# 设置入口点为“agent”
# 这意味着这是第一个被调用的节点
workflow.set_entry_point("agent")

# 添加条件边
workflow.add_conditional_edges(
    # 首先,定义起始节点。我们使用`agent`。
    # 这意味着这些边是在调用`agent`节点后采取的。
    "agent",
    # 接下来,传递决定下一个调用节点的函数。
    should_continue,
)

# 添加从`tools`到`agent`的普通边。
# 这意味着在调用`tools`后,接下来调用`agent`节点。
workflow.add_edge("tools", 'agent')

# 初始化内存以在图运行之间持久化状态
checkpointer = MemorySaver()

# 5.编译图
# 这将其编译成一个LangChain可运行对象,
# 这意味着你可以像使用其他可运行对象一样使用它。
# 注意,我们(可选地)在编译图时传递内存
app = workflow.compile(checkpointer=checkpointer)

# 6.执行图,使用可运行对象
final_state = app.invoke(
    {"messages": [HumanMessage(content="上海的天气怎么样?")]},
    config={"configurable": {"thread_id": 42}}
)
# 从 final_state 中获取最后一条消息的内容
result = final_state["messages"][-1].content
print(result)
final_state = app.invoke(
    {"messages": [HumanMessage(content="我问的那个城市?")]},
    config={"configurable": {"thread_id": 42}}
)
result = final_state["messages"][-1].content
print(result)

# 将生成的图片保存到文件
graph_png = app.get_graph().draw_mermaid_png()
with open("langgraph_hello.png", "wb") as f:
    f.write(graph_png)

现在,当我们传递相同的 "thread_id" 时,对话上下⽂将通过保存的状态(即存储的消息列 表)保留下来。

final_state = app.invoke(
 {"messages": [HumanMessage(content="我问的那个城市?")]},
 config={"configurable": {"thread_id": 42}}
)
result = final_state["messages"][-1].content
print(result)

逐步分解

1. 初始化模型和⼯具

我们使⽤ ChatOpenAI 作为我们的 LLM。注意:我们需要确保模型知道可以使⽤哪些 ⼯具。我们可以通过将 LangChain ⼯具转换为 OpenAI ⼯具调⽤格式来完成此操作,⽅法是使⽤ .bind_tools() ⽅法。 我们定义要使⽤的⼯具——在本例中是搜索⼯具。创建⾃⼰的⼯具⾮常容易——请参阅此 处的⽂档了解如何操作此处。

2. ⽤状态初始化图

我们通过传递状态模式(在本例中为 MessagesState )来初始化图( StateGrap h )。 MessagesState 是⼀个预构建的状态模式,它具有⼀个属性,⼀个 LangChain Mes sage 对象列表,以及将每个节点的更新合并到状态中的逻辑。

3. 定义图节点

我们需要两个主要节点 agent 节点:负责决定采取什么(如果有)⾏动。 调⽤⼯具的 tools 节点:如果代理决定采取⾏动,此节点将执⾏该⾏动。

4. 定义⼊⼝点和图边

⾸先,我们需要设置图执⾏的⼊⼝点—— agent 节点。 然后,我们定义⼀个普通边和⼀个条件边。条件边意味着⽬的地取决于图状态( MessageStat e )的内容。在本例中,⽬的地在代理(LLM)决定之前是未知的。 条件边:调⽤代理后,我们应该要么 a. 如果代理说要采取⾏动,则运⾏⼯具 b. 如果代理没有要求运⾏⼯具,则完成(回复⽤户)。 普通边:调⽤⼯具后,图应该始终返回到代理以决定下⼀步操作。

5. 编译图

当我们编译图时,我们将其转换为 LangChain Runnable,这会⾃动启⽤使⽤您的输⼊调 ⽤ .invoke() 、 .stream() 和 .batch() 。 我们还可以选择传递检查点对象以在图运⾏之间持久化状态,并启⽤内存、“⼈机交互”⼯ 作流、时间旅⾏等等。在本例中,我们使⽤ MemorySaver ——⼀个简单的内存中检查点。

6. 执⾏图 

a. LangGraph 将输⼊消息添加到内部状态,然后将状态传递给⼊⼝点节点 "agent" 。

b. "agent" 节点执⾏,调⽤聊天模型。

c. 聊天模型返回 AIMessage 。LangGraph 将其添加到状态中。

d. 图循环以下步骤,直到 AIMessage 上不再有 tool_calls 。 如果 AIMessage 具有 tool_calls ,则 "tools" 节点执⾏。 "agent" 节点再次执⾏并返回 AIMessage 。

e. 执⾏进度到特殊的 END 值,并输出最终状态。因此,我们得到所有聊天消息的列表作 为输出。

Graph(图) 

LangGraph 的核⼼是将代理⼯作流建模为图。你可以使⽤三个关键组件来定义代理的⾏为 1. 状态:⼀个共享的数据结构,表示应⽤程序的当前快照。它可以是任何 Python 类型,但通常 是 TypedDict 或 Pydantic BaseModel 。 2. 节点:编码代理逻辑的 Python 函数。它们接收当前 状态 作为输⼊,执⾏⼀些计算或副作 ⽤,并返回⼀个更新的 状态 。 3. 边:Python 函数,根据当前 状态 确定要执⾏的下⼀个 节点 。它们可以是条件分⽀或固 定转换。 通过组合 节点 和 边 ,你可以创建复杂的循环⼯作流,随着时间的推移发展 状态 。但是, 真正的⼒量来⾃于 LangGraph 如何管理 状态 。需要强调的是: 节点 和 边 不过是 Python 函数——它们可以包含 LLM 或简单的 Python 代码。 简⽽⾔之:节点完成⼯作。边指示下⼀步要做什么。 LangGraph 的底层图算法使⽤ 消息传递 来定义⼀个通⽤程序。当⼀个节点完成其操作时,它会 沿着⼀条或多条边向其他节点发送消息。这些接收节点然后执⾏其函数,将结果消息传递给下⼀组 节点,并且该过程继续进⾏。受到 Google 的 Pregel 系统的启发,该程序以离散的“超级步骤”进⾏。  

超级步骤可以被认为是图节点上的单个迭代。并⾏运⾏的节点属于同⼀个超级步骤,⽽顺序运⾏的 节点则属于不同的超级步骤。在图执⾏开始时,所有节点都处于 inactive 状态。当节点在任 何传⼊边(或“通道”)上收到新消息(状态)时,它将变为 active 状态。然后,活动节点运 ⾏其函数并响应更新。在每个超级步骤结束时,没有传⼊消息的节点通过将其标记为 inactiv e 来投票 halt 。当所有节点都处于 inactive 状态且没有消息在传输中时,图执⾏终⽌。

StateGraph 

StateGraph 类是使⽤的主要图类。它由⽤户定义的 状态 对象参数化。  

from langgraph.graph import StateGraph
from typing_extensions import TypedDict
class MyState(TypedDict)
 ...
graph = StateGraph(MyState)

 基类: 图 ⼀个图,其节点通过读取和写⼊共享状态进⾏通信。每个节点的签名是 State -> Partial. 每个状态键可以选择性地使⽤⼀个 reducer 函数进⾏注释,该函数将⽤于聚合从多个节点接收到 的该键的值。reducer 函数的签名是 (Value, Value) -> Value。 参数 state_schema ( 类型[任何] , 默认值: None ) – 定义状态的模式类。 config_schema ( 可选[类型[任何]] , 默认值: None ) – 定义配置的模式类。使⽤此⽅法在您的 API 中公开可配置参数。

示例:

# 从langgraph.graph模块导入START和StateGraph
from langgraph.graph import START, StateGraph

# 定义一个节点函数my_node,接收状态和配置,返回新的状态
def my_node(state, config):
    return {"x": state["x"] + 1,"y": state["y"] + 2}

# 创建一个状态图构建器builder,使用字典类型作为状态类型
builder = StateGraph(dict)
# 向构建器中添加节点my_node,节点名称将自动设置为'my_node'
builder.add_node(my_node)  # node name will be 'my_node'
# 添加一条边,从START到'my_node'节点
builder.add_edge(START, "my_node")
# 编译状态图,生成可执行的图
graph = builder.compile()
print(graph)
# 调用编译后的图,传入初始状态{"x": 1}
print(graph.invoke({"x": 1,"y":2}))

Compiling your graph(编译你的图)

要构建你的图,你⾸先定义状态,然后添加节点和边,最后进⾏编译。编译图究竟是什么,为什么 需要它? 编译是⼀个⾮常简单的步骤。它对图的结构进⾏⼀些基本检查(没有孤⽴的节点等等)。它也是你 可以指定运⾏时参数的地⽅,例如检查点和断点。你只需调⽤.compile⽅法即可编译你的图。  

#你必须在使⽤图之前编译它。
graph = graph_builder.compile(...)

 State(状态)

定义图时,你做的第⼀件事是定义图的 状态 。 状态 包含图的 模式 以及 归约器函数,它们指 定如何将更新应⽤于状态。 状态 的模式将是图中所有 节点 和 边 的输⼊模式,可以是 Ty pedDict 或者 Pydantic 模型。所有 节点 将发出对 状态 的更新,这些更新然后使⽤指 定的 归约器 函数进⾏应⽤。

Schema(模式)

指定图模式的主要⽂档化⽅法是使⽤ TypedDict 。但是,我们也⽀持 使⽤ Pydantic BaseModel 作为你的图状态,以添加默认值和其他数据验证。 默认情况下,图将具有相同的输⼊和输出模式。如果你想更改这⼀点,你也可以直接指定显式输⼊和输出模式。当你有许多键,其中⼀些是显式⽤于输⼊,⽽另⼀些是⽤于输出时,这很有⽤。查看此笔记本,了解如何使⽤。 默认情况下,图中的所有节点都将共享相同的状态。这意味着它们将读取和写⼊相同的状态通道。 可以在图中创建节点写⼊私有状态通道,⽤于内部节点通信——查看 此笔记本,了解如何执⾏此操作。

Reducers(归约器)

归约器是理解节点更新如何应⽤于 状态 的关键。 状态 中的每个键都有其⾃⼰的独⽴归约器函 数。如果未显式指定归约器函数,则假设对该键的所有更新都应该覆盖它。存在⼏种不同类型的归 约器,从默认类型的归约器开始

Default Reducer(默认归约器) 

这两个示例展示了如何使⽤默认归约器

from typing import TypedDict, List, Dict, Any


class State(TypedDict):
    foo: int
    bar: List[str]


def update_state(current_state: State, updates: Dict[str, Any]) -> State:
    # 创建一个新的状态字典
    new_state = current_state.copy()
    # 更新状态字典中的值
    new_state.update(updates)
    return new_state


# 初始状态
state: State = {"foo": 1, "bar": ["hi"]}

# 第一个节点返回的更新
node1_update = {"foo": 2}
state = update_state(state, node1_update)
print(state)  # 输出: {'foo': 2, 'bar': ['hi']}

# 第二个节点返回的更新
node2_update = {"bar": ["bye"]}
state = update_state(state, node2_update)
print(state)  # 输出: {'foo': 2, 'bar': ['bye']}

在此示例中,没有为任何键指定归约器函数。假设图的输⼊是 {"foo": 1, "bar": ["h i"]} 。然后,假设第⼀个 节点 返回 {"foo": 2} 。这被视为对状态的更新。请注意, 节 点 不需要返回整个 状态 模式——只需更新即可。应⽤此更新后, 状态 则变为 {"foo": 2, "bar": ["hi"]} 。如果第⼆个节点返回 {"bar": ["bye"]} ,则 状态 则变为 {"foo": 2, "bar": ["bye"]}

Nodes(节点)  

  在 LangGraph 中,节点通常是 Python 函数(同步或 async ),其中第⼀个位置参数是状态, (可选地),第⼆个位置参数是“配置”,包含可选的可配置参数(例如 thread_id )。 类似于 NetworkX ,您可以使⽤add_node⽅法将这些节点添加到图形中  

from langchain_core.runnables import RunnableConfig
from langgraph.graph import StateGraph, START
from langgraph.graph import END

# 初始化 StateGraph,状态类型为字典
graph = StateGraph(dict)

# 定义节点
def my_node(state: dict, config: RunnableConfig):
    print("In node: ", config["configurable"]["user_id"])
    return {"results": f"Hello, {state['input']}!"}

def my_other_node(state: dict):
    return state

# 将节点添加到图中
graph.add_node("my_node", my_node)
graph.add_node("other_node", my_other_node)

# 连接节点以确保它们是可达的
graph.add_edge(START, "my_node")
graph.add_edge("my_node", "other_node")

graph.add_edge("other_node", END)

# 编译图
print(graph.compile())

app = graph.compile();
# 将生成的图片保存到文件
graph_png = app.get_graph().draw_mermaid_png()
with open("node_case.png", "wb") as f:
    f.write(graph_png)

 在幕后,函数被转换为RunnableLambda,它为您的函数添加了批处理和异步⽀持,以及本地跟 踪和调试。 如果您在没有指定名称的情况下将节点添加到图形中,它将被赋予⼀个默认名称,该名称等同于函 数名称。

graph.add_node(my_node)
# You can then create edges to/from this node by referencing it as `"my_nod
e"`

START 节点

START 节点是⼀个特殊节点,它代表将⽤户输⼊发送到图形的节点。引⽤此节点的主要⽬的是 确定哪些节点应该⾸先被调⽤。

from langgraph.graph import START
graph.add_edge(START, "my_node")
graph.add_edge("my_node", "other_node")

END 节点

END 节点是⼀个特殊节点,它代表⼀个终端节点。当您想要指定哪些边在完成操作后没有动作 时,会引⽤此节点。

from langgraph.graph import END
graph.add_edge("other_node", END)

Edges(边)

边定义了逻辑如何路由以及图形如何决定停⽌。这是您的代理如何⼯作以及不同节点如何相互通信 的重要部分。有⼀些关键类型的边 普通边:直接从⼀个节点到下⼀个节点。 条件边:调⽤⼀个函数来确定下⼀个要转到的节点。 ⼊⼝点:⽤户输⼊到达时⾸先调⽤的节点。 条件⼊⼝点:调⽤⼀个函数来确定⽤户输⼊到达时⾸先调⽤的节点。⼀个节点可以有多个输出边。如果⼀个节点有多个输出边,则所有这些⽬标节点将在下⼀个超级步 骤中并⾏执⾏。

普通边 

如果您总是想从节点 A 到节点 B,您可以直接使⽤add_edge⽅法。

#示例:edges_case.py
graph.add_edge("node_a", "node_b")

条件边

如果您想选择性地路由到⼀个或多个边(或选择性地终⽌),您可以使⽤add_conditional_edges ⽅法。此⽅法接受节点的名称和⼀个“路由函数”,该函数将在该节点执⾏后被调⽤

graph.add_conditional_edges("node_a", routing_function)

 类似于节点, routing_function 接受图形的当前 state 并返回⼀个值。 默认情况下,返回值 routing_function ⽤作要将状态发送到下⼀个节点的节点名称(或节点 列表)。所有这些节点将在下⼀个超级步骤中并⾏运⾏。 您可以选择提供⼀个字典,该字典将 routing_function 的输出映射到下⼀个节点的名称。

graph.add_conditional_edges("node_a", routing_function, {True: "node_b", Fa
lse: "node_c"})

⼊⼝点

⼊⼝点是图形启动时运⾏的第⼀个节点。您可以从虚拟的START节点使⽤add_edge⽅法到要执⾏ 的第⼀个节点,以指定进⼊图形的位置。

from langgraph.graph import START
graph.add_edge(START, "my_node")

条件⼊⼝点

条件⼊⼝点允许您根据⾃定义逻辑从不同的节点开始。您可以从虚拟的START节点使⽤ add_conditional_edges来实现这⼀点。

from langgraph.graph import START
graph.add_conditional_edges(START, routing_function)

您可以选择提供⼀个字典,该字典将 routing_function 的输出映射到下⼀个节点的名称。

graph.add_conditional_edges(START, routing_my,{True: "my_node", False: "oth
er_node"})

 edges.case.py:

from langchain_core.runnables import RunnableConfig
from langgraph.graph import StateGraph, START, END

# 初始化 StateGraph,状态类型为字典
graph = StateGraph(dict)


# 定义节点
def my_node(state: dict, config: RunnableConfig):
    print("In node: ", config["configurable"]["user_id"])
    return {"results": f"Hello, {state['input']}!"}


def other_node(state: dict):
    return state


def node_a(state: dict):
    return {"result": "This is node B"}


def node_b(state: dict):
    return {"result": "This is node B"}


def node_c(state: dict):
    return {"result": "This is node C"}


# 将节点添加到图中
graph.add_node("my_node", my_node)
graph.add_node("other_node", other_node)
graph.add_node("node_a", node_a)
graph.add_node("node_b", node_b)
graph.add_node("node_c", node_c)

# 普通边
graph.add_edge("my_node", "other_node")
graph.add_edge("other_node", "node_a")


# 条件边和条件路由函数
def routing_function(state: dict):
    # 假设我们根据 state 中的某个键值来决定路由
    # 如果 state 中有 'route_to_b' 且其值为 True,则路由到 node_b,否则路由到 node_c
    return state.get('route_to_b', False)

graph.add_edge("node_b", END)
graph.add_edge("node_c", END)

#条件边
graph.add_conditional_edges("node_a", routing_function, {True: "node_b", False: "node_c"})
graph.add_edge(START, "my_node")
#条件入口点
#graph.add_conditional_edges(START,  routing_function, {True: "node_b", False: "node_c"})


#条件入口点

def routing_my(state: dict):
    # 假设我们根据 state 中的某个键值来决定路由
    # 如果 state 中有 'route_to_b' 且其值为 True,则路由到 node_b,否则路由到 node_c
    return state.get('route_to_my', False)


# 条件入口点
# graph.add_conditional_edges(START, routing_my,{True: "my_node", False: "other_node"})

# 编译图
app = graph.compile()
# 将生成的图片保存到文件
graph_png = app.get_graph().draw_mermaid_png()
with open("edges_case.png", "wb") as f:
    f.write(graph_png)

LangGraph 实现:持久化, Human-in-the-loop Persistence(持久化) 添加持久性内存

checkpointer(检查点)

LangGraph 具有⼀个内置的持久化层,通过检查点实现。当您将检查点与图形⼀起使⽤时,您可 以与该图形的状态进⾏交互。当您将检查点与图形⼀起使⽤时,您可以与图形的状态进⾏交互并管 理它。检查点在每个超级步骤中保存图形状态的检查点,从⽽实现⼀些强⼤的功能

⾸先,检查点通过允许⼈类检查、中断和批准步骤来促进⼈机交互⼯作流⼯作流。检查点对于这些 ⼯作流是必需的,因为⼈类必须能够在任何时候查看图形的状态,并且图形必须能够在⼈类对状态 进⾏任何更新后恢复执⾏。 其次,它允许在交互之间进⾏“记忆”。您可以使⽤检查点创建线程并在图形执⾏后保存线程的状 态。在重复的⼈类交互(例如对话)的情况下,任何后续消息都可以发送到该检查点,该检查点将 保留对其以前消息的记忆。 许多 AI 应⽤程序需要内存来跨多个交互共享上下⽂。在 LangGraph 中,通过 检查点 为任何 StateGraph 提供内存。 在创建任何 LangGraph ⼯作流时,您可以通过以下⽅式设置它们以持久保存其状态 1. ⼀个 检查点,例如 MemorySaver 2. 在编译图时调⽤ compile(checkpointer=my_checkpointer) 。 示例

# 导入所需的类型注解和模块
from typing import Annotated
from typing_extensions import TypedDict
from langgraph.graph.message import add_messages


# 定义一个状态类,包含一个消息列表,消息列表带有 add_messages 注解
class State(TypedDict):
    messages: Annotated[list, add_messages]


# 从 langchain_core.tools 导入工具装饰器
from langchain_core.tools import tool


# 定义一个名为 search 的工具函数,用于模拟网络搜索
@tool
def search(query: str):
    """Call to surf the web."""
    # 这是实际实现的占位符
    return ["The answer to your question lies within."]


# 将工具函数存入列表
tools = [search]

from langgraph.prebuilt import ToolNode

# 创建一个 ToolNode 实例,传入工具列表
tool_node = ToolNode(tools)

# 从 langchain_openai 导入 ChatOpenAI 模型
from langchain_openai import ChatOpenAI

# 创建一个 ChatOpenAI 模型实例,设置 streaming=True 以便可以流式传输 tokens
model = ChatOpenAI(temperature=0, streaming=True)

# 将工具绑定到模型上
bound_model = model.bind_tools(tools)

# 导入 Literal 类型
from typing import Literal


# 定义一个函数,根据状态决定是否继续执行
def should_continue(state: State) -> Literal["action", "__end__"]:
    """Return the next node to execute."""
    last_message = state["messages"][-1]
    # 如果没有函数调用,则结束
    if not last_message.tool_calls:
        return "__end__"
    # 否则继续执行
    return "action"


# 定义一个函数调用模型
def call_model(state: State):
    response = model.invoke(state["messages"])
    # 返回一个列表,因为这将被添加到现有列表中
    return {"messages": response}


# 从 langgraph.graph 导入 StateGraph 和 START
from langgraph.graph import StateGraph, START

# 定义一个新的图形工作流
workflow = StateGraph(State)

# 添加两个节点,分别是 agent 和 action
workflow.add_node("agent", call_model)
workflow.add_node("action", tool_node)

# 设置入口点为 agent
workflow.add_edge(START, "agent")

# 添加条件边,根据 should_continue 函数决定下一个节点
workflow.add_conditional_edges(
    "agent",
    should_continue,
)

# 添加从 action 到 agent 的普通边
workflow.add_edge("action", "agent")

# 从 langgraph.checkpoint.memory 导入 MemorySaver
from langgraph.checkpoint.memory import MemorySaver

# 创建一个 MemorySaver 实例
memory = MemorySaver()

# 编译工作流,生成一个 LangChain Runnable
app = workflow.compile(checkpointer=memory)

# 将生成的图片保存到文件
graph_png = app.get_graph().draw_mermaid_png()
with open("persistence_case.png", "wb") as f:
    f.write(graph_png)

# 从 langchain_core.messages 导入 HumanMessage
from langchain_core.messages import HumanMessage

# 设置配置参数
config = {"configurable": {"thread_id": "2"}}

# 创建一个 HumanMessage 实例,内容为 "hi! I'm bob"
input_message = HumanMessage(content="hi! I'm bob")

# 在流模式下运行应用程序,传入消息和配置,逐个打印每个事件的最后一条消息
for event in app.stream({"messages": [input_message]}, config, stream_mode="values"):
    event["messages"][-1].pretty_print()

# 创建一个 HumanMessage 实例,内容为 "what is my name?"
input_message = HumanMessage(content="what is my name?")

# 在流模式下运行应用程序,传入消息和配置,逐个打印每个事件的最后一条消息
for event in app.stream({"messages": [input_message]}, config, stream_mode="values"):
    event["messages"][-1].pretty_print()

# 创建一个 HumanMessage 实例,内容为 "what is my name?"
input_message = HumanMessage(content="what is my name?")

# 在流模式下运行应用程序,传入消息和新的配置,逐个打印每个事件的最后一条消息
for event in app.stream(
        {"messages": [input_message]},
        {"configurable": {"thread_id": "3"}},
        stream_mode="values",
):
    event["messages"][-1].pretty_print()

# 创建一个 HumanMessage 实例,内容为 "You forgot?"
input_message = HumanMessage(content="You forgot??")

# 在流模式下运行应用程序,传入消息和原来的配置,逐个打印每个事件的最后一条消息
for event in app.stream(
        {"messages": [input_message]},
        {"configurable": {"thread_id": "2"}},
        stream_mode="values",
):
    event["messages"][-1].pretty_print()

这适⽤于 StateGraph 及其所有⼦类,例如 MessageGraph。 以下是⼀个示例。

注意

在本操作指南中,我们将从头开始创建我们的代理,以保持透明度(但冗⻓)。您可以使⽤ cre ate_react_agent(model, tools=tool, checkpointer=checkpointer) (API ⽂档) 构造函数完成类似的功能。如果您习惯使⽤ LangChain 的 AgentExecutor 类,这可能更合适。

设置

⾸先,我们需要安装所需的软件包

%pip install --quiet -U langgraph langchain_openai

接下来,我们需要设置 OpenAI(我们将使⽤的 LLM)和 Tavily(我们将使⽤的搜索⼯具)的 API 密钥 可选地,我们可以设置 LangSmith 跟踪 的 API 密钥,这将为我们提供⼀流的可观察性。

设置状态

状态是所有节点的接⼝。 我们⾸先将定义要使⽤的⼯具。对于这个简单的示例,我们将创建⼀个占位符搜索引擎。但是,创 建⾃⼰的⼯具⾮常容易 - 请参阅 此处 的⽂档了解如何操作。  

# 导⼊所需的类型注解和模块
from typing import Annotated
from typing_extensions import TypedDict
from langgraph.graph.message import add_messages
# 定义⼀个状态类,包含⼀个消息列表,消息列表带有 add_messages 注解
class State(TypedDict):
 messages: Annotated[list, add_messages]

 设置工具

我们⾸先将定义要使⽤的⼯具。对于这个简单的示例,我们将创建⼀个占位符搜索引擎。

# 从 langchain_core.tools 导⼊⼯具装饰器
from langchain_core.tools import tool
# 定义⼀个名为 search 的⼯具函数,⽤于模拟⽹络搜索
@tool
def search(query: str):
 """Call to surf the web."""
 # 这是实际实现的占位符
 return ["The answer to your question lies within."]
# 将⼯具函数存⼊列表
tools = [search]

 现在我们可以创建我们的 ToolNode。此对象实际上运⾏LLM 要求使⽤的⼯具(即函数)。

from langgraph.prebuilt import ToolNode
# 创建⼀个 ToolNode 实例,传⼊⼯具列表
tool_node = ToolNode(tools)

 设置模型

现在我们需要加载 聊天模型 来为我们的代理提供动⼒。对于以下设计,它必须满⾜两个条件 1. 它应该与消息⼀起使⽤(因为我们的状态包含聊天消息列表) 2. 它应该与 ⼯具调⽤ ⼀起使⽤。

注意

这些模型要求不是使⽤ LangGraph 的⼀般要求 - 它们只是此特定示例的要求。 

# 从 langchain_openai 导⼊ ChatOpenAI 模型
from langchain_openai import ChatOpenAI
# 创建⼀个 ChatOpenAI 模型实例,设置 streaming=True 以便可以流式传输 tokens
model = ChatOpenAI(temperature=0, streaming=True)

 完成此操作后,我们应该确保模型知道它可以使⽤这些⼯具。我们可以通过将 LangChain ⼯具转 换为 OpenAI 函数调⽤格式,然后将其绑定到模型类来实现这⼀点。

# 将⼯具绑定到模型上
bound_model = model.bind_tools(tools)

 定义图

现在我们需要在我们的图中定义⼏个不同的节点。在 langgraph 中,节点可以是函数或 可运 ⾏的。我们需要为此定义两个主要节点 1. 代理:负责决定要采取哪些(如果有)操作。 2. ⼀个⽤于调⽤⼯具的函数:如果代理决定采取操作,则此节点将执⾏该操作。 我们还需要定义⼀些边。其中⼀些边可能是条件的。它们是条件的原因是,基于节点的输出,可能 会采⽤⼏种路径之⼀。在运⾏该节点之前,⽆法知道要采⽤哪条路径(LLM 决定)。 1. 条件边:在调⽤代理后,我们应该:a. 如果代理说要采取操作,则应调⽤调⽤⼯具的函数 b. 如果代理说它已完成,则应完成 2. 普通边:在调⽤⼯具后,它应该始终返回到代理以决定下⼀步要做什么 让我们定义节点,以及⼀个函数来决定如何采取哪些条件边。

# 导⼊ Literal 类型
from typing import Literal
# 定义⼀个函数,根据状态决定是否继续执⾏
def should_continue(state: State) -> Literal["action", "__end__"]:
 """Return the next node to execute."""
 last_message = state["messages"][-1]
 # 如果没有函数调⽤,则结束
 if not last_message.tool_calls:
 return "__end__"
 # 否则继续执⾏
 return "action"
# 定义⼀个函数调⽤模型
def call_model(state: State):
 response = model.invoke(state["messages"])
 # 返回⼀个列表,因为这将被添加到现有列表中
 return {"messages": response}

现在我们可以将所有内容放在⼀起并定义图!

# 从 langgraph.graph 导⼊ StateGraph 和 START
from langgraph.graph import StateGraph, START
# 定义⼀个新的图形⼯作流
workflow = StateGraph(State)
# 添加两个节点,分别是 agent 和 action
workflow.add_node("agent", call_model)
workflow.add_node("action", tool_node)
# 设置⼊⼝点为 agent
workflow.add_edge(START, "agent")
# 添加条件边,根据 should_continue 函数决定下⼀个节点
workflow.add_conditional_edges(
 "agent",
 should_continue,
)
# 添加从 action 到 agent 的普通边
workflow.add_edge("action", "agent")

 持久性

要添加持久性,我们在编译图时传⼊⼀个检查点

# 从 langgraph.checkpoint.memory 导⼊ MemorySaver
from langgraph.checkpoint.memory import MemorySaver
# 创建⼀个 MemorySaver 实例
memory = MemorySaver()


# 编译⼯作流,⽣成⼀个 LangChain Runnable
app = workflow.compile(checkpointer=memory)

 注意

如果您使⽤的是 LangGraph Cloud,则⽆需在编译图时传递检查点,因为它会⾃动完成。

# 将⽣成的图⽚保存到⽂件
graph_png = app.get_graph().draw_mermaid_png()
with open("persistence_case.png", "wb") as f:
 f.write(graph_png)

 

与代理交互

现在我们可以与代理进⾏交互,并看到它会记住以前的消息!

# 从 langchain_core.messages 导⼊ HumanMessage
from langchain_core.messages import HumanMessage
# 设置配置参数
config = {"configurable": {"thread_id": "2"}}
# 创建⼀个 HumanMessage 实例,内容为 "hi! I'm bob"
input_message = HumanMessage(content="hi! I'm bob")
# 在流模式下运⾏应⽤程序,传⼊消息和配置,逐个打印每个事件的最后⼀条消息
for event in app.stream({"messages": [input_message]}, config, stream_mode
="values"):
 event["messages"][-1].pretty_print()
# 创建⼀个 HumanMessage 实例,内容为 "what is my name?"
input_message = HumanMessage(content="what is my name?")
# 在流模式下运⾏应⽤程序,传⼊消息和配置,逐个打印每个事件的最后⼀条消息
for event in app.stream({"messages": [input_message]}, config, stream_mode
="values"):
 event["messages"][-1].pretty_print()

如果我们想开始新的对话,可以传⼊不同的线程 ID。瞧!所有的记忆都消失了!

# 创建⼀个 HumanMessage 实例,内容为 "what is my name?" input_message = HumanMessage(content="what is my name?") # 在流模式下运⾏应⽤程序,传⼊消息和新的配置,逐个打印每个事件的最后⼀条消息 for event in app.stream( {"messages": [input_message]}, {"configurable": {"thread_id": "3"}}, stream_mode="values", ): event["messages"][-1].pretty_print()

 所有检查点都将持久保存到检查点,因此您可以随时恢复以前的线程

# 创建⼀个 HumanMessage 实例,内容为 "You forgot??"
input_message = HumanMessage(content="You forgot??")
# 在流模式下运⾏应⽤程序,传⼊消息和原来的配置,逐个打印每个事件的最后⼀条消息
for event in app.stream(
 {"messages": [input_message]},
 {"configurable": {"thread_id": "2"}},
 stream_mode="values",
):
 event["messages"][-1].pretty_print()

Human-in-the-loop(⼈机交互)

添加断言

在某些节点执⾏之前或之后设置断点通常很有⽤。这可以⽤来在继续之前等待⼈⼯批准。当您“编 译”图形时,可以设置这些断点。您可以在节点执⾏之前(使⽤ interrupt_before )或节点 执⾏之后(使⽤ interrupt_after )设置断点。 使⽤断点时,您必须使⽤检查点。这是因为您的图形需要能够恢复执⾏。 为了恢复执⾏,您可以使⽤ None 作为输⼊调⽤您的图。

# Initial run of graph
graph.invoke(inputs, config=config)
# Let's assume it hit a breakpoint somewhere, you can then resume by passin
g in None
graph.invoke(None, config=config)

⼈机交互 (HIL) 在 代理系统 中⾄关重要。 断点 是⼀种常⻅的 HIL 交互模式,允许图在特定步骤 停⽌并寻求⼈为批准后再继续执⾏(例如,对于敏感操作)。 断点建⽴在 LangGraph 检查点 之上,检查点在每个节点执⾏后保存图的状态。 检查点保存在 线 程 中,这些线程保存图状态,并且可以在图执⾏完成后访问。 这使得图执⾏可以在特定点暂停, 等待⼈为批准,然后从最后⼀个检查点恢复执⾏。

让我们看看它的基本⽤法。 下⾯,我们做两件事 1. 我们使⽤ interrupt_before 指定步骤,来指定 断点。 2. 我们设置⼀个检查点来保存图的状态。

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

class State(TypedDict):
    input: str

def step_1(state):
    print("---Step 1---")
    pass

def step_2(state):
    print("---Step 2---")
    pass

def step_3(state):
    print("---Step 3---")
    pass

builder = StateGraph(State)
builder.add_node("step_1", step_1)
builder.add_node("step_2", step_2)
builder.add_node("step_3", step_3)
builder.add_edge(START, "step_1")
builder.add_edge("step_1", "step_2")
builder.add_edge("step_2", "step_3")
builder.add_edge("step_3", END)

# Set up memory
memory = MemorySaver()

# Add
graph = builder.compile(checkpointer=memory, interrupt_before=["step_3"])

# Input
initial_input = {"input": "hello world"}

# Thread
thread = {"configurable": {"thread_id": "1"}}

# 运行graph,直到第一次中断
for event in graph.stream(initial_input, thread, stream_mode="values"):
    print(event)

user_approval = input("Do you want to go to Step 3? (yes/no): ")

if user_approval.lower() == "yes":
    # If approved, continue the graph execution
    for event in graph.stream(None, thread, stream_mode="values"):
        print(event)
else:
    print("Operation cancelled by user.")

 

我们为检查点创建⼀个 线程 ID。 我们运⾏到步骤 3,如 interrupt_before 中定义。 在⽤户输⼊/批准后,我们恢复执⾏,⽅法是⽤ None 调⽤图。

Agent 使⽤案例: Multi-Agent Systems, Planning Agent  

Multi-Agent Systems(多代理系统)

协作

单个代理通常可以使⽤少量⼯具在⼀个域内有效地运⾏,但即使使⽤像 gpt-4 这样的强⼤模 型,它在使⽤许多⼯具时也可能效率较低。 解决复杂任务的⼀种⽅法是使⽤“分⽽治之”的⽅法:为每个任务或域创建⼀个专⻔的代理,并将 任务路由到正确的“专家”。 此笔记本(受 Wu 等⼈撰写的论⽂ AutoGen:通过多代理对话实现下⼀代 LLM 应⽤ 的启发)展 示了⼀种使⽤ LangGraph 进⾏此操作的⽅法。 ⽣成的图将类似于以下图

在我们开始之前,快速说明⼀下:以下展示如何在 LangGraph 中实现某些设计模式。如果模式适 合您的需求

%pip install -U langchain langchain_openai langsmith pandas langchain_exper
imental matplotlib langgraph langchain_core
setx TAVILY_API_KEY ""
# Optional, add tracing in LangSmith
setx LANGCHAIN_TRACING_V2 "true"
setx LANGCHAIN_API_KEY ""

创建代理

以下辅助函数将帮助创建代理。这些代理将成为图中的节点。 如果您只想查看图的外观,可以跳过此步骤。完整代码如下:

# 导入基本消息类、用户消息类和工具消息类
from langchain_core.messages import (
    BaseMessage,
    HumanMessage,
    ToolMessage,
)
# 导入聊天提示模板和消息占位符
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
# 导入状态图相关的常量和类
from langgraph.graph import END, StateGraph, START


# 定义一个函数,用于创建代理
def create_agent(llm, tools, system_message: str):
    """创建一个代理。"""
    # 创建一个聊天提示模板
    prompt = ChatPromptTemplate.from_messages(
        [
            (
                "system",
                "你是一个有帮助的AI助手,与其他助手合作。"
                " 使用提供的工具来推进问题的回答。"
                " 如果你不能完全回答,没关系,另一个拥有不同工具的助手"
                " 会接着你的位置继续帮助。执行你能做的以取得进展。"
                " 如果你或其他助手有最终答案或交付物,"
                " 在你的回答前加上FINAL ANSWER,以便团队知道停止。"
                " 你可以使用以下工具: {tool_names}。\n{system_message}",
            ),
            # 消息占位符
            MessagesPlaceholder(variable_name="messages"),
        ]
    )
    # 传递系统消息参数
    prompt = prompt.partial(system_message=system_message)
    # 传递工具名称参数
    prompt = prompt.partial(tool_names=", ".join([tool.name for tool in tools]))
    # 绑定工具并返回提示模板
    return prompt | llm.bind_tools(tools)


# 导入注解类型
from typing import Annotated

# 导入Tavily搜索工具
from langchain_community.tools.tavily_search import TavilySearchResults
# 导入工具装饰器
from langchain_core.tools import tool
# 导入Python REPL工具
from langchain_experimental.utilities import PythonREPL

# 创建Tavily搜索工具实例,设置最大结果数为5
tavily_tool = TavilySearchResults(max_results=5)

# 警告:这会在本地执行代码,未沙箱化时可能不安全
# 创建Python REPL实例
repl = PythonREPL()


# 定义一个工具函数,用于执行Python代码
@tool
def python_repl(
        code: Annotated[str, "要执行以生成图表的Python代码。"],
):
    """使用这个工具来执行Python代码。如果你想查看某个值的输出,
    应该使用print(...)。这个输出对用户可见。"""
    try:
        # 尝试执行代码matplotlib
        result = repl.run(code)
    except BaseException as e:
        # 捕捉异常并返回错误信息
        return f"执行失败。错误: {repr(e)}"
    # 返回执行结果
    result_str = f"成功执行:\n```python\n{code}\n```\nStdout: {result}"
    return (
            result_str + "\n\n如果你已完成所有任务,请回复FINAL ANSWER。"
    )

"""
result_str 值如下
成功执行:
```python
import matplotlib.pyplot as plt

# Data
years = ["2018", "2019", "2020", "2021", "2022"]
market_size = [10.1, 14.69, 22.59, 34.87, 62.5]

# Create the plot
plt.figure(figsize=(10, 6))
plt.plot(years, market_size, marker='o', linestyle='-', color='b')

# Adding titles and labels
plt.title("Global AI Software Market Size (2018-2022)")
plt.xlabel("Year")
plt.ylabel("Market Size (in billion USD)")
plt.grid(True)

# Display the plot
plt.show()
```
Stdout: 
"""

# 导入操作符和类型注解
import operator
from typing import Annotated, Sequence, TypedDict

# 导入OpenAI聊天模型
from langchain_openai import ChatOpenAI


# 定义一个对象,用于在图的每个节点之间传递
# 我们将为每个代理和工具创建不同的节点
class AgentState(TypedDict):
    # messages字段用于存储消息的序列,并且通过 Annotated 和 operator.add 提供了额外的信息,解释如何处理这些消息。
    messages: Annotated[Sequence[BaseMessage], operator.add]
    # sender 用于存储当前消息的发送者。通过这个字段,系统可以知道当前消息是由哪个代理生成的。
    sender: str


# 导入functools模块
import functools

# 导入AI消息类
from langchain_core.messages import AIMessage


# 辅助函数,用于为给定的代理创建节点
def agent_node(state, agent, name):
    # 调用代理
    result = agent.invoke(state)
    # 检查 result 是否是 ToolMessage 类型的实例
    if isinstance(result, ToolMessage):
        pass
    else:
        #将 tavily result 转换为 AIMessage 类型,并且将 name 作为发送者的名称附加到消息中。
        result = AIMessage(**result.dict(exclude={"type", "name"}), name=name)
    return {
        "messages": [result],
        # 由于我们有一个严格的工作流程,我们可以
        # 跟踪发送者,以便知道下一个传递给谁。
        "sender": name,
    }


# 创建OpenAI聊天模型实例
llm = ChatOpenAI(model="gpt-4o")

# 研究代理和节点
research_agent = create_agent(
    llm,
    [tavily_tool],
    system_message="你应该提供准确的数据供chart_generator使用。",
)
# 创建一个检索节点,部分应用函数(partial function)
# 其中 agent 参数固定为 research_agent,name 参数固定为 "Researcher"。
research_node = functools.partial(agent_node, agent=research_agent, name="Researcher")

# 图表生成器
chart_agent = create_agent(
    llm,
    [python_repl],
    system_message="你展示的任何图表都将对用户可见。",
)
# 创建图表生成节点
chart_node = functools.partial(agent_node, agent=chart_agent, name="chart_generator")

# 导入预构建的工具节点
from langgraph.prebuilt import ToolNode

# 定义工具列表
tools = [tavily_tool, python_repl]
# 创建工具节点
tool_node = ToolNode(tools)

# 任一代理都可以决定结束
from typing import Literal

# 定义路由器函数
def router(state) -> Literal["call_tool", "__end__", "continue"]:
    # 这是路由器
    messages = state["messages"]
    last_message = messages[-1]
    # 检查 last_message 是否包含工具调用(tool calls)
    if last_message.tool_calls:
        return "call_tool"
    #如果已经获取到最终答案,则返回结束节点
    if "FINAL ANSWER" in last_message.content:
        # 任何代理决定工作完成
        return "__end__"
    return "continue"


# 创建状态图实例
workflow = StateGraph(AgentState)

# 添加研究员节点
workflow.add_node("Researcher", research_node)
# 添加图表生成器节点
workflow.add_node("chart_generator", chart_node)
# 添加工具调用节点
workflow.add_node("call_tool", tool_node)

# 添加条件边
workflow.add_conditional_edges(
    "Researcher",
    router,
    {"continue": "chart_generator", "call_tool": "call_tool", "__end__": END},
)
workflow.add_conditional_edges(
    "chart_generator",
    router,
    {"continue": "Researcher", "call_tool": "call_tool", "__end__": END},
)

# 添加条件边
workflow.add_conditional_edges(
    "call_tool",
    # 这个 lambda 函数的作用是从状态中获取sender名称,以便在条件边的映射中使用。
    # 如果 sender 是 "Researcher",工作流将转移到 "Researcher" 节点。
    # 如果 sender 是 "chart_generator",工作流将转移到 "chart_generator" 节点。
    lambda x: x["sender"],
    {
        "Researcher": "Researcher",
        "chart_generator": "chart_generator",
    },
)
# 添加起始边
workflow.add_edge(START, "Researcher")
# 编译工作流图
graph = workflow.compile()

# 将生成的图片保存到文件
graph_png = graph.get_graph().draw_mermaid_png()
with open("collaboration.png", "wb") as f:
    f.write(graph_png)

# 事件流
events = graph.stream(
    {
        "messages": [
            HumanMessage(
                content="获取过去5年AI软件市场规模,"
                        " 然后绘制一条折线图。"
                        " 一旦你编写好代码,完成任务。"
            )
        ],
    },
    # 图中最多执行的步骤数
    {"recursion_limit": 150},
)
# 打印事件流中的每个状态
for s in events:
    print(s)
    print("----")

定义⼯具

我们还将定义⼀些代理将在未来使⽤的⼯具

# 导⼊注解类型
from typing import Annotated
# 导⼊Tavily搜索⼯具
from langchain_community.tools.tavily_search import TavilySearchResults
# 导⼊⼯具装饰器
from langchain_core.tools import tool
# 导⼊Python REPL⼯具
from langchain_experimental.utilities import PythonREPL
# 创建Tavily搜索⼯具实例,设置最⼤结果数为5
tavily_tool = TavilySearchResults(max_results=5)
# 警告:这会在本地执⾏代码,未沙箱化时可能不安全
# 创建Python REPL实例
repl = PythonREPL()
# 定义⼀个⼯具函数,⽤于执⾏Python代码
@tool
def python_repl(
 code: Annotated[str, "要执⾏以⽣成图表的Python代码。"],
):
 """使⽤这个⼯具来执⾏Python代码。如果你想查看某个值的输出,
 应该使⽤print(...)。这个输出对⽤户可⻅。"""
 try:
 # 尝试执⾏代码
 result = repl.run(code)
 except BaseException as e:
 # 捕捉异常并返回错误信息
 return f"执⾏失败。错误: {repr(e)}"
 # 返回执⾏结果
 result_str = f"成功执⾏:\n```python\n{code}\n```\nStdout: {result}"
 return (
 result_str + "\n\n如果你已完成所有任务,请回复FINAL ANSWER。"
 )

创建图

现在我们已经定义了⼯具并创建了⼀些辅助函数,将在下⾯创建各个代理,并告诉他们如何使⽤ LangGraph 相互交流。

定义状态 

我们⾸先定义图的状态。这只是⼀个消息列表,以及⼀个⽤于跟踪最新发送者的键

# 导⼊操作符和类型注解
import operator
from typing import Annotated, Sequence, TypedDict
# 导⼊OpenAI聊天模型
from langchain_openai import ChatOpenAI
# 定义⼀个对象,⽤于在图的每个节点之间传递
# 我们将为每个代理和⼯具创建不同的节点
class AgentState(TypedDict):
 messages: Annotated[Sequence[BaseMessage], operator.add]
 sender: str

定义代理的节点。 

 现在我们需要定义节点。⾸先,让我们定义代理的节点。

# 导⼊functools模块
import functools
# 导⼊AI消息类
from langchain_core.messages import AIMessage
# 辅助函数,⽤于为给定的代理创建节点
def agent_node(state, agent, name):
 # 调⽤代理
 result = agent.invoke(state)
 # 将代理输出转换为适合附加到全局状态的格式
 if isinstance(result, ToolMessage):
 pass
 else:
 result = AIMessage(**result.dict(exclude={"type", "name"}), name=n
ame)
 return {
 "messages": [result],
 # 由于我们有⼀个严格的⼯作流程,我们可以
 # 跟踪发送者,以便知道下⼀个传递给谁。
 "sender": name,
 }
# 创建OpenAI聊天模型实例
llm = ChatOpenAI(model="gpt-4o")
# 研究代理和节点
research_agent = create_agent(
 llm,
 [tavily_tool],
 system_message="你应该提供准确的数据供chart_generator使⽤。",
)
# 创建研究节点
research_node = functools.partial(agent_node, agent=research_agent, name
="Researcher")
# 图表⽣成器
chart_agent = create_agent(
 llm,
 [python_repl],
 system_message="你展示的任何图表都将对⽤户可⻅。",
)


# 创建图表⽣成节点
chart_node = functools.partial(agent_node, agent=chart_agent, name="chart_
generator")

定义图

我们现在可以将所有内容整合在⼀起,并定义图!

# 创建状态图实例
workflow = StateGraph(AgentState)
# 添加研究员节点
workflow.add_node("Researcher", research_node)
# 添加图表⽣成器节点
workflow.add_node("chart_generator", chart_node)
# 添加⼯具调⽤节点
workflow.add_node("call_tool", tool_node)
# 添加条件边
workflow.add_conditional_edges(
 "Researcher",
 router,
 {"continue": "chart_generator", "call_tool": "call_tool", "__end__": E
ND},
)
workflow.add_conditional_edges(
 "chart_generator",
 router,
 {"continue": "Researcher", "call_tool": "call_tool", "__end__": END},
)
# 添加条件边
workflow.add_conditional_edges(
 "call_tool",
 # 每个代理节点更新'sender'字段
 # ⼯具调⽤节点不更新,这意味着
 # 该边将路由回调⽤⼯具的原始代理
 lambda x: x["sender"],
 {
 "Researcher": "Researcher",
 "chart_generator": "chart_generator",
 },
)
# 添加起始边
workflow.add_edge(START, "Researcher")
# 编译⼯作流图
graph = workflow.compile()

# 将⽣成的图⽚保存到⽂件
graph_png = graph.get_graph().draw_mermaid_png()
with open("collaboration.png", "wb") as f:
 f.write(graph_png)

调用

图创建完毕后,您可以调⽤它!让我们让它为我们绘制⼀些统计数据。

# 事件流
events = graph.stream(
 {
 "messages": [
 HumanMessage(
 content="获取过去5年AI软件市场规模,"
 " 然后绘制⼀条折线图。"
 " ⼀旦你编写好代码,完成任务。"
 )
 ],
 },
 # 图中最多执⾏的步骤数
 {"recursion_limit": 150},
)
# 打印事件流中的每个状态
for s in events:
 print(s)
 print("----")

 结果:

Planning Agent  

Plan-and-Execute(计划并执⾏)

下⾯展示了如何创建⼀个“计划并执⾏”⻛格的代理。 这在很⼤程度上借鉴了 计划和解决 论⽂以 及 Baby-AGI 项⽬。 核⼼思想是先制定⼀个多步骤计划,然后逐项执⾏。 完成⼀项特定任务后,您可以重新审视计划 并根据需要进⾏修改。⼀般的计算图如下所示

这与典型的 ReAct ⻛格的代理进⾏了⽐较,在该代理中,您⼀次思考⼀步。 这种“计划并执⾏”⻛ 格代理的优势在于 1. 明确的⻓期规划(即使是真正强⼤的 LLM 也可能难以做到) 2. 能够使⽤更⼩/更弱的模型来执⾏步骤,仅在规划步骤中使⽤更⼤/更好的模型 以下演练演示了如何在 LangGraph 中实现这⼀点。 ⽣成的代理将留下类似以下示例的轨迹: (LangSmith).

设置

⾸先,我们需要安装所需的软件包。 

%pip install --quiet -U langgraph langchain-community langchain-openai tavi
ly-python

接下来,我们需要为 OpenAI(我们将使⽤的 LLM)和 Tavily(我们将使⽤的搜索⼯具)设置 API 密钥 可以选择设置 LangSmith 跟踪的 API 密钥,这将为我们提供⼀流的可观察性。

setx TAVILY_API_KEY ""
# Optional, add tracing in LangSmith
setx LANGCHAIN_TRACING_V2 "true"
setx LANGCHAIN_API_KEY ""

 定义工具

我们将⾸先定义要使⽤的⼯具。 对于这个简单的示例,我们将使⽤ Tavily 内置的搜索⼯具。

完整代码如下:

from langchain_community.tools.tavily_search import TavilySearchResults

# 创建TavilySearchResults工具,设置最大结果数为1
tools = [TavilySearchResults(max_results=1)]

from langchain import hub
from langchain_openai import ChatOpenAI
import asyncio
from langgraph.prebuilt import create_react_agent

# 从LangChain的Hub中获取prompt模板,可以进行修改
prompt = hub.pull("wfh/react-agent-executor")
prompt.pretty_print()

# 选择驱动代理的LLM,使用OpenAI的ChatGPT-4o模型
llm = ChatOpenAI(model="gpt-4o")
# 创建一个REACT代理执行器,使用指定的LLM和工具,并应用从Hub中获取的prompt
agent_executor = create_react_agent(llm, tools, messages_modifier=prompt)

# 调用代理执行器,询问“谁是美国公开赛的冠军”
#agent_executor.invoke({"messages": [("user", "谁是美国公开赛的获胜者")]})

import operator
from typing import Annotated, List, Tuple, TypedDict


# 定义一个TypedDict类PlanExecute,用于存储输入、计划、过去的步骤和响应
class PlanExecute(TypedDict):
    input: str
    plan: List[str]
    past_steps: Annotated[List[Tuple], operator.add]
    response: str


from pydantic import BaseModel, Field


# 定义一个Plan模型类,用于描述未来要执行的计划
class Plan(BaseModel):
    """未来要执行的计划"""

    steps: List[str] = Field(
        description="需要执行的不同步骤,应该按顺序排列"
    )


from langchain_core.prompts import ChatPromptTemplate

# 创建一个计划生成的提示模板
planner_prompt = ChatPromptTemplate.from_messages(
    [
        (
            "system",
            """对于给定的目标,提出一个简单的逐步计划。这个计划应该包含独立的任务,如果正确执行将得出正确的答案。不要添加任何多余的步骤。最后一步的结果应该是最终答案。确保每一步都有所有必要的信息 - 不要跳过步骤。""",
        ),
        ("placeholder", "{messages}"),
    ]
)
# 使用指定的提示模板创建一个计划生成器,使用OpenAI的ChatGPT-4o模型
planner = planner_prompt | ChatOpenAI(
    model="gpt-4o", temperature=0
).with_structured_output(Plan)

# 调用计划生成器,询问“当前澳大利亚公开赛冠军的家乡是哪里?”
#planner.invoke({"messages": [("user", "现任澳网冠军的家乡是哪里?")]})

from typing import Union


# 定义一个响应模型类,用于描述用户的响应
class Response(BaseModel):
    """用户响应"""

    response: str


# 定义一个行为模型类,用于描述要执行的行为。该类继承自 BaseModel。
# 类中有一个属性 action,类型为 Union[Response, Plan],表示可以是 Response 或 Plan 类型。
# action 属性的描述为:要执行的行为。如果要回应用户,使用 Response;如果需要进一步使用工具获取答案,使用 Plan。
class Act(BaseModel):
    """要执行的行为"""
    action: Union[Response, Plan] = Field(
        description="要执行的行为。如果要回应用户,使用Response。如果需要进一步使用工具获取答案,使用Plan。"
    )


# 创建一个重新计划的提示模板
replanner_prompt = ChatPromptTemplate.from_template(
    """对于给定的目标,提出一个简单的逐步计划。这个计划应该包含独立的任务,如果正确执行将得出正确的答案。不要添加任何多余的步骤。最后一步的结果应该是最终答案。确保每一步都有所有必要的信息 - 不要跳过步骤。

你的目标是:
{input}

你的原计划是:
{plan}

你目前已完成的步骤是:
{past_steps}

相应地更新你的计划。如果不需要更多步骤并且可以返回给用户,那么就这样回应。如果需要,填写计划。只添加仍然需要完成的步骤。不要返回已完成的步骤作为计划的一部分。"""
)

# 使用指定的提示模板创建一个重新计划生成器,使用OpenAI的ChatGPT-4o模型
replanner = replanner_prompt | ChatOpenAI(
    model="gpt-4o", temperature=0
).with_structured_output(Act)

from typing import Literal


# 定义一个异步主函数
async def main():
    # 定义一个异步函数,用于生成计划步骤
    async def plan_step(state: PlanExecute):
        plan = await planner.ainvoke({"messages": [("user", state["input"])]})
        return {"plan": plan.steps}

    # 定义一个异步函数,用于执行步骤
    async def execute_step(state: PlanExecute):
        plan = state["plan"]
        plan_str = "\n".join(f"{i + 1}. {step}" for i, step in enumerate(plan))
        task = plan[0]
        task_formatted = f"""对于以下计划:
{plan_str}\n\n你的任务是执行第{1}步,{task}。"""
        agent_response = await agent_executor.ainvoke(
            {"messages": [("user", task_formatted)]}
        )
        return {
            "past_steps": state["past_steps"] + [(task, agent_response["messages"][-1].content)],
        }

    # 定义一个异步函数,用于重新计划步骤
    async def replan_step(state: PlanExecute):
        output = await replanner.ainvoke(state)
        if isinstance(output.action, Response):
            return {"response": output.action.response}
        else:
            return {"plan": output.action.steps}

    # 定义一个函数,用于判断是否结束
    def should_end(state: PlanExecute) -> Literal["agent", "__end__"]:
        if "response" in state and state["response"]:
            return "__end__"
        else:
            return "agent"

    from langgraph.graph import StateGraph, START

    # 创建一个状态图,初始化PlanExecute
    workflow = StateGraph(PlanExecute)

    # 添加计划节点
    workflow.add_node("planner", plan_step)

    # 添加执行步骤节点
    workflow.add_node("agent", execute_step)

    # 添加重新计划节点
    workflow.add_node("replan", replan_step)

    # 设置从开始到计划节点的边
    workflow.add_edge(START, "planner")

    # 设置从计划到代理节点的边
    workflow.add_edge("planner", "agent")

    # 设置从代理到重新计划节点的边
    workflow.add_edge("agent", "replan")

    # 添加条件边,用于判断下一步操作
    workflow.add_conditional_edges(
        "replan",
        # 传入判断函数,确定下一个节点
        should_end,
    )

    # 编译状态图,生成LangChain可运行对象
    app = workflow.compile()

    # 将生成的图片保存到文件
    graph_png = app.get_graph().draw_mermaid_png()
    with open("plan_execute.png", "wb") as f:
        f.write(graph_png)

    # 设置配置,递归限制为50
    config = {"recursion_limit": 50}
    # 输入数据
    inputs = {"input": "2024年巴黎奥运会100米自由泳决赛冠军的家乡是哪里?请用中文答复"}
    # 异步执行状态图,输出结果
    async for event in app.astream(inputs, config=config):
        for k, v in event.items():
            if k != "__end__":
                print(v)


# 运行异步函数
asyncio.run(main())

工具代码:

#示例:plan_execute.py
from langchain_community.tools.tavily_search import TavilySearchResults
# 创建TavilySearchResults⼯具,设置最⼤结果数为1
tools = [TavilySearchResults(max_results=1)]

定义我们的执行代理

现在我们将创建要⽤于执⾏任务的执⾏代理。 请注意,对于此示例,我们将对每个任务使⽤相同
的执⾏代理,但这并⾮必须如此。

from langchain import hub
from langchain_openai import ChatOpenAI
import asyncio
from langgraph.prebuilt import create_react_agent
# 从LangChain的Hub中获取prompt模板,可以进⾏修改
prompt = hub.pull("wfh/react-agent-executor")
prompt.pretty_print()
# 选择驱动代理的LLM,使⽤OpenAI的ChatGPT-4o模型
llm = ChatOpenAI(model="gpt-4o")
# 创建⼀个REACT代理执⾏器,使⽤指定的LLM和⼯具,并应⽤从Hub中获取的prompt
agent_executor = create_react_agent(llm, tools, messages_modifier=prompt)
# 调⽤代理执⾏器,询问“谁是美国公开赛的冠军”
agent_executor.invoke({"messages": [("user", "谁是美国公开赛的获胜者")]})

定义状态

现在让我们从定义要跟踪此代理的状态开始。 ⾸先,我们需要跟踪当前计划。 让我们将其表示为字符串列表。 接下来,我们应该跟踪先前执⾏的步骤。 让我们将其表示为元组列表(这些元组将包含步骤及其 结果) 最后,我们需要⼀些状态来表示最终响应以及原始输⼊。

import operator
from typing import Annotated, List, Tuple, TypedDict
# 定义⼀个TypedDict类PlanExecute,⽤于存储输⼊、计划、过去的步骤和响应
class PlanExecute(TypedDict):
 input: str
 plan: List[str]
 past_steps: Annotated[List[Tuple], operator.add]
 response: str

 规划步骤

现在让我们考虑创建规划步骤。 这将使⽤函数调⽤来创建计划。

from langchain_core.pydantic_v1 import BaseModel, Field
# 定义⼀个Plan模型类,⽤于描述未来要执⾏的计划
class Plan(BaseModel):
 """未来要执⾏的计划"""
 steps: List[str] = Field(
 description="需要执⾏的不同步骤,应该按顺序排列"
 )
from langchain_core.prompts import ChatPromptTemplate
# 创建⼀个计划⽣成的提示模板
planner_prompt = ChatPromptTemplate.from_messages(
 [
 (
 "system",
 """对于给定的⽬标,提出⼀个简单的逐步计划。这个计划应该包含独⽴的任务,如
果正确执⾏将得出正确的答案。不要添加任何多余的步骤。最后⼀步的结果应该是最终答案。确保每
⼀步都有所有必要的信息 - 不要跳过步骤。""",
 ),
 ("placeholder", "{messages}"),
 ]
)
# 使⽤指定的提示模板创建⼀个计划⽣成器,使⽤OpenAI的ChatGPT-4o模型
planner = planner_prompt | ChatOpenAI(
 model="gpt-4o", temperature=0
).with_structured_output(Plan) 




# 调⽤计划⽣成器,询问“当前澳⼤利亚公开赛冠军的家乡是哪⾥?”
planner.invoke(
 {
 "messages": [
 ("user", "现任澳⽹冠军的家乡是哪⾥?")
 ]
 }
)




{'plan': ['查找2024年澳⼤利亚⽹球公开赛的冠军是谁', '查找该冠军的家乡是哪⾥', '⽤中⽂
回答该冠军的家乡']}

重新制定计划的步骤。 

现在,让我们创建⼀个根据上⼀步结果重新制定计划的步骤。

from typing import Union
# 定义⼀个响应模型类,⽤于描述⽤户的响应
class Response(BaseModel):
 """⽤户响应"""
 response: str
# 定义⼀个⾏为模型类,⽤于描述要执⾏的⾏为
class Act(BaseModel):
 """要执⾏的⾏为"""
 action: Union[Response, Plan] = Field(
 description="要执⾏的⾏为。如果要回应⽤户,使⽤Response。如果需要进⼀步使⽤
⼯具获取答案,使⽤Plan。"
 )
# 创建⼀个重新计划的提示模板
replanner_prompt = ChatPromptTemplate.from_template(
 """对于给定的⽬标,提出⼀个简单的逐步计划。这个计划应该包含独⽴的任务,如果正确执⾏
将得出正确的答案。不要添加任何多余的步骤。最后⼀步的结果应该是最终答案。确保每⼀步都有所
有必要的信息 - 不要跳过步骤。
你的⽬标是:
{input}
你的原计划是:
{plan}
你⽬前已完成的步骤是:
{past_steps}
相应地更新你的计划。如果不需要更多步骤并且可以返回给⽤户,那么就这样回应。如果需要,填写
计划。只添加仍然需要完成的步骤。不要返回已完成的步骤作为计划的⼀部分。"""
)
# 使⽤指定的提示模板创建⼀个重新计划⽣成器,使⽤OpenAI的ChatGPT-4o模型
replanner = replanner_prompt | ChatOpenAI(
 model="gpt-4o", temperature=0
).with_structured_output(Act)

创建图

现在我们可以创建图了!

from typing import Literal
# 定义⼀个异步主函数
async def main():
 # 定义⼀个异步函数,⽤于执⾏步骤
 async def execute_step(state: PlanExecute):
 plan = state["plan"]
 plan_str = "\n".join(f"{i + 1}. {step}" for i, step in enumerate(p
lan))
 task = plan[0]
 task_formatted = f"""对于以下计划:
{plan_str}\n\n你的任务是执⾏第{1}步,{task}。"""
 agent_response = await agent_executor.ainvoke(
 {"messages": [("user", task_formatted)]}
 )
 return {
 "past_steps": state["past_steps"] + [(task, agent_response["me
ssages"][-1].content)],
 }
 # 定义⼀个异步函数,⽤于⽣成计划步骤
 async def plan_step(state: PlanExecute):
 plan = await planner.ainvoke({"messages": [("user", state["inpu
t"])]})
 return {"plan": plan.steps}
 # 定义⼀个异步函数,⽤于重新计划步骤
 async def replan_step(state: PlanExecute):
 output = await replanner.ainvoke(state)
 if isinstance(output.action, Response):
 return {"response": output.action.response}
 else:
 return {"plan": output.action.steps}
 # 定义⼀个函数,⽤于判断是否结束
 def should_end(state: PlanExecute) -> Literal["agent", "__end__"]:
 if "response" in state and state["response"]:
 return "__end__"
 else:
 return "agent"
from langgraph.graph import StateGraph, START
# 创建⼀个状态图,初始化PlanExecute
workflow = StateGraph(PlanExecute)
# 添加计划节点
workflow.add_node("planner", plan_step)
# 添加执⾏步骤节点
workflow.add_node("agent", execute_step)
# 添加重新计划节点
workflow.add_node("replan", replan_step)
# 设置从开始到计划节点的边
workflow.add_edge(START, "planner")
# 设置从计划到代理节点的边
workflow.add_edge("planner", "agent")
# 设置从代理到重新计划节点的边
workflow.add_edge("agent", "replan")
# 添加条件边,⽤于判断下⼀步操作
workflow.add_conditional_edges(
 "replan",
 # 传⼊判断函数,确定下⼀个节点
 should_end,
)
# 编译状态图,⽣成LangChain可运⾏对象
app = workflow.compile()
# 将⽣成的图⽚保存到⽂件
 graph_png = app.get_graph().draw_mermaid_png()
 with open("plan_execute.png", "wb") as f:
 f.write(graph_png)

# 设置配置,递归限制为50
config = {"recursion_limit": 50}
# 输⼊数据
inputs = {"input": "2024年巴黎奥运会100⽶⾃由泳决赛冠军的家乡是哪⾥?请⽤中⽂答复"}
# 异步执⾏状态图,输出结果
async for event in app.astream(inputs, config=config):
 for k, v in event.items():
 if k != "__end__":
 print(v)
{'plan': ['查找2024年巴黎奥运会100⽶⾃由泳决赛冠军的名字', '查找该冠军的家乡']}
{'past_steps': [('查找2024年巴黎奥运会100⽶⾃由泳决赛冠军的名字', '2024年巴黎奥运会
男⼦100⽶⾃由泳决赛的冠军是中国选⼿潘展乐(Zhanle Pan)。')]}
{'plan': ['查找潘展乐的家乡']}
{'past_steps': [('查找2024年巴黎奥运会100⽶⾃由泳决赛冠军的名字', '2024年巴黎奥运会
男⼦100⽶⾃由泳决赛的冠军是中国选⼿潘展乐(Zhanle Pan)。'), ('查找潘展乐的家乡', '潘
展乐的家乡是浙江温州。')]}
{'response': '2024年巴黎奥运会100⽶⾃由泳决赛冠军潘展乐的家乡是浙江温州。'}

结束!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值