LangChain补充五:Agent之LangGraph的使用
一:LangGraph入门
https://www.51cto.com/article/781996.html
https://blog.csdn.net/weixin_41496173/article/details/139023846
https://blog.csdn.net/wjjc1017/article/details/138518087
https://langchain-ai.github.io/langgraph/
https://langchain-ai.github.io/langgraph/tutorials/
https://langchain-ai.github.io/langgraph/how-tos/
(一)Chain、LangChain代理、LangGraph
1.Chain:线性执行的顺序链
LangChain的核心优势在于其能够轻松构建自定义链,这些链通常是线性的,类似于有向无环图(DAG),每个步骤都严格按顺序执行,每个任务只有一个输出和一个后续任务,形成一个没有循环的线性流程。
在遇到复杂任务时,比如第一次搜索没有找到想要的内容,我们可能需要进行第二次、第三次搜索,甚至可能需要调用网络搜索来完成。在这种情况下,顺序执行的任务(DAG)显然无法满足需求。
请求方和搜索方之间需要经历多次来回沟通,请求方可能会要求搜索方根据反馈调整搜索策略,这种多次的循环沟通才能逐步逼近最终答案。
2.LangChain代理:循环图,但是无法控制
这种情况下,我们需要的不再是DAG,而是一个循环图,它能够描述多个参与者之间的多轮对话和互动,以确认最终的答案。这种循环图能够处理更模糊、更复杂的用例,因为它允许系统根据反馈进行调整和迭代。那么,在循环图的运行模式就是智能代理,也就是AI Agent。
![](https://img2024.cnblogs.com/blog/1309518/202407/1309518-20240718144444376-400574279.png)
3.LangGraph,可以对智能代理进行更多的控制
在实际应用过程中,我们发现需要对智能代理进行更多的控制。例如,我们可能希望智能代理始终首先调用特定工具,或者我们可能希望对工具的调用方式有更多的控制,甚至可能希望根据智能代理的状态使用不同的提示。为了解决这些问题,LangGraph提出了“状态机”的概念。通过状态机为图创建对应的状态机,这种方法可以更好地控制智能代理的行动流程,使其更加灵活和有效地处理复杂任务。
补充:LangChain代理和LangGraph对比
|
可靠性
|
灵活性
|
Langchain 代理
|
可靠性较低,因为 LLM 需要在每个步骤上做出正确的决策
|
更灵活,因为 LLM 可以选择任何动作序列
|
LangGraph
|
可靠性更高,因为控制流已经设置好,LLM 在每个节点上有具体的任务
|
灵活性较低,因为动作受限于在每个节点上设置控制流
|
(二)LangGraph概念
1.StateGraph(状态图):包含了状态表示的一个图结构
StateGraph是一个类,它负责表示整个图的结构;需要通过传入一个状态定义来初始化这个类(图状态),这个图状态代表了一个中心状态对象,它会在执行过程中不断更新。这个图状态对象由图中的节点更新,节点会以键值对的形式,返回对状态属性的操作。
状态对象的属性可以通过两种方式更新,在定义图状态的时候,需要指定属性的更新方式:(可以有多个属性,每个属性需要单独指定一种更新方式)
- 覆盖更新:如果一个属性需要被新的值替换,我们可以让节点返回这个新值进行替换。
- 增量更新:如果一个属性是一个动作列表(或类似的操作),我们可以在原有的列表上添加新的动作。
旅行为例:
1.输入目的地(Edges里面的起始边)
2.任务规划,包括预定航班、预订酒店(Nodes里面的各个节点)
3.任务规划之间是有关联关系的,比如航班夜晚,我们就不要酒店了,航班改中间位置就要修改酒店地址,之间有因果关系(Edges里面的边,分为普通边和条件边,普通边就是任务有序执行,条件边就是可能根据情况不一样)
4.StateGraph类就是这样的旅行计划,而节点就像是规划旅行的不同步骤,比如确定目的地、预定航班和预定酒店。每个步骤都会更新你的旅行计划,可能是完全替换旧的计划,也可能是添加新的信息到现有的计划中
2.Nodes(节点)
创建了StateGraph之后,需要向其中添加节点;每个节点都代表一个任务,它们执行的结果会影响StateGraph的状态。这些节点通过边相互连接,形成了一个有向无环图(DAG),确保了任务的正确执行顺序。
上面旅行计划的例子,Nodes(节点)就好像旅行计划中需要完成的任务,例如:预定航班、预订酒店。Nodes(节点)接受旅行计划(图状态对象)作为输入,并输出一个更新后的任务状态,例如:完成酒店的预订
3.Edges(边)
Edges(边)是连接Nodes(节点)并定义StateGraph(状态图)中节点执行顺序的关键部分。边主要分为以下几种:
- 起始边(Starting Edge):作为图的开始,比如在旅行计划中,起始边就是确定你的目的地。一旦目的地被确定,你的旅行计划就可以开始执行了。
- 普通边(Normal Edges):这些边表示一个节点总是要在另一个节点之后被调用。在旅行计划中,普通边就像是确定了任务执行的顺序。例如,在找到合适的航班之后,你可能会决定预订酒店。这个顺序确保了任务的有序执行。
- 条件边(Conditional Edges):使用函数(通常由LLM提供)来确定首先调用哪个节点。在旅行计划中,条件边就像是根据你的喜好或者天气情况来决定你的下一步行动。比如,如果你发现没有合适的航班,你可能会选择推迟预订酒店,而去查找火车车票。条件边提供了灵活性,使得系统可以根据不同的情况来调整执行的顺序。
边定义了节点之间的依赖关系和执行顺序。起始边确定了图的开始,普通边确保了任务的正确执行顺序,而条件边则根据特定的条件来决定下一步的操作
二:LangGraph使用
(一)步骤归纳
1.初始化model和tools,可以通过agent关联models和tools
2.定义图状态,包括各个属性比如:用户输入(覆盖),聊天历史(增量),中间步骤(代理采取的行动和相应的观察),代理结果(代理的响应)
3.定义图节点,包括1中的代理、工具
4.定义边的逻辑判断(条件边)
5.定义工作流状态图
a.通过图状态初始化工作流
b.添加节点、边
c.编译工作流成runnable
6.执行状态图
(二)实例一:状态图stateGraph和节点node的使用(不含图状态和注解)
from langgraph.graph import START, StateGraph
#1.不涉及model和tool
#2.不涉及图状态
#3.定义图节点,定义节点需要设置一个节点的action(函数)
def my_node(state):
return {"x": state["x"] + 1, "y": state["y"]-1,"z":10}
#4.不需要条件边
#5.定义工作流状态图
builder = StateGraph(dict) #注意:没有传入图状态,dict默认全部input的字段都作为图状态属性传入,比如后面的x,y都是StateGraph的state
builder.add_node("my_fair_node", my_node) #添加节点
builder.add_edge(START, "my_fair_node") #添加起始边
#6.编译
graph = builder.compile()
step1 = graph.invoke({"x": 1,"y":2})
print(step1)
![](https://img2024.cnblogs.com/blog/1309518/202407/1309518-20240718144444434-1958102322.png)
如果我们尝试更新节点的action,会发现每一步的图状态更新和节点的返回结果相关
def my_node(state):
return {"x": state["x"] + 1}
![](https://img2024.cnblogs.com/blog/1309518/202407/1309518-20240718144444242-1471684948.png)
我们传递了y,但是节点没有返回
(三)实例二:状态图stateGraph和节点node的使用(包含图状态和注解)
from langchain_core.runnables import RunnableConfig
from typing_extensions import Annotated, TypedDict
from langgraph.graph import StateGraph
#1.不涉及model和tool
#2.定义图状态
def reducer(a: list, b: list | None) -> list: #进行追加
if b is not None:
return a + b
return a
class State(TypedDict): #这里是图中状态的定义,也是标识运行时的input中哪些字段可以透传给状态图
#https://blog.csdn.net/randy521520/article/details/133826255
x: Annotated[list, reducer] #Annotated: 用于添加类型注解的装饰器。可以在类型提示中添加额外的元数据信息;这里表示更新list数据的时候,通过reducer方法进行更新(第一个参数时原数据list,第二个参数是要更新的数据int)
class ConfigSchema(TypedDict): #这里是标识运行时的RunnableConfig中哪些字段可以透传给状态图
r: float
#3.定义图节点,定义节点需要设置一个节点的action(函数)
def node_runner(state: State, config: RunnableConfig) -> dict: #传入图状态,可以进行修改;还可以传入config,config在调用时传递,同时还需要在创建状态图时再声明哪些变量可以传递到图中
r = config["configurable"].get("r", 1.0) #从config中获取配置字段值
print(state["x"])
x = state["x"][-1] #获取最新的状态
next_value = x * r * (1 - x) #随便的操作
return {"x": [next_value],"y":10} #注意:返回的值,会通过注解里面的reducer进行更新(作为第二个参数),所以类型要一致
#4.不需要条件边
#5.定义工作流状态图,没有边
graph = StateGraph(State, config_schema=ConfigSchema)
graph.add_node("A", node_runner) #添加节点,包括节点的action
graph.set_entry_point("A") #设置进入节点
graph.set_finish_point("A") #设置结束节点
#6.编译
compiled = graph.compile()
step1 = compiled.invoke({"x": [0.5,1],"y":0.3}, {"configurable": {"r": 3.0}})
print(step1)
![](https://img2024.cnblogs.com/blog/1309518/202407/1309518-20240718144444314-338301611.png)
可以看到,我们显式定义了图状态里面的属性后,节点返回的数据在更新图状态时只会去获取图状态定义中的字段进行更新
(四)实例三:包含了model和tool的使用
这里用到了一个新的模型anthropic.claude-3-5-sonnet,Claude 3 是一系列最先进的人工智能(AI)模型,可让您根据自己的需求,从智能、速度和成本方面考虑选择最适合的组合。并且该模型能够可靠地从图像等非结构化数据中提取信息。
https://docs.anthropic.com/zh-CN/docs/intro-to-claude
但是有国家限制,离谱了,得到API Key也没有用,还要配合技术才行
from typing import Literal
from langchain_core.messages import ToolMessage
from langchain_openai import ChatOpenAI
from langchain_core.tools import tool
from langgraph.checkpoint import MemorySaver
from langgraph.graph import END, StateGraph, MessagesState
from langgraph.prebuilt import ToolNode
from langchain.prompts import MessagesPlaceholder,ChatPromptTemplate
#1.初始化model和tools,可以通过agent关联models和tools
@tool
def search(query: str):
"""Call to surf the web."""
# This is a placeholder, but don't tell the LLM that...
if "sf" in query.lower() or "san francisco" in query.lower():
return ["It's 60 degrees and foggy."]
return ["It's 90 degrees and sunny."]
tools = [search]
prompt = ChatPromptTemplate.from_messages(
[
("system", "You are a helpful assistant"),
MessagesPlaceholder("chat_history", optional=True),
("human", "{messages}"),
MessagesPlaceholder("agent_scratchpad", optional=True),
]
)
#model在这里用于选取tools
model = prompt | ChatOpenAI(model="gpt-3.5-turbo", temperature=0).bind_tools(tools)
#2.定义图状态,这里使用的MessagesState,属性如下:
# class MessagesState(TypedDict):
# messages: Annotated[list[AnyMessage], add_messages]
# 3.定义图节点
# tool_node = ToolNode(tools) 这也是一种方法,定义tool和model的运行action
tools_by_name = {tool.name: tool for tool in tools}
def tool_node(state: dict):
result = []
for tool_call in state["messages"][-1].tool_calls:
tool = tools_by_name[tool_call["name"]]
observation = tool.invoke(tool_call["args"])
result.append(ToolMessage(content=observation, tool_call_id=tool_call["id"]))
return {"messages": result}
def call_model(state: MessagesState):
response = model.invoke(state)
return {"messages": [response]} #add_messages兼容list和非list,都会转成list
# 4.定义边的逻辑判断(条件边),判断是否继续
def should_continue(state: MessagesState) -> Literal["tools", "__end__"]: #Literal用于限制返回的值的可选值
messages = state['messages']
last_message = messages[-1]
if last_message.tool_calls: #判断models是否返回tools调用,有则告诉调用tools节点,否则结束
return "tools"
return END
# 5.定义工作流状态图
# 5.a.通过图状态初始化工作流
workflow = StateGraph(MessagesState)
# 5.b.添加节点、边
workflow.add_node("agent", call_model)
workflow.add_node("tools", tool_node)
workflow.set_entry_point("agent")
workflow.add_conditional_edges(
"agent",
should_continue, #判断下一个调用的节点
)
workflow.add_edge("tools", 'agent')
# 5.c.编译工作流成一个runnable,通过invoke调用
app = workflow.compile()
# 6.执行状态图
final_state = app.invoke(
{"messages": "what is the weather in sf"},
config={"configurable": {"thread_id": 42}}
)
print(final_state["messages"][-1].content)
![](https://img2024.cnblogs.com/blog/1309518/202407/1309518-20240718144444322-1350524920.png)