LangGragh基础
LangGragh基础
1.0 介绍
LangGraph 是由 LangChain 团队开发的一个开源框架,旨在帮助开发者构建基于大型语言模型(LLM)的复
杂、有状态、多主体的应用。它通过将工作流表示为图结构(graph),提供了更高的灵活性和控制能力,特别适
合需要循环逻辑、状态管理以及多主体协作的场景,比如智能代理(agent)和多代理工作流。
LangGraph 是为智能体和工作流设计一套底层编排框架
官方文档:https://langchain-ai.github.io/langgraph/
1.1 工具构建
我们智能体肯定是需要使用工具的,现在先来学习工具怎么创建,先前在学习langchain时其实就在function
calling中用到了工具,这和那里是一样的
import json
import os
import requests
from langchain.chat_models import init_chat_model
from pydantic import BaseModel,Field
from langchain_core.tools import tool
os.environ["OPENAI_API_KEY"] = os.getenv("DASHSCOPE_API_KEY")
os.environ["OPENAI_BASE_URL"] = "https://dashscope.aliyuncs.com/compatible-mode/v1"
os.environ
llm=init_chat_model(
model="qwen-plus",
model_provider="openai"
)
class Weather(BaseModel):
loc :str =Field(...,description="城市的英文名")
@tool(args_schema=Weather)
def get_weather(loc:str)->dict :
"""
查询即时天气的函数
:param loc(str): 必要参数,用于表示查询天气的具体城市名称,注意中国的城市需要用英文名称代替,
例如如果需要查询北京市天气,则loc参数需要输入'Beijing
:return (dict): 通过api查询 返回天气查询的结果,包含完整的天气查询信息
"""
url="https://api.openweathermap.org/data/2.5/weather"
params={
"q":loc,
"appid":"your_api",
"units":"metric",
"lang": "zh_cn"
}
response=requests.get(url, params)
print(response.text)
return json.dumps(response.json())
print(get_weather("HeFei"))
注意我们使用@tool(args_schema=Weather)中的内容是对传入参数的补充说明,但此时我们工具描述中对参数
的说明很清晰了,其实也可以去掉
1.2 预构建agent
我们现在构建一个agent 自动调用上面的工具,获得结果:
import json
import os
import requests
from langchain.chat_models import init_chat_model
from pydantic import BaseModel,Field
from langchain_core.tools import tool
from langgraph.prebuilt import create_react_agent
os.environ["OPENAI_API_KEY"] = os.getenv("DASHSCOPE_API_KEY")
os.environ["OPENAI_BASE_URL"] = "https://dashscope.aliyuncs.com/compatible-mode/v1"
os.environ
llm=init_chat_model(
model="qwen-plus",
model_provider="openai"
)
class Weather(BaseModel):
loc :str =Field(...,description="城市的英文名")
@tool(args_schema=Weather)
def get_weather(loc:str)->dict :
"""
查询即时天气的函数
:param loc(str): 必要参数,用于表示查询天气的具体城市名称,注意中国的城市需要用英文名称代替,
例如如果需要查询北京市天气,则loc参数需要输入'Beijing
:return (dict): 通过api查询 返回天气查询的结果,包含完整的天气查询信息,以json字符串的格式返回
"""
url="https://api.openweathermap.org/data/2.5/weather"
params={
"q":loc,
"appid":"your_api",
"units":"metric",
"lang": "zh_cn"
}
response=requests.get(url, params)
return json.dumps(response.json())
tools=[get_weather]
messages=[("system","你是一个天气查询智能体,当用户向你提问某地天气时,你会通过工具调用告诉用户当时的天气")]
agent=create_react_agent(model=llm,tools=tools)
messages.append(("human","今天合肥的天气如何"))
response=agent.invoke({"messages":messages})
print(response["messages"][-1].content)
agent自动处理了function calling的整个过程,相比我们使用langchain,简化了很多
react agent可以实现串联工具调用和并联工具调用,下面来看串联的结果:
import json
import os
import requests
from langchain.chat_models import init_chat_model
from pydantic import BaseModel,Field
from langchain_core.tools import tool
from langgraph.prebuilt import create_react_agent
os.environ["OPENAI_API_KEY"] = os.getenv("DASHSCOPE_API_KEY")
os.environ["OPENAI_BASE_URL"] = "https://dashscope.aliyuncs.com/compatible-mode/v1"
os.environ
llm=init_chat_model(
model="qwen-plus",
model_provider="openai"
)
class Weather(BaseModel):
loc :str =Field(...,description="城市的英文名")
@tool
def write_weather(weather:str)->str:
"""
将输入的天气结果写入到文件中
:param weather(str): 一串字符串,包含城市和对应的天气描述
:return (str): 返回一个字符串,表明是否写入成功
"""
try:
with open("weather.txt","a",encoding="UTF-8") as f:
f.write(f"{weather}\n")
return "success"
except Exception as e :
return "false"
@tool(args_schema=Weather)
def get_weather(loc:str)->dict :
"""
查询即时天气的函数
:param loc(str): 必要参数,用于表示查询天气的具体城市名称,注意中国的城市需要用英文名称代替,
例如如果需要查询北京市天气,则loc参数需要输入'Beijing
:return (dict): 通过api查询 返回天气查询的结果,包含完整的天气查询信息,以json字符串的格式返回
"""
url="https://api.openweathermap.org/data/2.5/weather"
params={
"q":loc,
"appid":"your_api",
"units":"metric",
"lang": "zh_cn"
}
response=requests.get(url, params)
return json.dumps(response.json())
tools=[get_weather,write_weather]
messages=[("system","你是一个天气查询智能体,当用户向你提问某地天气时,你会通过工具调用获得该地区的天气,并且将其通过工具写入到本地文件中")]
agent=create_react_agent(model=llm,tools=tools)
messages.append(("human","今天合肥的天气如何"))
response=agent.invoke({"messages":messages})
print(response["messages"][-1].content)
我们先前使用的都是自定义工具,现在看一下怎么调用内部网络搜索工具进行网络搜索:
import json
import os
import requests
from langchain_tavily import TavilySearch
from dotenv import load_dotenv
from langchain.chat_models import init_chat_model
from pydantic import BaseModel,Field
from langchain_core.tools import tool
from langgraph.prebuilt import create_react_agent
os.environ["OPENAI_API_KEY"] = os.getenv("DASHSCOPE_API_KEY")
os.environ["OPENAI_BASE_URL"] = "https://dashscope.aliyuncs.com/compatible-mode/v1"
load_dotenv()
os.environ
llm=init_chat_model(
model="qwen-plus",
model_provider="openai"
)
search=TavilySearch(max_result=5, topic="general",tavily_api_key=os.getenv("TAVILY"))
tools=[search]
messages=[("system","你是一个查询智能体,当用户向你提问问题时,通过工具进行网络搜索获得资料,对用户的问题进行回答")]
agent=create_react_agent(model=llm,tools=tools)
messages.append(("human","昨日中国有有什么新闻吗"))
response=agent.invoke({"messages":messages})
print(response["messages"][-1].content)
我们可以通过指定工具最大调用次数对工具调用进行限制
response=agent.invoke(
{"messages":messages},
{"recursion_limt":2}
)
通过这种方式就将工具调用次数限制到2
1.3 agent上下文记忆管理
我们之前管理上下文时,需要手动维护上下文的messages,在Langgragh中提供了更便捷的上下文管理方式:
import json
import os
import requests
from langchain_tavily import TavilySearch
from dotenv import load_dotenv
from langchain.chat_models import init_chat_model
from pydantic import BaseModel,Field
from langchain_core.tools import tool
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.prebuilt import create_react_agent
os.environ["OPENAI_API_KEY"] = os.getenv("DASHSCOPE_API_KEY")
os.environ["OPENAI_BASE_URL"] = "https://dashscope.aliyuncs.com/compatible-mode/v1"
load_dotenv()
os.environ
llm=init_chat_model(
model="qwen-plus",
model_provider="openai"
)
search=TavilySearch(max_result=5, topic="general",tavily_api_key=os.getenv("TAVILY"))
tools=[search]
messages=[("system","你是一个查询智能体,当用户向你提问问题时,通过工具进行网络搜索获得资料,对用户的问题进行回答")]
checkpointer=InMemorySaver()
agent=create_react_agent(model=llm,
tools=tools,
checkpointer=checkpointer)
config={
"configurable":{
"thread_id":"1"
}
}
messages.append(("human","你好,我叫mr_bird,请记住"))
response=agent.invoke(
{"messages":messages},
config
)
print(response["messages"][-1].content)
response=agent.invoke(
{"messages":[("human","你还记得我叫什么吗")]},
config
)
print(response["messages"][-1].content)
首先通过checkpointer=InMemorySaver() 创建InMemorySaver() 对象,在创建agent过程中传入, 表明要进行
记忆管理,因为需要做到线程隔离,所以我们对于不同的线程需要添加不同的编号:
config={
"configurable":{
"thread_id":"1"
}
}
最后再调用大模型时加上线程编号,
response=agent.invoke(
{"messages":messages},
config
)
然后我们大模型就会自动的维护messages,每次调用后,将messages保存在内存中,不需要我们手动进行维
护,后续的内容直接传入,不需要将整个历史对话全部传入了
上面程序输出的结果为:
你好,mr_bird!很高兴认识你,有什么问题或需要帮助的吗?
当然记得,你叫 mr_bird!有什么我可以帮你的吗?
很明显他是有记忆的,如果想要查看保存的记忆,可以这样:
history=agent.get_state(config)
print(history)
这样就可以看到保存在内存中的历史记录了
1.4 使用langgraph cli将项目打包
我们写好的项目,可以通过langgraph cli将其打包,作为服务发布,发布后还可以通过langsmith等其他工具进行
查看和调试
我们将上面的两个工具结合一下,命名为Graph.py
import os
import json
import requests
from langchain_tavily import TavilySearch
from dotenv import load_dotenv
from langchain_community.chat_models import ChatTongyi
from pydantic import BaseModel, Field
from langchain_core.tools import tool
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.prebuilt import create_react_agent
load_dotenv()
llm = ChatTongyi(model="qwen-plus")
search = TavilySearch(
max_results=5, # 修正参数名
topic="general",
tavily_api_key=os.getenv("TAVILY")
)
class Weather(BaseModel):
loc: str = Field(..., description="城市的英文名")
@tool(args_schema=Weather)
def get_weather(loc: str) -> dict:
"""
查询即时天气的函数
:param loc(str): 必要参数,用于表示查询天气的具体城市名称,注意中国的城市需要用英文名称代替,
例如如果需要查询北京市天气,则loc参数需要输入'Beijing'
:return (dict): 通过api查询 返回天气查询的结果,包含完整的天气查询信息
"""
url = "https://api.openweathermap.org/data/2.5/weather"
params = {
"q": loc,
"appid": os.getenv("OPENWEATHER_API_KEY"), # 使用环境变量
"units": "metric",
"lang": "zh_cn"
}
response = requests.get(url, params=params)
print(response.text)
return json.dumps(response.json())
prompt = """
你是一名乐于助人的智能助手,擅长根据用户的问题选择合适的工具来查询信息并回答。
当用户的问题涉及**天气信息**时,你应优先调用`get_weather`工具,查询用户指定城市的实时天气,并在回答中总结查询结果。
当用户的问题涉及**新闻、事件、实时动态**时,你应优先调用`tool`工具,检索相关的最新信息,并在回答中简要概述。
如果问题既包含天气又包含新闻,请先使用`get_weather`查询天气,再使用`search`查询新闻,最后将结果合并后回复用户。
所有回答应使用**简体中文**,条理清晰、简洁友好。
"""
tools = [search, get_weather]
graph = create_react_agent(
model=llm,
tools=tools,
prompt=prompt
)
在项目文件夹下新建一个langgraph.json文件,内容如下:
{
"dependencies": ["./"],
"graphs": {
"chatbot": "./Graph.py:graph"
},
"env": ".env"
}
其中env指明当前存储apikey等环境设置的文件,chatbot为我们当前的项目名,后面指定图文件的地址,也就是
上面我们的Graph.py
如果我们想要使用langsmith,还得在.env中加入Langsmith的apikey

