LangGraph从新手到老师傅 - 11 - 自定义序列化处理复杂数据

前言

在构建复杂的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}

这个节点函数展示了核心的序列化处理模式:

  1. 进入节点时 :从person_data字典反序列化为Person对象
  2. 处理逻辑 :对Person对象进行业务操作(更新年龄和爱好)
  3. 离开节点时 :将更新后的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"])

在调用图时,我们:

  1. 先将初始的Person对象序列化为字典
  2. 将序列化后的字典传入图的invoke方法
  3. 从结果中恢复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=['编程', '跑步', '阅读'])

我们可以看到:

  1. Person对象的状态在多次调用之间被正确地保存和恢复
  2. 每次调用后,对象的属性(年龄和爱好)都按照预期更新
  3. 保存的状态数据是字典类型,而不是直接的Person对象
  4. 我们可以从保存的状态数据中成功恢复Person对象

实际应用场景

自定义序列化在很多实际应用场景中都非常有用:

  1. 业务对象持久化:保存和恢复包含业务逻辑的复杂对象
  2. 领域模型集成:将LangGraph与现有的领域模型集成
  3. 复杂状态管理:管理包含多个相关对象的复杂状态
  4. 外部系统集成:在不同系统之间传递结构化数据
  5. 数据版本控制:实现对象的版本控制和演化

总结

在本文中,我们学习了一种简单可靠的方法来实现LangGraph中的自定义序列化:

  1. 让对象自己处理序列化 :为自定义类实现to_dict()from_dict()方法
  2. 在状态中存储序列化数据:使用字典等可序列化数据结构作为状态
  3. 在节点函数中处理转换:进入节点时反序列化,离开节点时序列化
  4. 使用标准Checkpoint Saver:避免修改LangGraph内部类

这种方法遵循了"MAKE IT WORK FIRST"的原则,先实现核心功能,再考虑优化和扩展。它简单、可靠,适用于大多数自定义数据类型的场景。

记住,在实际应用中,保持代码简单直接比过度设计更重要。先让它工作,然后再根据实际需求进行优化和改进。

相关推荐
XZSSWJS7 小时前
机器学习基础-day03-机器学习中的线性回归
人工智能·机器学习·线性回归
茫然无助7 小时前
机器学习:后篇
人工智能·机器学习
XZSSWJS7 小时前
机器学习基础-day04-数学方法实现线性回归
人工智能·机器学习·线性回归
大千AI助手7 小时前
梯度爆炸问题:深度学习中的「链式核弹」与拆弹指南
人工智能·深度学习·梯度·反向传播·梯度爆炸·指数效应·链式法则
THMAIL7 小时前
机器学习从入门到精通 - 卷积神经网络(CNN)实战:图像识别模型搭建指南
linux·人工智能·python·算法·机器学习·cnn·逻辑回归
Kingsdesigner7 小时前
PS大神级AI建模技巧!效率翻倍工作流,悄悄收藏!
人工智能·ui·adobe·aigc·ux·设计师·photoshop
AIGC小火龙果7 小时前
AI代码管家:告别烂代码的自动化魔法
人工智能·经验分享·搜索引擎·自动化·aigc·ai编程
东风西巷7 小时前
Topaz Video AI:AI驱动的视频增强与修复工具
人工智能·音视频