【从零到一实现一个 AI Agent 框架 · 第四篇】04. 任务规划:拆解复杂目标 -

04. 任务规划:拆解复杂目标

从零到一实现一个 AI Agent 框架 · 第四篇


1. 为什么需要任务规划?

先看一个场景。

没有任务规划:

复制代码
用户:帮我调研三家竞品,每家出报告,最后汇总对比
      ↓
LLM:好的,我一次性全做了...
      → 搜索 A → 搜索 B → 搜索 C
      → 上下文中混合了 A/B/C 的数据
      → 开始写报告,发现 A 的数据忘了记在哪了
      → 重新搜索 A...
      → 上下文越来越长,质量越来越差

有任务规划:

复制代码
用户:调研三家竞品,出报告,汇总对比
      ↓
Agent Loop:
      创建 4 个任务:
        T1: 调研竞品 A → 状态 pending
        T2: 调研竞品 B → 状态 pending
        T3: 调研竞品 C → 状态 pending
        T4: 汇总对比   → 状态 pending(依赖 T1, T2, T3)
      
      开始执行 T1(标记 in_progress)
      完成 T1 → 标记 completed → T4 解除一个依赖
      开始执行 T2...
      全部完成 → 开始 T4

区别在哪?Agent 知道自己在做什么、做到哪了、下一步该干嘛。

这就是任务规划------让 Agent 能拆解、追踪、管理多步骤任务。


2. 从零开始:最小任务模型

别看 Axon 的实现,我们先自己推------一个任务系统最少需要什么?

2.1 任务的数据结构

typescript 复制代码
// 最小任务
interface Task {
  id: string;         // 唯一标识
  text: string;       // 任务描述
  status: 'pending' | 'in_progress' | 'completed';
}

就这么简单?是的。但很快就发现问题了------

复制代码
用户:先调研竞品 A 和 B,都完成后出汇总报告

Agent:
  T1: 调研 A (pending)
  T2: 调研 B (pending)
  T3: 出汇总报告 (pending)
  
  如果 Agent 先做了 T3,A 和 B 的数据还没拿到
  报告写了个寂寞。

2.2 加上依赖关系

typescript 复制代码
interface Task {
  id: string;
  text: string;
  status: 'pending' | 'in_progress' | 'completed';
  blockedBy: string[];  // 依赖哪些任务
}

现在 T3 可以声明依赖 T1 和 T2:

复制代码
T1: 调研 A (pending)        blockedBy: []
T2: 调研 B (pending)        blockedBy: []
T3: 出汇总报告 (pending)     blockedBy: ['T1', 'T2']

Agent 想开始 T3?先检查 blockedBy ------ T1 和 T2 没完成就不让开始。

这就是 DAG(有向无环图) 的核心:节点是任务,边是依赖关系。

设计原则: 依赖关系必须是无环的。如果 T1 依赖 T2,T2 依赖 T3,T3 依赖 T1------死锁了。


3. 工程演进:任务系统要解决哪些问题?

3.1 Agent 一次只能做一件事

假设 Agent 同时标记 T1 和 T2 为 in_progress

复制代码
T1: 调研 A (in_progress)    ← Agent 正在搜数据
T2: 调研 B (in_progress)    ← Agent 也在写报告?

Agent 大脑:"等等,我到底在干嘛?"

LLM 是单线程的。它不能一边搜数据一边写报告。

解决方案:强制只允许一个任务处于 in_progress

typescript 复制代码
function markInProgress(taskId: string) {
  // 检查有没有其他任务已经在进行
  const currentTask = tasks.find(t => t.status === 'in_progress');
  if (currentTask) {
    throw new Error(`任务 ${currentTask.id} 正在进行中`);
  }
  // 才能标记
  tasks.get(taskId).status = 'in_progress';
}

3.2 任务完成了,然后呢?

一个任务完成时,依赖它的任务可能就"解封"了。

