【Agent智能体】 任务规划工作流

概述

learn-claude-code 项目通过渐进式设计,构建了一个完整的 Agent 任务规划系统。任务规划工作流从简单的内存待办清单(s03)演进到复杂的持久化任务图(s07),实现了从目标分解到任务执行的完整闭环。

一、任务规划架构演进

1.1 三层任务规划体系

plain 复制代码
目标分解工作流演进:
s03 TodoWrite (内存清单) → s07 Task System (持久化任务图) → s12 Worktree (任务隔离)

1.2 核心设计理念

"没有计划的 Agent 走哪算哪" - 先列步骤再动手,完成率翻倍

"大目标要拆成小任务,排好序,记在磁盘上" - 文件持久化的任务图,为多 Agent 协作打基础

任务规划工作流架构

2.1 整体架构图

2.2 核心组件交互

graphql 复制代码
┌─────────────────┐    ┌─────────────────┐    ┌─────────────────┐
│   用户/主Agent   │    │   任务规划系统   │    │   任务执行系统   │
│                 │    │                 │    │                 │
│  • 提出复杂目标  │───▶│  • 目标分解      │───▶│  • 工具调用      │
│  • 接收最终结果  │◀───│  • 依赖分析      │◀───│  • 子Agent调度   │
│                 │    │  • 任务排序      │    │  • 状态更新      │
└─────────────────┘    └─────────────────┘    └─────────────────┘
                              │                         │
                              ▼                         ▼
                    ┌─────────────────┐    ┌─────────────────┐
                    │   持久化存储     │    │   环境隔离      │
                    │                 │    │                 │
                    │  • .tasks/目录  │    │  • Worktree隔离 │
                    │  • JSON任务文件 │    │  • 独立上下文    │
                    └─────────────────┘    └─────────────────┘

三、s03 TodoWrite:基础任务规划

3.1 核心机制

python 复制代码
class TodoManager:
    """内存中的待办清单管理器"""
    def __init__(self):
        self.items = []

    def update(self, items: list) -> str:
        """更新待办清单,强制同一时间只能有一个in_progress任务"""
        validated, in_progress_count = [], 0
        for item in items:
            status = item.get("status", "pending")
            if status == "in_progress":
                in_progress_count += 1
            validated.append({
                "id": item["id"],
                "text": item["text"],
                "status": status
            })

        if in_progress_count > 1:
            raise ValueError("只能有一个进行中的任务")

        self.items = validated
        return self.render()

    def render(self) -> str:
        """格式化显示任务状态"""
        result = []
        for item in self.items:
            if item["status"] == "completed":
                marker = "[x]"
            elif item["status"] == "in_progress":
                marker = "[>]"
            else:
                marker = "[ ]"
            result.append(f"{marker} #{item['id']}: {item['text']}")
        return "\n".join(result)

3.2 工作流程图

3.3 提醒机制

python 复制代码
# 跟踪待办清单更新频率
rounds_since_todo = 0
used_todo = False

for block in response.content:
    if block.name == "todo":
        used_todo = True
        rounds_since_todo = 0
        break

if not used_todo:
    rounds_since_todo += 1

# 如果连续3轮未更新待办清单,注入提醒
if rounds_since_todo >= 3 and messages:
    last = messages[-1]
    if last["role"] == "user" and isinstance(last.get("content"), list):
        last["content"].insert(0, {
            "type": "text",
            "text": "请更新你的待办清单。"
        })

四、s07 Task System:高级任务规划

4.1 持久化任务图架构

plain 复制代码
tasks/ 目录结构:
├── task_1.json
├── task_2.json
├── task_3.json
└── task_4.json

任务依赖关系图(DAG):
      task_1 (completed)
        / \
       /   \
      v     v
task_2     task_3 (pending)
(pending)    \
       \     \
        \     v
         \-> task_4 (pending)

4.2 TaskManager 核心实现

python 复制代码
import json
from pathlib import Path
from typing import Dict, List, Optional

