Agent plantask 的架构推导

拨开迷雾看本质:Agent plantask 的架构推导

在 Eino 的 Agent 体系中,单个 Agent 可以通过 ReAct 不断迭代,但当任务复杂到一定程度(比如开发一个带有前后端和数据库的完整 Feature)时,纯靠 LLM 脑子里的 Context 会发生严重的"幻觉"和"遗忘"。

这时候,我们需要引入计划与任务管理(Plan & Task) 。这正是 adk/middlewares/plantask 包存在的意义。


1. 寻找"第一性原理":最朴素的任务记录

如果没有任何框架,一个开发者在接手复杂任务时,最朴素的做法就是建一个 TODO.md

markdown 复制代码
// 第一性原理:朴素的 TODO 列表
1. [ ] 搭建数据库表结构
2. [ ] 编写 CRUD 接口
3. [ ] 联调测试

对于 Agent 来说,如果我们让它自己维护这个列表,它只能把这个列表塞进自己的 Messages 上下文里。每次调用模型,都要带上这串文本。

致命痛点

  1. Context 爆炸与遗忘:随着任务推进,TODO 列表越来越长,修改记录越来越多,LLM 的上下文窗口会被撑爆,且极其容易在多轮对话中"改错"或"遗忘"某些任务状态。
  2. 多 Agent 无法协作:如果我想让 Coder Agent 去做任务 1,让 Tester Agent 去做任务 2,由于 TODO 列表只存在于主 Agent 的脑子里,子 Agent 根本不知道全局进度。

2. 第一次演进:存储外置与工具化(CRUD 范式)

推导解决 :把"脑子里的记忆"剥离出来,变成"外部的持久化存储"。并且,给 Agent 提供一组专门操作这个存储的 工具(Tools)。这就变成了标准的 CRUD。

go 复制代码
// 第一次演进:工具化的 CRUD 任务管理
type Task struct {
    ID     string
    Status string // pending, in_progress, completed
    Title  string
}

// 提供给大模型的工具箱
func TaskCreate(title string) { db.Save(&Task{Title: title}) }
func TaskList() []Task { return db.FindAll() }
func TaskUpdate(id, status string) { db.Update(id, status) }

这就是 adk/middlewares/plantask/plantask.goBeforeAgent 注入的四个工具的本质:

它在 Agent 执行前,硬塞了四个 Tool:TaskCreateTaskGetTaskUpdateTaskList

映射到源码


3. 第二次演进:文件系统后端的妥协(The Filesystem Backend)

新痛点 :上面推导的 db.Save() 只是个概念。在真实的云原生或沙盒环境中,这个 db 应该是什么?

如果我们用 MySQL 或 Redis,就会引入极重的外部依赖,让框架变得难以本地测试和部署。

推导解决 :利用 Eino 已有的 filesystem 抽象(见 adk/middlewares/filesystem),把每个 Task 存成一个独立的 JSON 文件!

go 复制代码
// 第二次演进:基于文件系统的 Task 存储
func TaskCreate(title string) {
    id := getNextID() // 读取 .highwatermark 文件
    content := toJSON(&Task{ID: id, Title: title})
    fs.Write(fmt.Sprintf("%s.json", id), content)
}

映射到源码

  • task.go 定义了 Backend 接口,它直接借用了 filesystem.ReadRequest 等结构。
  • task_create.go 展示了典型的文件锁与自增 ID 的实现方式:通过读取 .highwatermark 文件来获取下一个 Task ID,并将其保存为 <ID>.json

4. 第三次演进:复杂的依赖图拓扑(Blocks / BlockedBy)

新痛点 :简单的列表只适合单线程、无脑顺序执行的工作。但在真实项目中,"写接口(Task 2)"必须在"建表(Task 1)"之后完成;而"写文档(Task 3)"可以和"写接口"同时进行。

如果只有一个大模型在跑,它确实可以自己决定先做谁再做谁。但如果是多 Agent 协作网络(比如通过 Workflow 启动了 3 个平行的 Worker Agent),它们都在看着同一块任务看板:

  • 无序的灾难:如果没有依赖关系,Worker B 可能会抢到"写接口",但此时 Worker A 的"建表"还没做完,Worker B 直接报错。
  • 动态规划的需求 :任务并不是在一开始就全部排好序塞进队列的!大模型在执行"建表"时,可能会动态发现新问题并创建新任务 ,这就要求任务系统不能是一个死板的静态队列,而必须是一个支持动态修改的有向无环图(DAG)

