Graph 概念解析

     1.基本概念

        Graph:在LangGraph中Graph是一个用来处理数据的有向图,由节点(Node)和连接节点的边(Edge)构成,其处理的数据称为状态(State) 。

        State:状态可理解为所有节点共享的数据结构,有一定的模式,并且随着数据的流转而变化。

        Node:节点封装了业务逻辑,以当前的状态作为输入,并且在完成业务处理后更新状态

        Edge:把节点串联起来。边根据当前的状态判断一个节点执行完成后下一个节点是哪个,在Graph中的边有固定边和条件边两种边,固定边是指不管当前状态如何,都必然流转到一个固定的节点,条件边则根据当前的状态,把执行权流转到不同的节点。

        super-step:图中的一次迭代处理称为super-step,多个并行执行的节点视为一个super-step,串行执行的节点分属不同的super-step

        在LangGraph中State是一种数据结构,而Node和Edge实际上都是函数。super-step是一个逻辑上的概念。

      2.State

      2.1简述

        State在所有节点和边中共享,也就是说是Graph中所有Node和Edge的输入,当然State是随时间变化的。

        State是一种数据结构,由数据和规约函数构成,规约函数用于对State执行更新操作。

        每个节点接收最新的State数据后,执行内部业务逻辑处理,然后发出更新数据,在框架中调用规约函数用更新数据对State进行更新。

        State中可以使用三种数据类型保存数据,最直接的是使用TypedDict;如果想给每项数据赋予缺省值,可以使用@dataclass;当然也可以使用Pydantic BaseModel。可根据实际需要或个人偏好选择。

      2.2多模式

        一般情况下,在一个Graph中所有的Node使用同一个State(数据结构),但LangGraph是支持多数据结构并存的。比如,输入和输出状态可以分别使用不同的数据结构,还可以定义在内部节点内流转的私有数据结构。

       2.2.1输入和输出使用不同的数据结构

        当共用的State中包含很多的数据项,并且输入和输出数据分别关注不同的数据项,为了提高内聚性,输入和输出可以使用不同的数据结构。

        有一点需要主要,虽然输入和输出可以采用不同的数据结构,但事实上公共的State仍然是存在的,并且在创建图时需要传入的,所以输出和输出数据结构只能是State的子集。

        以下是一个简单示例:

from langgraph.graph import StateGraph, START, END
from typing_extensions import TypedDict

# 定义输入数据结构
class InputState(TypedDict):
    person: str

# 定义输出数据结构
class OutputState(TypedDict):
    birthday: str
    deathdate: str

# 全局数据结构,实际为输出和输出的合集
class OverallState(InputState, OutputState):
    pass

# 接受输入数据结构作为输入的节点
def answer_node(state: InputState):
    #根据输入数据中的人物查询生卒日期
    return {"birthday": "1090年1月26日", "deathdate": "1151年9月15日"}

# 构建图,传入全局数据接口,并通过关键字指定输入和输出数据结构
builder = StateGraph(OverallState, input_schema=InputState, output_schema=OutputState)
builder.add_node(answer_node)  # Add the answer node
builder.add_edge(START, "answer_node")  # Define the starting edge
builder.add_edge("answer_node", END)  # Define the ending edge
graph = builder.compile()  # Compile the graph

# Invoke the graph with an input and print the result
print(graph.invoke({"question": "韩世忠"}))

        运行结果如下:

{'birthday': '1000年5月5日', 'deathdate': '1100年5月4日'}

      2.2.2使用私有数据结构

        有时需要在部分节点之间传递一些与全局数据结构State无关的数据,此时需要使用私有数据结构实现。

        以下是一个简单示例:

from langgraph.graph import StateGraph, START, END
from typing_extensions import TypedDict

class GlobalState(TypedDict):#全局数据结构
    global_data: str
class Node1Output(TypedDict): #节点1输出的私有数据
    private_data: str

def node_1(state: GlobalState) -> Node1Output:#输入为全局数据,输出为私有数据
    output = {"private_data": "1"}
    print(f"Entered node `node_1`:\n\tInput: {state}.\n\tReturned: {output}")
    return output

