☰
Agent 状态存储三模式:ThreadState 字段、middleware.state_schema 与实例属性
2026/10/15 4:09:35 网站建设 项目流程

Agent 状态存储三模式:ThreadState 字段、middleware.state_schema 与实例属性

摘要:在create_agent+ 中间件架构里,能放数据的地方其实有三类,但它们的归属、生命周期和持久化行为完全不同:顶层集中声明的ThreadState字段、中间件类属性state_schema声明的字段、以及中间件实例自身的self.xxx。选错位置会直接招来两种典型事故——并发下跨会话串数据,或者状态压根没进 checkpoint 导致重启即丢。本文给出三者的对照表、各自的写法与边界,并补上 reducer 合并规则与选型判据。

一、概述:先问一句"这份数据属于谁"

在create_agent+ 中间件架构中,共有三类存放数据的位置,三者归属、生命周期、持久化特性完全不同:

  1. 中心化ThreadState字段
  2. 去中心化middleware.state_schema声明字段
  3. 中间件实例自身成员属性(装配注入,进程级配置)
核心区分要点:判断数据是属于单个对话会话(thread),还是整个服务进程全局固定配置。线程级动态业务数据必须放入 AgentState;全局静态配置才使用中间件实例属性。

二、三种存储模式对比

存储位置声明方式数据归属生命周期Checkpoint 持久化多 thread 隔离典型用途
中心化 AgentState 字段顶层手写ThreadState(AgentState)类,集中定义所有字段单个对话 thread 的状态实例thread 会话生命周期✅ 保存✅ 隔离plan、todos、goal 等业务状态
去中心化state_schema字段中间件类属性state_schema,装配阶段框架自动归集拼装进全局 AgentState单个对话 thread 的状态实例thread 会话生命周期✅ 保存✅ 隔离plan、todos;谁使用谁声明,可插拔,无隐身字段
中间件实例属性self.xxx中间件__init__构造函数,装配实例化时注入配置中间件实例本身,不属于 AgentState服务进程生命周期,服务重启销毁❌ 不持久化❌ 全部 thread 共享,并发易串数据进程级静态配置:全局 prompt、超时、模型地址、全局开关

一句话记法:前两者是"图纸 + 每会话一份的活数据",第三者只是"进程里的一块常量内存"。


三、模式详细说明

3.1 模式一:中心化 ThreadState(传统方案)

所有状态字段统一在顶层ThreadState一次性声明,直接传给create_agent(state_schema=ThreadState)。

class ThreadState(AgentState): goal: str plan: str todos: list[str] agent = create_agent(model=model, middleware=[...], state_schema=ThreadState)
  • 优点:结构直观,一眼看到全部状态。
  • 缺点:新增/删除中间件时,需要手动维护ThreadState;删除中间件容易残留隐身字段(字段还在 schema 里,但已经没有任何中间件读写它)。
  • 适用:小型 Agent,状态字段少。

3.2 模式二:去中心化 middleware.state_schema(推荐方案)

每个中间件在类属性state_schema中声明自己依赖的状态字段;Agent 装配阶段自动收集全部中间件的 schema,动态合并生成全局 AgentState。

class PlanMiddleware(AgentMiddleware): state_schema = PlanState # 类属性,声明本中间件要用到的字段 def wrap_model_call(self, request, handler): return handler(request)
装配公式:全局最终Schema = 框架基础AgentState ⊕ middleware1.state_schema ⊕ middleware2.state_schema ...

特点:

  1. 字段与使用它的中间件内聚在一起;移除中间件,装配时不再纳入该字段,无隐身字段。
  2. 支持自定义 reducer:控制多源更新合并策略。
  • plan:单文本,默认覆盖式 reducer,新规划直接替换旧规划。
  • todos:任务列表,自定义追加 reducer,新旧任务列表增量合并,避免丢失任务。
  1. 所有字段归属于 thread 状态,存入 checkpoint,不同对话数据隔离。
⚠️约束:state_schema必须是类属性,不能写在__init__的实例属性里——装配阶段实例还没创建,框架无法识别。

3.3 写状态时要分钩子:dict / Command / ExtendedModelResponse

框架对不同钩子规定了不同的返回契约,写状态时必须配套使用:

钩子类型钩子写状态的返回形式
节点式before_agent/before_model/after_model/after_agent直接返回dict[str, Any]({"key": value})
包裹式(模型)wrap_model_call返回ExtendedModelResponse(model_response=..., command=Command(update={...}))
包裹式(工具)wrap_tool_call直接返回Command
@wrap_model_call(state_schema=UsageTrackingState) def track_usage(request: ModelRequest, handler) -> ExtendedModelResponse: response = handler(request) return ExtendedModelResponse( model_response=response, command=Command(update={"last_model_call_tokens": 150}), )
多个中间件同时更新同一个 key 时:带 reducer 的字段按 reducer 规则合并;没有 reducer 的字段"最后写入者获胜",且由于外层中间件在handler()返回后才写入,通常是外层覆盖内层。