typescript 复制代码
function completeTask(taskId: string) {
  const task = tasks.get(taskId);
  task.status = 'completed';

  // 找所有依赖这个任务的任务
  for (const downstream of findDownstreamTasks(taskId)) {
    // 从 blockedBy 中移除这个任务
    downstream.blockedBy = downstream.blockedBy.filter(id => id !== taskId);

    // 如果 blockedBy 空了,说明所有依赖都完成了
    if (downstream.blockedBy.length === 0) {
      // 任务现在可以执行了
      // 但状态保持 pending,等 Agent 自己来 markInProgress
    }
  }
}

注意:Axon 不自动开始下游任务。它只是解除阻塞。要不要开始,由 Agent 决定。

3.3 对话压缩了,任务还在吗?

如果对话被压缩了,任务信息会丢失吗?

不会。 任务状态持久化在磁盘上:

复制代码
.axon/projects/{projectId}/tasks.json

每次 task_createtask_updatetask_delete 都会写入这个文件。对话压缩后重新加载:

typescript 复制代码
// 恢复任务列表
const tasks = JSON.parse(fs.readFileSync(TASKS_FILE, 'utf-8'));
// Agent 看到:"哦,我还有 3 个任务没做完"

3.4 Agent 删了任务怎么办?

如果 Agent 删除了 T1,而 T3 依赖 T1:

复制代码
T3: blockedBy: ['T1', 'T2']
     │
     └── T1 被删了,T3 永远 blocked

解决方案:删除时自动清理引用。

typescript 复制代码
function deleteTask(taskId: string) {
  // 从所有其他任务的 blockedBy 中移除
  for (const task of tasks) {
    task.blockedBy = task.blockedBy.filter(id => id !== taskId);
  }
  // 再删除任务本身
  tasks.remove(taskId);
}

4. 代码解剖:Axon 的任务系统

核心文件是 src/tool/task.ts。先看整体设计:
#mermaid-svg-crWhMyW7FNHRsT84{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-crWhMyW7FNHRsT84 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-crWhMyW7FNHRsT84 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-crWhMyW7FNHRsT84 .error-icon{fill:#552222;}#mermaid-svg-crWhMyW7FNHRsT84 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-crWhMyW7FNHRsT84 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-crWhMyW7FNHRsT84 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-crWhMyW7FNHRsT84 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-crWhMyW7FNHRsT84 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-crWhMyW7FNHRsT84 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-crWhMyW7FNHRsT84 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-crWhMyW7FNHRsT84 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-crWhMyW7FNHRsT84 .marker.cross{stroke:#333333;}#mermaid-svg-crWhMyW7FNHRsT84 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-crWhMyW7FNHRsT84 p{margin:0;}#mermaid-svg-crWhMyW7FNHRsT84 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-crWhMyW7FNHRsT84 .cluster-label text{fill:#333;}#mermaid-svg-crWhMyW7FNHRsT84 .cluster-label span{color:#333;}#mermaid-svg-crWhMyW7FNHRsT84 .cluster-label span p{background-color:transparent;}#mermaid-svg-crWhMyW7FNHRsT84 .label text,#mermaid-svg-crWhMyW7FNHRsT84 span{fill:#333;color:#333;}#mermaid-svg-crWhMyW7FNHRsT84 .node rect,#mermaid-svg-crWhMyW7FNHRsT84 .node circle,#mermaid-svg-crWhMyW7FNHRsT84 .node ellipse,#mermaid-svg-crWhMyW7FNHRsT84 .node polygon,#mermaid-svg-crWhMyW7FNHRsT84 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-crWhMyW7FNHRsT84 .rough-node .label text,#mermaid-svg-crWhMyW7FNHRsT84 .node .label text,#mermaid-svg-crWhMyW7FNHRsT84 .image-shape .label,#mermaid-svg-crWhMyW7FNHRsT84 .icon-shape .label{text-anchor:middle;}#mermaid-svg-crWhMyW7FNHRsT84 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-crWhMyW7FNHRsT84 .rough-node .label,#mermaid-svg-crWhMyW7FNHRsT84 .node .label,#mermaid-svg-crWhMyW7FNHRsT84 .image-shape .label,#mermaid-svg-crWhMyW7FNHRsT84 .icon-shape .label{text-align:center;}#mermaid-svg-crWhMyW7FNHRsT84 .node.clickable{cursor:pointer;}#mermaid-svg-crWhMyW7FNHRsT84 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-crWhMyW7FNHRsT84 .arrowheadPath{fill:#333333;}#mermaid-svg-crWhMyW7FNHRsT84 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-crWhMyW7FNHRsT84 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-crWhMyW7FNHRsT84 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-crWhMyW7FNHRsT84 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-crWhMyW7FNHRsT84 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-crWhMyW7FNHRsT84 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-crWhMyW7FNHRsT84 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-crWhMyW7FNHRsT84 .cluster text{fill:#333;}#mermaid-svg-crWhMyW7FNHRsT84 .cluster span{color:#333;}#mermaid-svg-crWhMyW7FNHRsT84 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-crWhMyW7FNHRsT84 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-crWhMyW7FNHRsT84 rect.text{fill:none;stroke-width:0;}#mermaid-svg-crWhMyW7FNHRsT84 .icon-shape,#mermaid-svg-crWhMyW7FNHRsT84 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-crWhMyW7FNHRsT84 .icon-shape p,#mermaid-svg-crWhMyW7FNHRsT84 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-crWhMyW7FNHRsT84 .icon-shape .label rect,#mermaid-svg-crWhMyW7FNHRsT84 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-crWhMyW7FNHRsT84 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-crWhMyW7FNHRsT84 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-crWhMyW7FNHRsT84 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} DAG引擎
权限层
Agent端
完成自动解除阻塞
删除清理
task_create
持久化存储
task_update
task_list
task_delete
权限检查
依赖解析
标记下游可执行
清理引用