class Node2Input(TypedDict):#节点2输入的私有数据
    private_data: str

def node_2(state: Node2Input) -> GlobalState:#输入为私有数据,输出为全局数据
    output = {"global_data": "2"}
    print(f"Entered node `node_2`:\n\tInput: {state}.\n\tReturned: {output}")
    return output

# 仅访问全局数据,不能访问私有数据
def node_3(state: GlobalState) -> GlobalState:
    output = {"global_data": "3"}
    print(f"Entered node `node_3`:\n\tInput: {state}.\n\tReturned: {output}")
    return output

#把三个节点串接起来。不需要显示的增加节点和边
builder = StateGraph(GlobalState).add_sequence([node_1, node_2, node_3])
builder.add_edge(START, "node_1")
graph = builder.compile()

# 调用,看输出结果
response = graph.invoke(
    {
        "global_data":"0",
    }
)

print()
print(f"Output of graph invocation: {response}")

        输出如下:

Entered node `node_1`:
    Input: {'global_data': '0'}.
    Returned: {'private_data': '1'}
Entered node `node_2`:
    Input: {'private_data': '1'}.
    Returned: {'global_data': '2'}
Entered node `node_3`:
    Input: {'global_data': '2'}.
    Returned: {'global_data': '3'}

Output of graph invocation: {'global_data': '3'}

      2.3 规约器(Reducer)

        Reducer用于更新全局数据结构中的数据,全局数据结构中的每项数据都可以指定自己的规约器。如果未指定,则使用缺省的规约器,缺省规约器用新数据覆盖原有数据。

        2.3.1缺省规约器

        以下代码中,全局数据中每一项都采取覆盖策略。在每个节点都可以更新全局数据,在return中返回的数据是需要更新的项。也就是说在一个节点内,仅返回需要更新的项即可,而不是返回整个全局数据State。由框架在完成State的更新。

from langgraph.graph import StateGraph, START, END
from typing_extensions import TypedDict

class State(TypedDict):
    data: str
    arr: list[str]

def node_1(state: State) :#更新全局数据中的data项
    output = {"data": "new_data"}
    print(f"Entered node `node_1`:\n\tInput: {state}.\n\tReturned: {output}")
    return output

def node_2(state: State) :#更新全局数据中的arr项
    output = {"arr": ["new_item"]}
    print(f"Entered node `node_2`:\n\tInput: {state}.\n\tReturned: {output}")
    return output

#两个节点的一个简单图
builder = StateGraph(State).add_sequence([node_1, node_2,])
builder.add_edge(START, "node_1")
graph = builder.compile()

# Invoke the graph with the initial state
response = graph.invoke(
    {
        "data":"old_data",
        "arr":["old_item"],        
    }
)

print()
print(f"Output of graph invocation: {response}")

        运行结果如下:

Entered node `node_1`:
    Input: {'data': 'old_data', 'arr': ['old_item']}.
    Returned: {'data': 'new_data'}
Entered node `node_2`:
    Input: {'data': 'new_data', 'arr': ['old_item']}.
    Returned: {'arr': ['new_item']}

Output of graph invocation: {'data': 'new_data', 'arr': ['new_item']}

        2.3.2追加规约器

        针对全局数据中的列表项,一般使用追加规约器,从而可以保存历史数据。 对以上代码中的的State进行修改,其他代码不变:

class State(TypedDict):
    data: str
    arr:  Annotated[list[str], add]

        运行结果如下:

Entered node `node_1`:
    Input: {'data': 'old_data', 'arr': ['old_item']}.
    Returned: {'data': 'new_data'}
Entered node `node_2`:
    Input: {'data': 'new_data', 'arr': ['old_item']}.
    Returned: {'arr': ['new_item']}

Output of graph invocation: {'data': 'new_data', 'arr': ['old_item', 'new_item']}

        注意:在实际的聊天类应用中,规约器使用add_message。

     3.Node

     3.1普通节点

        创建一个Node时,必须传入State参数,可选择性的传入Runtime或RunnableConfig参数,示例代码如下:

def node(state: State, runtime: Runtime[Context], config: RunnableConfig):

    print("In node: ", runtime.context.user_id)

    print("In node with thread_id: ", config["configurable"]["thread_id"])

    return {"results": f"Hello, {state['input']}!"}

        其中config保存了配置信息,比如thread,tag等,runtime保存了运行时的上下文或其他信息,比如Store。

        把节点增加到图中需要调用add_node方法,比如把上面的节点node增加到图中:

#add_node中的第一个参数为节点名,第二个节点。如果为传入节点名,则生成一个与函数

#名相同的节点

builder.add_node("mynode", node)

    3.2特殊节点

        在Langgraph中有两类特殊节点:START和END。

        START节点的作用是把用户的输入传入图中,其所指向的节点是第一个被执行普通节点。

        END节点的作用是表示一个轮次处理的结束,并不执行任何操作。

    4.Edge

        在LangGraph中边用于控制流传逻辑,定义了agent如何运转以及节点之间如何交互。边有四种类型:普通边、条件边、入口点和条件入口点。

    4.1普通边

        普通边用户连接两个节点,第一个参数为起点,第二个参数为终点,比如:

graph.add_edge("node1", "node2")

    4.2条件边

        控制流从一个节点流出后,如果有多个目的地,则需要使用条件边。使用条件边需要先定义路由函数,比如:

#路由函数的输入为全局数据结构state,并根据state中的数据动态决定下一个节点是谁

def make_seal_router(
    state: State,
):
    message = state.messages[-1].content
    prompt = f"""
    根据输入信息判断是否表达了将马上为用户制作印章的意图,返回结果如下:
    - "yes": 如果表达了将马上为用户制作印章的意图
    - "no": 如果未表达将马上为用户制作印章的意图

    输入信息:{message}
    """
    response = llm.predict(prompt)
    if  response == "yes":
        return "make_seal_bot" #如果大模型能够制作印章,则进入make_seal_bot节点
    return END

        在图中增加条件边:

#从premake_node出发的两条边,分别指向make_seal_bot和END
graph_builder.add_conditional_edges(
    "premake_node",
    make_seal_router,
    {"make_seal_bot": "make_seal_bot", END: END}
)

    4.3入口点

        入口点用户指定图中第一个执行的节点,直接通过在START和第一个执行节点之间建一个边即可,具体代码如下:

#第一个执行的节点是node1

from langgraph.graph import START

graph.add_edge(START, "node1")

    4.4条件入口点

        当工作流START节点出发存在多个目的节点时,使用条件入口点。此时仍需要先创建一个路由函数,然后增加条件边。

        比如,如下代码中由大模型识别用户的意图,然后根据不同的意图路由至不同的节点:

def entry_router(
    state: State,
):
    prompt = f"""
    根据用户输入分析用户的意图类型,返回以下之一:
    - make: 用户想制作电子印章
    - manage: 用户想对印章进行管理,比如对印章冻结、解冻、注销、续期和变更
    - grant: 用户想把印章授权给其他用户管理管理或使用
    — consult: 用户想咨询印章相关的业务知识或技术知识
    — chat: 其他
    用户输入{state.messages}
    """
    response = llm.predict(prompt)
    match response:
        case "make":
            return "premake_node"
        case "manage":
            return "premanage_node"
        case _:
            return "chatbot

        然后增加条件边,具体代码如下:

graph_builder.add_conditional_edges(
    START,
    entry_router,
    {"premake_node": "premake_node", "premanage_node": "premanage_node","chatbot": "chatbot"}
)

     5.Send

       在上面的讨论中,图中的节点和边全部是事先确定的。但有些情况下,节点是不能实现确定的。比如大数据处理MapReduce的reduce任务数并不能事先确定,此时需要使用Send来实现。使用Send一方面可以实时指定数量的下游节点,另一方面给不同的下游节点提供不同的输入数据。

        具体示例代码如下:

