在职前端Leader学习/转行 AI Agent -DAY31

0 阅读21分钟

2026.08.13 09:37

学习路线

Python -> Python进阶 -> Python数据分析 -> LangChain -> 机器学习 -> 神经网络 -> NLP -> Coze -> Dify -> 大模型应用基础 -> 大模型微调 -> 多模态 -> vibeCoding

复习巩固总结

1. 短期记忆

1.1 基于内存的持久化器

可以发现,每次执行的输出完全相同,而我们并没有更改 thread_id ,之所以看不到上次运行的状态是因为每次运行创建新的Saver(),历史State被丢弃了。

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 一致,历史状态就可以和当前调用串联起来。

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()

# 按照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 工具中访问长期记忆

# 基于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 在中间件中访问长期记忆

# 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)

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(转换)

文本拆分器,冗余过滤器,元数据提取器,多语言转换器,对话转换器

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)负责将 文本 转换为 向量表示

# 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

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(检索)

# 扫描数据
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定位区别

对比维度LangChainLangGraph
定位Agent 高层开发框架底层编排框架 & Agent Runtime
核心入口create_agentStateGraph / @entrypoint
适用场景结构直接的 Agent 应用复杂工作流、持久化状态、长时间运行、人工介入
流程控制Agent 循环自动管理精细控制节点、边、条件分支
学习成本较低较高

对于大多数 Agent 项目,从 LangChain 的 create_agent 开始即可;需要复杂工作流编排、确定性步骤与 Agent 步骤混合、长时间运行或底层状态控制时,再引入 LangGraph。

5. langgraph三要素组成

State(状态):LangGraph 运行过程中的共享数据结构,用于表示应用在某一时刻的状态快照。它承载了图运行所需的上下文信息、中间结果和后续节点需要读取的数据

Node(节点):LangGraph 中的具体执行单元

Edges(边):用于定义节点之间的流转关系

LangGraph学习

6. 完成基础的graph流程

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. 图结构可视化展示

raw_mermaid = graph.get_graph().draw_mermaid()
print(raw_mermaid)

Langgraph也支持将mermaid转换为图片字节流

png_bytes = graph.get_graph().draw_mermaid_png()

draw_mermaid_png()底层会先调用draw_mermaid()得到mermaid语法的图表代码,然后调用在线服务渲染为mermaid图片。

默认的mermaid在线渲染服务链接为:mermaid.ink,由于网络问题可能存在渲染失败的情况,通常重试即可成功。

可以通过draw_mermaid_png()的base_url参数替换可用的mermaid在线渲染服务,

from IPython.display import display, Image
png_bytes = graph.get_graph().draw_mermaid_png()
png = Image(png_bytes)
display(png)

也可以将图片字节流转换为PNG文件落盘

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)

快捷用法

from IPython.display import display
display(graph)

8. 状态的多种实现dataclass

官方推荐了三种定义Schema的方式:TypedDict、dataclass、Pydantic

在上述基础graph中已经使用过TypedDict定义状态了

# 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

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计算图的状态时,这三种方式都要求字段名称完全一致。只是处理方式不同。具体规则如下

  1. TypedDict

TypedDict将输入字段视为字典的Key,不匹配时抛出KeyError异常。

  1. dataclass

dataclass将输入字段视为类的属性,不匹配时抛出TypeError(类型错误)异常。

  1. 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)
# 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 提供的一种类型注解扩展机制

第一个参数是被注解的原始类型,后续参数是附加的元数据

Annotated[Type, reducer_function]
from typing import TypedDict, Annotated
class OverAllState(TypedDict):
    logs: Annotated[list[str], my_reducer]
    cur_id: str

常用内置Reducer函数

  1. operator.add 是 Python 内置的加法操作函数

等价于 a(第一个参数)+b(第二个参数)

from operator import add
print(f"{add(1,2) = }")
print(f"{add([1,2], [3,4]) = }")
print(f"{add(['a','b'], ['c']) = }")
  1. langgraph.graph.message.add_messages

add_messages 是 LangGraph 中专用于合并消息列表的 Reducer 函数,常用于维护对话历史类的状态字段。

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

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 会使用默认的状态更新行为:后一次更新值会覆盖该字段原有的状态值。

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。

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,则按照默认规则使用节点返回的新值覆盖原值。
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 逻辑进行合并。

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. 节点的并行执行

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 状态设计中的规范。以下规则属于工程上的最佳实践

  1. 输入状态和输出状态通常应是全局状态的子集
class InputState(TypedDict):
    username: str

class OutputState(TypedDict):
    graph_output: str

class OverAllState(TypedDict):
    username: str
    nickname: str
    graph_output: str
  1. 私有状态和全局状态应尽量避免字段重名

虽然某些情况下程序仍然可以运行,但容易让人误以为该字段是全局共享字段,从而造成理解混乱。

  1. 节点函数应明确声明入参状态类型和返回状态类型
  2. 节点函数中不应该访问入参状态类型中不存在的字段

节点实际接收到的状态会按照其入参类型进行裁剪。因此,如果节点入参声明为 InputState,就不应该在节点内部访问 InputState 中不存在的字段。

def node_1(state: InputState) -> OverAllState:
    return {
        "nickname": state["username"]
    }

如果在该函数中访问:

state["nickname"]

而 nickname 不属于 InputState,运行时就可能抛出 KeyError。 5. 节点函数返回的字典应尽量和返回类型注解保持一致 节点返回的是对状态的部分更新,不是完整状态

不过,从工程规范上讲,节点返回字典中的字段最好和函数返回类型注解保持一致,这样更利于阅读、调试和维护。

17. 状态的源码实现逻辑

17.1 状态的记录

LangGraph 的状态并不是简单保存在一个普通字典中,而是会被拆分成多个可读写的状态字段。每个状态字段在底层通常对应一个 Channel。

这些状态字段会在不同阶段被记录到状态图中。

  1. StateGraph 记录状态字段的核心方法是 _add_schema(),_add_schema() 会解析传入的状态 Schema,并将其中声明的字段记录到图中,使这些字段成为图运行时可以读写的状态字段。
  2. 创建 StateGraph 时,会记录 state_schema、input_schema 和 output_schema 中的字段。LangGraph 会解析这些 Schema,并将其中涉及的字段加入图的状态管理体系。
  3. 调用 add_node() 添加节点时,也可能记录节点入参声明的状态 Schema。如果这个输入状态类型之前没有被图记录过,LangGraph 也会通过 _add_schema() 将其加入图中。
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 状态的访问

  1. 调用图时,输入会按照 input_schema 进行约束。如果创建图时声明了 input_schema,那么外部输入会按照 input_schema 进行约束。如果没有声明 input_schema,则通常按照 state_schema 作为图的输入 Schema。input_schema 的作用不是“只让第一个节点可见”,而是约束图的外部输入结构
  2. 节点接收到的状态会按照节点入参类型进行裁剪。每个节点能读取哪些字段,主要取决于该节点第一个参数的类型注解。
def node_1(state: InputState) -> OverAllState:
    ...

如果访问了入参状态中不存在的字段,例如:

state["nickname"]

就可能抛出:

KeyError
  1. 节点返回的是状态更新,不需要返回完整状态

例如:

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。