Planning(规划)是 agent 把复杂请求拆分成离散、有序、可追踪步骤的方式。AgentScope 不让模型仅靠自由形式的推理来同时兼顾多步目标,而是提供一小组内置工具,让 agent 通过普通的工具调用来维护一份显式、结构化的任务清单 —— 任务的创建、查询与更新都走工具调用。
AgentScope 内置了四个 plan 工具:
| Tool | 操作 | 只读 |
|---|
TaskCreate | 向任务清单末尾追加新任务 | 否 |
TaskGet | 按 ID 获取单个任务的完整信息(描述、状态、依赖) | 是 |
TaskList | 列出所有任务及其状态、owner、阻塞关系 | 是 |
TaskUpdate | 更新任务的状态、字段或依赖边,亦可删除任务 | 否 |
四者都是状态注入式工具(is_state_injected = True):agent 运行时把当前的 AgentState 注入每次调用,工具直接读写 agent.state.tasks_context。这意味着任务清单以 agent 为作用域,并随 agent 的其余持久化状态一同跨越推理步骤、ReAct 轮次以及 human-in-the-loop 暂停而保留下来。
当工作跨越三步以上的非平凡步骤、涉及子任务之间的依赖,或需要把进度对用户可见时,plan 工具最有用。对于一锤子或纯对话的请求,即使装配了 plan 工具,agent 也会按设计跳过 —— 工具的 prompt 已明确告诉它不要为琐碎工作做计划。
使用 Plan 工具
装配工具
像其他内置工具一样实例化并注册到 Toolkit:
from agentscope.agent import Agent
from agentscope.tool import (
Toolkit,
TaskCreate,
TaskGet,
TaskList,
TaskUpdate,
)
toolkit = Toolkit(
tools=[
TaskCreate(),
TaskGet(),
TaskList(),
TaskUpdate(),
],
)
agent = Agent(
name="planner",
system_prompt="You are a planning assistant.",
model=model,
toolkit=toolkit,
)
每个工具的 description 已经包含详细 prompt,说明何时调用、何时跳过以及如何解读输出,因此不需要额外的 system prompt 工程。check_permissions() 硬编码为 ALLOW —— plan 工具是纯内存状态变更,不会触发用户提示。
任务生命周期
典型的规划循环如下:
登记工作
收到新指令时,agent 对每个离散步骤分别调用一次 TaskCreate,提供一句简短的命令式 subject 和更详尽的 description。新任务按创建顺序追加;id 是稳定且单调递增的数字串("1"、"2"……)。
查看队列
TaskList 返回每个任务一行的紧凑摘要(id、状态、subject、owner、blocked-by),agent 据此挑选下一个可做的任务 —— 通常是 ID 最小且无未解 blocked_by 的 pending 任务。
认领并开始
开始工作前,agent 调用 TaskUpdate 把任务的 status 置为 in_progress(多 agent 场景下还可设置 owner)。
获取完整上下文
TaskGet 返回特定任务的完整描述、依赖边与元数据 —— 当描述较长时,在执行前调用很有帮助。
完成或重新规划
完成时,TaskUpdate 把状态翻转为 completed。若 agent 发现了新工作,则回到 TaskCreate;若某个任务已无需做,则把状态置为 deleted(硬删除,同时会修正所有引用了该任务的其他任务的依赖边)。
状态流转刻意保持线性:
pending → in_progress → completed
(或)
↘ deleted(任意状态均可,硬删除)
表达依赖
任务暴露两条对称的依赖边:
blocks —— 在本任务完成前不能开始的任务 ID 列表。
blocked_by —— 必须在本任务开始前完成的任务 ID 列表。
TaskUpdate 接受 add_blocks 与 add_blocked_by 参数。每次调用都会自动修改两端,保持数据一致:
# 创建好任务 "1" 与 "2" 后,让 "2" 依赖 "1":
await TaskUpdate()(
task_id="2",
add_blocked_by=["1"],
_agent_state=agent.state,
)
# 此时:task "2".blocked_by == ["1"] 且 task "1".blocks == ["2"]
任务被删除时,其 ID 会从其他所有任务的 blocks 与 blocked_by 中移除,保证依赖图始终有效。
TaskList 会标注每个仍有未解 blocked_by 的任务,TaskGet 则返回完整的依赖边列表。agent 据此优先选择无阻塞的工作,但执行层面是仅建议性的 —— 运行时不会阻止模型去做一个被阻塞的任务。请把依赖边视为协调信号,而非硬性闸门。
所有任务状态都存在 agent 自身上,位于 agent.state.tasks_context。相关类型如下:
class Task(BaseModel):
id: str # 单调递增的数字串,由 TaskCreate 分配
subject: str # 一句话命令式描述
description: str # 详细的需求 / 上下文
state: Literal["pending", "in_progress", "completed"] = "pending"
owner: str | None = None
blocks: list[str] = [] # 被本任务阻塞的任务 ID
blocked_by: list[str] = [] # 阻塞本任务的任务 ID
metadata: dict[str, Any] = {}
created_at: str # 创建时设置的 ISO-8601 时间戳
class TaskContext(BaseModel):
tasks: list[Task] = []
AgentState.tasks_context 是 agent state 模型上的常规字段,这意味着:
- 可被序列化保存。 调用
agent.state_dict()(或宿主平台用于持久化 AgentState 的任何机制)都会完整保存任务清单,恢复 state 时计划也一并恢复。
- 以 agent 为单位。 两个 agent 默认不共享任务清单;多 agent 协调由开发者自行处理(如把所有 plan 工具调用路由到一个专门的 planner agent,或手动在 agent 之间同步 state)。
- 可在 LLM 循环之外修改。 任何能拿到
agent.state 的代码 —— middleware、应用代码、评测器 —— 都可以直接读写任务。plan 工具没有特权访问;它们只是面向 LLM 的便利接口,操作的是同一份数据结构。
自定义任务
由于任务存在 agent.state.tasks_context,开发者可以绕过 LLM 直接以编程方式管理任务。常见场景:
- 预置(Seeding):用其他渠道(另一个 agent、工作流引擎、静态分析)生成的现成计划喂给 agent。
- 导入(Importing):从外部追踪系统(Jira、GitHub issues、内部任务库)导入既有工作项。
- 迁移(Migrating):把 state 从一个 agent 实例迁移到另一个,或恢复部分已完成的计划。
- 评测(Evaluation):在 agent 推理前由测试 harness 注入 ground-truth 任务。
下面的示例在 agent 第一次 reply 之前预置了两个有依赖关系的任务:
from agentscope.agent import Agent
from agentscope.state import Task
from agentscope.tool import Toolkit, TaskCreate, TaskGet, TaskList, TaskUpdate
agent = Agent(
name="planner",
system_prompt="You are a planning assistant.",
model=model,
toolkit=Toolkit(
tools=[TaskCreate(), TaskGet(), TaskList(), TaskUpdate()],
),
)
agent.state.tasks_context.tasks.extend(
[
Task(
id="1",
subject="Fetch project requirements",
description="Read README.md and CONTRIBUTING.md in the repo root.",
metadata={"source": "seed"},
),
Task(
id="2",
subject="Draft an implementation plan",
description="Produce a step-by-step plan based on the requirements.",
blocked_by=["1"],
metadata={"source": "seed"},
),
],
)
# 保持反向边一致:
agent.state.tasks_context.tasks[0].blocks.append("2")
直接修改 tasks_context 时,你需要自行保证:
- ID 唯一且可解析。
TaskCreate 取下一个 ID 的方式是 max(int(task.id) for task in tasks) + 1。非数字 ID 在计算下一个 ID 时被忽略,但也不会被重新分配 —— 请使用数字串 ID("1"、"2"……)以让自动分配持续工作。
- 依赖边双向一致。
blocks 与 blocked_by 必须同步。TaskUpdate 会自动维护;手动修改不会。
- 状态值合法。
Task.state 只接受 pending、in_progress、completed。deleted 是 TaskUpdate 暴露的操作,而非存储的状态 —— 想手动删除任务,直接从列表中移除(并清理它的依赖边)即可。
也可以随时清空或重置计划:
agent.state.tasks_context.tasks.clear()
agent 下一轮就会看到一个空计划并从头开始。
延伸阅读
- Tool —— toolkit、
ToolBase 接口,以及状态注入式工具如何拿到 AgentState。
- Agent —— agent 生命周期,包括
AgentState 的创建、恢复与持久化。