#根据当前的数据列表生成Send列表

def continue_to_asses(state: OverallState):

    return [Send("assesments", {"subject": s}) for s in state['persons']]

#增加条件边

graph.add_conditional_edges("node", continue_to_asses)

     6.Command

        Command是一个非常重要的类,具体应用常见包括:

        1)在节点内部同时完成更新的跳转控制

        2)子图跳转

        3)在工具内修改全局数据

        4)在用户回环中从中断恢复

      6.1 在节点内部完成更新和跳转控制

        前面所述工作流的流转和对全局数据的更新是分开的,其中边(包括条件边)控制流转,在节点内部进行全局数据的更新。如果需要在节点内部同时控制流转和更新全局数据,则需要使用Command。

        一个简单的示例如下:

def my_node(state: State) -> Command[Literal["next_node"]]:

    return Command(

        update={"data": "100"}, #更新全局数据

       goto="next_node" )#进入next_node。控制流转

       使用Command还可以根据不同条件路由到不同的节点:

#与增加一个条件边一样

def my_node(state: State) -> Command[Literal["next_node"]]:

    if state["data"] == "100":

        return Command(update={"data": "-100"}, goto="next_node")

    else:

        return Command(update={"data": "500"}, goto="prev_node")

      6.2 子图跳转

        在包括子图的复杂图中。如果想从一个子图中的某个节点跳转至另外一个子图(或者是父图的某个节点),则需要使用Command,具体代码如下:

def my_node(state: State) -> Command[Literal["other_subgraph"]]: return Command( update={"foo": "bar"}, goto="other_subgraph", # where `other_subgraph` is a node in the parent graph graph=Command.PARENT )

      6.3 在工具内部更新全局数据结构

        根据配置中包括的统一社会信用代码,调用外部接口获取企业信息,然后更新全局数据结构和历史对话,具体代码如下:

@tool def lookup_ent_info(tool_call_id: Annotated[str, InjectedToolCallId], config: RunnableConfig):

    ent_info = get_ent_info(config.get("configurable", {}).get("uniscid"))#调用外部接口

    return Command( update={

        # 更新全局数据结构

        "ent_info": ent_info,

        # 更新历史对话

        "messages": [ToolMessage("Successfully looked up enterprise information",         

        tool_call_id=tool_call_id)] }

)

      6.4 在用户回环中从中断恢复

        具体参见使用langgraph创建工作流系列4:人机回环

(Kriging_NSGA2)克里金模型结合多目标遗传算法求最优因变量及对应的最佳自变量组合研究(Matlab代码实现)内容概要:本文介绍了克里金模型(Kriging)与多目标遗传算法NSGA-II相结合的方法,用于求解最优因变量及其对应的最佳自变量组合,并提供了完整的Matlab代码实现。该方法首先利用克里金模型构建高精度的代理模型,逼近复杂的非线性系统响应,减少计算成本;随后结合NSGA-II算法进行多目标优化,搜索帕累托前沿解集,从而获得多个最优折衷方案。文中详细阐述了代理模型构建、算法集成流程及参数设置,适用于工程设计、参数反演等复杂优化问题。此外,文档还展示了该方法在SCI一区论文中的复现应用,体现了其科学性与实用性。; 适合人群:具备一定Matlab编程基础,熟悉优化算法和数值建模的研究生、科研人员及工程技术人员,尤其适合从事仿真优化、实验设计、代理模型研究的相关领域工作者。; 使用场景及目标:①解决高计算成本的多目标优化问题,通过代理模型降低仿真次数;②在无法解析求导或函数高度非线性的情况下寻找最优变量组合;③复现SCI高水平论文中的优化方法,提升科研可信度与效率;④应用于工程设计、能源系统调度、智能制造等需参数优化的实际场景。; 阅读建议:建议读者结合提供的Matlab代码逐段理解算法实现过程,重点关注克里金模型的构建步骤与NSGA-II的集成方式,建议自行调整测试函数或实际案例验证算法性能,并配合YALMIP等工具包扩展优化求解能力。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值