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

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

  1. TypedDict

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

  1. dataclass

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

  1. Pydantic

Pydantic对输入字段进行校验,不匹配时抛出ValidationError异常。

图节点返回的是对于状态的更新,如果返回字段和状态字段不匹配,上述三种Schema定义方式的行为是统一的:状态更新会被忽略。

推荐优先使用 TypedDict 定义

大多数官方案例也采用 TypedDict 方式定义状态 Schema。这种方式写法简洁、结构清晰

相比普通 dictTypedDict 可以提供更明确的字段约束和类型提示;相比 dataclassTypedDict 更贴近 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函数

  1. 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']) = }")
  1. 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

leftright 的类型均为 MessagesMessages 可以理解为 LangChain 消息对象的列表,其中每个元素都是 BaseMessage 或其子类的实例,常见子类包括:

  • HumanMessage:用户的输入消息;
  • AIMessage:AI 的回复消息;
  • SystemMessage:系统提示消息;
  • ToolMessage:工具调用的结果消息。

add_messages 处理的是对话消息序列,而非普通的字符串列表。

BaseMessage 包含一个可选的 id 属性,用于唯一标识一条消息。add_messages 在合并 leftright 时,不是简单地执行列表拼接,而是依据消息的 id 进行合并:

  • right 中的某条消息的 idleft 中不存在,则将该消息追加到结果列表末尾;
  • right 中的某条消息的 idleft 中已有消息的 id 相同,则使用 right 中的新消息替换 left 中的旧消息。

可以概括为:在保留历史消息的基础上追加新消息,并允许通过相同的消息 id 覆盖已有消息,并非简单地对 leftright 求"并集"

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

  1. 输入状态和输出状态通常应是全局状态的子集
python 复制代码
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 中不存在的字段。

python 复制代码
def node_1(state: InputState) -> OverAllState:
    return {
        "nickname": state["username"]
    }

如果在该函数中访问:

css 复制代码
state["nickname"]

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

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

17. 状态的源码实现逻辑

17.1 状态的记录

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

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

  1. StateGraph 记录状态字段的核心方法是 _add_schema()_add_schema() 会解析传入的状态 Schema,并将其中声明的字段记录到图中,使这些字段成为图运行时可以读写的状态字段。
  2. 创建 StateGraph 时,会记录 state_schemainput_schemaoutput_schema 中的字段。LangGraph 会解析这些 Schema,并将其中涉及的字段加入图的状态管理体系。
  3. 调用 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 状态的访问

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

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

python 复制代码
state["nickname"]

就可能抛出:

python 复制代码
KeyError
  1. 节点返回的是状态更新,不需要返回完整状态

例如:

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

相关推荐
WILLF1 小时前
Python vs JavaScript 异常处理对比
前端·python
岁月留痕1681 小时前
4 Dart 篇:异步(并发)
前端
MartinYeung51 小时前
[论文学习]AdInject:通过广告投放对Web代理发起真实世界黑盒攻击
前端·学习
用户2181697049301 小时前
Flutter (十六) 组件通信
前端
mONESY1 小时前
React 前端如何不傻等后端接口?
前端·javascript·后端
岁月留痕1681 小时前
6 Flutter 篇:Flutter 分层式架构设计
前端
乘风gg1 小时前
AI Coding:从单兵提效到多 Agent 的团队全链路协作模式
前端·ai编程·claude
万维易源1 小时前
免费药品信息查询:用API 读懂常用药
java·前端·数据库·药品信息·药品查询·药品查询api
勾勾圈圈蛋蛋1 小时前
Vue2 与 Vue3 响应式数据原理详解
前端