2026.08.13 09:37
学习路线
rust
Python -> Python进阶 -> Python数据分析 -> LangChain -> 机器学习 -> 神经网络 -> NLP -> Coze -> Dify -> 大模型应用基础 -> 大模型微调 -> 多模态 -> vibeCoding
复习巩固总结
1. 短期记忆
1.1 基于内存的持久化器
可以发现,每次执行的输出完全相同,而我们并没有更改 thread_id ,之所以看不到上次运行的状态是因为每次运行创建新的Saver(),历史State被丢弃了。
python
from langgraph.checkpoint.memory import InMemorySaver
checkpointer = InMemorySaver() # 1、创建了内存级的记忆存储
agent = create_agent(
model=model,
tools=[],
checkpointer=checkpointer, # 2、让agent具备了存储的能力
)
# 3、同一个thread_id共享记忆的
config = {
"configurable": {
"thread_id": 1
}
}
print("\n第n轮对话:")
response1 = agent.invoke(
{
"messages": [HumanMessage("我叫张三")]
},
config=config # 4、传入invoke()当中
)
print(f"Agent: {response1['messages'][-1].content}")
1.2 基于外部存储介质的持久化器
根据输出判断,状态是累积的。由此可以得出结论:即便重新创建 Saver() ,只要 thread_id 一致,历史状态就可以和当前调用串联起来。
python
from langgraph.checkpoint.postgres import PostgresSaver
DB_URL = "postgresql://langchain_user:abcd1234@119.45.246.196:5432/langchain_db?sslmode=disable"
with PostgresSaver.from_conn_string(DB_URL) as checkpointer:
# 初始化postgresql数据库
checkpointer.setup()
agent = create_agent(
model = model,
checkpointer=checkpointer
)
config = {
"configurable": {
"thread_id": "1"
}
}
response1 = agent.invoke(
{
"messages": [HumanMessage("你好,我是康师傅")]
},
config=config
)
for msg in response1["messages"]:
msg.pretty_print()
2. 长期记忆
2.1 search()
python
# 按照namespace前缀搜索 + 按照filter过滤
from langgraph.store.memory import InMemoryStore
store = InMemoryStore()
namespace1 = ("users", "Alice", "memories")
key1 = 'preferences'
value1 = {
"course": "计算机组成原理",
"sports": "跑步",
"food": "紫光园奶皮子酸奶"
}
# ...
store.put(namespace1, key1, value1)
store.put(namespace2, key2, value2)
store.put(namespace3, key3, value3)
for item in store.search(("users", "Bob")):
print(item)
for item in store.search(("users",), filter={"food": "紫光园奶皮子酸奶"}):
print(item)
# 按照语义搜索
from langgraph.store.memory import InMemoryStore
from langchain.embeddings import init_embeddings
def embed(text: list[str]) -> list[list[float]]:
return [[1.0] * 6 for _ in range(len(text))]
embedding_model = init_embeddings(
model="openai:text-embedding-3-large",
api_key=os.getenv("CLOSEAI_API_KEY"),
base_url=os.getenv("CLOSEAI_BASE_URL"),
)
index_config = {
# 使用嵌入函数
# "embed": embed,
# "dims": 6,
# "fields": ["$", "course"]
# 使用嵌入模型赋值
"embed": embed,
"dims": 2048,
"fields": ["$"]
}
store = InMemoryStore(
index = index_config
)
namespace1 = ("users", "Alice", "memories")
# ...
store.put(namespace1, key1, value1)
store.put(namespace2, key2, value2)
store.put(namespace3, key3, value3)
for item in store.search(("users", ), query="数电模电"):
print(item)
2.2 工具中访问长期记忆
python
# 基于InMemoryStore
from typing import NotRequired
from langchain.agents import AgentState, create_agent
from langchain.tools import tool, ToolRuntime
store = InMemoryStore()
# 自定义一个继承于AgentState的类
class CustomState(AgentState):
user_id: NotRequired[str]
# 保存用户信息到长期记忆中
@tool(parse_docstring=True)
def save_user_info(name: str, runtime: ToolRuntime) -> str:
"""
将客户信息保存在长期记忆中
Args:
name: 用户名
runtime: 工具的运行时
Returns:
str: 保存状态
"""
namespace = ("users",)
key = runtime.state["user_id"]
value = {"name": name}
runtime.store.put(namespace, key, value)
return "saved"
@tool(parse_docstring=True)
def get_user_info(runtime: ToolRuntime) -> str:
"""
从长期记忆中获取客户的信息
Args:
runtime: 工具的运行时
Returns:
str: 用户信息
"""
namespace = ("users",)
key = runtime.state["user_id"]
item = runtime.store.get(namespace, key)
return str(item.value) if item else "unknown"
agent = create_agent(
model=model,
tools=[save_user_info, get_user_info],
store=store,
state_schema=CustomState,
system_prompt="用户提及个人信息时,可以使用工具保存用户信息。如果用户询问个人信息时,可以尝试使用工具读取用户信息"
)
response1 = agent.invoke({
"messages": [HumanMessage("你好,很高兴认识你,我是小花")],
"user_id": "user-1"
})
for msg in response1["messages"]:
msg.pretty_print()
# 基于PostgresStore
DB_URL = "postgresql://langchain_user:abcd1234@118.195.166.24:5432/langchain_db?sslmode=disable"
with PostgresStore.from_conn_string(DB_URL) as store:
store.setup()
agent = create_agent(
model=model,
tools=[save_user_info, get_user_info],
store=store,
state_schema=CustomState,
system_prompt="用户提及个人信息时,可以使用工具保存用户信息。如果用户询问个人信息时,可以尝试使用工具读取用户信息"
)
response1 = agent.invoke({
"messages": [HumanMessage("你好,很高兴认识你,我是小花")],
"user_id": "user-1"
})
for msg in response1["messages"]:
msg.pretty_print()
2.3 在中间件中访问长期记忆
python
# Node-style hooks中访问:runtime.store
# 入参中已经有Runtime,Runtime中有store直接取即可
def before_model(self, state: StateT, runtime: Runtime[ContextT]) -> dict[str, Any] | None:
# Wrap-style hooks中访问:request.runtime.store
def wrap_model_call(
self,
request: ModelRequest[ContextT],
handler: Callable[[ModelRequest[ContextT]], ModelResponse[ResponseT]],
) -> ModelResponse[ResponseT] | AIMessage | ExtendedModelResponse[ResponseT]:
def wrap_tool_call(
self,
request: ToolCallRequest,
handler: Callable[[ToolCallRequest], ToolMessage | Command[Any]],
) -> ToolMessage | Command[Any]:
3. RAG
环节1:Source(数据源)
原始数据源类型多样:如:视频、图片、文本、代码、文档等
环节2:Load(加载)
加载到 内存 ,成为 文档(Document)对象 ,包含 文档内容(content) 和相关 元数据信息(metadata)
python
loader = TextLoader(
file_path="../asset/load/01-langchain-utf-8.txt",
encoding="utf-8"
)
loader = CSVLoader(
file_path="../asset/load/02-load.csv",
)
json_loader=JSONLoader(
file_path="../asset/load/03-load.json",
jq_schema=".messages[].content"
)
# PDF一般用 MinerU 导入
loader = PyPDFLoader(
file_path="https://arxiv.org/pdf/alg-geom/9202012",
extraction_mode="plain",
)
loader = UnstructuredWordDocumentLoader(
file_path="../asset/load/05-sgg_chat.docx",
mode="single",
)
loader = UnstructuredMarkdownLoader(
file_path="../asset/load/06-load.md",
mode= "single",
strategy="fast"
)
环节3:Transform(转换)
文本拆分器,冗余过滤器,元数据提取器,多语言转换器,对话转换器
python
from langchain_text_splitters import RecursiveCharacterTextSplitter
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=10,
chunk_overlap=0,
add_start_index=True,
separators=["\n\n", "\n", "。", "!", "?", "......", ",", ""], # 添加中文标点,默认 ["\n\n", "\n", " ", ""]
)
# split_text:传入的参数类型:文本内容(或字符串),返回值类型:字符串列表
text="LangChain框架特性\n\n多模型集成(GPT/Claude)\n记忆管理功能\n链式调用设计。文档分析场景示例:需要处理PDF/Word等格式。"
paragraphs = text_splitter.split_text(text)
# create_documents:传入的参数类型:字符串列表,返回值类型:Document对象列表
list=["LangChain框架特性\n\n多模型集成(GPT/Claude)\n记忆管理功能\n链式调用设计。文档分析场景示例:需要处理PDF/Word等格式。"]
paragraphs = text_splitter.create_documents(list)
for i,chunk in enumerate(paragraphs):
print(f"块{i + 1},长度:{len(chunk)}")
print(chunk)
print('-' * 50)
with open("../asset/load/09-ai.txt", encoding="utf-8") as f:
state_of_the_union = f.read() #返回的是字符串
texts = text_splitter.create_documents([state_of_the_union])
for text in texts:
print(f"🔥{text.page_content}")
# split_documents:传入的参数类型:Document对象列表,返回值类型:Document对象列表
loader = PyPDFLoader("../asset/load/04-load.pdf")
docs = loader.load() # 返回Document对象构成的list
paragraphs = text_splitter.split_documents(docs)
for para in paragraphs:
print(para)
print('-------')
环节4:Embed(嵌入)
文档嵌入模型(Text Embedding Models)负责将 文本 转换为 向量表示
python
# 1. 句子的向量化
text = "你好,很高兴认识你"
embed_docs = embedding_model.embed_query(text)
# 2. 文档的向量化
# 2.1 文本列表
texts = ["Hi there!"]
embeded_docs = embedding_model1.embed_documents(texts)
# 2.2 loader文件
loader = CSVLoader("../asset/load/02-load.csv", encoding="utf-8")
docs = loader.load_and_split()
texts = [doc.page_content for doc in docs]
embeded_docs = embedding_model1.embed_documents(texts)
环节5:Store(存储)
使用 Milvus 作为向量存储,数据模型:Database -> Collection -> Partition -> Entity
python
from pymilvus import MilvusClient
client = MilvusClient("http://localhost:19530")
# 封装为可以插入的数据格式
data = [
{
"id": i,
"vector": vectors[i],
"text": texts[i],
"source": "demo"
} for i in range(len(texts))
]
# 列出所有数据库
existed_databases = client.list_databases()
# 创建数据库
client.create_database(db_name=db_name)
# 删除数据库
client.drop_database(db_name=db_name)
# 切换数据库
client.use_database(db_name=db_name)
# 查看数据库下的collections
collections = client.list_collections()
# 创建collection
client.create_collection(
collection_name=collenction_name,
dimension=2048, # 与能用的模型维度一致
metric_type="COSINE" # 表示余弦相似度
)
# 删除collection
client.drop_collection(collection_name=collenction_name)
# 查看collection元数据
metadata = client.describe_collection(collection_name=collenction_name)
# 插入数据
insert_res = client.upsert(
collection_name=collenction_name,
data=data
)
# 手动flush
client.flush(collection_name=collenction_name)
环节6:Retrieve(检索)
python
# 扫描数据
iterator = client.query_iterator(
collection_name=collenction_name, # from collection
filter="", # where 1=1
output_fields=["*"], # select *
)
# 通过主键查询数据
res = client.get(
collection_name=collenction_name,
ids=[0, 1, 2]
)
# 相似度检索
query_vector = embed_model.embed_query(question)
results = client.search(
collection_name=collenction_name,
data=[query_vector],
limit=3,
output_fields=["text", "source", "id"]
)
4. langchain和langgraph定位区别
| 对比维度 | LangChain | LangGraph |
|---|---|---|
| 定位 | Agent 高层开发框架 | 底层编排框架 & Agent Runtime |
| 核心入口 | create_agent |
StateGraph / @entrypoint |
| 适用场景 | 结构直接的 Agent 应用 | 复杂工作流、持久化状态、长时间运行、人工介入 |
| 流程控制 | Agent 循环自动管理 | 精细控制节点、边、条件分支 |
| 学习成本 | 较低 | 较高 |
对于大多数 Agent 项目,从 LangChain 的 create_agent 开始即可;需要复杂工作流编排、确定性步骤与 Agent 步骤混合、长时间运行或底层状态控制时,再引入 LangGraph。
5. langgraph三要素组成
State(状态):LangGraph 运行过程中的共享数据结构,用于表示应用在某一时刻的状态快照。它承载了图运行所需的上下文信息、中间结果和后续节点需要读取的数据
Node(节点):LangGraph 中的具体执行单元
Edges(边):用于定义节点之间的流转关系
LangGraph学习
6. 完成基础的graph流程
python
from typing import TypedDict, Annotated
from operator import add
from langgraph.graph import StateGraph, START, END
# 1 定义状态
class OverAllState(TypedDict):
# logs: list[str]
# 日志类型还是 list[str] 更新的方式不是覆盖,而是 add 追加
logs: Annotated[list[str], add]
cur_id: str
# 2 定义节点
def node_1(state: OverAllState) -> OverAllState:
pre_id = state["cur_id"]
return {
"logs": ["node_1 运行完毕"],
"cur_id": pre_id + ", node_1"
}
def node_2(state: OverAllState) -> OverAllState:
pre_id = state["cur_id"]
return {
"logs": ["node_2 运行完毕"],
"cur_id": pre_id + ", node_2"
}
# 3 定义边
# 3.1 创建图 获取建造者
# builder = StateGraph(OverAllState)
builder = StateGraph(state_schema=OverAllState)
# 3.2 添加节点
builder.add_node("node_1", node_1)
builder.add_node("node_2", node_2)
# 3.3 添加边
builder.add_edge(START, "node_1")
# builder.add_edge(node_1, node_2)
builder.add_edge("node_1", "node_2")
builder.add_edge("node_2", END)
# 3.4 获取图
graph = builder.compile()
# 3.5 运行图
result = graph.invoke({"cur_id": "start"})
print(result)
7. 图结构可视化展示
python
raw_mermaid = graph.get_graph().draw_mermaid()
print(raw_mermaid)
Langgraph也支持将mermaid转换为图片字节流
python
png_bytes = graph.get_graph().draw_mermaid_png()
draw_mermaid_png()底层会先调用draw_mermaid()得到mermaid语法的图表代码,然后调用在线服务渲染为mermaid图片。
默认的mermaid在线渲染服务链接为:mermaid.ink,由于网络问题可能存在渲染失败的情况,通常重试即可成功。
可以通过draw_mermaid_png()的base_url参数替换可用的mermaid在线渲染服务,
python
from IPython.display import display, Image
png_bytes = graph.get_graph().draw_mermaid_png()
png = Image(png_bytes)
display(png)
也可以将图片字节流转换为PNG文件落盘
python
png_bytes = graph.get_graph().draw_mermaid_png()
print(png_bytes)
png_filename = 'first_demo_graph.png'
with open(png_filename, "wb") as f:
f.write(png_bytes)
快捷用法
python
from IPython.display import display
display(graph)
8. 状态的多种实现dataclass
官方推荐了三种定义Schema的方式:TypedDict、dataclass、Pydantic
在上述基础graph中已经使用过TypedDict定义状态了
python
# from typing import TypedDict, Annotated
from typing import Annotated
from operator import add
from langgraph.graph import StateGraph, START, END
from dataclasses import dataclass
# 1 定义状态
@dataclass
# class OverAllState(TypedDict):
class OverAllState:
# 日志类型还是 list[str] 更新的方式不是覆盖,而是 add 追加
logs: Annotated[list[str], add]
cur_id: str
# 2 定义节点
def node_1(state: OverAllState) -> OverAllState:
# pre_id = state["cur_id"]
pre_id = state.cur_id
# return {
# "logs": ["node_1 运行完毕"],
# "cur_id": pre_id + ", node_1"
# }
return OverAllState(
logs=["node_1 运行完毕"],
cur_id=pre_id + ", node_1"
)
def node_2(state: OverAllState) -> OverAllState:
# pre_id = state["cur_id"]
pre_id = state.cur_id
return {
"logs": ["node_2 运行完毕"],
"cur_id": pre_id + ", node_2"
}
# 3 定义边
# 3.1 创建图 获取建造者
builder = StateGraph(state_schema=OverAllState)
# 3.2 添加节点
builder.add_node("node_1", node_1)
builder.add_node("node_2", node_2)
# 3.3 添加边
builder.add_edge(START, "node_1")
# builder.add_edge(node_1, node_2)
builder.add_edge("node_1", "node_2")
builder.add_edge("node_2", END)
# 3.4 获取图
graph = builder.compile()
# 3.5 运行图
# result = graph.invoke({"cur_id": "start"})
result = graph.invoke(OverAllState([], "start"))
print(result)
9. 状态的多种实现pydantic
python
from typing import Annotated
from operator import add
from langgraph.graph import StateGraph, START, END
# from dataclasses import dataclass
from pydantic import BaseModel
# 1 定义状态
# @dataclass
# class OverAllState:
class OverAllState(BaseModel):
# 日志类型还是 list[str] 更新的方式不是覆盖,而是 add 追加
logs: Annotated[list[str], add]
cur_id: str
# 2 定义节点
def node_1(state: OverAllState) -> OverAllState:
pre_id = state.cur_id
# return OverAllState(
# logs=["node_1 运行完毕"],
# cur_id=pre_id + ", node_1"
# )
return {
"logs": ["node_1 运行完毕"],
"cur_id": pre_id + ", node_1"
}
def node_2(state: OverAllState) -> OverAllState:
pre_id = state.cur_id
return {
"logs": ["node_2 运行完毕"],
"cur_id": pre_id + ", node_2"
}
# 3 定义边
# 3.1 创建图 获取建造者
builder = StateGraph(state_schema=OverAllState)
# 3.2 添加节点
builder.add_node("node_1", node_1)
builder.add_node("node_2", node_2)
# 3.3 添加边
builder.add_edge(START, "node_1")
# builder.add_edge(node_1, node_2)
builder.add_edge("node_1", "node_2")
builder.add_edge("node_2", END)
# 3.4 获取图
graph = builder.compile()
# 3.5 运行图
# result = graph.invoke(OverAllState([], "start"))
result = graph.invoke({"cur_id": "start"})
print(result)
学习LangChain的结构化输出时我们提到:Pydantic对格式要求最严格,如果模型返回的内容不符合结构化Schema的要求,则抛出ValidationError。而其余方式都不会对模型的返回结果进行校验,即便模型返回的内容不符合结构化要求,也会原样返回给用户。
而作为LangGraph计算图的状态时,这三种方式都要求字段名称完全一致。只是处理方式不同。具体规则如下
- TypedDict
TypedDict将输入字段视为字典的
Key,不匹配时抛出KeyError异常。
- dataclass
dataclass将输入字段视为类的
属性,不匹配时抛出TypeError(类型错误)异常。
- Pydantic
Pydantic对输入字段进行校验,不匹配时抛出
ValidationError异常。
图节点返回的是对于状态的更新,如果返回字段和状态字段不匹配,上述三种Schema定义方式的行为是统一的:状态更新会被忽略。
推荐优先使用 TypedDict 定义
大多数官方案例也采用 TypedDict 方式定义状态 Schema。这种方式写法简洁、结构清晰
相比普通 dict,TypedDict 可以提供更明确的字段约束和类型提示;相比 dataclass,TypedDict 更贴近 LangGraph 中状态的更新方式,它又更加轻量,不会引入额外的数据校验开销。TypedDict 是定义 LangGraph State Schema 的首选方式。
10. 状态合并定义reducer
Reducer 定义了如何将多个节点对同一状态键的更新合并
Reducer 的核心特征:
- 函数签名:
(Value, Value) -> Value,接收当前值和更新值,返回合并后的新值 - 注解定义:通过
Annotated[Type, reducer_function]为状态键指定 Reducer - 默认行为:未指定 Reducer 的状态键使用覆盖策略(Last-Write-Wins)
python
# left: 从最开始的位置和并到当前节点位置的值
# right: 当前节点的值
# 返回值:合并后的值
def my_reducer(left: list[str], right: list[str]) -> list[str]:
return left + right
# 1. node1 运行之后的值
left = ["start", "node1 运行完毕"]
right = ["node2 运行完毕"]
# 2. 合并
merged = my_reducer(left, right)
print(merged)
left:当前已累计的状态值;right:本次待合并的新值。
函数内部通过 left + right 将两个列表合并,并返回合并后的结果。
11. reducer在langgraph中的应用
Annotated[] 是 Python 提供的一种类型注解扩展机制
第一个参数是被注解的原始类型,后续参数是附加的元数据
python
Annotated[Type, reducer_function]
python
from typing import TypedDict, Annotated
class OverAllState(TypedDict):
logs: Annotated[list[str], my_reducer]
cur_id: str
常用内置Reducer函数
operator.add是 Python 内置的加法操作函数
等价于 a(第一个参数)+b(第二个参数)
python
from operator import add
print(f"{add(1,2) = }")
print(f"{add([1,2], [3,4]) = }")
print(f"{add(['a','b'], ['c']) = }")
- langgraph.graph.message.add_messages
add_messages 是 LangGraph 中专用于合并消息列表的 Reducer 函数,常用于维护对话历史类的状态字段。
python
def add_messages(
left: Messages, # 状态中已有的消息列表
right: Messages, # 当前节点返回的消息更新值
*,
format: Literal["langchain-openai"] | None = None, # 可选参数,用于指定返回消息的格式,通常无需手动设置
) -> Messages:
...
return merged
left 与 right 的类型均为 Messages。Messages 可以理解为 LangChain 消息对象的列表,其中每个元素都是 BaseMessage 或其子类的实例,常见子类包括:
HumanMessage:用户的输入消息;AIMessage:AI 的回复消息;SystemMessage:系统提示消息;ToolMessage:工具调用的结果消息。
add_messages 处理的是对话消息序列,而非普通的字符串列表。
BaseMessage 包含一个可选的 id 属性,用于唯一标识一条消息。add_messages 在合并 left 与 right 时,不是简单地执行列表拼接,而是依据消息的 id 进行合并:
- 若
right中的某条消息的id在left中不存在,则将该消息追加到结果列表末尾; - 若
right中的某条消息的id与left中已有消息的id相同,则使用right中的新消息替换left中的旧消息。
可以概括为:在保留历史消息的基础上追加新消息,并允许通过相同的消息 id 覆盖已有消息,并非简单地对 left 与 right 求"并集"
2026.08.13 20:33
python
from langchain.messages import SystemMessage, HumanMessage, AIMessage
from langgraph.graph.message import add_messages
left = [
SystemMessage(content="你是一个专业的翻译", id="1"),
HumanMessage(content="你好", id="2"),
AIMessage(content="你好,我是专业的翻译", id="3"),
]
right = [
HumanMessage(content="我是老王,你是小王", id="2"),
AIMessage(content="好的我记住了", id="3"),
HumanMessage(content="你是谁?", id="4"),
AIMessage(content="我是小王", id="5"),
HumanMessage(content="你是小王吗?", id="1"),
]
merged = add_messages(left, right)
print(merged)
如果某个 State 字段没有显式定义 Reducer,LangGraph 会使用默认的状态更新行为:后一次更新值会覆盖该字段原有的状态值。
python
from langgraph.graph import StateGraph, START, END
from typing import TypedDict
class OverAllState(TypedDict):
logs: list[str]
id: str
def node_a(state: OverAllState):
return {
"logs": ["node_a"],
"id": "node_a"
}
def node_b(state: OverAllState):
return {
"logs": ["node_b"],
"id": "node_b"
}
builder = StateGraph(state_schema=OverAllState)
builder.add_node("node_a", node_a)
builder.add_node("node_b", node_b)
builder.add_edge(START, "node_a")
builder.add_edge("node_a", "node_b")
builder.add_edge("node_b", END)
graph = builder.compile()
result = graph.invoke({"logs": ["START"], "id": "start"})
print('=' * 30, '-> result <-', '=' * 30)
print(result)
12. 节点中调用状态
在 LangGraph 中,节点本质上是一个可调用对象,通常定义为普通 Python 函数。
节点函数的第一个参数通常是当前运行图的状态对象,也就是 State。
python
from typing import TypedDict, Annotated
from operator import add
from langgraph.graph import StateGraph, START, END
# 1. 定义状态
class OverAllState(TypedDict):
logs: Annotated[list[str], add]
cur_id: str
# 2. 定义节点
def node_1(state: OverAllState) -> OverAllState:
for k, v in state.items():
print(f"k:{k} v:{v}")
return state
builder = StateGraph(state_schema=OverAllState)
builder.add_node("node_1", node_1)
builder.add_edge(START, "node_1")
builder.add_edge("node_1", END)
graph = builder.compile()
# result = graph.invoke({"cur_id": "start"})
result = graph.invoke({"logs": ["start"], "cur_id": "start"})
13. 节点更新状态
在 LangGraph 中,节点函数通常不需要返回更新后的完整状态
也就是说,节点的返回值可以只包含需要修改的状态字段。
-
对于节点没有返回的字段,LangGraph 会保留其原有状态值;
-
对于节点返回的字段,LangGraph 会根据该字段是否配置了
Reducer来决定如何合并更新值。- 如果字段配置了
Reducer,则使用对应的Reducer函数将旧值和新值合并; - 如果字段没有配置
Reducer,则按照默认规则使用节点返回的新值覆盖原值。
- 如果字段配置了
python
from typing import TypedDict, Annotated
from operator import add
from langgraph.graph import StateGraph, START, END
# 1. 定义状态
class OverAllState(TypedDict):
logs: Annotated[list[str], add]
cur_id: str
# 2. 定义节点
def node_1(state: OverAllState) -> OverAllState:
for k, v in state.items():
print(f"k:{k} v:{v}")
# return state
return {
# "logs": ["node_1 运行完毕"]
"cur_id": "node_1"
}
builder = StateGraph(state_schema=OverAllState)
builder.add_node("node_1", node_1)
builder.add_edge(START, "node_1")
builder.add_edge("node_1", END)
graph = builder.compile()
result = graph.invoke({"logs": ["start"], "cur_id": "start"})
print(result)
在某些场景下,我们可能并不希望继续执行 Reducer 的聚合逻辑,而是希望本次更新直接覆盖旧值。这时可以使用 Overwrite。
需要注意的是,Overwrite 只影响当前这一次更新,并不会修改状态字段本身的 Reducer 定义。后续节点如果继续正常返回该字段的更新值,仍然会按照原来的 Reducer 逻辑进行合并。
python
from typing import TypedDict, Annotated
from operator import add
from langgraph.graph import StateGraph, START, END
from langgraph.types import Overwrite
# 1. 定义状态
class OverAllState(TypedDict):
# 规约的方式是add追加合并
logs: Annotated[list[str], add]
cur_id: str
# 2. 定义节点
def node_1(state: OverAllState) -> OverAllState:
for k, v in state.items():
print(f"k:{k} v:{v}")
# return state
return {
"logs": ["node_1 运行完毕"]
}
def node_2(state: OverAllState) -> OverAllState:
for k, v in state.items():
print(f"k:{k} v:{v}")
# return state
return {
# "logs": ["node_2 运行完毕"]
"logs": Overwrite(["node_2 运行完毕"])
}
def node_3(state: OverAllState) -> OverAllState:
for k, v in state.items():
print(f"k:{k} v:{v}")
# return state
return {
"logs": ["node_3 运行完毕"]
}
builder = StateGraph(state_schema=OverAllState)
builder.add_node("node_1", node_1)
builder.add_node("node_2", node_2)
builder.add_node("node_3", node_3)
builder.add_edge(START, "node_1")
builder.add_edge("node_1", "node_2")
builder.add_edge("node_2", "node_3")
builder.add_edge("node_3", END)
graph = builder.compile()
result = graph.invoke({"logs": ["start"], "cur_id": "start"})
print(result)
14. 节点的并行执行
python
from time import sleep
from typing import TypedDict, Annotated
from operator import add
from langgraph.graph import StateGraph, START, END
from langgraph.types import Overwrite
# 1. 定义状态
class OverAllState(TypedDict):
# 规约的方式是add追加合并
logs: Annotated[list[str], add]
# 如果出现并行节点 同时更新状态 往下游节点传递的时候 必须要有reducer
# cur_id: str
cur_id: Annotated[str, add]
# 2. 定义节点
def node_1(state: OverAllState) -> OverAllState:
for k, v in state.items():
# print(f"k:{k} v:{v}")
print(f"1k:{k} v:{v}")
return {
"logs": ["node_1 运行完毕"]
}
def node_2(state: OverAllState) -> OverAllState:
for k, v in state.items():
# print(f"k:{k} v:{v}")
print(f"2k:{k} v:{v}")
return {
# "logs": Overwrite(["node_2 运行完毕"])
"logs": ["node_2 运行完毕"],
"cur_id": "node_2"
}
def node_3(state: OverAllState) -> OverAllState:
sleep(1)
for k, v in state.items():
# print(f"k:{k} v:{v}")
print(f"3k:{k} v:{v}")
return {
"logs": ["node_3 运行完毕"],
"cur_id": "node_3"
}
# def node_4(state: OverAllState) -> OverAllState:
def node_4(state: OverAllState) -> OverAllState:
sleep(2)
for k, v in state.items():
# print(f"k:{k} v:{v}")
print(f"4k:{k} v:{v}")
return {
# "logs": ["node_3 运行完毕"]
"logs": ["node_4 运行完毕"]
}
builder = StateGraph(state_schema=OverAllState)
builder.add_node("node_1", node_1)
builder.add_node("node_2", node_2)
builder.add_node("node_3", node_3)
builder.add_node("node_4", node_4)
builder.add_edge(START, "node_1")
builder.add_edge("node_1", "node_2")
builder.add_edge("node_1", "node_3")
builder.add_edge("node_2", "node_4")
builder.add_edge("node_3", "node_4")
builder.add_edge("node_4", END)
graph = builder.compile()
result = graph.invoke({"logs": ["START"], "cur_id": "start"})
print('=' * 30, '-> result <-', "=" * 30)
print(result)
15. langgraph的4种状态
- 全局状态 / 内部状态:图内部主要使用的状态,创建
StateGraph时传递给state_schema参数。 - 输入状态:图对外接收输入时使用的状态,创建
StateGraph时传递给input_schema参数。 - 输出状态:图最终对外返回结果时使用的状态,创建
StateGraph时传递给output_schema参数。 - 私有状态:图内部节点之间传递的临时状态,通常不作为图的输入,也不作为图的最终输出。
16. 状态的使用规范
LangGraph 状态设计中的规范。以下规则属于工程上的最佳实践
- 输入状态和输出状态通常应是全局状态的子集
python
class InputState(TypedDict):
username: str
class OutputState(TypedDict):
graph_output: str
class OverAllState(TypedDict):
username: str
nickname: str
graph_output: str
- 私有状态和全局状态应尽量避免字段重名
虽然某些情况下程序仍然可以运行,但容易让人误以为该字段是全局共享字段,从而造成理解混乱。
- 节点函数应明确声明入参状态类型和返回状态类型
- 节点函数中不应该访问入参状态类型中不存在的字段
节点实际接收到的状态会按照其入参类型进行裁剪。因此,如果节点入参声明为
InputState,就不应该在节点内部访问InputState中不存在的字段。
python
def node_1(state: InputState) -> OverAllState:
return {
"nickname": state["username"]
}
如果在该函数中访问:
css
state["nickname"]
而 nickname 不属于 InputState,运行时就可能抛出 KeyError。 5. 节点函数返回的字典应尽量和返回类型注解保持一致 节点返回的是对状态的部分更新,不是完整状态
不过,从工程规范上讲,节点返回字典中的字段最好和函数返回类型注解保持一致,这样更利于阅读、调试和维护。
17. 状态的源码实现逻辑
17.1 状态的记录
LangGraph 的状态并不是简单保存在一个普通字典中,而是会被拆分成多个可读写的状态字段。每个状态字段在底层通常对应一个 Channel。
这些状态字段会在不同阶段被记录到状态图中。
StateGraph记录状态字段的核心方法是_add_schema(),_add_schema()会解析传入的状态 Schema,并将其中声明的字段记录到图中,使这些字段成为图运行时可以读写的状态字段。- 创建
StateGraph时,会记录state_schema、input_schema和output_schema中的字段。LangGraph 会解析这些 Schema,并将其中涉及的字段加入图的状态管理体系。 - 调用
add_node()添加节点时,也可能记录节点入参声明的状态 Schema。如果这个输入状态类型之前没有被图记录过,LangGraph 也会通过_add_schema()将其加入图中。
python
class PrivateState(TypedDict):
greeting: str
def node_3(state: PrivateState) -> OutputState:
return {
"graph_output": state["greeting"]
}
当 node_3 被添加到图中时,PrivateState 中的 greeting 字段会被记录到图中,从而成为图内部可以传递的状态字段。 4. 总结
- 全局状态、输入状态、输出状态通常在创建
StateGraph时被记录。 - 私有状态通常在调用
add_node()添加节点时,根据节点入参类型注解被记录。 - 被记录后的状态字段,底层会成为图运行时可以读写的状态字段。
17.2 状态的访问
- 调用图时,输入会按照
input_schema进行约束。如果创建图时声明了input_schema,那么外部输入会按照input_schema进行约束。如果没有声明input_schema,则通常按照state_schema作为图的输入 Schema。input_schema的作用不是"只让第一个节点可见",而是约束图的外部输入结构 - 节点接收到的状态会按照节点入参类型进行裁剪。每个节点能读取哪些字段,主要取决于该节点第一个参数的类型注解。
python
def node_1(state: InputState) -> OverAllState:
...
如果访问了入参状态中不存在的字段,例如:
python
state["nickname"]
就可能抛出:
python
KeyError
- 节点返回的是状态更新,不需要返回完整状态
例如:
python
def node_1(state: InputState) -> OverAllState:
return {
"nickname": "Dear " + state["username"]
}
这里虽然返回类型注解是 OverAllState,但函数实际只返回了 nickname 一个字段。这是允许的 4. 节点返回值的应用主要由字段名称和图中已记录的状态字段决定。节点返回的字典会根据字段名称写入对应状态字段,并按照该字段的 Reducer 规则进行合并。不过,为了代码清晰,仍然推荐让节点的返回值和返回类型注解保持一致。 5. 最终输出会按照 output_schema 进行裁剪
如果 output_schema 只包含:
graph_output
那么最终 graph.invoke() 的返回结果就只会包含 graph_output。