推导解决 :必须在 Task 模型中引入拓扑依赖关系,即 Blocks(我阻塞了谁)和 BlockedBy(谁阻塞了我)。

  • 当 Agent 调用 TaskList 时,它会主动过滤掉那些 len(BlockedBy) > 0(前置任务未完成)的任务。
  • 当 Agent 动态添加依赖时(比如发现任务 C 必须依赖任务 A),必须严格校验环形依赖(防止出现 A 等 B,B 等 C,C 等 A 的死锁)。
go 复制代码
// 第三次演进:防环的依赖管理 (DAG)
type Task struct {
    Blocks    []string // 我阻塞了谁
    BlockedBy []string // 谁阻塞了我
}

func UpdateTaskDeps(taskId, blockedById string) error {
    // 每次动态修改依赖图时,必须检查是否成环
    if hasCyclicDependency(taskId, blockedById) {
        return error("环形依赖!")
    }
    // 双向更新边
    task[taskId].BlockedBy.append(blockedById)
    task[blockedById].Blocks.append(taskId)
}

映射到源码

  • task.go 并没有进行全局的拓扑排序(不需要排成一个线性队列),而是实现了一个用于插入依赖时防环 的 DFS(深度优先搜索)算法 hasCyclicDependency。因为只要图里没有环,各个 Agent 就可以靠局部视角(看谁的 BlockedBy 是空的)安全地拉取任务。
  • task_update.go 实现了复杂的双向依赖更新逻辑。当大模型通过工具调用 AddBlockedBy 时,不仅更新当前任务文件,还要去修改被阻塞任务的 Blocks 数组。

5. 第四次演进:API 化与防遗忘的主动唤醒(The Watchdog)

随着在真实 Agent Teams 场景中的落地,纯靠 LLM 自主维护看板暴露了两个致命问题。

痛点 1:LLM 的沉浸式遗忘

在复杂的编码任务中,大模型经常会在连续地改代码、运行测试中"上头"。如果它连续 10 多轮对话都没有调用 TaskUpdate 更新进度,整个系统的看板状态就会停滞,其他协作 Agent 也会被卡死。这被称为**"沉浸式遗忘"**。

推导解决 1:状态机的看门狗(Task Reminder)

必须把"更新进度"这个动作,从被动等待 LLM 觉醒 ,变成框架层的主动提醒

利用 Middleware 钩子(BeforeModelRewriteState),在每次调用大模型前,像看门狗一样扫描 Messages 历史:

  • 动作 :向前追溯历史消息,计算自上次调用 TaskCreate/TaskUpdate 工具以来,经过了多少个 Assistant 轮次。
  • 唤醒 :如果超过阈值(如默认 10 轮),框架会强行在上下文末尾注入一条 <system-reminder> 消息,不仅包含温和的提示语,还会动态拼装上当前的最新任务列表
  • 映射到源码task_reminder.go#L56-L104 实现了 computeTurnStats 用于统计轮距;task_reminder.go#L134-L214 实现了拦截和动态提示词注入。这成功地将 LLM 从"局部细节"拉回"全局进度把控"。

痛点 2:框架与 Agent 的单向隔离

早期的 CRUD 仅仅是作为 Tool 暴露给大模型的。如果外部宿主系统(Host Code)想要在 Agent 启动前预置初始任务 ,或者通过人类干预删掉一个错误任务,缺少开放的编程接口。

推导解决 2:剥离逻辑的编程接口(Programmatic API)

将任务修改的底层能力与 Tool Wrapper 解耦,提供纯 Go 调用的 CreateTaskDeleteTask

  • DAG 的自愈能力 :最典型的体现是 DeleteTask 的实现。如果强行删掉一个任务节点,它的 ID 还会残留在其他任务的 BlockedBy 数组里,导致其他任务永远无法解锁(死锁)。因此,task_api.go#L143-L188 必须遍历整个系统的所有任务,将这个被删节点的 ID 从所有 BlocksBlockedBy 数组中安全剔除,保证图拓扑的完整性。

6. 批判性总结

优势:将"思考"降维为"工程"

  • 彻底解放 Context :大模型不需要在脑子里记住几百行的 TODO 列表。它只需要通过 TaskList 扫一眼,找到 status: pending 且没有 blockedBy 的任务,然后 TaskGet 看详情,干完后 TaskUpdate 标记完成。这极大地降低了长链路任务的幻觉率。
  • 天然适配多 Agent 并发 :因为状态被外置到文件系统中,配合 Owner 字段(认领机制),多个 Agent 完全可以像人类工程师看 Jira 面板一样,异步地抢单、干活、交付。