class TaskManager:
    """持久化任务管理器"""

    def __init__(self, tasks_dir: Path):
        self.dir = tasks_dir
        self.dir.mkdir(exist_ok=True)
        self._next_id = self._max_id() + 1

    def _max_id(self) -> int:
        """获取当前最大任务ID"""
        max_id = 0
        for path in self.dir.glob("task_*.json"):
            try:
                task_id = int(path.stem.split("_")[1])
                max_id = max(max_id, task_id)
            except (ValueError, IndexError):
                continue
        return max_id

    def create(self, subject: str, description: str = "") -> Dict:
        """创建新任务"""
        task = {
            "id": self._next_id,
            "subject": subject,
            "description": description,
            "status": "pending",
            "blockedBy": [],  # 前置依赖
            "blocks": []      # 后置依赖
        }

        path = self.dir / f"task_{self._next_id}.json"
        path.write_text(json.dumps(task, indent=2))
        self._next_id += 1
        return task

    def update(self, task_id: int, **updates) -> Optional[Dict]:
        """更新任务状态或依赖关系"""
        path = self.dir / f"task_{task_id}.json"
        if not path.exists():
            return None

        task = json.loads(path.read_text())

        # 更新状态
        if "status" in updates:
            new_status = updates["status"]
            if new_status not in ["pending", "in_progress", "completed"]:
                raise ValueError(f"无效状态: {new_status}")

            old_status = task["status"]
            task["status"] = new_status

            # 如果任务完成,清理依赖
            if new_status == "completed" and old_status != "completed":
                self._clear_dependency(task_id)

        # 更新依赖关系
        if "blockedBy" in updates:
            task["blockedBy"] = updates["blockedBy"]
            # 双向同步依赖
            for blocked_id in updates["blockedBy"]:
                self._add_block(blocked_id, task_id)

        path.write_text(json.dumps(task, indent=2))
        return task

    def _clear_dependency(self, completed_id: int):
        """清理已完成任务的依赖"""
        for path in self.dir.glob("task_*.json"):
            task = json.loads(path.read_text())
            if completed_id in task["blockedBy"]:
                task["blockedBy"].remove(completed_id)
                path.write_text(json.dumps(task, indent=2))

    def _add_block(self, blocker_id: int, blocked_id: int):
        """添加阻塞关系"""
        blocker_path = self.dir / f"task_{blocker_id}.json"
        if blocker_path.exists():
            blocker = json.loads(blocker_path.read_text())
            if blocked_id not in blocker["blocks"]:
                blocker["blocks"].append(blocked_id)
                blocker_path.write_text(json.dumps(blocker, indent=2))

    def list_all(self) -> List[Dict]:
        """列出所有任务"""
        tasks = []
        for path in sorted(self.dir.glob("task_*.json")):
            task = json.loads(path.read_text())
            
            # 格式化显示
            if task["status"] == "completed":
                marker = "[x]"
            elif task["status"] == "in_progress":
                marker = "[>]"
            else:
                marker = "[ ]"
            
            # 显示阻塞关系
            blocked_by = ""
            if task["blockedBy"]:
                blocked_by = f" (blocked by: {task['blockedBy']})"
            
            tasks.append({
                "display": f"{marker} #{task['id']}: {task['subject']}{blocked_by}",
                "task": task
            })
        return tasks

4.3 任务工具集成

python 复制代码
# 工具注册到Agent
TOOL_HANDLERS = {
    "bash": bash_handler,
    "read_file": read_file_handler,
    "write_file": write_file_handler,
    "edit_file": edit_file_handler,
    
    # 任务管理工具
    "task_create": lambda **kw: task_manager.create(**kw),
    "task_update": lambda **kw: task_manager.update(**kw),
    "task_list": lambda **kw: task_manager.list_all(),
    "task_get": lambda **kw: task_manager.get(**kw),
}

五、任务规划工作流演示

5.1搭建一个简单的sso前后端交互系统

1.根据需求,生成任务,设置任务状态为pending,并根据任务内容拆解为todo列表