3.4 模式三:中间件实例属性 self.xxx(装配注入,进程级配置)

实例化中间件时,在构造函数__init__传入静态配置,保存到self.xxx。

class StaticConfigMiddleware(AgentMiddleware): def __init__(self, global_prompt: str, timeout: int): # 装配阶段注入,进程级配置,不属于 AgentState self.global_prompt = global_prompt self.timeout = timeout def wrap_model_call(self, request, handler): # 所有对话 thread 共用这一份配置 return handler(request)

特点:

  1. 装配阶段实例化中间件时一次性注入,运行期一般只读、不修改。
  2. 不属于 AgentState,不会写入 checkpoint,服务重启后配置不会自动恢复(靠代码重新注入)。
  3. 同一个中间件实例被所有对话线程共用;绝对不能存放 plan / todos 这类线程独有的业务数据,并发会跨会话串数据。
  • ✅适用场景:全局固定 system prompt、调用超时、模型后端地址、全局日志标签、全局功能开关。

📌官方还有一个更"正统"的位置:context_schema。

AgentMiddleware除了state_schema(持久化、跨调用保留)之外,还支持context_schema——官方定义是「只读、不在多次调用之间持久化」的运行时上下文,通常配合runtime.context使用。也就是说,配置类数据其实有两档:

位置粒度是否持久化适用
context_schema+runtime.context单次 invoke 的运行时上下文❌调用时才确定、只读的上下文(如当前用户身份、trace_id)
中间件实例self.xxx进程级常量❌服务启动时就固定、全进程共享的配置(模型地址、超时)

两者都不进 checkpoint,区别只在生命周期粒度和是否由框架注入读取。


四、Reducer 合并规则补充(仅针对 AgentState 内字段)

Reducer 用于多处同时更新同一个 state 字段时,定义新旧值的合并逻辑,装配阶段绑定到字段。

  1. 覆盖式(plan默认):新值直接替换旧值,适合"整体方案"这类以最新为准的字段。
  2. 追加式(todos自定义merge_todos):新旧列表拼接,适合持续新增任务的任务清单。
中间件实例属性self.xxx不存在 reducer,它不属于状态合并体系。

五、关键边界与易错点

#易错点正确认知
1把 Schema 当成运行时数据Schema 是装配期静态图纸,定义字段、类型、reducer;State 实例才是运行时真实业务数据,每个 thread 独立一份
2直接改request.state写状态不能改这个视图对象;应通过钩子返回{key: value}或Command(update={})写入
3把state_schema写进__init__去中心化模式下state_schema必须是类属性,放到__init__里框架识别不到
4把 plan / todos 塞进self.xxx业务动态数据严禁放实例属性:并发串数据 + 重启丢失,双重事故
5运行期修改实例属性当配置中心实例属性只适合只读静态全局配置,运行期尽量不要改动

六、选型决策建议

  1. 数据是单会话独有的动态业务数据,需要持久化保存:

优先选择去中心化middleware.state_schema声明(高内聚、无隐身字段);简单小型 Agent 可选用中心化ThreadState。

  1. 数据是全服务共享、装配后基本不变的静态配置:

使用中间件实例self.xxx,装配时注入。

判断树:

这份数据会随对话变化吗? ├─ 会 → 需要跨轮次恢复吗? │ ├─ 需要 → middleware.state_schema(或小型 Agent 用 ThreadState) │ └─ 不需要 → 仍是 state_schema,只是不依赖 checkpoint └─ 不会 → 所有会话共用同一份吗? ├─ 是 → 中间件实例 self.xxx(装配注入) └─ 否 → 重新审视设计,多半是伪需求

七、配套测试

分界碑拓扑测试test_graph_topology_boundary,验证去中心化 schema 拼装逻辑:

  1. 装配阶段所有 middleware 的state_schema正确归集合并;
  2. reducer 合并逻辑符合预期;
  3. 声明字段成功纳入 checkpoint 快照;
  4. 中间件钩子读写状态的边界符合契约。

八、一句话总结

线程级活数据进 state_schema(进 checkpoint、按 thread 隔离),进程级死配置进 self.xxx(不持久化、全局共享)。中间那条线一旦踩错,轻则状态丢失,重则并发串数据——而且这两种故障都很难在单线程调试时复现。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询