前面的文章介绍了 Deep Agents 的架构,并实现了一个简单的 Research Agent。
Research Agent 的执行过程很明确:搜索资料、读取网页、整理结果。
而实际任务中,有些往往要较为复杂。
例如,让 Agent 检查某个版本是否具备上线条件。它需要查看代码变更、检查测试结果、调查异常,最后给出发布建议。如果发现新的问题,还需要调整后续工作。
Deep Agents 通过 Planning 机制管理这类任务,使 Agent 能够制定计划、记录进度,并根据执行结果调整安排。
本文重点介绍 TodoListMiddleware、write_todos 及其运行原理。
一、Planning 的基本原理
普通 Agent 通过循环完成任务:
LLM 推理 → 工具调用 → 获取结果 → 继续推理
模型根据已有消息和工具结果,决定下一步行动。
复杂任务可能涉及几十次工具调用。执行过程中,模型既要处理当前问题,又要记住哪些工作已经完成。
这些信息通常散布在消息历史中。随着任务推进,准确掌握进度会变得越来越困难。
Planning 为此引入了任务列表。
以版本发布检查为例:
[进行中] 查看版本变更
[待执行] 检查自动化测试
[待执行] 评估发布风险
[待执行] 形成上线建议
任务列表保存在 Agent 的 State 中,供模型在执行过程中读取和更新。
State 是 LangGraph 保存运行状态的数据结构。例如,messages 保存消息历史,todos 保存任务计划。
Todo 的数据结构
Deep Agents 使用的 Todo 可以表示为:
from typing import TypedDict, Literal
class Todo(TypedDict):
content: str
status: Literal[
"pending",
"in_progress",
"completed",
]
例如:
todo: Todo = {
"content": "检查自动化测试",
"status": "in_progress",
}
status 支持三种状态:
| 状态 | 含义 |
|---|---|
pending | 待执行 |
in_progress | 进行中 |
completed | 已完成 |
Todo 只包含任务内容和状态,没有专门的优先级、截止时间或依赖关系字段。这样的结构已经能够支持基本的任务规划和进度管理。
二、Planning 的实现机制
Deep Agents 使用 TodoListMiddleware 提供 Planning 能力。
Middleware 是中间件,用于向 Agent 添加额外功能,例如任务规划、工具管理和错误处理。
从 Deep Agents 0.7 开始,Planning 改为按需启用,需要显式配置:
from deepagents import create_deep_agent
from langchain.agents.middleware import (
TodoListMiddleware,
)
agent = create_deep_agent(
model="openai:gpt-5.5,
middleware=[
TodoListMiddleware(),
],
)
create_deep_agent() 用于创建 Agent。middleware 接收中间件列表。
启用 TodoListMiddleware 后,Agent 获得以下能力:
- 通过
write_todos创建和更新任务列表。 - 在 State 的
todos字段中保存计划。 - 根据中间件提供的指令管理任务进度。
write_todos 的参数
write_todos 接收一个 todos 参数,类型为:
todos: list[Todo]
list[Todo] 表示由多个 Todo 字典组成的列表。
例如,模型可能生成以下工具参数:
{
"todos": [
{
"content": "查看版本变更",
"status": "in_progress"
},
{
"content": "检查自动化测试",
"status": "pending"
},
{
"content": "评估发布风险",
"status": "pending"
}
]
}
工具输入通过 Pydantic 模型校验,确保数据结构符合要求。
write_todos 每次接收完整的任务列表,并用新列表更新 State 中的 todos。
例如,第一项任务完成后,模型需要保留其他任务,同时将对应状态改为 completed。
由于每次更新都会替换整份列表,并行调用可能导致冲突。因此,中间件会检查并阻止同一轮模型响应中的多个 write_todos 调用。
自定义 Planning 指令
TodoListMiddleware 支持两个配置参数:
| 参数 | 作用 |
|---|---|
system_prompt | 指导模型何时以及如何维护计划 |
tool_description | 自定义 write_todos 的工具描述 |
例如:
planning = TodoListMiddleware(
system_prompt=(
"处理多阶段任务时,使用write_todos"
"制定计划并记录进度。"
"发现新问题时及时调整计划。"
),
)
自定义 system_prompt 会替换中间件默认的 Planning 指令。一般情况下,保留默认配置即可。
三、一个完整的 Planning 示例
下面实现一个版本发布检查 Agent。
我们要实现的需求如下:
检查本次软件版本是否具备上线条件,并给出发布建议。
Agent 需要查询代码变更和测试结果。如果发现失败项,还要调查具体原因。
为了理解 Planning,示例中的工具均使用模拟数据。
安装依赖:
pip install -U deepagents langchain-openai
配置 OPENAI_API_KEY 环境变量后,运行以下代码:
from deepagents import create_deep_agent
from langchain.agents.middleware import (
TodoListMiddleware,
)
def get_release_changes() -> str:
"""查询本次版本的代码变更。"""
return (
"本次修改涉及登录状态续期、"
"订单折扣计算和个人中心页面样式。"
)
def get_test_report() -> str:
"""查询自动化测试结果。"""
return (
"共120项测试,118项通过,2项失败。"
"失败项:login_refresh、"
"discount_calculation。"
)
def get_failure_details(test_name: str) -> str:
"""根据测试名称查询失败详情。"""
failures = {
"login_refresh":
"Token过期后未能正常续期。",
"discount_calculation":
"部分订单折扣金额计算错误。",
}
return failures.get(
test_name,
"没有找到对应测试",
)
agent = create_deep_agent(
model="openai:gpt-5.5",
tools=[
get_release_changes,
get_test_report,
get_failure_details,
],
middleware=[
TodoListMiddleware(),
],
system_prompt=(
"你是一名软件发布检查助手。"
"处理多步骤任务时,使用write_todos"
"制定计划并更新进度。"
"发现测试失败时,调查具体原因。"
"根据工具结果评估发布风险,"
"不得将失败的检查视为通过。"
),
)
request = {
"messages": [
{
"role": "user",
"content": (
"检查本次版本是否具备上线条件,"
"并给出发布建议。"
),
}
]
}
result = agent.invoke(request)
todos = result.get("todos") or []
if todos:
print("任务计划:")
for todo in todos:
print(todo["status"], todo["content"])
else:
print("本次执行没有创建任务计划。")
print("\n检查结论:")
print(result["messages"][-1].content)
这里通过 create_deep_agent() 创建 Agent,并传入四个主要参数:
| 参数 | 作用 |
|---|---|
model | 指定语言模型 |
tools | 注册业务工具 |
middleware | 配置中间件 |
system_prompt | 定义 Agent 的工作要求 |
最后,通过 agent.invoke(request) 执行 Agent,正常结束后返回 State。
其中,result["messages"] 保存消息记录,result.get("todos") 用于读取任务列表。
一次可能的执行过程如下:
创建任务计划
↓
查询版本变更
↓
检查自动化测试
↓
发现两项测试失败
↓
调整任务计划
↓
调查失败原因
↓
评估发布风险
↓
输出检查结论
根据示例中模拟的数据,Agent 有理由建议暂缓发布,因为存在影响发布的问题。
实际执行顺序由模型决定,任务列表也可能有所不同。
四、为什么有时没有 todos
运行上面的代码时,可能发现一个现象:
有些模型会返回完整的 todos,有些模型则没有。即使使用同一个模型,多次执行相同任务,也可能出现不同结果。
这是 Planning 的正常行为。
启用了 TodoListMiddleware,并不意味着模型一定会调用 write_todos。
中间件提供任务规划能力,模型根据任务和工具说明决定是否使用。
例如,模型可能认为当前任务只需几次工具调用,于是直接查询数据并给出结论,没有创建任务列表。
PlanningState 的可选字段
从 Deep Agents 的源码可以看到,Planning 的 State 定义包含:
class PlanningState(AgentState[ResponseT]):
todos: Annotated[
NotRequired[list[Todo]],
OmitFromInput,
]
这里有一个类型值得关注。
NotRequired 表示字段可以不存在。
因此,todos 是一个可选的状态字段。
如果模型始终没有调用 write_todos,State 中就可能不存在该字段。
这也是示例采用以下写法的原因:
todos = result.get("todos") or []
无论字段不存在、值为 None,还是空列表,最终都能得到可遍历的列表。
为什么模型行为不同
模型是否使用 Planning,主要受以下因素影响:
- 任务复杂度:简单任务可能直接执行。
- 模型能力与工具选择策略:不同模型对规划必要性的判断存在差异。
- Prompt 与上下文:指令可以引导工具使用,但不能保证模型一定调用。
同一模型的不同运行结果,也可能因生成过程中的选择而发生变化。
如果强制必须使用 Planning,也可以尝试在
system_prompt中加强约束,要求必须调用write_todos。
检查实际执行情况
我们可以在 invoke() 之后增加:
print("State字段:", list(result.keys()))
for message in result.get("messages", []):
for call in getattr(
message, "tool_calls", []
):
print("工具调用:", call["name"])
通过这些记录,可以判断模型是否调用了 write_todos。
如果没有调用,通常说明模型选择了直接执行。
如果存在调用,但最终没有 todos,则需要进一步检查对应工具是否成功执行,以及运行过程中是否发生中断或异常。
五、观察 Planning 的执行过程
invoke() 返回最终结果。如果希望了解 Agent 怎样创建和修改计划,可以进一步查看工具调用记录。
查看 write_todos 调用
LangChain 使用 AIMessage 保存模型消息,其中的 tool_calls 记录模型发起的工具调用。
在执行结束后增加:
for message in result["messages"]:
for call in getattr(
message, "tool_calls", []
):
if call["name"] == "write_todos":
print(call["args"]["todos"])
这样可以看到模型历次提交的任务列表。运行结果如下:
[{'content': '获取版本变更与测试报告概况', 'status': 'completed'}, {'content': '调查 login_refresh 失败详情', 'status': 'in_progress'}, {'content': '调查 discount_calculation 失败详情', 'status': 'pending'}, {'content': '评估风险并给出发布建议', 'status': 'pending'}]
[{'content': '获取版本变更与测试报告概况', 'status': 'completed'}, {'content': '调查 login_refresh 失败详情', 'status': 'completed'}, {'content': '调查 discount_calculation 失败详情', 'status': 'completed'}, {'content': '评估风险并给出发布建议', 'status': 'completed'}]
使用 stream() 实时观察
如果希望在执行过程中获取计划变化,可以使用 stream()。
LangGraph 支持多种流式输出模式:
| 模式 | 返回内容 |
|---|---|
values | 每个步骤结束后的完整 State |
updates | 每个步骤产生的状态更新 |
messages | 模型生成过程中的消息数据 |
观察 Planning 时,可以使用 values:
last_todos = None
for state in agent.stream(
request,
stream_mode="values",
):
todos = state.get("todos") or []
snapshot = [
(t["content"], t["status"])
for t in todos
]
if todos and snapshot != last_todos:
print("\n计划更新:")
for todo in todos:
print(
todo["status"],
todo["content"],
)
last_todos = snapshot
stream() 逐步返回执行状态。
last_todos 保存上一次的任务列表,避免重复输出。
例如,当 Agent 发现测试失败后,可能产生这样的更新:
completed 查看版本变更
completed 检查自动化测试
in_progress 调查失败测试
pending 评估发布风险
pending 形成上线建议
通过这种方式,我们可以观察任务进度,也可以将状态展示在前端界面中。
六、write_todos 的底层实现
write_todos 如何将计划保存到 State?
关键在于 LangGraph 的 Command。
中间件内部的核心逻辑可以简化为:
from langgraph.types import Command
from langchain_core.messages import ToolMessage
def update_todos(runtime, todos):
return Command(
update={
"todos": todos,
"messages": [
ToolMessage(
content="Updated todo list",
tool_call_id=runtime.tool_call_id,
)
],
}
)
这里省略了部分类型声明和处理逻辑,仅展示状态更新过程。
Command 用于向 LangGraph 提交状态更新。
其中,update 指定需要修改的字段:todos 保存新任务列表,messages 记录工具执行结果。
ToolMessage 表示工具返回的消息,tool_call_id 用于关联模型发起的工具调用。
ToolRuntime
中间件内部通过 ToolRuntime 获取运行时信息。
相关函数签名可以表示为:
def _write_todos(
runtime: ToolRuntime,
todos: list[Todo],
) -> Command:
...
ToolRuntime 由框架注入,包含当前工具调用 ID 等信息。模型只需要提供 todos 参数。
-> Command 表示函数返回一个 Command 对象。
因此,Planning 的状态更新过程可以概括为:
LLM 生成计划
↓
调用 write_todos
↓
校验工具参数
↓
返回 Command
↓
更新 State.todos
↓
Agent 继续执行
总结下来,模型负责生成和调整计划,write_todos 提交更新,LangGraph 保存状态。
七、计划调整与能力边界
Planning 支持根据执行结果调整任务。
例如,原来的发布检查计划为:
查看变更 → 检查测试 → 评估风险 → 给出建议
发现测试失败后,可以增加调查任务:
查看变更
↓
检查测试
↓
调查失败原因
↓
评估风险
↓
给出建议
这样的调整,同样可以通过 write_todos 完成。
当然,需要注意,计划也不宜频繁修改。只有新信息影响执行方向、任务范围或完成条件时,调整才有必要。
completed 只表示任务状态已经更新,不能直接证明工作结果正确。
此外,Planning 也不负责中断恢复。对于需要长期运行的任务,还要结合 Checkpointer 等持久化机制。
总结
Deep Agents 通过 TodoListMiddleware 提供 Planning 能力,由 write_todos 创建和更新任务列表,并将计划保存在 LangGraph State 中。
模型可以根据任务制定计划,调用工具完成工作,再依据执行结果调整安排。开发者则可以通过 invoke()、stream() 和工具调用记录观察执行过程。
Planning 是一项可选能力。启用中间件后,模型仍可能选择直接执行,此时 State 中不一定存在 todos 字段。
这套机制改善了复杂任务的完成方式,但执行质量仍然取决于任务安排、工具结果和完成标准。
社区讨论
参与讨论
有问题或想法?欢迎继续讨论。