json 复制代码
{
  "id": 1,
  "subject": "\u8bbe\u8ba1\u5e76\u5b9e\u73b0\u5b8c\u6574\u7684SSO\u7cfb\u7edf",
  "description": "\u8bbe\u8ba1\u5e76\u5b9e\u73b0\u4e00\u4e2a\u5b8c\u6574\u7684\u5355\u70b9\u767b\u5f55\uff08SSO\uff09\u7cfb\u7edf\uff0c\u5305\u542b\uff1a\n1. \u524d\u7aef\uff1aVue.js 3 + TypeScript + Pinia \u72b6\u6001\u7ba1\u7406\n2. \u524d\u7aef\u670d\u52a1\uff1aNode.js + Express \u9759\u6001\u6587\u4ef6\u670d\u52a1\n3. \u540e\u7aef\uff1aPython FastAPI + JWT \u8ba4\u8bc1\n4. \u6570\u636e\u5e93\uff1aSQLite + SQLAlchemy ORM\n5. \u67b6\u6784\uff1a\u524d\u540e\u7aef\u5206\u79bb\uff0cRESTful API",
  "status": "completed",
  "owner": "lead",
  "blockedBy": [],
  "blocks": []
}

2.调用write_file工具进行文件编辑,并在完成一项todo时,更新todo列表

3.在todo列表每一项都完成后,更新整个任务状态为completed

4.最后成果展示

python 复制代码
### 📁 项目结构

sso-system/

├── backend/ # FastAPI后端

│ ├── routers/ # API路由

│ ├── schemas/ # 数据模型

│ └── main.py # 主应用

├── frontend/ # Vue.js前端

│ ├── src/

│ │ ├── api/ # API调用

│ │ ├── store/ # 状态管理

│ │ ├── router/ # 路由配置

│ │ └── views/ # 页面组件

│ └── index.html # 主页面

├── static-server/ # Node.js静态服务

├── database/ # 数据库模型

├── start.sh # 启动脚本

├── stop.sh # 停止脚本

└── README.md # 项目文档

复制代码

启动过程有bug,然后继续让agent帮忙修复了一下:

5.2 动态执行流程

六、总结

learn-claude-code 项目的任务规划工作流体现了从简单到复杂的渐进式设计:

  1. s03 TodoWrite:提供基础的待办清单管理,解决"注意力漂移"问题
  2. s07 Task System:升级为持久化任务图,支持依赖管理和并行执行
  3. 完整工作流:目标分解 → 任务创建 → 依赖分析 → 调度执行 → 状态更新 → 依赖解除

这种设计使得Agent能够处理复杂的多步骤任务,同时保持上下文清晰、状态持久化,为后续的多Agent协作和长期任务管理奠定了基础。

通过运行提供的示例,可以验证任务规划系统如何将复杂目标分解为可执行的任务序列,并按照依赖关系有序执行,最终完成整体目标。

备注:

在调试过程中,发现该开源项目支持的是Anthropic兼容格式的api(https://api.deepseek.com/anthropic

而DeepSeek官方提供的api格式为(https://api.deepseek.com/chat/completions),直接调用DeepSeek-V3.2模型无法调通,所以让AI帮忙写了一个适配器,

python 复制代码
#!/usr/bin/env python3
"""
Model Adapter - 统一模型接口,支持 Anthropic 和 OpenAI/DeepSeek
"""

import os
from abc import ABC, abstractmethod
from typing import List, Dict, Any, Optional
from dataclasses import dataclass


@dataclass
class ToolCall:
    """统一工具调用格式"""
    id: str
    name: str
    input: Dict[str, Any]


@dataclass
class ModelResponse:
    """统一模型响应格式"""
    content: List[Any]
    stop_reason: Optional[str]
    text: Optional[str] = None


class BaseModelClient(ABC):
    """模型客户端抽象基类"""
    
    @abstractmethod
    def create_message(self, model: str, system: str, messages: List[Dict], 
                      tools: List[Dict], max_tokens: int = 8000) -> ModelResponse:
        pass
    
    @abstractmethod
    def format_tool_result(self, tool_use_id: str, content: str) -> Dict:
        pass


class AnthropicClient(BaseModelClient):
    """Anthropic 客户端适配器"""
    
    def __init__(self, api_key: str, base_url: Optional[str] = None):
        from anthropic import Anthropic
        self.client = Anthropic(api_key=api_key, base_url=base_url)
    
    def create_message(self, model: str, system: str, messages: List[Dict],
                      tools: List[Dict], max_tokens: int = 8000) -> ModelResponse:
        response = self.client.messages.create(
            model=model,
            system=system,
            messages=messages,
            tools=tools,
            max_tokens=max_tokens
        )
        
        # 提取文本内容
        text = None
        for block in response.content:
            if hasattr(block, 'text'):
                text = block.text
                break
        
        return ModelResponse(
            content=response.content,
            stop_reason=response.stop_reason,
            text=text
        )
    
    def format_tool_result(self, tool_use_id: str, content: str) -> Dict:
        return {
            "type": "tool_result",
            "tool_use_id": tool_use_id,
            "content": content
        }


class OpenAIClient(BaseModelClient):
    """OpenAI/DeepSeek 客户端适配器"""
    
    def __init__(self, api_key: str, base_url: Optional[str] = None):
        from openai import OpenAI
        # 为 DeepSeek 确保 base_url 正确
        if base_url and "deepseek.com" in base_url and not base_url.endswith("/v1"):
            base_url = base_url.rstrip("/") + "/v1"
        self.client = OpenAI(api_key=api_key, base_url=base_url)
    
    def _convert_tools_to_openai(self, tools: List[Dict]) -> List[Dict]:
        """将 Anthropic 格式工具转换为 OpenAI 格式"""
        openai_tools = []
        for tool in tools:
            openai_tools.append({
                "type": "function",
                "function": {
                    "name": tool["name"],
                    "description": tool["description"],
                    "parameters": tool["input_schema"]
                }
            })
        return openai_tools
    
    def _convert_messages_to_openai(self, messages: List[Dict]) -> List[Dict]:
        """将 Anthropic 格式消息转换为 OpenAI 格式"""
        openai_messages = []
        for msg in messages:
            role = msg["role"]
            content = msg["content"]
            
            if isinstance(content, list):
                # 处理复杂内容
                text_content = []
                tool_calls = []
                tool_results = []
                
                for item in content:
                    if isinstance(item, dict):
                        if item.get("type") == "text":
                            text_content.append(item.get("text", ""))
                        elif item.get("type") == "tool_use":
                            # 收集工具调用
                            tool_calls.append({
                                "id": item["id"],
                                "type": "function",
                                "function": {
                                    "name": item["name"],
                                    "arguments": item.get("input", "{}")
                                }
                            })
                        elif item.get("type") == "tool_result":
                            # 收集工具结果
                            tool_results.append({
                                "tool_use_id": item["tool_use_id"],
                                "content": item["content"]
                            })
                
                if role == "assistant":
                    # 处理 assistant 消息
                    assistant_msg = {"role": "assistant"}
                    if text_content:
                        assistant_msg["content"] = "\n".join(text_content)
                    else:
                        assistant_msg["content"] = None
                    if tool_calls:
                        # 将 JSON input 转换为字符串
                        import json
                        for tc in tool_calls:
                            if isinstance(tc["function"]["arguments"], dict):
                                tc["function"]["arguments"] = json.dumps(tc["function"]["arguments"])
                        assistant_msg["tool_calls"] = tool_calls
                    openai_messages.append(assistant_msg)
                elif role == "user" and tool_results:
                    # 处理用户消息中的工具结果
                    # OpenAI 需要每个工具结果作为独立消息
                    for result in tool_results:
                        openai_messages.append({
                            "role": "tool",
                            "tool_call_id": result["tool_use_id"],
                            "content": result["content"]
                        })
                    # 如果还有文本内容,添加为用户消息
                    if text_content:
                        openai_messages.append({
                            "role": "user",
                            "content": "\n".join(text_content)
                        })
                else:
                    # 普通用户消息
                    if text_content:
                        openai_messages.append({
                            "role": role,
                            "content": "\n".join(text_content)
                        })
            else:
                # 简单内容
                openai_messages.append({
                    "role": role,
                    "content": content
                })
        
        return openai_messages
    
    def create_message(self, model: str, system: str, messages: List[Dict],
                      tools: List[Dict], max_tokens: int = 8000) -> ModelResponse:
        # 转换工具格式
        openai_tools = self._convert_tools_to_openai(tools)
        
        # 转换消息格式
        openai_messages = [{"role": "system", "content": system}]
        openai_messages.extend(self._convert_messages_to_openai(messages))
        
        response = self.client.chat.completions.create(
            model=model,
            messages=openai_messages,
            tools=openai_tools if tools else None,
            max_tokens=max_tokens
        )
        
        message = response.choices[0].message
        content = []
        
        # 处理文本内容
        if message.content:
            content.append({"type": "text", "text": message.content})
        
        # 处理工具调用
        if message.tool_calls:
            for tool_call in message.tool_calls:
                import json
                content.append({
                    "type": "tool_use",
                    "id": tool_call.id,
                    "name": tool_call.function.name,
                    "input": json.loads(tool_call.function.arguments)
                })
        
        stop_reason = "tool_use" if message.tool_calls else "end_turn"
        
        return ModelResponse(
            content=content,
            stop_reason=stop_reason,
            text=message.content
        )
    
    def format_tool_result(self, tool_use_id: str, content: str) -> Dict:
        return {
            "type": "tool_result",
            "tool_use_id": tool_use_id,
            "content": content
        }


def create_model_client() -> BaseModelClient:
    """工厂函数:根据环境变量创建对应的模型客户端"""
    provider = os.getenv("MODEL_PROVIDER", "").lower()
    
    # 检查可用的API密钥
    has_anthropic_key = bool(os.getenv("ANTHROPIC_API_KEY"))
    has_openai_key = bool(os.getenv("OPENAI_API_KEY"))
    has_deepseek_key = bool(os.getenv("DEEPSEEK_API_KEY"))
    
    # 如果没有指定provider,根据可用的API密钥自动选择
    if not provider:
        if has_anthropic_key:
            provider = "anthropic"
        elif has_openai_key or has_deepseek_key:
            provider = "openai"  # 默认为openai,因为DeepSeek也使用OpenAI的接口
        else:
            raise ValueError("No API key found. Please set ANTHROPIC_API_KEY, OPENAI_API_KEY, or DEEPSEEK_API_KEY")
    
    if provider == "anthropic":
        api_key = os.getenv("ANTHROPIC_API_KEY")
        base_url = os.getenv("ANTHROPIC_BASE_URL")
        return AnthropicClient(api_key=api_key, base_url=base_url)
    
    elif provider == "openai" or provider == "deepseek":
        api_key_openai = os.getenv("OPENAI_API_KEY")
        api_key_deepseek = os.getenv("DEEPSEEK_API_KEY")
        base_url_openai = os.getenv("OPENAI_BASE_URL")
        base_url_deepseek = os.getenv("DEEPSEEK_BASE_URL")
        
        # 根据 provider 优先选择对应的环境变量
        if provider == "deepseek":
            api_key = api_key_deepseek or api_key_openai
            base_url = base_url_deepseek or base_url_openai
        else:
            api_key = api_key_openai or api_key_deepseek
            base_url = base_url_openai or base_url_deepseek
        
        return OpenAIClient(api_key=api_key, base_url=base_url)
    
    else:
        raise ValueError(f"Unknown model provider: {provider}")
相关推荐
hhzz4 分钟前
【OpenCV 入门到精通 04】视频入门与绘图交互:从摄像头到鼠标画笔
人工智能·python·opencv·计算机视觉·音视频·交互
Thomas.Sir4 分钟前
第40课:TensorFlow|模型推理部署前置知识【离线推理、接口调用基础】
人工智能·python·tensorflow
2601_962387826 分钟前
多源数据查询指南:MySQL 联邦、Python 聚合与 Presto 引擎全对比
python·mysql·数据集成·presto·联邦查询
元岳数字人小元9 分钟前
数字人交互的用户体验设计与场景交互感受
运维·人工智能·开源·人机交互·交互
weixin1997010801613 分钟前
《Mercari OTA 直连接入:日本二手电商出海的API通道与600次/分钟限流实战》(附Python源码)
开发语言·数据库·python
zr想努力13 分钟前
GAS学习(UE5自用)
学习·ue5
秋饼14 分钟前
Spring AI 2.0 接入 DeepSeek V4.1 Flash 生产级实战
java·ai·技术分享·后端开发
她说..20 分钟前
MySQL JSON 处理学习文档
学习·mysql·json
资讯综合39 分钟前
一年级数学机构怎么选?与学习阶段相匹配的线上思维课程与线下机构差异分析
学习
半亩码田43 分钟前
【.NET新特性·第17篇】EF Core 10 与 Aspire 13.1
python·flask·.net