dataclass / pydantic BaseModel / TypedDict 跨进程传递;LangGraph + Redis Checkpointer 基础概念
dataclass / pydantic BaseModel / TypedDict 跨进程传递
背景
langgragh项目中中有个字段使用 @dataclass 定义,嵌套在 State(TypedDict)里。
该 State 会通过 AsyncRedisSaver 序列化并跨进程(API ↔ arq worker)传递。
问题:这里用 dataclass 是否合适?
三者本质区别
dataclass |
pydantic BaseModel |
TypedDict |
|
|---|---|---|---|
| 运行时是什么 | 真正的类,可实例化 | 真正的类,可实例化 | 纯类型标注,运行时就是普通 dict |
| 是否校验数据 | ❌ 不会,类型不对也能构造成功 | ✅ 会,类型不对直接 ValidationError |
❌ 不会,只给类型检查器看 |
| 转 dict/JSON | 无内置方法,需 dataclasses.asdict() |
内置 .model_dump() / .model_dump_json() |
本来就是 dict |
| 从 dict 反序列化并校验 | 无内置校验能力,A(**data) 不检查类型 |
内置 B.model_validate(data),类型错会报错 |
无实际转换,cast() 只是骗过类型检查器 |
一句话类比:dataclass 是"造个盒子装数据",pydantic 是"造个带门禁的盒子,进出都要查身份证"。
为什么这对本项目是实际问题
嵌套内字段要经历:构造 → 存入 State → 序列化进 Redis → 另一进程读出 → 反序列化还原。
- dataclass 没有严格校验的反序列化机制,LangGraph 底层序列化器(
JsonPlusSerializer)多依赖pickle 或"记录类路径+字段字典"方式兜底还原,能跑通是因为序列化器实现细节凑巧对了,不是设计上保证的行为。 - 换成 pydantic model 后,任何反序列化路径都会强制走
model_validate,类型错误(比如top_k变成字符串)会在第一时间报错,而不是带着错误值一路往下传。
结论:嵌套内字段属性 建议从 @dataclass 改成 pydantic BaseModel
附:AdaptiveRagState(顶层 State)要不要也改成 pydantic?
不建议,为langgragh自身原因:
AdaptiveRagState是 LangGraph 引擎自己维护、合并的图执行状态,设计上就是按TypedDict/dict 语义走的(Annotated[..., reducer]这类写法依赖这个机制)。- 若把顶层 State 换成 pydantic BaseModel,LangGraph 每一步(每个 superstep)都会把整个 state 重新构造+校验一遍——不管这一步具体改了哪个字段。
messages这种只增不减的字段会让这个开销随对话变长而放大(近似 O(n²))。 - 嵌套内字段属性不受此影响:它是 TypedDict 里的一个普通字段值,只有代码里显式
return {"kb_config": KbConfig(...)}时才会重新构造+校验一次,次数由自己代码控制。
最终结论:
AdaptiveRagState:保持TypedDict。KbConfig:改为 pydanticBaseModel。
LangGraph + Redis Checkpointer 基础概念
图(Graph) vs 状态(State)
- 图对象:节点、边、执行逻辑,定义在代码里,只存在于进程内存中,
不会被序列化、不会进 Redis。每个进程启动时各自在内存里重新构建一份。 - 状态(State):图执行过程中产生的数据(消息、变量值等)。
每执行完一步,这份数据会被 checkpointer 序列化后写入 Redis。
类比:图 = 造好的机器(说明书写死,各进程各造一台);
状态 = 机器跑到第几步、当前变量值是多少的进度记录。
Checkpointer 的作用
AsyncRedisSaver(或其他后端如 Postgres/SQLite)是 LangGraph 提供的持久化组件:
- 负责把每一步的 State 序列化成字节,写入存储后端
- 负责把存储的字节反序列化,还原成 State 对象供后续读取/续跑
图对象本身不需要经过这个组件,只有"跑到哪一步、状态数据是什么"需要。
为什么多进程场景必须靠存储层同步
同一份图结构可能在多个进程里各自加载(比如 Web 服务进程 + 后台任务进程)。
进程之间没有共享内存,唯一能同步"某个会话进度"的方式就是通过共享的存储后端:
- 进程 A 执行完一步 → 序列化 → 写入 Redis
- 进程 B 需要这个会话的状态 → 从 Redis 读出 → 反序列化 → 还原
序列化时:dataclass vs pydantic 的差异
两者都能被序列化/反序列化,能不能存取不是区别点。真正的区别在"还原后数据是否被校验":
| 反序列化时的行为 | |
|---|---|
dataclass |
字段名对得上就能构造成功,不检查类型,脏数据会被无声传递下去 |
pydantic BaseModel |
走类型校验,字段类型不对会直接抛错,问题出现的位置和根因一致 |
一句话:序列化机制两者都支持,pydantic 的优势是在"数据跨进程/跨版本流转",这个环节多一层类型把关,属于稳健性增强,不代表另一种写法"跑不通"。
浙公网安备 33010602011771号