三亩地 三亩地SAN MU DI · CODE DIARY
ARTICLE DETAIL

日记详情

真实记录编程学习的某一天,欢迎挑你感兴趣的翻一翻。

DeepAgents框架架构05:状态治理与结果回写

DeepAgents框架架构05:状态治理与结果回写

状态治理与结果回写:ToolMessage/Command/PrivateState的三层分离

给模型看的 ≠ 写进状态的 ≠ 暴露给调用方的。三件事必须走三条通道,混淆任何一个都会导致不可排查的状态污染。


一、状态是Agent系统的血液

Agent系统本质上是一个状态机。每一轮循环都在更新状态:用户输入写进消息历史、工具结果写进消息历史、模型输出写进消息历史。状态流经模型的上下文窗口,驱动下一步推理。

但这里有一个容易被忽视的致命问题:并不是所有状态都应该被模型看到。

举个典型场景:

  • 子Agent内部跑了30轮推理,生成了5次工具调用,中间出过一次错误后来修复
  • 这些中间过程对子Agent自身是必要的——它在逐步解决问题
  • 但对主Agent来说,这30轮消息是纯粹的噪音——它只需要知道"任务完成了,结果是X"

如果把子Agent的全部中间消息写进主Agent的状态,结果就是:上下文窗口瞬间被撑爆,Token成本暴增,主Agent迷失在子Agent的内部细节中。

DeepAgents通过三层状态视图分离解决了这个问题。


二、三层视图分离:每个受众看到不同的状态

DeepAgents将Agent状态分为三个层次,每层有不同的受众:

治理细节 - Private State

记忆内容 / 技能索引 / ...

系统状态 - State

messages / async_tasks / ...

模型视图 - 对话消息

ToolMessage / AIMessage

模型视图

这是模型通过上下文窗口读到的内容——经过裁剪和过滤的消息流。子Agent的内部消息被_EXCLUDED_STATE_KEYS阻断,只留下最终结果的ToolMessage。模型不需要知道子Agent内部跑了几轮,只需要知道结果。

系统状态

这是Agent运行时的完整状态快照——messagesasync_taskstodos等所有状态字段。框架通过LangGraph的StateGraph管理这些状态,中间件通过Command机制更新它们。

治理细节

这是中间件的私有数据——记忆内容、技能索引、缓存元数据等。通过PrivateStateAttr隔离,模型不可见,调用方也不可见。

三层分离的核心价值:每层只暴露给它真正需要的信息。多暴露一层,就多一层的污染风险。


三、同步结果回写:ToolMessage的干净交付

子Agent跑完,怎么把结果交给主Agent

同步子Agent执行完毕后,结果回传经历了三层过滤:

子Agent内部messages
30轮对话

_EXCLUDED_STATE_KEYS 过滤
阻断对话历史、待办列表、长期记忆

只保留最后一条AI消息的内容

包装为 ToolMessage

Command(update=...) 写回主Agent

主Agent看到干净的 ToolMessage

具体来说,_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,不需要遍历聊天历史。

AsyncSubAgentMiddleware
发起任务

后台开始处理

主Agent主动取消

任务正常完成
结果写入账本

任务异常
错误信息写入账本

主Agent主动取消

自动重试
可配置次数与间隔

重试耗尽

pending

running

cancelled

success

error


五、PatchToolCallsMiddleware:状态合法性的自动修复

悬空工具调用问题

这是Agent系统中一个常见但容易被忽视的Bug:

用户工具执行器模型用户工具执行器模型工具B尚未执行消息序列不合法[...assistant(tool_calls=[A,B]), tool_result(A), user("换方案")]生成工具调用 [call_A, call_B]工具A执行完毕,返回结果插入新消息: "别搞了,换一个方案"

工具B的调用悬空了——模型说要调但没结果。下次调模型时,API会因为消息序列不合法而报错。

怎么修补

PatchToolCallsMiddlewarebefore_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=...)
一次原子操作

update.messages
ToolMessage

update.async_tasks
任务状态字典

PrivateStateAttr
治理元数据

模型视图
模型可见

系统状态
框架可见

治理细节
中间件可见
模型不可见

  • 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的状态始终干净、可追踪、可恢复。对一个生产系统来说,状态干净比模型聪明更重要。

← 返回列表