前言
在构建复杂的AI应用时,我们经常需要处理自定义的对象或复杂的数据类型。然而,标准的序列化机制可能无法直接处理这些特殊类型的数据。LangGraph提供了灵活的方式来处理这种情况,本文将介绍一种简单可靠的方法来实现自定义序列化。
为什么需要自定义序列化?
标准的序列化机制(如JSON序列化)只能处理基本的数据类型(如字符串、数字、列表、字典等)。但在实际应用中,我们经常需要处理:
- 自定义的类和对象
- 包含复杂关系的数据结构
- 包含不可序列化属性的对象
- 需要特殊处理的业务对象
当这些复杂数据类型需要被保存到Checkpoint中时,我们就需要实现自定义的序列化和反序列化逻辑。
示例功能概述
我们将创建一个处理自定义Person
类的应用,展示如何实现自定义序列化:
- 定义一个包含多个属性的
Person
类 - 实现序列化和反序列化方法
- 在节点函数中处理序列化逻辑
- 展示如何在多次调用之间保存和恢复复杂对象
完整代码实现
让我们先来看完整的代码实现:
python
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
LangGraph Checkpoint 示例4: 自定义序列化
这个示例展示了如何使用自定义的序列化逻辑来处理复杂数据类型。
应用功能:创建一个处理自定义对象的图,并实现序列化和反序列化方法。
"""
from typing_extensions import TypedDict
from langgraph.graph import StateGraph, START, END
from langgraph.checkpoint.memory import InMemorySaver
print("======= Checkpoint示例4: 自定义序列化 =======")
# 定义一个自定义类
class Person:
def __init__(self, name: str, age: int, hobbies: list):
self.name = name
self.age = age
self.hobbies = hobbies
def __repr__(self):
return f"Person(name='{self.name}', age={self.age}, hobbies={self.hobbies})"
# 添加方法将对象转换为可序列化的格式
def to_dict(self):
return {
"_type": "Person",
"name": self.name,
"age": self.age,
"hobbies": self.hobbies
}
# 静态方法从字典中恢复对象
@staticmethod
def from_dict(data):
return Person(data["name"], data["age"], data["hobbies"])
# 定义状态类型(使用可序列化的数据结构)
class PersonState(TypedDict):
person_data: dict # 存储序列化后的Person数据
updated: bool
# 定义节点函数 - 包含序列化和反序列化逻辑
def update_person(state: PersonState) -> PersonState:
"""更新Person对象的信息"""
# 从序列化数据中恢复Person对象
person_data = state["person_data"]
person = Person.from_dict(person_data)
# 更新年龄和爱好
person.age += 1
if "阅读" not in person.hobbies:
person.hobbies.append("阅读")
# 将更新后的对象序列化为字典
updated_person_data = person.to_dict()
return {"person_data": updated_person_data, "updated": True}
# 创建StateGraph
person_graph = StateGraph(PersonState)
# 添加节点
person_graph.add_node("update", update_person)
# 设置入口点和出口点
person_graph.add_edge(START, "update")
person_graph.add_edge("update", END)
# 使用标准的InMemorySaver
checkpointer = InMemorySaver()
# 编译图并添加checkpointer
compiled_graph = person_graph.compile(checkpointer=checkpointer)
# 创建会话配置
session_config = {"configurable": {"thread_id": "person-session"}}
# 创建一个Person对象并序列化
initial_person = Person(name="张三", age=30, hobbies=["编程", "跑步"])
initial_person_data = initial_person.to_dict()
print(f"初始Person对象: {initial_person}")
# 执行第一次调用 - 传入序列化后的数据
result1 = compiled_graph.invoke({"person_data": initial_person_data, "updated": False}, config=session_config)
# 从结果中恢复Person对象以显示
result1_person = Person.from_dict(result1["person_data"])
print(f"第一次调用后: {result1_person}")
# 执行第二次调用,应该使用保存的状态
result2 = compiled_graph.invoke({}, config=session_config)
result2_person = Person.from_dict(result2["person_data"])
print(f"第二次调用后: {result2_person}")
# 执行第三次调用
result3 = compiled_graph.invoke({}, config=session_config)
result3_person = Person.from_dict(result3["person_data"])
print(f"第三次调用后: {result3_person}")
# 验证状态是否正确保存
print("======= 验证状态保存 =======")
saved_state = compiled_graph.get_state(session_config)
if saved_state:
print(f"保存的状态数据类型: {type(saved_state.values['person_data']).__name__}")
print(f"保存的状态数据: {saved_state.values['person_data']}")
# 从保存的状态中恢复Person对象
restored_person = Person.from_dict(saved_state.values['person_data'])
print(f"从保存状态恢复的对象: {restored_person}")
else:
print("未能获取到保存的状态")
执行结果如下
css
======= Checkpoint示例4: 自定义序列化 =======
初始Person对象: Person(name='张三', age=30, hobbies=['编程', '跑步'])
第一次调用后: Person(name='张三', age=31, hobbies=['编程', '跑步', '阅读'])
第二次调用后: Person(name='张三', age=32, hobbies=['编程', '跑步', '阅读'])
第三次调用后: Person(name='张三', age=33, hobbies=['编程', '跑步', '阅读'])
======= 验证状态保存 =======
保存的状态数据类型: dict
保存的状态数据: {'_type': 'Person', 'name': '张三', 'age': 33, 'hobbies': ['编程', '跑步', '阅读']}
从保存状态恢复的对象: Person(name='张三', age=33, hobbies=['编程', '跑步', '阅读'])
代码解析
1. 自定义类设计
python
class Person:
def __init__(self, name: str, age: int, hobbies: list):
self.name = name
self.age = age
self.hobbies = hobbies
def __repr__(self):
return f"Person(name='{self.name}', age={self.age}, hobbies={self.hobbies})"
def to_dict(self):
return {
"_type": "Person",
"name": self.name,
"age": self.age,
"hobbies": self.hobbies
}
@staticmethod
def from_dict(data):
return Person(data["name"], data["age"], data["hobbies"])
我们创建了一个简单的Person
类,并为其实现了两个关键方法:
to_dict()
:将对象转换为可序列化的字典格式,包含一个特殊的_type
字段用于标识对象类型from_dict()
:静态方法,从字典中恢复Person
对象
这两个方法是实现自定义序列化和反序列化的基础,遵循了"先让它工作"的原则,保持简单直接。
2. 状态类型定义
python
class PersonState(TypedDict):
person_data: dict # 存储序列化后的Person数据
updated: bool
关键设计点:我们在状态中存储的是序列化后的字典数据(person_data
),而不是直接存储Person
对象。这是一种更简单可靠的方法,避免了与LangGraph内部序列化机制的兼容性问题。
3. 节点函数中的序列化处理
python
def update_person(state: PersonState) -> PersonState:
"""更新Person对象的信息"""
# 从序列化数据中恢复Person对象
person_data = state["person_data"]
person = Person.from_dict(person_data)
# 业务逻辑:更新年龄和爱好
person.age += 1
if "阅读" not in person.hobbies:
person.hobbies.append("阅读")
# 将更新后的对象序列化为字典
updated_person_data = person.to_dict()
return {"person_data": updated_person_data, "updated": True}
这个节点函数展示了核心的序列化处理模式:
- 进入节点时 :从
person_data
字典反序列化为Person
对象 - 处理逻辑 :对
Person
对象进行业务操作(更新年龄和爱好) - 离开节点时 :将更新后的
Person
对象序列化为字典
这种方法的优点是简单直接,不需要修改或继承LangGraph的内部类。
4. 使用标准Checkpoint Saver
python
# 使用标准的InMemorySaver
checkpointer = InMemorySaver()
# 编译图并添加checkpointer
compiled_graph = person_graph.compile(checkpointer=checkpointer)
我们使用了标准的InMemorySaver
,不需要创建自定义的Checkpoint saver类。这符合"先让它工作"的原则,避免了不必要的复杂性。
5. 调用和状态管理
python
# 创建一个Person对象并序列化
initial_person = Person(name="张三", age=30, hobbies=["编程", "跑步"])
initial_person_data = initial_person.to_dict()
# 执行第一次调用 - 传入序列化后的数据
result1 = compiled_graph.invoke({"person_data": initial_person_data, "updated": False}, config=session_config)
# 从结果中恢复Person对象以显示
result1_person = Person.from_dict(result1["person_data"])
在调用图时,我们:
- 先将初始的
Person
对象序列化为字典 - 将序列化后的字典传入图的
invoke
方法 - 从结果中恢复
Person
对象以便显示
执行结果分析
执行上述代码,你会看到类似以下的输出:
css
======= Checkpoint示例4: 自定义序列化 =======
初始Person对象: Person(name='张三', age=30, hobbies=['编程', '跑步'])
第一次调用后: Person(name='张三', age=31, hobbies=['编程', '跑步', '阅读'])
第二次调用后: Person(name='张三', age=32, hobbies=['编程', '跑步', '阅读'])
第三次调用后: Person(name='张三', age=33, hobbies=['编程', '跑步', '阅读'])
======= 验证状态保存 =======
保存的状态数据类型: dict
保存的状态数据: {'_type': 'Person', 'name': '张三', 'age': 33, 'hobbies': ['编程', '跑步', '阅读']}
从保存状态恢复的对象: Person(name='张三', age=33, hobbies=['编程', '跑步', '阅读'])
我们可以看到:
Person
对象的状态在多次调用之间被正确地保存和恢复- 每次调用后,对象的属性(年龄和爱好)都按照预期更新
- 保存的状态数据是字典类型,而不是直接的
Person
对象 - 我们可以从保存的状态数据中成功恢复
Person
对象
实际应用场景
自定义序列化在很多实际应用场景中都非常有用:
- 业务对象持久化:保存和恢复包含业务逻辑的复杂对象
- 领域模型集成:将LangGraph与现有的领域模型集成
- 复杂状态管理:管理包含多个相关对象的复杂状态
- 外部系统集成:在不同系统之间传递结构化数据
- 数据版本控制:实现对象的版本控制和演化
总结
在本文中,我们学习了一种简单可靠的方法来实现LangGraph中的自定义序列化:
- 让对象自己处理序列化 :为自定义类实现
to_dict()
和from_dict()
方法 - 在状态中存储序列化数据:使用字典等可序列化数据结构作为状态
- 在节点函数中处理转换:进入节点时反序列化,离开节点时序列化
- 使用标准Checkpoint Saver:避免修改LangGraph内部类
这种方法遵循了"MAKE IT WORK FIRST"的原则,先实现核心功能,再考虑优化和扩展。它简单、可靠,适用于大多数自定义数据类型的场景。
记住,在实际应用中,保持代码简单直接比过度设计更重要。先让它工作,然后再根据实际需求进行优化和改进。