这需要在langsmith 官网获得
对于一个完整的项目,还需要定义requirements.txt
最后我们切换到我们的项目目录,执行 langgraph dev
则可以启动了
最后我们在其提供的url中可以看到langsmith服务

提供了studio 和 当前服务的api调用文档
当我们使用langgraph studio进行会话时,会自动维护上下文,并且进行会话隔离,这都是自动完成的
1.5 构建一个图
不同节点状态的识别是靠langgraph中定义的state, 不同节点之间输入输出要统一,依赖的是
TypeDict进行状态检验
下面是一个使用例子:
from typing import TypedDict
class Contact(TypedDict):
name:str
email:str
phone:str
def send_email(contac:Contact)->None:
print(f"send email to {contac['name']} at {contac['email']}")
contac_info:Contact = {
"name":"bob",
"email":"555@mail.com",
"phone":"1111"
}
send_email(contac_info)
接下来我们来定义一个StateGraph 的图的实例:
from typing import TypedDict
from langgraph.graph import StateGraph
class InputState(TypedDict):
query:str
class OutputState(TypedDict):
answer:str
class OverallState(InputState,OutputState):
pass
build=StateGraph(OverallState,input_schema=InputState,output_schema=OutputState)
-
OverallState:图的“内部全局状态”类型(TypedDict)。包含运行期间会被各节点读写的全部
键集合,通常是输入键、过程中的中间键与输出键的并集。
-
input_schema=InputState:图的“入口输入”模式。限制
.invoke\(\)等调用时必须/可传入的字段与类型;必须是 OverallState 的子集;用于运行时校验与生成 schema。
-
output_schema=OutputState:图的“出口输出”模式。限制编译后执行返回的字段与类型;必
须是 OverallState 的子集;通常只返回这些键(除非编译时选择返回完整状态)。
接下来我们继续构建整个图,在langgraph中,我们可以将函数作为节点,我们先定义两个节点:
def agent_node(input:InputState):
print("this is a agent node")
return
def action_node(input:InputState):
return {"answer":"this is output"}
将其添加到图中:
build.add_node("agent_node",agent_node) #将节点加入到图中,第一个参数是节点的名称,如果不写就会拿函数名作为节点名
build.add_node("action_node",action_node)
再连边:
build.add_edge(START,"agent_node")
build.add_edge("agent_node","action_node")
build.add_edge("action_node",END)
在Langgraph中,图的入口为START节点,出口为END节点
接下来进行编译
graph=build.compile()
这个图就构建好了,可以进行使用了
from typing import TypedDict
from langgraph.graph import StateGraph,START,END
class InputState(TypedDict):
query:str
class OutputState(TypedDict):
answer:str
class OverallState(InputState,OutputState):
pass
def agent_node(input:InputState):
print("this is a agent node")
return
def action_node(input:InputState):
return {"answer":"this is output"}
build=StateGraph(OverallState,input_schema=InputState,output_schema=OutputState)
build.add_node("agent_node",agent_node) #将节点加入到图中,第一个参数是节点的名称,如果不写就会拿函数名作为节点名
build.add_node("action_node",action_node)
build.add_edge(START,"agent_node")
build.add_edge("agent_node","action_node")
build.add_edge("action_node",END)
graph=build.compile()
result=graph.invoke({"query":"你好"})
print(result)
输出为:
this is a agent node
{'answer': 'this is output'}
上面的例子没有体现状态的传递过程,下面这个将包含状态传递:
from typing import TypedDict
from langgraph.graph import StateGraph,START,END
class InputState(TypedDict):
query:str
class OutputState(TypedDict):
answer:str
class OverallState(InputState,OutputState):
pass
def agent_node(state:InputState):
print("this is a agent node")
return {"query":state["query"]}
def action_node(state:InputState):
return {"answer":f"this is output i have receive the query {state['query']}"}
build=StateGraph(OverallState,input_schema=InputState,output_schema=OutputState)
build.add_node("agent_node",agent_node) #将节点加入到图中,第一个参数是节点的名称,如果不写就会拿函数名作为节点名
build.add_node("action_node",action_node)
build.add_edge(START,"agent_node")
build.add_edge("agent_node","action_node")
build.add_edge("action_node",END)
graph=build.compile()
result=graph.invoke({"query":"你好"})
print(result)
输出结果为:
this is a agent node
{'answer': 'this is output i have receive the query 你好'}
1.6 与大模型交互
from typing import TypedDict
from langgraph.graph import StateGraph,START,END
from langchain_community.chat_models import ChatTongyi
from langchain_core.prompts import ChatPromptTemplate
import os
llm=ChatTongyi(
model="qwen-plus",
api_key= os.getenv("DASHSCOPE_API_KEY")
)
class InputState(TypedDict):
query:str
class OutputState(TypedDict):
answer:str
class OverallState(InputState,OutputState):
pass
def llm_node(state:InputState):
llm = ChatTongyi(
model="qwen-plus",
api_key=os.getenv("DASHSCOPE_API_KEY")
)
messages=[("system","你是一个ai小助手,可以帮用户解决各种问题"),
("human",state["query"])]
response=llm.invoke(messages)
return {"answer":response.content}
build=StateGraph(OverallState,input_schema=InputState,output_schema=OutputState)
build.add_node("llm_node",llm_node) #将节点加入到图中,第一个参数是节点的名称,如果不写就会拿函数名作为节点名
build.add_edge(START,"llm_node")
build.add_edge("llm_node",END)
graph=build.compile()
result=graph.invoke({"query":"你好"})
print(result)
我们可以在状态中新加一个大模型消息,可以直接放在输入的格式类中,也可以新建一个中间的内部消息
格式类
from typing import TypedDict,Optional
from langgraph.graph import StateGraph,START,END
from langchain_community.chat_models import ChatTongyi
from langchain_core.prompts import ChatPromptTemplate
import os
llm=ChatTongyi(
model="qwen-plus",
api_key= os.getenv("DASHSCOPE_API_KEY")
)
class InputState(TypedDict):
query:str
llm_answer:Optional[str]
class OutputState(TypedDict):
answer:str
class OverallState(InputState,OutputState):
pass
def llm_node(state:InputState):
llm = ChatTongyi(
model="qwen-plus",
api_key=os.getenv("DASHSCOPE_API_KEY")
)
messages=[("system","你是一个ai小助手,可以帮用户解决各种问题"),
("human",state["query"])]
response=llm.invoke(messages)
return {"llm_answer":response.content}
def llm_trans(state:InputState):
llm = ChatTongyi(
model="qwen-plus",
api_key=os.getenv("DASHSCOPE_API_KEY")
)
messages=[("system","你是一个资深翻译家,可以将用户的任何语言输入翻译为日语"),
("human",state["llm_answer"])]
response = llm.invoke(messages)
return {"answer": response.content}
build=StateGraph(OverallState,input_schema=InputState,output_schema=OutputState)
build.add_node("llm_node",llm_node) #将节点加入到图中,第一个参数是节点的名称,如果不写就会拿函数名作为节点名
build.add_node("llm_trans",llm_trans)
build.add_edge(START,"llm_node")
build.add_edge("llm_node","llm_trans")
build.add_edge("llm_trans",END)
graph=build.compile()
result=graph.invoke({"query":"请介绍一下你自己"})
print(result)
实际的状态流转过程
初始状态:{"query": "请介绍一下你自己"}
经过 llm_node 后:
{
"query": "请介绍一下你自己", # 自动保留
"llm_answer": "我是通义千问..." # 新添加
}
传递给 llm_trans:完整状态都会传递
LangGraph 使用状态合并策略:
返回的字典会与现有状态合并
相同键会被覆盖
新键会被添加
未返回的键会保留原值
其内部原理如下:我们在不同节点间转移的状态其实是OverallState,而不单单是我们在节点中指定的接
受数据类型,比如这个state:InputState当OverallState传递到节点中时,会根据我们输入的指定类型
给切片,如果输入指定的是InputState,则会从OverallState中切片出InputState的字段,形成
InputState进行传递,返回的时候也是,我们将返回的字段直接接入到了OverallState中,对其中对应的
字段进行更新,这就是整个维护流程
在传递状态过程中,stategraph内部并没有对字段进行检查机制,即检查必填字段是否填写,字段类型是
否正确,对于我们的返回类型设置,即使其中有些字段缺失,即使是必填字段也不会报错,不存在的字段
会为none,这点需要我们注意,一句话总结就是可少不能多
我们还需要注意,我们每个节点return的结果并不是直接传入到下一个节点,而是返回去更新我们全局的
State ,下一个节点根据我们输入的变量的类型定义,将可以匹配上的字段进行切分,转换为符合输入变
量类型的变量
1.7 Reducer
Reducer 是一个函数,它定义了当一个新值出现时,如何根据旧的状态值计算出最终的新状态值。
一个标准的 Reducer 函数接收两个参数,并返回一个值:
old_value: 状态字段更新前的值。new_value: 当前节点为这个字段返回的新值。- 返回值: 经过计算后,该字段最终应该更新成的值。
在传递状态过程中的这个状态实时更新利用的是langgraph中的Reducer函数 ,如果我们没有对其更新操
作进行指定,所有更新都对键进行的是覆盖操作
首先我们来看一个例子:
import operator
from typing import TypedDict,Optional,Annotated,List
from langgraph.graph import StateGraph,START,END
from langchain_community.chat_models import ChatTongyi
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.messages import BaseMessage, HumanMessage, AIMessage
import os
class State(TypedDict):
messages:Annotated[List[BaseMessage],operator.add]
def llm_node(state:State):
llm = ChatTongyi(
model="qwen-plus",
api_key=os.getenv("DASHSCOPE_API_KEY")
)
messages=state["messages"]
response=llm.invoke(messages)
return {"messages":[response]}
build=StateGraph(State)
build.add_node("llm_node",llm_node) #将节点加入到图中,第一个参数是节点的名称,如果不写就会拿函数名作为节点名
build.add_edge(START,"llm_node")
build.add_edge("llm_node",END)
graph=build.compile()
query="介绍一下你自己"
result=graph.invoke({"messages":[HumanMessage(content=query)]})
print(result["messages"][-1].content)
该例子的重要代码是:
class State(TypedDict):
messages:Annotated[List[BaseMessage],operator.add]
当写下 messages: Annotated[List, operator.add] 时,就是在告诉 LangGraph:“对于 messages
这个字段,请不要使用默认的‘覆盖’Reducer。请切换成 operator.add 这个函数作为它的 Reducer。”
我们定义的这个字段是一个存储BaseMessage的list 并且使用Annotated 附加了元数据
operator.add 该StateGraph识别到该元数据后,后续在进行更新过程就会进行追加,而不是覆盖,从而
维护整个Messages列表
1.5 MessageGraph
我们先前使用operator.add作为Reducer函数其实是具有局限性的,他只能不断的向我们的状态中添加新
的内容,不能对已有的内容进行覆盖更新,langgraph提供的MessageGraph就是解决这个问题的
MessageGraph继承自StateGraph