关键代码(简化版):

typescript 复制代码
// src/tool/task.ts(核心逻辑)

// 任务存储
const tasks = new Map<string, Task>();

// task_create
async function handleTaskCreate({ text, blockedBy }: CreateArgs): Promise<string> {
  const id = generateId();

  // 校验依赖任务的 ID 是否有效
  if (blockedBy) {
    for (const depId of blockedBy) {
      if (!tasks.has(depId)) {
        return `错误:依赖任务 ${depId} 不存在`;
      }
    }
  }

  const task: Task = {
    id,
    text,
    status: 'pending',
    blockedBy: blockedBy || [],
  };

  tasks.set(id, task);
  persistToDisk();  // 写入 tasks.json
  return `任务创建成功,ID: ${id}`;
}

// task_update
async function handleTaskUpdate({ id, status }: UpdateArgs): Promise<string> {
  const task = tasks.get(id);
  if (!task) return `错误:任务 ${id} 不存在`;

  if (status === 'in_progress') {
    // 确保没有其他任务在进行中
    const current = Array.from(tasks.values())
      .find(t => t.status === 'in_progress');
    if (current) {
      return `错误:任务 ${current.id} 正在进行中,不能同时进行多个任务`;
    }
  }

  if (status === 'completed') {
    // 自动解除下游依赖
    for (const [tid, t] of tasks) {
      const before = t.blockedBy.length;
      t.blockedBy = t.blockedBy.filter(dep => dep !== id);
      if (before > 0 && t.blockedBy.length === 0) {
        // 依赖全部解除,但状态保持 pending
      }
    }
  }

  task.status = status;
  persistToDisk();
  return `任务 ${id} 已更新为 ${status}`;
}

// task_list
async function handleTaskList(): Promise<string> {
  // 返回所有任务的概要
  const lines = Array.from(tasks.values()).map(t => {
    const deps = t.blockedBy.length > 0
      ? ` [依赖: ${t.blockedBy.join(', ')}]`
      : '';
    return `- ${t.id}: ${t.text} --- ${t.status}${deps}`;
  });
  return lines.join('\n') || '暂无任务';
}

