03 状态管理:生产级Agent的分水岭(Part 2)#
状态管理定义了 Agent 执行过程中的可变数据如何表示、持久化、版本化和恢复。协议视角下,状态管理还要决定哪些状态可以被外部看见:Thread history、Task status、Artifact、State Snapshot、Trace metadata,分别暴露给不同类型的客户端。
通用概念#
子概念:
状态表示 (State Schema):数据的形状——类型化的结构(TypedDict)、消息列表、JSON blob
状态持久化 (Persistence):数据存到哪——内存、数据库、服务端托管
状态版本化 (Versioning):能否查看/回滚历史——快照链、消息追加、无版本
状态作用域 (Scope):数据对谁可见——全局、Agent 级、Channel 级
增量更新 (Update Mechanism):如何修改状态——Reducer 函数、直接覆盖、追加消息
持久化光谱#
各框架在状态持久化上的立场差异巨大,形成了一条光谱:
最左端(无持久化):内存状态,进程退出即丢失。代表:Claude SDK、Agents SDK、AutoGen默认
中间层(自建持久化):开发者自行实现Checkpointer,可选择PG/Redis/SQLite。代表:LangGraph
最右端(服务端托管):状态完全由平台管理,开发者看不到内部结构。代表:OpenAI Assistants
状态持久化从"进程内临时状态"到"服务端托管状态"形成一条光谱,生产 Agent 必须明确自己站在哪一段。
跨框架映射#
概念 |
LangGraph |
OpenAI Assistants |
Agents SDK |
AutoGen |
Claude SDK |
|---|---|---|---|---|---|
状态表示 |
TypedDict + Channel级Reducer |
Thread(消息列表+元数据) |
RunContext(Python对象) |
ChatCompletionContext + 共享状态 |
对话历史(隐式) |
持久化 |
Checkpointer(PG/Redis/SQLite) |
服务端托管(不透明) |
无(手动 save_state/load_state) |
无内置 |
无内置 |
版本控制 |
Checkpoint链(parent_id,类Git) |
Thread 消息历史(追加制) |
无 |
无 |
无 |
状态作用域 |
Channel 级(每个字段独立Reducer) |
Thread 级 |
Agent 级 |
Agent/Team 级 |
Session 级 |
增量更新 |
Annotated[list, add_messages] 等Reducer |
追加消息 |
直接修改 |
追加消息 |
直接修改 |
状态五层分层:不要把所有东西都叫Memory#
Agent 领域最容易混淆的词是 Memory。更清晰的做法是把状态拆成五层:
层级 |
典型名称 |
内容 |
主要问题 |
|---|---|---|---|
Conversation |
Messages / Thread |
用户、模型、工具消息 |
上下文窗口、裁剪、摘要 |
Run State |
State / Context |
当前执行的结构化变量 |
类型、Reducer、并发更新 |
Checkpoint |
Snapshot / Savepoint |
某一步之后的完整可恢复状态 |
存储、版本、回滚 |
Artifact |
File / Report / Code diff |
Agent 产出的外部结果 |
生命周期、权限、可追溯 |
Semantic Memory |
Long-term Memory |
跨会话沉淀的用户偏好或知识 |
检索、污染、遗忘 |
很多框架说自己支持 Memory,实际只支持其中一层。生产设计必须先问清楚:要保存的是对话、运行状态、可恢复快照、文件产物,还是长期记忆?
Session/Thread/Run/Step/Checkpoint/Artifact边界关系#
Session/Thread:长期上下文边界,回答"这是谁的哪段任务"
Run:一次执行边界,回答"这次具体跑了什么"
Step:最小可观测执行单元,回答"哪一步调用了模型或工具"
Checkpoint:恢复边界,回答"失败后从哪里继续"
Artifact:产物边界,回答"结果在哪里、由哪次执行产生"
OpenAI Assistants 把 Thread 和 Run 显式暴露出来;LangGraph 把 Thread 作为 configurable.thread_id,把 Run 隐含在一次 invoke/stream 中;Agents SDK 更强调 Runner 和 Session。名字不同,但边界是相同的。
如果所有 Runtime 都能基于统一 Agent Protocol 表达 Thread -> Run -> Step -> Artifact 这条链路,那么控制台、前端、评测系统、审计系统就不需要理解每个框架的内部状态结构。
并发Run五种策略对比#
并发会话处理的关键,是不要把 Thread / Session 和 Run 混成一个概念。Thread / Session 是长期上下文边界,Run 是一次执行边界;同一个用户可以有多个 Thread,同一个 Thread 也可能在短时间内收到多个 Run 请求。
生产 Runtime 必须明确一个问题:同一个 Thread 上是否允许多个 Run 同时执行?
策略 |
行为 |
优势 |
代价 |
典型场景 |
|---|---|---|---|---|
串行队列 |
同一 Thread 的 Run 按顺序排队执行 |
语义最稳定,消息顺序清晰 |
延迟增加,长任务会阻塞后续输入 |
多轮对话、客服、需要强上下文连续性的任务 |
拒绝新 Run |
Thread 已有运行中 Run 时直接返回 conflict/busy |
实现简单,避免状态冲突 |
用户体验生硬,需要前端解释和重试 |
后台任务、审批流、一次只允许一个执行的场景 |
取消并覆盖 |
新 Run 到来时取消旧 Run,用最新输入重新执行 |
交互体验直接,适合"以最后一次为准" |
旧 Run 的部分进度和副作用需要可追溯或可回滚 |
搜索、草稿生成、用户频繁改需求的交互 |
分叉新 Run |
从同一个 Checkpoint 分叉出多个 Run 并行执行 |
适合 A/B 测试、方案比较、探索式任务 |
需要清晰标记分支、Artifact 归属和最终采纳关系 |
Prompt对比、策略实验、研究任务 |
乐观并发 |
Run 开始时记录 state version,提交时检查是否冲突 |
并发度高,适合低冲突写入 |
冲突检测和合并逻辑复杂 |
多Agent并行写不同state channel |
这些策略没有绝对优劣,关键是把语义放进协议和 Runtime 状态机里。客户端需要知道新 Run 是被排队、拒绝、取消旧任务、创建分支,还是等待冲突解决;观测系统也要能把每个事件、Artifact 和错误归属到具体 Run。
五类并发冲突#
并发写状态时,Runtime 至少要处理五类冲突:
消息顺序冲突:两个 Run 同时向同一个 Thread 追加消息,最终历史如何排序
状态版本冲突:两个 Run 基于同一份 State Snapshot 修改同一个字段,谁覆盖谁
Artifact 归属冲突:多个 Run 生成同名文件或报告,哪个是正式产物
Workspace 副作用冲突:多个 Run 同时改同一份代码、浏览器页面或外部系统资源
事件流归属冲突:前端同时订阅多个 Run 时,如何用 run_id、step_id、event_id 恢复和去重
Thread 不应该被简单当成一把全局锁。更稳的设计是:Thread 承载上下文,Run 承载执行,Checkpoint 承载版本,Event 承载进展,Artifact 承载产物;并发控制策略则明确写进 Run 创建语义和状态迁移规则。
状态迁移与Schema演进#
持久化一旦进入生产,就会遇到 Schema 演进问题:今天保存的 Checkpoint,三个月后代码升级还能不能恢复?
生产 Runtime 需要考虑:
状态版本号:每个快照记录 schema version
迁移函数:加载旧快照时转换到新结构
兼容窗口:保留多久的旧状态可恢复
失败策略:迁移失败时是终止、降级,还是创建新 Run
这也是服务端托管状态和自建 Checkpoint 的核心差异:托管方案隐藏迁移复杂度,但也隐藏了控制权;自建方案控制力强,但必须承担 schema 演进成本。
LangGraph Checkpoint模型 vs OpenAI Thread模型#
LangGraph Checkpoint模型(最完整方案)#
每个节点执行后自动快照(不需要手动调用 save)
快照具备链式结构,支持"时间旅行"(任意节点回滚、重放)
Content-addressed blob 存储,类似 git 的存储方式,大状态只存一次
允许运行时修改 Agent 的上下文信息——意味着可以运行时让 Agent 自进化
代价:学习曲线陡峭,Reducer 函数的语义需要理解,Checkpoint 存储占空间。
OpenAI Thread模型(另一极端)#
你不需要(也无法)管理状态——服务端全包
状态只能通过追加消息来修改,不能直接改内部状态
没有回滚——你只能创建新 Thread
代价:零控制权。调试困难,无法做"如果当时走了另一条路"的分析。
AutoGen/Claude SDK/Agents SDK#
基本没有内置持久化,把这个问题留给开发者。对于短生命周期的 Agent 这没问题,但一旦需要跨请求保持状态(如人机协作工作流、事后评测、版本管理等),就必须自己搭建。
本章结论#
状态管理回答"Run 执行到一半时,哪些东西必须被保存,以及多个 Run 同时发生时如何保持一致"。它承接上一章的 Runtime Loop,也直接支撑后面的中断恢复、错误回滚、并发会话和执行回放。
状态持久化是区分"玩具"和"生产"的分水岭。 没有持久化的 Agent 无法在进程崩溃后恢复,无法支持真正的 Human-in-the-Loop,无法调试"为什么 Agent 走了这条路",也无法从同一个状态分叉执行不同策略。并发 Run 进一步要求 Runtime 明确队列、拒绝、取消覆盖、分叉和乐观并发等策略。
真正难的不是保存,而是恢复。恢复要求状态 schema、工具副作用、外部资源、权限上下文都能重新对齐;只把 messages 存进数据库,并不等于具备生产级状态管理。