代价与局限:沉重的锁与碎文件地狱

  1. 全局大锁的悲哀 :在 task_create.go 和其他几个工具中,我们可以看到 t.lock.Lock()。因为底层依赖的是文件系统(特别是读取 .highwatermark 自增 ID),为了防止并发创建任务导致 ID 冲突,整个 Task 系统的写入是被强制串行化的
  2. 碎文件性能瓶颈 :每次 TaskList(见 task_list.go),都要 LsInfo 扫目录,然后在一个 for 循环里去 ReadUnmarshal 每一个 .json 文件。如果一个项目有 100 个 Task,每次 List 都要进行 100 次 IO 读取!这在文件系统上是极度昂贵的。
  3. 一致性危机 :在 TaskUpdate 更新双向依赖时,它需要修改两个不同的 JSON 文件。但文件系统是没有**事务(Transaction)**的!如果改了第一个文件,在改第二个文件时进程崩溃了,整个任务依赖图就彻底撕裂了。

7. 生态站位:它与 Supervisor / PlanExecute 的关系

一个非常关键的问题是:Agent 怎么认领任务?这个中间件需要依赖特定的框架模式(如 DeepAgent / Supervisor / PlanExecute)吗?

答案是:不需要。它是一个纯粹的"底层工具提供者(Provider)",它完全不关心是谁在用它。

7.1 它到底提供了什么?

plantask 中间件的本质,仅仅是向挂载了它的 Agent 的 Tools 列表里,硬塞了四个外部工具 (CRUD)。

没有 提供任何路由逻辑,没有提供任何"调度循环",它甚至不知道自己在一个什么架构里运行。

Agent 是怎么认领任务的?

完全靠大模型(LLM)的自主推理(ReAct)

  • 框架层面没有任何"分发(Dispatch)"代码。
  • 而是 LLM 看到自己手里有一个 TaskList 工具,它主动调用。
  • TaskList 会把所有任务(包含 pending、completed 以及带有 blockedBy 的任务)的简要信息返回给模型。
  • LLM 阅读这些返回的文本后,自己进行逻辑判断:"这个 Task 1 是 pending 的,并且它的 blockedBy 是空的,所以我可以做"。于是它主动调用 TaskUpdate(id=1, owner="self", status="in_progress")
7.1.1 过滤过滤(Filter)vs 暴露(Expose):Eino 为什么选择让模型自己判断?

在这里有一个经典的设计分歧:当调用 TaskList 时,到底应该在代码层面过滤掉 不可做的任务(只返回真正可执行的任务),还是把所有任务的状态都暴露给大模型,让大模型自己判断?

Eino 选择的是后者(暴露给大模型判断) 。参见 task_list.go,它的返回逻辑只是简单地把所有 Task 拼成字符串:

text 复制代码
#1 [completed] 搭建数据库表结构 [owner: agent-a]
#2 [pending] 编写查询接口 [blocked by #1]
#3 [pending] 编写 API 文档

为什么这种设计更好?

  1. 全局视野与规划能力 :如果框架直接把 #2 过滤掉,大模型就成了"瞎子",它只知道自己现在能做 #3,完全不知道未来还有个 #2 需要做。这会破坏大模型对整个项目的全局上下文理解 (比如它可能在写 #3 文档时,需要预判 #2 接口的设计)。
  2. 信任智能涌现 :早期的低智模型(如 GPT-3)可能会无视 [blocked by] 标签强行接单,所以老一代的框架倾向于在代码里做死卡(Hard Filter)。但现代模型(GPT-4/Claude 3)完全能够理解 blocked by 的语义。把决策权交给模型,体现了"让模型做大脑,代码只做手脚"的架构哲学。
  3. 减少隐式魔法 :如果工具名叫 TaskList,却暗中过滤了部分数据,这是对接口语义的破坏。如果需要过滤,应该提供另一个名为 GetAvailableTasks 的工具,而不是在 TaskList 里做手脚。

7.2 各种模式下如何使用 plantask

因为它是纯底层的工具集,所以它可以被随意插拔到任何模式中,产生不同的化学反应:

  1. 单体单线程 ReAct(最简单)
    • 一个人包揽所有。LLM 自己列 Plan,自己做,做完一个自己打钩。此时它主要用来解决"Context 遗忘"问题,充当"外挂记忆"。
  2. Supervisor 模式(包工头与打工人)
    • 可以让 Supervisor Agent 挂载 TaskCreateTaskList(它只负责拆解任务、看进度)。
    • 让底下的 Specialist Agents 挂载 TaskGetTaskUpdate(它们只负责接活、干活、改状态)。
    • 此时,看板成了上下级之间通信的桥梁,代替了臃肿的上下文传递。
  3. PlanExecute 模式的升维(内置 Plan vs 外挂 PlanTask)
    • adk/prebuilt/planexecute 中,Eino 提供了一个内置的轻量级 Plan 机制(把步骤写在 Messages 里,靠图循环执行)。但如果把它与 plantask 中间件对比,会发现它们在存储介质、执行拓扑和协作能力上存在巨大的代差:
维度 PlanExecute 内置 Plan plantask 外挂中间件
存储介质 内存(Context 数组) Plan 存在大模型的 Messages 历史记录里。 持久化(文件系统/DB) Plan 存在外部的 JSON 文件里,大模型通过工具去读。
执行拓扑 线性队列(Queue) 只能一步步串行往下做(Step 1 -> Step 2 -> Step 3),不支持复杂的条件分支和依赖。 有向无环图(DAG) 支持 BlocksBlockedBy,可以表达"任务 A 和 B 必须先做完,才能做 C"这种复杂的非线性依赖。
协作能力 单机单线程 只有当前那个 Executor 知道 Plan 是什么,无法让多个 Agent 一起干活。 分布式多机协同 看板是公开的,支持引入 Owner 字段。完全可以启动多个不同职能的 Agent 并发抢单干活。
抗遗忘与容错 极易幻觉 随着任务执行,Context 越来越长,大模型很容易忘记最初的 Plan 甚至"自己篡改"历史。一旦崩溃,除非有外部 Checkpoint,否则全丢。 绝对准确 每次干活前都通过 TaskGet 现查文件,文件里写了什么就是什么。进程崩溃重启后,直接读目录就能恢复所有状态。

什么时候用谁?

  • 如果任务是 "写个简单的 Python 爬虫" (3-5 个步骤),直接用 PlanExecute 内置的 Plan,轻量快捷,不需要额外的文件系统 IO 开销。
  • 如果任务是 "从零开发一个带前后端的电商网站" (包含几十个步骤、复杂的接口依赖、需要前后端 Agent 协作),必须抛弃内置 Plan,引入 plantask 。让 Planner Agent 专门负责调 TaskCreate 画出庞大的 DAG 图,然后让多个 Executor Agent 作为 Worker 盯着看板疯狂清空任务。

总结plantask 不是一种独立的 Agent 拓扑,而是一块大家都能看到并修改的"共享白板" 。它将多 Agent 之间的点对点通信(RPC/Transfer) ,降维成了基于共享存储的异步协同(Pub/Sub)

更优解的探讨

基于文件系统存储 JSON 是为了"轻量"和"便携"(特别是为了配合 Sandbox 环境),但它牺牲了关系型数据的核心特性。

更好的做法是:

  • 既然是中间件,就应该把存储层彻底抽象为 Repository 接口,提供 SQLite/BoltDB 的本地实现作为默认选项。
  • 一张表存 Task,一张表存 Edge(依赖关系),这样不仅能利用 SQL 的事务保证一致性,还能把 TaskList 的时间复杂度从 O ( N × I O ) O(N \times IO) O(N×IO) 降到真正的 O ( 1 ) O(1) O(1) 级查询。
相关推荐
AImatters2 小时前
出海营销变天了:当Agentic AI重构创意、投放与归因
人工智能·亚马逊云科技·出海·agentic ai·易点天下
你们补药再卷啦2 小时前
上下文工程(1/4)笔记
人工智能
以为你知道啊2 小时前
从源代码自动生成 OpenAPI 3.1.0 规范文件 + Redoc 可视化文档的技能
人工智能
LaughingZhu2 小时前
Product Hunt 每日热榜 | 2026-03-26
人工智能·经验分享·深度学习·神经网络·产品运营
彩旗工作室2 小时前
腾讯云上调用大模型的全部入口整理(2026最新版)
人工智能·大模型·云计算·腾讯云
科技观察2 小时前
《观澜社张庆与中信证券合作,共筑金融新生态》
大数据·人工智能·金融
实在智能RPA2 小时前
实在 Agent 支持本地化部署吗?深度解析企业级私有化 AI 智能助理的技术架构与落地实践
人工智能·ai·架构
金融RPA机器人丨实在智能2 小时前
2026产业跃迁:基于大模型的自主智能体产品如何重塑企业生产力?实在Agent商业实战全解析
人工智能·ai
shangjian0072 小时前
AI-大语言模型LLM-LangGraphV1.0学习笔记-context_schema和config_schema
人工智能