// task_delete
async function handleTaskDelete({ id }: DeleteArgs): Promise<string> {
  if (!tasks.has(id)) return `错误:任务 ${id} 不存在`;

  // 从所有依赖中清理
  for (const task of tasks.values()) {
    task.blockedBy = task.blockedBy.filter(dep => dep !== id);
  }

  tasks.delete(id);
  persistToDisk();
  return `任务 ${id} 已删除`;
}

几个关键设计点:

① DAG 依赖通过 blockedBy 字段实现

不是图数据库,不是拓扑排序,就是一个字段------"我依赖谁"。够简单,够用。

② 删除自动清理引用

不会出现悬挂的依赖 ID。

③ 持久化在每次变更时触发

不是定时保存,不是退出时保存------每次操作都写盘。确保压缩后不丢。


5. 动手实验:用 Axon 的任务系统

打开你的 Axon 终端:

实验一:多步骤调研

复制代码
用户:调研三家竞品,然后汇总对比

Agent 应该怎么做?你可以引导它:

复制代码
用户:先用 task_create 创建三个调研任务,然后创建汇总任务,设置依赖

看看 Agent 会不会正确设置 blockedBy

如果 Agent 做对了,task_list 的输出应该是:

复制代码
- 1: 调研竞品 A --- pending
- 2: 调研竞品 B --- pending
- 3: 调研竞品 C --- pending
- 4: 汇总对比 --- pending [依赖: 1, 2, 3]

实验二:并行 vs 串行

复制代码
用户:任务 1 和 2 没有依赖关系,能不能同时做?

试试看。Agent 尝试同时 task_update("1", "in_progress")task_update("2", "in_progress")。看看会不会被拒绝。

实验三:压缩后恢复

复制代码
用户:创建一个任务,然后说 "compact"
      压缩完,再问 "我还有哪些任务?"

如果任务系统正常工作,Agent 应该能正确恢复所有任务状态。

动手改代码

想深入一点?打开 src/tool/task.ts,试试:

  1. 去掉单 in_progress 限制 → 看看会出现什么问题
  2. 改成自动开始下游任务 → 依赖解除后自动标记 in_progress
  3. 给任务加优先级字段priority: 'high' | 'medium' | 'low'

上一篇Agent 大脑:主循环是怎么转起来的下一篇Skill 系统:注入专业能力

当 Agent 需要专业知识(金融分析、医疗报告、法律文书),怎么让它"瞬间学会"?Skill 系统就是干这个的。

相关推荐
机器之心1 小时前
世界模型评测的最大盲区,被这个新基准捅破了
人工智能·openai
机器之心1 小时前
Claude、GLM、GPT谁才是真正的AI软件工程师?首个持续更新Visual Spec-to-App Benchmark发布
人工智能·openai
dreamread1 小时前
2026子平格局分析排盘工具怎么选:看规则线索、复盘记录和AI边界
人工智能·软件工具·传统文化
mooyuan天天1 小时前
大模型全自动攻防渗透:HexStrike-AI 与 MCP 联动部署实战教程(从零搭建完整避坑手册)
人工智能·ai安全·hexstrike
东风破_1 小时前
LLM 为什么记不住你?无状态、messages 和上下文管理
javascript·人工智能
如此这般英俊2 小时前
手搓Claude Code-第六章 subagent
数据结构·人工智能·python·语言模型·自然语言处理
EAI-Robotics2 小时前
机器人操作鲁棒性:当机械手遇上真实世界的不确定性
人工智能·算法·机器人
shushangyun_2 小时前
2026智能采购商城系统选型指南:如何引领企业数字化采购升级
java·大数据·数据库·人工智能·机器学习
风行南方2 小时前
密码学之分组密码
网络·密码学