状态治理与结果回写:ToolMessage/Command/PrivateState的三层分离
给模型看的 ≠ 写进状态的 ≠ 暴露给调用方的。三件事必须走三条通道,混淆任何一个都会导致不可排查的状态污染。
一、状态是Agent系统的血液
Agent系统本质上是一个状态机。每一轮循环都在更新状态:用户输入写进消息历史、工具结果写进消息历史、模型输出写进消息历史。状态流经模型的上下文窗口,驱动下一步推理。
但这里有一个容易被忽视的致命问题:并不是所有状态都应该被模型看到。
举个典型场景:
- 子Agent内部跑了30轮推理,生成了5次工具调用,中间出过一次错误后来修复
- 这些中间过程对子Agent自身是必要的——它在逐步解决问题
- 但对主Agent来说,这30轮消息是纯粹的噪音——它只需要知道"任务完成了,结果是X"
如果把子Agent的全部中间消息写进主Agent的状态,结果就是:上下文窗口瞬间被撑爆,Token成本暴增,主Agent迷失在子Agent的内部细节中。
DeepAgents通过三层状态视图分离解决了这个问题。
二、三层视图分离:每个受众看到不同的状态
DeepAgents将Agent状态分为三个层次,每层有不同的受众:
模型视图
这是模型通过上下文窗口读到的内容——经过裁剪和过滤的消息流。子Agent的内部消息被_EXCLUDED_STATE_KEYS阻断,只留下最终结果的ToolMessage。模型不需要知道子Agent内部跑了几轮,只需要知道结果。
系统状态
这是Agent运行时的完整状态快照——messages、async_tasks、todos等所有状态字段。框架通过LangGraph的StateGraph管理这些状态,中间件通过Command机制更新它们。
治理细节
这是中间件的私有数据——记忆内容、技能索引、缓存元数据等。通过PrivateStateAttr隔离,模型不可见,调用方也不可见。
三层分离的核心价值:每层只暴露给它真正需要的信息。多暴露一层,就多一层的污染风险。
三、同步结果回写:ToolMessage的干净交付
子Agent跑完,怎么把结果交给主Agent
同步子Agent执行完毕后,结果回传经历了三层过滤:
具体来说,_EXCLUDED_STATE_KEYS是一个白名单机制——它定义了哪些主Agent状态不能传给子Agent,也定义了哪些子Agent状态不能回传给主Agent。两边隔离,双向过滤。
为什么ToolMessage而不是其他格式
ToolMessage是LangChain/LangGraph中工具调用的标准返回格式。用ToolMessage包装子Agent结果,意味着主Agent像处理普通工具结果一样处理子Agent的输出——不需要特殊通道,不需要特殊解析逻辑。
这体现了统一接口的架构原则:对主Agent来说,调task(子Agent委派)和调read_file(文件读取)是同一种交互模式——工具调用→ToolMessage返回→继续推理。
四、异步结果回写:async_tasks状态账本
异步任务不能靠聊天历史记住
同步任务的结果立刻返回给模型,模型可以"记住"它。但异步任务可能跑几分钟甚至几小时——等它跑完时,模型上下文可能已经更新了很多轮,甚至会话都断了又恢复了。
因此DeepAgents专门设计了async_tasks状态字段:
# AsyncSubAgentStateasync_tasks:dict[str,AsyncTaskStatus]# key = task_id, value = 任务状态这个字段不依赖模型记忆——它是结构化状态,通过LangGraph的StateGraph持久化。即使模型"忘了"有异步任务在跑,框架通过Reducer机制保证任务状态不会丢失。
Reducer合并,不是直接覆盖
async_tasks的更新用的是Reducer模式:
# 不是 state["async_tasks"] = new_tasks (覆盖)# 而是 state["async_tasks"] = {**state["async_tasks"], **new_tasks} (合并)为什么必须用Reducer?因为同一时刻可能有多个异步任务在并发执行。一个任务完成时只更新自己的状态条目,不能覆盖其他任务的状态。Reducer保证多任务并发更新时的数据一致性。
异步任务的生命周期
| 状态 | 含义 | 触发条件 |
|---|---|---|
pending | 已登记,等待执行 | AsyncSubAgentMiddleware发起任务 |
running | 执行中 | 后台开始处理 |
success | 执行成功 | 任务正常完成,结果写入账本 |
error | 执行失败 | 任务异常,错误信息写入账本 |
cancelled | 已取消 | 主Agent主动取消 |
主Agent通过list_async_tasks工具随时查看所有异步任务的状态——不需要记住task_id,不需要遍历聊天历史。
五、PatchToolCallsMiddleware:状态合法性的自动修复
悬空工具调用问题
这是Agent系统中一个常见但容易被忽视的Bug:
工具B的调用悬空了——模型说要调但没结果。下次调模型时,API会因为消息序列不合法而报错。
怎么修补
PatchToolCallsMiddleware在before_agent()阶段检查消息序列,发现悬空工具调用时,自动补充一条ToolMessage(content="Tool call cancelled."),让消息序列恢复合法格式。
关键点:这个修补发生在模型推理之前。模型还没看到用户的新消息,中间件先帮它把"上一轮说要做但没做完的事"收个尾。模型下一轮推理时面对的是一个干净合法的消息序列。
六、Command机制:同时更新消息和其他状态
为什么需要Command
普通的工具返回只能更新messages。但如果一个工具执行后还需要更新其他状态字段——比如子Agent完成后不仅要返回结果,还要更新async_tasks账本——就需要一种能力:在一次操作中同时更新多个状态字段。
DeepAgents用Command(update=...)实现这一点:
# 子Agent完成后的状态更新Command(update={"messages":[ToolMessage(content=result,tool_call_id=call_id)],"async_tasks":{task_id:{"status":"success","result":result}}})一个Command,两种更新,原子操作。
和三层的对应关系
Command.update["messages"]→ 更新模型视图(模型可见)Command.update["async_tasks"]→ 更新系统状态(框架可见)PrivateStateAttr→ 更新治理细节(中间件可见,模型不可见)
三层各走各的通道,互不污染。
七、状态设计的工程准则
这些设计加在一起,形成了DeepAgents状态治理的工程准则:
准则一:区分"模型该看的"和"系统该记的"
模型上下文窗口是昂贵的资源。只放模型推理必需的信息——当前任务、最近几轮对话、工具调用结果。系统元数据(任务状态、配置、缓存)走独立状态字段。
准则二:状态更新走Reducer,不走直接赋值
多Agent多工具并发执行时,直接赋值必然导致覆盖丢失。Reducer模式保证并发安全。
准则三:状态异常主动修复,不等报错
悬空工具调用、消息序列不合法等问题,通过PatchToolCalls在每一轮开始前主动修复。修复在前,推理在后——不让脏数据进入模型上下文。
准则四:治理状态私有化
记忆内容、技能元数据、中间件内部状态走PrivateStateAttr。这些信息是给中间件用的,模型不应该看到,调用方也不应该看到。暴露治理状态就是暴露实现细节。
八、小结
状态是Agent系统最容易被忽视的核心资产。代码写错了可以改,状态脏了就很难清——因为脏状态会持续影响后续所有推理。
DeepAgents的状态治理遵循一个简单原则:
给模型看的、写进状态的、暴露给调用方的 —— 三件事走三条通道。
ToolMessage→ 模型看到干净的对话流Command(update=...)→ 框架管理完整的状态数据PrivateStateAttr→ 中间件持有治理元数据async_tasks+ Reducer → 并发安全的异步任务追踪PatchToolCallsMiddleware→ 主动修复状态异常,不让脏数据进模型
这五层保障加起来,让Agent的状态始终干净、可追踪、可恢复。对一个生产系统来说,状态干净比模型聪明更重要。