当使用MessageGraph时,对于已有的信息会进行给更新,对于未添加的信息才会进行追加,我们对上面
的代码用MessageGraph进行替换,有
import operator
from typing import TypedDict,Optional,Annotated,List
from langgraph.graph import StateGraph,START,END,MessageGraph
from langchain_community.chat_models import ChatTongyi
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.messages import BaseMessage, HumanMessage, AIMessage
import os
def llm_node(state:List[BaseMessage]):
llm = ChatTongyi(
model="qwen-plus",
api_key=os.getenv("DASHSCOPE_API_KEY")
)
messages=state
response=llm.invoke(messages)
return [response]
build=MessageGraph()
build.add_node("llm_node", llm_node)
build.set_entry_point("llm_node")
build.set_finish_point("llm_node")
graph=build.compile()
result=graph.invoke([("human","介绍一下你自己")])
print(result)
我们查看输出:发现每一个messages都有一个id
[HumanMessage(content='介绍一下你自己', additional_kwargs={}, response_metadata={}, id='2079f2cf-0155-444e-b3bf-c5021fef633c'), AIMessage(content='你好啊! 👋 很高兴认识你!我是Qwen(通义千问),一个热爱学习、乐于助人的AI伙伴。我特别喜欢和人类朋友们交流,无论是解答问题、创作文字,还是闲聊天南地北,都让我感到非常开心。\n\n我擅长理解和表达各种形式的内容,可以帮你写故事、写邮件、做计算,也可以陪你聊天解闷。虽然我已经学到了很多知识,但我知道还有更多需要学习的地方。我很期待能和你一起探索新的知识,分享有趣的想法。\n\n最重要的是,我希望能成为你可靠的朋友和助手。不管你有什么需求,我都会认真倾听,用心帮助。那么,现在让我们开始愉快的交流吧!有什么我可以帮你的吗? 😊', additional_kwargs={}, response_metadata={'model_name': 'qwen-plus', 'finish_reason': 'stop', 'request_id': '04b50913-0cb7-900e-8f95-54682bb63b73', 'token_usage': {'input_tokens': 14, 'output_tokens': 155, 'total_tokens': 169, 'prompt_tokens_details': {'cached_tokens': 0}}}, id='run--0cf80080-b7dc-44d8-a04d-d9e158c36558-0')]
Client disconnected
如果我们没有指定消息的id,则会自动生成一个id,通过不同的Id来区分不同的Message,对于同一id的
消息进行覆盖
我们来看一个例子:
import os
from typing import List
from langchain_core.messages import BaseMessage, HumanMessage, AIMessage
from langchain_community.chat_models import ChatTongyi
from langgraph.graph import MessageGraph
# --- 节点 1: 创建一个带特定ID的初始消息 ---
def initial_response_node(state: List[BaseMessage]):
print("--- 步骤 1: 运行初始响应节点 ---")
placeholder_message = AIMessage(
content="正在思考,请稍候...",
id="ai_response_to_update"
)
return [placeholder_message]
# --- 节点 2: 创建一个内容更新、但ID相同的消息 ---
def update_response_node(state: List[BaseMessage]):
print("\n--- 步骤 2: 运行更新响应节点 ---")
llm = ChatTongyi(
model="qwen-plus",
api_key=os.getenv("DASHSCOPE_API_KEY")
)
final_response = llm.invoke(state)
updated_message = AIMessage(
content=final_response.content,
id="ai_response_to_update"
)
return [updated_message]
# --- 构建图 ---
build = MessageGraph()
build.add_node("initial_response", initial_response_node)
build.add_node("update_response", update_response_node)
build.set_entry_point("initial_response")
build.add_edge("initial_response", "update_response")
build.set_finish_point("update_response")
graph = build.compile()
# --- 运行并观察状态变化 ---
query = "请用三句话介绍一下长城。"
final_state_list = None
for step_output in graph.stream([HumanMessage(content=query)]):
final_state_list = list(step_output.values())[0]
print(f"\n--- ‘{list(step_output.keys())[0]}’ 节点运行后的消息列表 ---")
print(final_state_list)
# 循环结束后,final_state_list 变量中保存的就是最终的消息列表
if final_state_list:
last_message = final_state_list[-1]
print("\n--- 最终结果 ---")
print(last_message.content)
else:
print("未能获取到结果。")
我们在这先人为构造一个占位符性质的消息,等待后续更新
def initial_response_node(state: List[BaseMessage]):
print("--- 步骤 1: 运行初始响应节点 ---")
placeholder_message = AIMessage(
content="正在思考,请稍候...",
id="ai_response_to_update"
)
return [placeholder_message]
后续真正的AIMessage就会对其进行替换,输出的结果如下:
--- 步骤 1: 运行初始响应节点 ---
--- ‘initial_response’ 节点运行后的消息列表 ---
[AIMessage(content='正在思考,请稍候...', additional_kwargs={}, response_metadata={}, id='ai_response_to_update')]
--- 步骤 2: 运行更新响应节点 ---
--- ‘update_response’ 节点运行后的消息列表 ---
[AIMessage(content='长城是中国古代最伟大的军事防御工程之一,始建于春秋战国时期,后经多个朝代修缮扩建。它横贯中国北方,全长超过两万公里,被誉为世界七大奇迹之一。如今,长城不仅是中国的象征,也是世界文化遗产,吸引着无数游客前来参观。', additional_kwargs={}, response_metadata={}, id='ai_response_to_update')]
--- 最终结果 ---
长城是中国古代最伟大的军事防御工程之一,始建于春秋战国时期,后经多个朝代修缮扩建。它横贯中国北方,全长超过两万公里,被誉为世界七大奇迹之一。如今,长城不仅是中国的象征,也是世界文化遗产,吸引着无数游客前来参观。
Client disconnected
对于下面的输出代码:
for step_output in graph.stream([HumanMessage(content=query)]):
final_state_list = list(step_output.values())[0]
print(f"\n--- ‘{list(step_output.keys())[0]}’ 节点运行后的消息列表 ---")
print(final_state_list)
graph.stream代表流式输出,每运行完一个节点,都会将节点返回的消息直接返回
MessageGraph除了上面这种写法,还可以由StateGraph指定Reducer方法实现
import operator
from typing import TypedDict,Optional,Annotated,List
from langgraph.graph import StateGraph,START,END,MessageGraph
from langchain_community.chat_models import ChatTongyi
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.messages import BaseMessage, HumanMessage, AIMessage
from langgraph.graph.message import add_messages
import os
class State(TypedDict):
messages:Annotated[List[BaseMessage],add_messages]
def llm_node(state:State):
llm = ChatTongyi(
model="qwen-plus",
api_key=os.getenv("DASHSCOPE_API_KEY")
)
messages=state["messages"]
response=llm.invoke(messages)
return {"messages":[response]}
build=StateGraph(State)
build.add_node("llm_node", llm_node)
build.set_entry_point("llm_node")
build.set_finish_point("llm_node")
graph=build.compile()
result=graph.invoke({"messages":[("human","介绍一下你自己")]})
print(result["messages"])
不过这种写法就不能直接传入消息列表了,需要传入字典
1.6 Router agent
接下来我们来学习Router策略的agent Router策略就是在构建状态图中使用条件边,根据条件选择下一
个‘节点,从而选择不同的路径
先看一个简单的例子:
import operator
from typing import TypedDict,Optional,Annotated,List
from langgraph.graph import StateGraph,START,END,MessageGraph
from langchain_community.chat_models import ChatTongyi
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.messages import BaseMessage, HumanMessage, AIMessage
from langgraph.graph.message import add_messages
import os
def node_a(state):
return {"x":state["x"]+1}
def node_b(state):
return {"x":1}
def node_c(state):
return {"x":-1}
def router_function(state):
if state["x"]>1 :
return "node_b"
else :
return "node_c"
graph=StateGraph(dict)
graph.add_node("node_a",node_a)
graph.add_node("node_b",node_b)
graph.add_node("node_c",node_c)
graph.add_conditional_edges("node_a",router_function)
graph.set_finish_point("node_b")
graph.set_finish_point("node_c")
graph.set_entry_point("node_a")
build=graph.compile()
result=build.invoke({"x":1})
print(result)
核型代码是
graph.add_conditional_edges("node_a",router_function)
通过传入路由函数来进行条件判断,返回需要跳转的节点名,当然我们也可也通过传入参数实现返回标志
根据标志来进行跳转
下面来看用法:
import operator
from typing import TypedDict,Optional,Annotated,List
from langgraph.graph import StateGraph,START,END,MessageGraph
from langchain_community.chat_models import ChatTongyi
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.messages import BaseMessage, HumanMessage, AIMessage
from langgraph.graph.message import add_messages
import os
def node_a(state):
return {"x":state["x"]+1}
def node_b(state):
return {"x":1}
def node_c(state):
return {"x":-1}
def router_function(state):
if state["x"]>1 :
return True
else :
return False
graph=StateGraph(dict)
graph.add_node("node_a",node_a)
graph.add_node("node_b",node_b)
graph.add_node("node_c",node_c)
graph.add_conditional_edges("node_a",router_function,{True:"node_a",False:"node_b"})
graph.set_finish_point("node_b")
graph.set_finish_point("node_c")
graph.set_entry_point("node_a")
build=graph.compile()
result=build.invoke({"x":1})
print(result)
关键就在于:
graph.add_conditional_edges("node_a",router_function,{True:"node_a",False:"node_b"})
如果不传入第三个参数,也就是不提供字典映射关系时,我们需要在路由函数中直接返回节点名,如果想返回到END节点,直接传入END即可,还有传入字典的简化版本,不过效果和不传入参数没有区别
1.7 格式化输出:
第一种方式:
import json
from langchain_community.chat_models import ChatTongyi
from pydantic import BaseModel, Field
import os
class UserInfo(BaseModel):
name : str = Field(...,description="用户的姓名")
gender: str =Field(...,description="用户的性别,man or woman")
llm = ChatTongyi(
model="qwen-plus",
api_key=os.getenv("DASHSCOPE_API_KEY")
)
struct_llm=llm.with_structured_output(UserInfo)
response=struct_llm.invoke([("system","提取用户的输入,以json格式返回"),
("human","我是bob,男")
])
print(json.dumps(response.model_dump(), ensure_ascii=False))
UserInfo 是继承自BaseModel的一个Pandatic数据模型类,我们可以直接用.访问其定义的属性,使用
model_dump方法可以将其变为字典
第二种方式,使用TypedDict
import json
from langchain_community.chat_models import ChatTongyi
from pydantic import BaseModel, Field
from typing import TypedDict, Annotated
import os
class UserInfo(TypedDict):
name :Annotated[str,...,"用户的姓名"]
gender: Annotated[str,...,"用户的性别,man or woman"]
llm = ChatTongyi(
model="qwen-plus",
api_key=os.getenv("DASHSCOPE_API_KEY")
)
struct_llm=llm.with_structured_output(UserInfo)
response=struct_llm.invoke([("system","提取用户的输入,以json格式返回"),
("human","我是bob,男")
])
print(response)
返回的对象是一个字典,其中 name :Annotated[str,...,"用户的姓名"]种的...表示占位符
1.8 格式化输出构建router agent
我们结合格式化输出,和router agent的思想,可以构造一个基础的router agent
import json
import operator
from langchain_core.messages import BaseMessage, HumanMessage, AIMessage, SystemMessage
import sqlite3
from langchain_community.chat_models import ChatTongyi
from pydantic import BaseModel, Field
from typing import TypedDict, Annotated, Union, List
import os
from sqlalchemy import create_engine, text
from sqlalchemy.orm import sessionmaker
from langgraph.graph import StateGraph
DB_PATH = "./data.db"
CREATE_USER_INFO_SQL = """
CREATE TABLE IF NOT EXISTS user_info (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
gender TEXT NOT NULL,
age TEXT NOT NULL
);
"""
def create_user_info_table(db_path: str = DB_PATH) -> None:
con = sqlite3.connect(db_path)
try:
with con:
con.execute(CREATE_USER_INFO_SQL)
finally:
con.close()
if not os.path.exists(DB_PATH):
create_user_info_table()
DB_URL = "sqlite:///./data.db"
engine = create_engine(DB_URL, echo=True, future=True)
Session = sessionmaker(bind=engine, autoflush=False, autocommit=False, future=True)
class UserInfo(TypedDict):
"""
获取用户的信息
"""
name: Annotated[str, ..., "用户的姓名"]
gender: Annotated[str, ..., "用户的性别,man or woman"]
age: Annotated[str, ..., "用户的年龄"]
class Response(TypedDict):
"""
直接恢复
"""
response: Annotated[str, ..., "对用户的回复"]
class FinalResponse(TypedDict):
finresponse: Union[UserInfo, Response]
class State(TypedDict):
messages: Annotated[List[BaseMessage], operator.add]
def chat_with_model(state: State):
message = state["messages"]
llm = ChatTongyi(
model="qwen-plus",
api_key=os.getenv("DASHSCOPE_API_KEY")
)
struct_llm = llm.with_structured_output(FinalResponse)
response = struct_llm.invoke(message)
return {"messages": [AIMessage(content=json.dumps(response,ensure_ascii=False))]}
def insert_db(state: State):
session = Session()
try:
last_msg = state["messages"][-1]
content = getattr(last_msg, "content", None)
# 解析 AIMessage.content(期望为 JSON 字符串)
if isinstance(content, str):
try:
data = json.loads(content)
except json.JSONDecodeError:
return {"messages": [AIMessage(content="未检测到有效 JSON,跳过入库。")]}
elif isinstance(content, dict):
data = content
else:
return {"messages": [AIMessage(content="消息内容不是 JSON 或字典,跳过入库。")]}
fin = data.get("finresponse", {})
if not isinstance(fin, dict):
return {"messages": [AIMessage(content="结构化输出缺少 `finresponse` 字段,跳过入库。")]}
# 仅当包含用户信息字段时才入库
if not all(k in fin for k in ("name", "gender", "age")):
return {"messages": [AIMessage(content="未包含用户信息字段 `name`/`gender`/`age`,跳过入库。")]}
params = {
"name": str(fin["name"]),
"gender": str(fin["gender"]),
"age": str(fin["age"]),
}
session.execute(
text("INSERT INTO user_info (name, gender, age) VALUES (:name, :gender, :age)"),
params,
)
session.commit()
return {"messages": [AIMessage(content="数据已成功存储至 SQLite 数据库。")]}
except Exception as e:
session.rollback()
return {"messages": [AIMessage(content=f"数据存储失败,错误原因:{e}")]}
finally:
session.close()
def final_answer(state: State):
message = state["messages"][-1]
print(message.content)
return
def router_function(state: State):
last_msg = state["messages"][-1]
content = getattr(last_msg, "content", "")
try:
data = json.loads(content) if isinstance(content, str) else content
except json.JSONDecodeError:
return "final_answer"
if not isinstance(data, dict):
return "final_answer"
fin = data.get("finresponse")
# 判断是否为 UserInfo 结构
if isinstance(fin, dict) and all(k in fin for k in ("name", "gender", "age")):
return "insert_db"
# 判断是否为 Response 结构
if isinstance(fin, dict) and "response" in fin:
return "final_answer"
return "final_answer"
graph=StateGraph(State)
graph.add_node("chat_with_model",chat_with_model)
graph.add_node("insert_db",insert_db)
graph.add_node("final_answer",final_answer)
graph.set_entry_point("chat_with_model")
graph.add_conditional_edges("chat_with_model",router_function)
graph.set_finish_point("final_answer")
graph.set_finish_point("insert_db")
build=graph.compile()
response=build.invoke({"messages":[HumanMessage(content="我叫bob,男 24岁")]})
1.9 构建tools agent
构建tools agent 需要使用langgraph里的预定义组件tool node 使用toolnode的条件是状态中必须包含
messages的消息列表字段,然后就是使用toolnode的上一条必须是aimessage而且包含toolcalls的相关数据
我们使用时直接将aimessage传入就可以执行函数了
import json
import requests
from langgraph.prebuilt import ToolNode
import operator
from langchain_tavily import TavilySearch
from langchain_core.messages import BaseMessage, HumanMessage, AIMessage, SystemMessage
from langchain_community.chat_models import ChatTongyi
from pydantic import BaseModel, Field
from typing import TypedDict, Annotated, Union, List
import os
from dotenv import load_dotenv
from langgraph.graph import StateGraph
from langchain_core.tools import tool
load_dotenv()
class Weather(BaseModel):
loc :str =Field(...,description="城市的英文名")
@tool(args_schema=Weather)
def get_weather(loc:str)->dict :
"""
查询即时天气的函数
:param loc(str): 必要参数,用于表示查询天气的具体城市名称,注意中国的城市需要用英文名称代替,
例如如果需要查询北京市天气,则loc参数需要输入'Beijing
:return (dict): 通过api查询 返回天气查询的结果,包含完整的天气查询信息,以json字符串的格式返回
"""
url="https://api.openweathermap.org/data/2.5/weather"
params={
"q":loc,
"appid":os.getenv("WEATHER"),
"units":"metric",
"lang": "zh_cn"
}
response=requests.get(url, params)
return json.dumps(response.json())
class State(TypedDict):
messages: Annotated[List[BaseMessage], operator.add]
def chat_with_llm(state:State):
messages=state["messages"]
response=llm_with_tool.invoke(messages)
print(response)
return {"messages":[response]}
def tools_action(state: State):
print("正在进行工具调用")
last = state["messages"][-1]
tool_calls = getattr(last, "tool_calls", None)
if tool_calls:
# 正确:以 dict 形态调用 ToolNode
tool_state = tool_node.invoke({"messages": state["messages"]}) # {"messages": [ToolMessage, ...]}
# 合并工具返回的消息后再让模型回复
new_msgs = state["messages"] + tool_state["messages"]
ai_msg = llm_with_tool.invoke(new_msgs)
# 返回“工具消息 + 新 AI 回复”作为增量写回状态
return {"messages": tool_state["messages"] + [ai_msg]}
else:
return {"messages": []}
search=TavilySearch(max_result=5, topic="general",tavily_api_key=os.getenv("TAVILY"))
tools=[get_weather,search]
tool_node= ToolNode(tools)
llm = ChatTongyi(
model="qwen-plus",
api_key=os.getenv("DASHSCOPE_API_KEY"),
)
llm_with_tool=llm.bind_tools(tools,parallel_tool_calls=True)
graph=StateGraph(State)
graph.add_node("chat_with_llm",chat_with_llm)
graph.add_node("tools_action",tools_action)
graph.set_entry_point("chat_with_llm")
graph.set_finish_point("tools_action")
graph.add_edge("chat_with_llm","tools_action")
build=graph.compile()
response=build.invoke({"messages":[
SystemMessage(content="""
你是一个可以并行调用多个工具的智能助手。
请一次性并行执行以下两个任务,并在一次回复中返回所有工具调用:
1. 查询天气(使用 get_weather 工具)
2. 查询新闻(使用 TavilySearch 工具)
请不要分多次调用,而是一次性调用两个工具。
"""
),
HumanMessage(content="请并行执行以下两个任务:1. 查询今天合肥的天气;2. 查询上个月日本的新闻")]})
answer=response["messages"][-1].content
print(answer)
关键代码是:
tool_state = tool_node.invoke({"messages": state["messages"]}) # {"messages": [ToolMess
返回的是一个只包含toolmessage的messages字典
需要注意,想要实现多函数并行调用,需要使用参数
llm_with_tool=llm.bind_tools(tools,parallel_tool_calls=True)
2.0 构建React Agent
我们使用预构建组件可以直接构建ReactAgent
import json
import operator
from langchain_tavily import TavilySearch
from langchain_core.tools import tool
from langchain_core.messages import BaseMessage, HumanMessage, AIMessage, SystemMessage
import sqlite3
from langgraph.prebuilt import create_react_agent
import requests
from langchain_community.chat_models import ChatTongyi
from pydantic import BaseModel, Field
from typing import TypedDict, Annotated, Union, List
import os
from dotenv import load_dotenv
from sqlalchemy import create_engine, text
from sqlalchemy.orm import sessionmaker
from langgraph.graph import StateGraph
DB_PATH = "./data.db"
CREATE_WEATHER_INFO_SQL = """
CREATE TABLE IF NOT EXISTS weather_info (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
main TEXT NOT NULL
);
"""
def create_weather_info_table(db_path: str = DB_PATH) -> None:
con = sqlite3.connect(db_path)
try:
with con:
con.execute(CREATE_WEATHER_INFO_SQL)
finally:
con.close()
if not os.path.exists(DB_PATH):
create_weather_info_table()
DB_URL = "sqlite:///./data.db"
engine = create_engine(DB_URL, echo=True, future=True)
Session = sessionmaker(bind=engine, autoflush=False, autocommit=False, future=True)
load_dotenv()
class WeatherLoa(BaseModel):
loc :str =Field(...,description="""城市的英文名,必要参数,用于表示查询天气的具体城市名称,
注意中国的城市需要用英文名称代替,例如如果需要查询北京市天气,则loc参数需要输入'Beijing""")
@tool(args_schema=WeatherLoa)
def get_weather(loc:str)->str :
"""
查询即时天气的函数,当用户需要查询某地参数时,使用该工具进行查询,返回的是一个json字符串
"""
url="https://api.openweathermap.org/data/2.5/weather"
params={
"q":loc,
"appid":os.getenv("WEATHER"),
"units":"metric",
"lang": "zh_cn"
}
response=requests.get(url, params)
return json.dumps(response.json(),ensure_ascii=False)
class WeatherIns(BaseModel):
name:str =Field(...,description="所查询的城市名称,以中文表示")
main:str =Field(...,description="查询城市的天气简短描述,不超过五个字,以中文表示")
@tool(args_schema= WeatherIns)
def insert_weather(name:str,main:str)->str:
"""
将天气信息插入到数据库中,当用户需要将天气数据插入时执行该函数
"""
try:
with sqlite3.connect(DB_PATH) as con:
con.execute("INSERT INTO weather_info (name, main) VALUES (?, ?)", (name, main))
con.commit()
return f"成功插入天气信息:城市 {name},天气 {main}"
except Exception as e:
return f"插入失败:{str(e)}"
class QueryWea(BaseModel):
name:str =Field(...,description="需要查询的城市的名称,中文形式")
@tool(args_schema=QueryWea)
def query_weather(name: str) -> str:
"""
当用户需要从数据库中查询天气时使用
"""
try:
with sqlite3.connect(DB_PATH) as con:
cursor = con.execute(
"SELECT name, main FROM weather_info WHERE name = ?",
(name,)
)
result = cursor.fetchone()
if result:
city_name, weather_main = result
return f"查询成功:{city_name} 的天气为 {weather_main}"
else:
return f"未找到城市 {name} 的天气信息"
except Exception as e:
return f"查询失败:{str(e)}"
search=TavilySearch(max_result=5, topic="general",tavily_api_key=os.getenv("TAVILY"))
tools=[get_weather,search,query_weather,insert_weather]
llm=ChatTongyi(
model="qwen-plus"
)
graph = create_react_agent(llm, tools=tools)
result=graph.invoke({"messages":[HumanMessage(content="帮我查询今天合肥的天气,并将其插入到数据库中")]})
2.1 流式输出
基础的流式输出
from langchain_community.chat_models import ChatTongyi
from dotenv import load_dotenv
import os
import asyncio
load_dotenv()
llm=ChatTongyi(
model="qwen-plus"
)
chunks=[]
async def stream_out():
async for chunk in llm.astream("介绍一下你自己,你可以干什么"):
chunks.append(chunk)
print(chunk.content,end="")
asyncio.run(stream_out())
此时返回的每一个chunk是AIMessageChunk类型,使用+可以对其进行拼接
上面采用的是langchain中的流式输出,接下来我们来学习langgraph中的流式输出过程
langgraph中的流式输入模式分五种
values
def print_stream(stream):
for sub_stream in stream:
# print(sub_stream) # 就是上面的示例中非流式直接调用的全部信息
message = sub_stream["messages"][-1]
message.pretty_print()
input_message = {"messages": ["你好,南京现在的天气怎么样?"]}
print_stream(graph.stream(input_message, stream_mode="values"))只吐出当前节点的返回值
values 经过每个节点后返回整个完整的状态
updates 该模式下每次只返回更新的内容,而不是整个状态字典
比如我们经过一个agent节点添加了一个aimessage 则此时使用流式输出返回的只是一个包含当前单个消息的字典
debug 模式因为我们要进行调试,希望返回尽可能多的信息,所以返回的信息会尽可能多
同样,我们一般使用异步进行调用
async def asystream():
async for chunk in graph.astream(input={"messages": ["你好?"]}, stream_mode="values"):
message = chunk["messages"][-1].pretty_print()
asyncio.run(asystream())
messages 模式
与其他模式的区别:
# values 模式:返回完整状态
{"messages": [msg1, msg2, msg3]}
# updates 模式:返回增量状态
{"messages": [new_msg]}
# messages 模式:逐个返回消息对象
(msg1, metadata1)
(msg2, metadata2)
(msg3, metadata3)
其中元数据是一个字典,一般包含下面的内容:
{
"langgraph_step": 1, # 当前执行步骤
"langgraph_node": "agent", # 当前执行的节点名称
"langgraph_triggers": [...], # 触发该步骤的条件
"langgraph_path": [...], # 执行路径
"langgraph_checkpoint_ns": "", # 检查点命名空间
"checkpoint_id": "...", # 检查点ID
"parent_id": "...", # 父节点ID
}
返回的时间:
AI 开始思考时:返回 AIMessage 或 AIMessageChunk
工具调用时:返回 ToolMessage
节点处理完成时:返回相应类型的消息对象
流式生成过程中:逐块返回 AIMessageChunk
async def asystream():
gathered = None
async for msg, metadata in graph.astream({"messages": ["你好,帮我查询现在西安的天气"]},
stream_mode="messages"):
# 打印内容(排除用户消息)
if msg.content and not isinstance(msg, HumanMessage):
print(msg.content, end="", flush=True)
# 处理AI消息块的聚合
if isinstance(msg, AIMessageChunk):
if gathered is None:
gathered = msg
else:
gathered = gathered + msg
# 如果有工具调用块,打印当前聚合的工具调用
if msg.tool_call_chunks and gathered.tool_calls:
print(f"\n工具调用: {gathered.tool_calls}")
# 处理完整的AI消息
elif isinstance(msg, AIMessage) and msg.tool_calls:
print(f"\n工具调用: {msg.tool_calls}")
# 流结束后的最终处理
if gathered and gathered.tool_calls:
print(f"\n最终工具调用: {gathered.tool_calls}")
其中aimessage在未完全生成时是以aimessaagechunk形式,当完全生成后以aimessage形式返回
2.2 事件流
对于上述使用的.stream()或.astream()仅流式传输链中最后一步的输出,这对于一些对话聊天类的应用程序来说基本就足够了,但是当我们的AI Agent是一个使用了多个大模型调用的更复杂的链时,我们有时希望在最终输出中也使用到一些中间值。例如,在构建RAG对话应用程序时,很多场景都是最终生成的响应 + 检索到的源文档一起返回给用户,例如:

async def asystream():
async for event in graph.astream_events({"messages": ["你好,请你介绍一下你自己"]}, version="v2"):
kind = event["event"]
print(f"{kind}: {event['name']}")
event 字段表示事件的类型,name 字段表示触发事件的组件名称,每一个事件都是一个包含信息的字典,当前只是
打印了其中两个字段罢了
其中事件的内容在data字段中,通过事件名称,我们就可以做到获取我们需要的事件信息
async for event in graph.astream_events({"messages": ["你好,请你介绍一下你自己"]}, version="v2"):
kind = event["event"]
if kind == "on_chat_model_stream":
print(event, end="|", flush=True)
2.3 checkpointer和短期记忆
我们如果想要在多轮对话中保留历史记忆,就要进行记忆的存储,像我们之前直接与大模型对话一样维护messages列表一样去维护记忆
而checkpointer就可以实现这个功能,checkpointer本质是对全局状态进行快照保存,在每一个节点执行完成之后,
进行快照,保存当前全局状态和一些元数据,将其存在内存,缓存或者数据库中,元数据大概是下面这些
checkpoint_id:当前快照唯一 ID。
parent_checkpoint_id:父快照(便于回溯 / 分支)。
metadata:线程 id、图名、节点名、时间戳等。
pending_sends / writes(实现内用于调度的增量信息)
在下一轮对话开始后,如果我们设置了检查点且checkpointer中有信息,会将最新的快照加入到当前的这一轮的对话中,会除去元数据后
加入到全局状态中
我们可以选择不同的方式去存储检查点,第一种MemorySaver 将其存到内存中
import asyncio
from langchain_core.messages import BaseMessage, HumanMessage, AIMessage, SystemMessage,AIMessageChunk
from langgraph.graph.message import add_messages
from langchain_community.chat_models import ChatTongyi
from pydantic import BaseModel, Field
from typing import TypedDict, Annotated, Union, List
from dotenv import load_dotenv
from langgraph.graph import StateGraph,START,END
from langgraph.checkpoint.memory import MemorySaver
load_dotenv()
llm=ChatTongyi(
model="qwen-plus"
)
class State(TypedDict):
messages:Annotated[List[BaseMessage],add_messages]
def chat_with_llm(state:State):
messages=state["messages"]
response=llm.invoke(messages)
return {"messages":[response]}
graph=StateGraph(State)
graph.add_node("chat_with_llm",chat_with_llm)
graph.add_edge(START,"chat_with_llm")
graph.add_edge("chat_with_llm",END)
config={"configurable":{"thread_id":"1"}}
checkpointer=MemorySaver()
build=graph.compile(checkpointer=checkpointer)
while True:
user_input = input("用户: ")
if user_input.lower() in ["退出", "quit", "exit"]:
break
input_message = {"messages": [HumanMessage(content=user_input)]}
result = build.invoke(input_message, config)
print(result["messages"][-1].content)
每一轮对话checkpoint的读取是根据线程id进行判断的
但当前是将记忆存储在内存中,肯定是不行的,接下来学习将数据存放在数据库中
langgraph这里直接提供的是sqlite数据库
import asyncio
from langchain_core.messages import BaseMessage, HumanMessage, AIMessage, SystemMessage,AIMessageChunk
from langgraph.graph.message import add_messages
from langchain_community.chat_models import ChatTongyi
from pydantic import BaseModel, Field
from typing import TypedDict, Annotated, Union, List
from dotenv import load_dotenv
from langgraph.checkpoint.sqlite import SqliteSaver
from langgraph.graph import StateGraph,START,END
from langgraph.checkpoint.memory import MemorySaver
load_dotenv()
llm=ChatTongyi(
model="qwen-plus"
)
class State(TypedDict):
messages:Annotated[List[BaseMessage],add_messages]
def chat_with_llm(state:State):
messages=state["messages"]
response=llm.invoke(messages)
return {"messages":[response]}
graph=StateGraph(State)
graph.add_node("chat_with_llm",chat_with_llm)
graph.add_edge(START,"chat_with_llm")
graph.add_edge("chat_with_llm",END)
config={"configurable":{"thread_id":"1"}}
with SqliteSaver.from_conn_string(":memory:") as checkpointer:
build=graph.compile(checkpointer=checkpointer)
while True:
user_input = input("用户: ")
if user_input.lower() in ["退出", "quit", "exit"]:
break
input_message = {"messages": [HumanMessage(content=user_input)]}
result = build.invoke(input_message, config)
print(result["messages"][-1].content)
我们在这里创建SqliteSaver时选择的是:memory:此时创建的数据库会在内存中,而不是存储在硬盘
想要实现这一目的只需要改一处即可
with SqliteSaver.from_conn_string("checkpoint.sqlite") as checkpointer:
build=graph.compile(checkpointer=checkpointer)
while True:
user_input = input("用户: ")
if user_input.lower() in ["退出", "quit", "exit"]:
break
input_message = {"messages": [HumanMessage(content=user_input)]}
result = build.invoke(input_message, config)
print(result["messages"][-1].content)
此时会在本地自动的去搜索数据库checkpoint.sqlite如果没有则会创建数据库,将我们的检查点存放
在数据库中对应的表里
此时如果我们再次开启这个会话,会面临:
-
不会每次创建新数据库:如果
checkpoint.sqlite文件已存在,会直接连接到现有数据库 -
不会创建新表:如果表已存在,会复用现有表结构
-
会保留原有数据:之前的检查点数据会被保留在数据库中
-
会获取历史数据:由于使用了相同的
thread_id="1",系统会加载该线程的历史消息
不过我们现在使用with在管理资源,在with执行结束后,创建的sqlite连接对象会被关闭,但带来的问题
是只能在with的作用域内部使用数据据连接,这样不是很方便,这时候可以使用ExitStack来自动管理
资源,此时等待程序运行结束会自动释放资源
stack=ExitStack()
checkpointer= stack.enter_context(SqliteSaver.from_conn_string(":memory:"))
build=graph.compile(checkpointer=checkpointer)
while True:
user_input = input("用户: ")
if user_input.lower() in ["退出", "quit", "exit"]:
break
input_message = {"messages": [HumanMessage(content=user_input)]}
result = build.invoke(input_message, config)
print(result["messages"][-1].content)
2.4 长期记忆和Store
我们发现当前虽然保留了历史会话,但局限于一个会话或者说一个线程中,无法跨线程获取,长期记忆就
是解决这个问题的
在长期记忆中,通过namespace识别不同的存储,通过Store存储一个命名空间中不同的线程中我们设置
的记忆,我们可以存储一些结构化的数据,首先来看下如何创建和添加数据:
import asyncio
from langgraph.store.memory import InMemoryStore
from langchain_core.messages import BaseMessage, HumanMessage, AIMessage, SystemMessage,AIMessageChunk
from langgraph.graph.message import add_messages
from langchain_community.chat_models import ChatTongyi
from pydantic import BaseModel, Field
import uuid
from contextlib import ExitStack
from typing import TypedDict, Annotated, Union, List
from dotenv import load_dotenv
from langgraph.checkpoint.sqlite import SqliteSaver
from langgraph.graph import StateGraph,START,END
from langgraph.checkpoint.memory import MemorySaver
load_dotenv()
llm=ChatTongyi(
model="qwen-plus"
)
in_memory_store=InMemoryStore()
user_id="1"
namespace_for_memory=(user_id,"memories")
memory_id=str(uuid.uuid4())
memory={"user":"你好,我今年21岁"}
in_memory_store.put(namespace_for_memory,memory_id,memory)
print(in_memory_store.search(namespace_for_memory))
在langgraph中,一个命名空间是一个元组,由两部分组成,这两部分都需要是字符串
namespace_for_memory=(user_id,"memories") 在这里传入的第一个参数是用户分区键,第二个参数
是命名空间段,二者共同组成了命名空间来存放我们设置的这个用户的数据,其中第一个参数来区分用
户,第二个参数可以理解成文件夹,来存放不同的数据
[Item(namespace=['1', 'memories'], key='77f514a4-a33c-4462-a9c1-12c11c9b6a25', value={'user': '你好,我今年21岁'}, created_at='2025-08-29T08:38:16.577779+00:00', updated_at='2025-08-29T08:38:16.577779+00:00', score=None)]
Client disconnected
其中的key是我们给记忆设置的id,也就是先前的memory_id,用来标识我们存放的数据
下面是store的应用:
import asyncio
from uuid import uuid4
from langgraph.store.memory import InMemoryStore
from langchain_core.messages import BaseMessage, HumanMessage, AIMessage, SystemMessage,AIMessageChunk
from langgraph.graph.message import add_messages
from langchain_community.chat_models import ChatTongyi
from pydantic import BaseModel, Field
import uuid
from langgraph.store.base import BaseStore
from langchain_core.runnables import RunnableConfig
from contextlib import ExitStack
from typing import TypedDict, Annotated, Union, List
from dotenv import load_dotenv
from langgraph.checkpoint.sqlite import SqliteSaver
from langgraph.graph import StateGraph,START,END
from langgraph.checkpoint.memory import MemorySaver
load_dotenv()
llm=ChatTongyi(
model="qwen-plus"
)
class State(TypedDict):
messages:Annotated[List[BaseMessage],add_messages]
def chat_with_llm(state:State,config:RunnableConfig,*,store:BaseStore):
user_id=config["configurable"]["user_id"]
namespace=("memorys",user_id)
memories=store.search(namespace)
info = "\n".join([d.value["data"] for d in memories])
last_message=state["messages"][-1]
messages=state["messages"]
query="query:"+last_message.content
store.put(namespace,str(uuid.uuid4()),{"data":query})
system_promopt=[HumanMessage(content=f"根据历史会话信息:{info}回答问题")]
response=llm.invoke(messages+system_promopt)
answer="answer:"+response.content
store.put(namespace,str(uuid.uuid4()),{"data":answer})
return {"messages":[response]}
graph=StateGraph(State)
graph.add_node("chat_with_llm",chat_with_llm)
graph.add_edge(START,"chat_with_llm")
graph.add_edge("chat_with_llm",END)
config={"configurable":{"user_id":"6"}}
in_memory_store=InMemoryStore()
build=graph.compile(store=in_memory_store)
while True:
user_input = input("用户: ")
if user_input.lower() in ["退出", "quit", "exit"]:
break
input_message = {"messages": [HumanMessage(content=user_input)]}
result = build.invoke(input_message, config)
print(result["messages"][-1].content)
我们在这里使用store去维护了整个消息历史会话,但需要注意,维护历史对话使用checkpoint就行了,
store一般维护一些用户跨线程的结构化数据,而不是当前为了方便将其存储在了内存中,真正使用的时
候存储在本地即可,这里只是为了演示功能所以才这样写,这里使用到了langgraph中很核型的一个功
能,依赖注入,这也是langchain中不具有的,我们查看节点函数,发现
def chat_with_llm(state:State,config:RunnableConfig,*,store:BaseStore)
这里除了我们定义的状态State,还传递了别的值,按照我们之前的理解这里只能传递状态啊,其实还有
一些langgraph中规定的配置性的系统级参数也是可以传输的
| 参数名 / 类型 | 注入条件 | 用途 |
|---|---|---|
state: State |
必须,Graph 自动传入 | 当前图运行时的状态(你定义的 TypedDict 或数据模型) |
config: RunnableConfig |
可选 | 流程运行的上下文配置(thread_id、tags、metadata 等流控信息) |
store: BaseStore |
前提:调用 graph.compile(store=my_store) 时传入 store |
用于访问持久化存储(Key-Value 数据库) |
checkpointer: BaseCheckpointSaver |
前提:调用 graph.compile(checkpointer=my_ckpt) 时传入 |
保存/恢复运行时的执行快照(断点续跑) |
logger |
自动注入(Python 原生 logging.Logger) | 节点级别的日志记录器 |
callbacks (或 CallbackManager) |
自动注入 | LangChain 的事件回调管理(调试、埋点、链路跟踪) |
llm / 你在 graph.compile() 注入的任何 dependency |
前提:在 graph.compile() 或 RunnableConfig.configurable 里注入 |
注入任意自定义依赖(模型实例、工具类、API 客户端等) |
我们参数传递中的那个*表示后面的参数必须使用关键字参数传递,而不能使用位置参数,原因如下:
config: RunnableConfig - 这是LangGraph的核心参数,框架会自动识别这个类型注解并注入,不需要强制为keyword-only参数。
store: BaseStore - 这是可选的依赖注入参数,LangGraph要求它必须是keyword-only参数(用*分隔),这样可以:避免与位置参数冲突,明确表示这是一个可选的依赖注入参数,保持API的清晰性
2.5 断点
我们构建智能体的过程中,如果需要智能体执行一些敏感操作,比如删除操作,完全依赖智能体肯定是不安全的,
我们需要一些人为接入过程,这就是人在闭环过程,在langgraph中,实现这部分依赖的是断点,如果我们检测到
了存在删除操作,就进入断点,等待用户操作确认后再继续执行,下面是一个样例代码:
from langchain_core.messages import BaseMessage, HumanMessage, AIMessage, SystemMessage,AIMessageChunk
from langgraph.graph.message import add_messages
from langchain_community.chat_models import ChatTongyi
from pydantic import BaseModel, Field
from typing import TypedDict, Annotated, Union, List
from dotenv import load_dotenv
from langgraph.graph import StateGraph,START,END
from langgraph.checkpoint.memory import MemorySaver
load_dotenv()
class State(TypedDict):
messages:Annotated[List[BaseMessage],add_messages]
flag:int
human:int
class FlagJudge(BaseModel):
flag: int = Field(description="判断用户是否需要进行删除操作。需要=1,不需要=0")
def llm_judge(state:State):
last_user_content=state["messages"][-1].content
msg = [
SystemMessage(content="你是一个判断助手。请严格按 JSON Schema 输出,仅返回字段 {flag}。"),
HumanMessage(content=last_user_content),
]
result=llm_struct.invoke(msg)
return {"flag":result.flag}
def excute_judge(state:State):
if state["human"] :
print("用户同意进行删除操作,已进行删除")
else:
print("用户拒绝删除,删除失败")
return {}
def response(state:State):
msgs=state["messages"]
ai_msg=llm.invoke(msgs)
return {"messages":[ai_msg]}
def router(state:State):
return "excute_judge" if state["flag"]==1 else "response"
def stream_print(user_input):
last_len=0
for chunk in build.stream(user_input,config,stream_mode="values"):
msgs=chunk["messages"]
if len(msgs)>last_len:
last=msgs[-1]
if last.type!="human":
print(last.content)
last_len=len(msgs)
llm=ChatTongyi(
model="qwen-plus"
)
llm_struct = llm.with_structured_output(FlagJudge)
graph=StateGraph(State)
graph.add_node("llm_judge",llm_judge)
graph.add_node("response",response)
graph.add_node("excute_judge",excute_judge)
graph.set_entry_point("llm_judge")
graph.add_conditional_edges("llm_judge",router,["response","excute_judge"])
graph.add_edge("response",END)
graph.add_edge("excute_judge",END)
config={"configurable":{"thread_id":"1"}}
checkpointer=MemorySaver()
build=graph.compile(checkpointer=checkpointer,interrupt_before=["excute_judge"])
query=input("请输入你的问题")
message={"messages":[HumanMessage(content=query)],
"flag":0,
"human":0
}
stream_print(message)
snapshot=build.get_state(config)
if snapshot.next and "excute_judge" in snapshot.next:
approval_raw= input("检测到删除操作,同意输入1,不同意输入2")
approval =1 if approval_raw=="1" else 0
build.update_state(config,{"human":approval})
stream_print(None)
执行到断点时,程序会中止
interrupt_before=["excute_judge"] 我们传入的这个参数代表在你节点之前进入断点,也可也选择在某
个节点之后进入断点
我们既然有处理断点逻辑,自然有正常输出部分
if snapshot.next and "excute_judge" in snapshot.next:
approval_raw= input("检测到删除操作,同意输入1,不同意输入2")
approval =1 if approval_raw=="1" else 0
build.update_state(config,{"human":approval})
stream_print(None)
这部分代码来判断是否进入到断点,snapshot是我们获得的当前线程最新的checkpoint,我们获得后将其存储再
在内存中,通过检测当前状态下一个要进入的节点是什么来判断是否进入了断点,如果确实进入了断点,就接受用
户输入后将判断值更新到当前运行的全局状态中,整个过程需要用到检查点
上面的这种断点方式称为静态断点
需要注意,在流式输出中处理和恢复中断的行为是错误的
2.6 动态断点
相比静态断点只能加在节点执行前或者节点执行后,动态断点可以加载节点的任何位置,在执行到断点时,会触发
断点,然后返回一个中断对象,线程阻塞,然后等待用户操作后恢复,需要注意,恢复后是从头又重新执行的,只
不过遇到断点时不会再停止,而是会将断点的返回值设置为我们人为设置的恢复值,然后继续进行执行现在看一个
基础的例子:
所以在使用断点时,一般会将断点放在开头或者单独的一小节
from typing import TypedDict
import uuid
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.constants import START
from langgraph.graph import StateGraph
from langgraph.types import interrupt, Command
class State(TypedDict):
some_text: str
def human_node(state: State):
value = interrupt(
{
"text_to_revise": state["some_text"]
}
)
return {
"some_text": value
}
# Build the graph
graph_builder = StateGraph(State)
graph_builder.add_node("human_node", human_node)
graph_builder.add_edge(START, "human_node")
checkpointer = InMemorySaver()
graph = graph_builder.compile(checkpointer=checkpointer)
# Pass a thread ID to the graph to run it.
config = {"configurable": {"thread_id": uuid.uuid4()}}
# Run the graph until the interrupt is hit.
result = graph.invoke({"some_text": "original text"}, config=config)
print(result['__interrupt__'])
# > [
# > Interrupt(
# > value={'text_to_revise': 'original text'},
# > resumable=True,
# > ns=['human_node:6ce9e64f-edef-fe5d-f7dc-511fa9526960']
# > )
# > ]
print(result["__interrupt__"])
# > [Interrupt(value={'text_to_revise': 'original text'}, id='6d7c4048049254c83195429a3659661d')]
print(graph.invoke(Command(resume="Edited text"), config=config))
# > {'some_text': 'Edited text'}
可以看到输出的结果很清晰了
如果同时有多个分支并行执行,而且这多个分支都有中断,整个图会在所有的中断触发后再统一返回中断对象,只
要有可运行的分支,就不会返回而是挂起,我们在恢复中断时也是一次所有的中断同时恢复,如果不同的中断需要
返回不同的值,可以通过中断id进行指定,构建字典对值进行映射
下面是例子:
from typing import TypedDict
import uuid
from langchain_core.runnables import RunnableConfig
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.constants import START
from langgraph.graph import StateGraph
from langgraph.types import interrupt, Command
class State(TypedDict):
text_1: str
text_2: str
def human_node_1(state: State):
value = interrupt({"text_to_revise": state["text_1"]})
return {"text_1": value}
def human_node_2(state: State):
value = interrupt({"text_to_revise": state["text_2"]})
return {"text_2": value}
graph_builder = StateGraph(State)
graph_builder.add_node("human_node_1", human_node_1)
graph_builder.add_node("human_node_2", human_node_2)
# Add both nodes in parallel from START
graph_builder.add_edge(START, "human_node_1")
graph_builder.add_edge(START, "human_node_2")
checkpointer = InMemorySaver()
graph = graph_builder.compile(checkpointer=checkpointer)
thread_id = str(uuid.uuid4())
config: RunnableConfig = {"configurable": {"thread_id": thread_id}}
result = graph.invoke(
{"text_1": "original text 1", "text_2": "original text 2"}, config=config
)
# Resume with mapping of interrupt IDs to values
resume_map = {
i.interrupt_id: f"human input for prompt {i.value}"
for i in parent.get_state(thread_config).interrupts
}
print(graph.invoke(Command(resume=resume_map), config=config))
# > {'text_1': 'edited text for original text 1', 'text_2': 'edited text for original text 2'}
使用动态断点我们进行人为介入变得更方便:
from typing import Literal
from langgraph.types import interrupt, Command
def human_approval(state: State) -> Command[Literal["some_node", "another_node"]]:
is_approved = interrupt(
{
"question": "Is this correct?",
# Surface the output that should be
# reviewed and approved by the human.
"llm_output": state["llm_output"]
}
)
if is_approved:
return Command(goto="some_node")
else:
return Command(goto="another_node")
# Add the node to the graph in an appropriate location
# and connect it to the relevant nodes.
graph_builder.add_node("human_approval", human_approval)
graph = graph_builder.compile(checkpointer=checkpointer)
# After running the graph and hitting the interrupt, the graph will pause.
# Resume it with either an approval or rejection.
thread_config = {"configurable": {"thread_id": "some_id"}}
graph.invoke(Command(resume=True), config=thread_config)
Command[Literal["some_node", "another_node"]]:
后面Literal["some_node", "another_node"] 表明返回的值是一个字面量,且这个字面量只能在给定的这两
个里面选
定义了我们返回的值是控制指令,其中 Command(goto="some_node")告诉了我们跳转到对应的节点
如果同时还需要返回值,则需要再加一个upadate字段
def human_approval(state):
ok = interrupt({"question": "...", "llm_output": state["llm_output"]})
return Command(
update={"approved": ok}, # 可选:同时写状态
goto="some_node" if ok else "another_node" # 动态跳转
)
这种写法和
def human_approval(state):
ok = interrupt({...})
return {"approved": ok}
graph_builder.add_conditional_edges(
"human_approval",
lambda s: "some_node" if s["approved"] else "another_node"
)
这种写法一致
对于上面我们静态断点的案例,我们可以使用动态断点进行优化,结果如下:
from langchain_core.messages import BaseMessage, HumanMessage, AIMessage, SystemMessage,AIMessageChunk
from langgraph.graph.message import add_messages
from langchain_community.chat_models import ChatTongyi
from pydantic import BaseModel, Field
from langgraph.types import interrupt, Command
from typing import TypedDict, Annotated, Union, List
from dotenv import load_dotenv
from langgraph.graph import StateGraph,START,END
from langgraph.checkpoint.memory import MemorySaver
load_dotenv()
class State(TypedDict):
messages:Annotated[List[BaseMessage],add_messages]
flag:int
class FlagJudge(BaseModel):
flag: int = Field(description="判断用户是否需要进行删除操作。需要=1,不需要=0")
def llm_judge(state:State):
last_user_content=state["messages"][-1].content
msg = [
SystemMessage(content="你是一个判断助手。请严格按 JSON Schema 输出,仅返回字段 {flag}。"),
HumanMessage(content=last_user_content),
]
result=llm_struct.invoke(msg)
print({"flag":result.flag})
return {"flag":result.flag}
def excute_judge(state:State):
value=interrupt("触发中断操作 输入1 进行删除,输入 0拒绝删除 ")
if value==1:
print("用户同意进行删除操作,已进行删除")
else:
print("用户拒绝删除,删除失败")
return {}
def response(state:State):
msgs=state["messages"]
ai_msg=llm.invoke(msgs)
return {"messages":[ai_msg]}
def router(state:State):
return "excute_judge" if state["flag"]==1 else "response"
def stream_print(user_input):
global interrupted,last_len,pending
for chunk in build.stream(user_input,config,stream_mode="values"):
msgs=chunk["messages"]
if len(msgs)>last_len:
last=msgs[-1]
if last.type!="human":
print(last.content)
last_len=len(msgs)
llm=ChatTongyi(
model="qwen-plus"
)
llm_struct = llm.with_structured_output(FlagJudge)
graph=StateGraph(State)
graph.add_node("llm_judge",llm_judge)
graph.add_node("response",response)
graph.add_node("excute_judge",excute_judge)
graph.set_entry_point("llm_judge")
graph.add_conditional_edges("llm_judge",router,["response","excute_judge"])
graph.add_edge("response",END)
graph.add_edge("excute_judge",END)
interrupted=True
pending=None
last_len=0
config={"configurable":{"thread_id":"1"}}
checkpointer=MemorySaver()
build=graph.compile(checkpointer=checkpointer)
query=input("请输入你的问题")
message={"messages":[HumanMessage(content=query)],
"flag":0
}
pending=message
while True:
stream_print(pending)
snap = build.get_state(config)
interrupts = snap.interrupts
if not interrupts:
break # 没有中断,流程结束
intr = interrupts[-1]
tip = intr.value
while True:
ans = input(f"{tip}:").strip()
if ans in {"0", "1"}:
# 关键:下一轮的输入用 Command(resume=...)
pending = Command(resume=int(ans))
break
print("输入不合法,请重试。")
此时实现了多次循环断点判断逻辑,我第一次想在流式输出中进行断点处理操作,但在流式输出中是检测不出中断
的,因为流式输出中的chunk只在节点执行完成后返回,过没执行中断,自然不会包含中断,如果执行了中断,下
一个chunk也不会返回,chunk中本身也不会包含中断信息,state中倒是包含中断信息,但是和上面一样的原因,
在流式输出中获得的state是在断点触发前的,所以也不会有断点信息,我们将处理操作放在外部才是正确的。
2.7 MultiAgent
我们先前学习的都是单agent结构,但如果需要解决复杂的问题时,由于模型上下文的长度限制,绑定工具较多的
可能错误调用,还有图过于复杂时状态传递可能出现问题等因素,单一agent往往无法解决过于复杂的问题, 此时
就需要使用多agent,我们通过构建多个状态图,可以将某个图作为另一个子图,实现多agent之间的连接
但多agent之间怎么进行通信呢,这就得分情况考虑了,先看第一种,图和他的子图之间有共同键,此时可以直接
通过共同的键进行通信
from langchain_core.messages import BaseMessage, HumanMessage, AIMessage, SystemMessage,AIMessageChunk
from langgraph.graph.message import add_messages
from langchain_community.chat_models import ChatTongyi
from pydantic import BaseModel, Field
from langgraph.types import interrupt, Command
from typing import TypedDict, Annotated, Union, List
from dotenv import load_dotenv
from langgraph.graph import StateGraph,START,END
from langgraph.checkpoint.memory import MemorySaver
load_dotenv()
llm=ChatTongyi(
model="qwen-plus"
)
class ParentState(TypedDict):
user_input:str
final_response:str
def parent_node(state:ParentState):
message=state["user_input"]
response=llm.invoke(message)
return {"final_response":response.content}
class SubGraphState(TypedDict):
final_response:str
summary:str
def subgraph_node1(state:SubGraphState):
content=state["final_response"]
message=[("system","请对用户传入的消息进行简单总结,不改变原表达的内容,尽可能的完整"),
("human",content)]
summary=llm.invoke(message).content
return {"summary":summary}
def subgraph_node2(state:SubGraphState):
summary=state["summary"]
message=[("system",f"现在给你提供一段文本{state["final_response"]},用户将给你进行总结后的文本,请根据信息的完整度等要素对总结的质量"
f"进行打分,分数由0到10,只需要返回数字即可"),("human",state["summary"])]
response=llm.invoke(message)
return {"final_response":response.content}
subgraph_builder=StateGraph(SubGraphState)
subgraph_builder.add_node(subgraph_node1)
subgraph_builder.add_node(subgraph_node2)
subgraph_builder.add_edge("subgraph_node1","subgraph_node2")
subgraph_builder.add_edge(START,"subgraph_node1")
subgraph=subgraph_builder.compile()
parentgraph_builder=StateGraph(ParentState)
parentgraph_builder.add_node("node2",subgraph)
parentgraph_builder.add_node("node1",parent_node)
parentgraph_builder.add_edge("node1","node2")
parentgraph_builder.set_entry_point("node1")
parentgraph_builder.set_finish_point("node2")
parentgraph=parentgraph_builder.compile()
# for chunk in subgraph.stream({"final_response":"介绍一下未来几年的agent应用发展趋势","summary":"介绍一下agent"},stream_mode="updates"):
# print(chunk)
for chunk in parentgraph.stream({"user_input":"告诉我未来几年agent发展趋势"},stream_mode="values"):
print(chunk)
{'user_input': '告诉我未来几年agent发展趋势'}
{'user_input': '告诉我未来几年agent发展趋势', 'final_response': '未来几年,人工智能代理(AI Agent)的发展将经历快速演进,受到技术进步、市场需求和应用场景的推动。以下是从技术、应用、生态和伦理等多个维度对未来几年AI Agent发展趋势的分析:\n\n---\n\n## 🚀 一、技术层面的发展趋势\n\n### 1. **更强的自主决策与推理能力**\n- **趋势**:随着大模型(如LLM、多模态模型)的发展,Agent将具备更强的推理、规划、记忆和学习能力。\n- **技术支撑**:强化学习、思维链(Chain-of-Thought)、工具调用(Tool Calling)、记忆机制(Memory Systems)等。\n- **目标**:从“执行指令”向“自主思考+执行”转变。\n\n### 2. **多模态交互能力增强**\n- Agent将不仅限于文本交互,而是融合**语音、图像、视频、传感器数据**等多模态输入。\n- 支持更自然、更贴近人类的交互方式,如手势识别、语音对话、视觉理解等。\n\n### 3. **模块化与可扩展性增强**\n- 未来Agent系统将采用**模块化架构**,便于快速集成新功能(如新工具、新技能)。\n- 支持“插件式”扩展,如接入API、数据库、外部知识库等。\n\n### 4. **持续学习与自适应能力**\n- Agent将具备**持续学习能力**(Continual Learning),在与环境互动中不断优化策略。\n- 支持个性化适应不同用户、场景和任务。\n\n---\n\n## 🧩 二、应用场景的拓展趋势\n\n### 1. **个人助理型Agent**\n- 智能助手将更强大,能完成复杂任务,如日程安排、旅行规划、财务建议、健康监测等。\n- 例如:基于Agent的“数字员工”、“虚拟管家”。\n\n### 2. **企业级智能代理**\n- 在客服、销售、运营、研发等环节中,Agent将承担更多自动化、智能化任务。\n- 如:智能客服、销售助手、数据分析师、流程自动化Agent。\n\n### 3. **教育与培训**\n- 个性化学习路径推荐、虚拟教师、实时答疑、互动式教学等。\n- 支持沉浸式学习体验,如VR+Agent结合。\n\n### 4. **医疗健康**\n- 医疗辅助诊断、个性化健康管理、远程护理、心理陪伴等。\n- 结合IoT设备实现更精准的健康监测与干预。\n\n### 5. **游戏与元宇宙**\n- Agent将作为NPC(非玩家角色)或虚拟角色,具备更真实的行为逻辑和互动能力。\n- 支持动态剧情生成、个性化角色成长路径。\n\n---\n\n## 🌐 三、生态系统与平台化趋势\n\n### 1. **Agent平台兴起**\n- 未来将出现更多**Agent开发平台**和**Agent市场**,支持开发者创建、部署和交易Agent。\n- 类似于今天的App Store或Chrome插件市场。\n\n### 2. **去中心化与分布式Agent网络**\n- 借助区块链、边缘计算等技术,构建**去中心化的Agent网络**。\n- 实现更安全、更灵活的协作模式,适用于跨组织协作、隐私保护等场景。\n\n### 3. **Agent与数字孪生结合**\n- Agent将与物理世界的“数字孪生”系统深度融合,用于城市治理、工业控制、交通调度等领域。\n\n---\n\n## 📉 四、挑战与风险趋势\n\n### 1. **伦理与安全问题**\n- Agent自主决策可能引发伦理问题(如偏见、歧视、隐私泄露)。\n- 需要建立**可解释性机制**、**伦理约束框架**和**安全控制机制**。\n\n### 2. **监管与合规挑战**\n- 随着Agent在金融、医疗、教育等敏感领域的应用增加,政府将加强监管。\n- 合规性要求(如GDPR、AI法案)将成为Agent设计的重要考量。\n\n### 3. **用户信任与接受度**\n- 用户对AI代理的信任程度将直接影响其普及速度。\n- 需要提升透明度、可解释性和用户控制权。\n\n---\n\n## 📈 五、未来3-5年预测(2025-2030)\n\n| 时间 | 核心特征 | 代表场景 |\n|------|-----------|------------|\n| 2025-2026 | 初步实现任务自动化、单点智能代理 | 智能客服、个人助理、数据分析Agent |\n| 2027-2028 | 多模态交互、模块化架构普及 | 企业级Agent、教育辅导Agent、医疗助手 |\n| 2029-2030 | 自主学习、平台化生态形成 | Agent市场、去中心化协作、元宇宙角色 |\n\n---\n\n## 🧠 总结\n\n未来几年,AI Agent将从“工具”逐步演变为“伙伴”,具备更强的**智能性、交互性、适应性**。它将深刻改变人机协作的方式,推动各行各业的数字化、智能化转型。\n\n---\n\n如果你关注某个具体领域(如教育、医疗、游戏等),我可以进一步细化该领域的Agent发展趋势。需要的话请告诉我!'}
{'user_input': '告诉我未来几年agent发展趋势', 'final_response': '9'}
可以看到此时的输出是正确的,整个图的逻辑没有问题,实现了多个图之间的通信
需要注意的是,我们子图是作为父图的一个节点加入到整个图中的,所以我们无论是通过流式输出还是其他方式获
得的键都只包含父图定义的键
上面这种是父子图之间有共同键,我们可以直接通过共同键传递数据,如果父子图之间没有共同键呢?我们的策略
是通过创建父图中的一个转换节点,在转换节点中根据父图中的键来构造子图的键,然后调用子图,获得子图的返
回键后又将其构造为父图的键返回
具体操作如下:
from langchain_core.messages import BaseMessage, HumanMessage, AIMessage, SystemMessage,AIMessageChunk
from langgraph.graph.message import add_messages
from langchain_community.chat_models import ChatTongyi
from pydantic import BaseModel, Field
from langgraph.types import interrupt, Command
from typing import TypedDict, Annotated, Union, List
from dotenv import load_dotenv
from langgraph.graph import StateGraph,START,END
from langgraph.checkpoint.memory import MemorySaver
load_dotenv()
llm=ChatTongyi(
model="qwen-plus"
)
class ParentState(TypedDict):
user_input:str
llm_output:str
final_output:str
def parent_node1(state:ParentState):
message=state["user_input"]
response=llm.invoke(message)
return {"llm_output":response.content}
def parent_node2(state:ParentState):
text=state["llm_output"]
subgraph_response=subgraph.invoke({"text":text})
print()
return {"final_output":subgraph_response["summary"]}
class SubGraphState(TypedDict):
text:str
summary:str
def subgraph_node(state:SubGraphState):
content=state["text"]
message=[("system","请对用户传入的消息进行简单总结,不超过50字"),
("human",content)]
summary=llm.invoke(message).content
return {"summary":summary}
subgraph_builder=StateGraph(SubGraphState)
subgraph_builder.add_node(subgraph_node)
subgraph_builder.add_edge(START,"subgraph_node")
subgraph_builder.add_edge("subgraph_node",END)
subgraph=subgraph_builder.compile()
parentgraph_builder=StateGraph(ParentState)
parentgraph_builder.add_node("node2",parent_node2)
parentgraph_builder.add_node("node1",parent_node1)
parentgraph_builder.add_edge("node1","node2")
parentgraph_builder.set_entry_point("node1")
parentgraph_builder.set_finish_point("node2")
parentgraph=parentgraph_builder.compile()
for chunk in parentgraph.stream({"user_input":"介绍一下你自己"},stream_mode="values"):
print(chunk)

浙公网安备 33010602011771号