TodoWriteTool 深入解析:Agent任务追踪与状态管理

0 阅读9分钟

一个可靠的Agent必须"知道自己在做什么"——TodoWriteTool为Agent提供了结构化的任务列表管理能力,让复杂任务的执行过程可追踪、可恢复、可审计。

环境准备

本文示例代码基于以下技术栈:

组件版本要求
JDK17+
Spring Boot3.2+
Spring AI2.0.0-M3+
spring-ai-agent-utils0.7.0

Maven依赖

<dependencies>
    <!-- Spring AI 核心 -->
    <dependency>
        <groupId>org.springframework.ai</groupId>
        <artifactId>spring-ai-core</artifactId>
        <version>2.0.0-M3</version>
    </dependency>
    
    <!-- Spring AI Agent Utils -->
    <dependency>
        <groupId>org.springaicommunity</groupId>
        <artifactId>spring-ai-agent-utils</artifactId>
        <version>0.7.0</version>
    </dependency>
    
    <!-- 可选:持久化支持 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>
</dependencies>

一、核心问题:为什么Agent需要任务追踪?

1.1 LLM的"迷失中间"问题

研究表明,大语言模型在处理长列表时存在显著的位置偏差

┌─────────────────────────────────────────────────────────────────────┐
│                    LLM列表处理位置偏差                               │
├─────────────────────────────────────────────────────────────────────┤
│                                                                     │
│  输入任务列表:                                                      │
│  1. 分析需求文档                      ← 高关注度                     │
│  2. 设计数据库schema                   ← 中等关注度                   │
│  3. 实现用户认证模块                   ← 低关注度 ⚠️                  │
│  4. 实现权限管理模块                   ← 低关注度 ⚠️                  │
│  5. 实现审计日志模块                   ← 低关注度 ⚠️                  │
│  6. 编写单元测试                       ← 中等关注度                   │
│  7. 部署到测试环境                     ← 高关注度                     │
│                                                                     │
│  执行结果:                                                          │
│  ✅ 分析需求文档                                                     │
│  ✅ 设计数据库schema                                                 │
│  ❌ 实现用户认证模块(遗漏)                                          │
│  ❌ 实现权限管理模块(遗漏)                                          │
│  ✅ 实现审计日志模块(跳跃执行)                                       │
│  ❌ 编写单元测试(遗漏)                                              │
│  ✅ 部署到测试环境                                                   │
│                                                                     │
│  根因:中间位置的任务容易被LLM"遗忘"或忽略                            │
│                                                                     │
└─────────────────────────────────────────────────────────────────────┘

Stanford研究数据

  • 列表开头任务执行率:~90%
  • 列表中间任务执行率:~60%
  • 列表末尾任务执行率:~85%

1.2 TodoWriteTool的设计哲学

核心约束:同一时刻只能有一个任务处于in_progress状态

part3-state-constraints.drawio.png

设计意图

  1. 强制串行化:防止Agent分散注意力
  2. 显式状态:每个任务状态清晰可见
  3. 可审计性:执行历史可追溯

二、状态机与生命周期

2.1 状态定义

public enum TodoStatus {
    PENDING,        // 待处理
    IN_PROGRESS,    // 进行中(唯一性约束)
    COMPLETED,      // 已完成
    CANCELLED       // 已取消
}

2.2 状态转换图

part3-state-machine.drawio.png

2.3 TodoItem数据结构

public record TodoItem(
    String id,              // 唯一标识
    String content,         // 任务描述
    TodoStatus status,      // 当前状态
    Instant createdAt,      // 创建时间
    Instant updatedAt       // 最后更新时间
) {
    public static TodoItem create(String id, String content) {
        Instant now = Instant.now();
        return new TodoItem(id, content, TodoStatus.PENDING, now, now);
    }
    
    public TodoItem withStatus(TodoStatus newStatus) {
        return new TodoItem(id, content, newStatus, createdAt, Instant.now());
    }
}

2.4 TodoList完整结构

public class TodoList {
    
    private final List<TodoItem> items = new ArrayList<>();
    private int currentInProgressIndex = -1;  // -1表示无进行中任务
    
    /**
     * 创建新任务列表(替换现有列表)
     */
    public void replaceAll(List<TodoItem> newItems) {
        items.clear();
        items.addAll(newItems);
        currentInProgressIndex = -1;
        
        // 验证:新列表中只能有一个in_progress
        long inProgressCount = items.stream()
            .filter(item -> item.status() == TodoStatus.IN_PROGRESS)
            .count();
        
        if (inProgressCount > 1) {
            throw new IllegalStateException(
                "只能有一个任务处于IN_PROGRESS状态");
        }
        
        // 更新currentInProgressIndex
        for (int i = 0; i < items.size(); i++) {
            if (items.get(i).status() == TodoStatus.IN_PROGRESS) {
                currentInProgressIndex = i;
                break;
            }
        }
    }
    
    /**
     * 更新单个任务状态
     */
    public void updateItem(String id, TodoStatus newStatus) {
        int index = findIndexById(id);
        if (index == -1) {
            throw new IllegalArgumentException("任务不存在: " + id);
        }
        
        TodoItem oldItem = items.get(index);
        TodoItem newItem = oldItem.withStatus(newStatus);
        
        // 状态转换验证
        validateTransition(oldItem.status(), newStatus, index);
        
        items.set(index, newItem);
        
        // 更新currentInProgressIndex
        if (newStatus == TodoStatus.IN_PROGRESS) {
            currentInProgressIndex = index;
        } else if (oldItem.status() == TodoStatus.IN_PROGRESS) {
            currentInProgressIndex = -1;
        }
    }
    
    private void validateTransition(TodoStatus from, TodoStatus to, int index) {
        // 规则1:IN_PROGRESS唯一性
        if (to == TodoStatus.IN_PROGRESS && currentInProgressIndex != -1 
            && currentInProgressIndex != index) {
            throw new IllegalStateException(
                "已有任务正在进行中,请先完成或取消当前任务");
        }
        
        // 规则2:COMPLETED不可变
        if (from == TodoStatus.COMPLETED) {
            throw new IllegalStateException("已完成的任务不可更改状态");
        }
        
        // 规则3:PENDING只能变成IN_PROGRESS或CANCELLED
        if (from == TodoStatus.PENDING && 
            to != TodoStatus.IN_PROGRESS && to != TodoStatus.CANCELLED) {
            throw new IllegalStateException(
                "PENDING任务只能转为IN_PROGRESS或CANCELLED");
        }
        
        // 规则4:IN_PROGRESS只能变成COMPLETED或CANCELLED
        if (from == TodoStatus.IN_PROGRESS && 
            to != TodoStatus.COMPLETED && to != TodoStatus.CANCELLED) {
            throw new IllegalStateException(
                "IN_PROGRESS任务只能转为COMPLETED或CANCELLED");
        }
    }
    
    /**
     * 获取当前进行中的任务
     */
    public Optional<TodoItem> getCurrentTask() {
        if (currentInProgressIndex == -1) {
            return Optional.empty();
        }
        return Optional.of(items.get(currentInProgressIndex));
    }
    
    /**
     * 获取下一个待处理任务
     */
    public Optional<TodoItem> getNextPending() {
        return items.stream()
            .filter(item -> item.status() == TodoStatus.PENDING)
            .findFirst();
    }
    
    /**
     * 计算完成进度
     */
    public double getProgress() {
        if (items.isEmpty()) return 0;
        
        long completed = items.stream()
            .filter(item -> item.status() == TodoStatus.COMPLETED)
            .count();
        
        return (double) completed / items.size();
    }
}

三、与ChatMemory的关系

3.1 为什么要结合ChatMemory?

TodoWriteTool的状态需要持久化到对话历史中,原因:

  1. 跨轮次保持:用户可能在多个对话轮次中逐步完成任务
  2. 上下文同步:LLM需要看到最新的任务状态
  3. 恢复能力:会话中断后可恢复进度

3.2 工作原理

part3-todo-chatmemory-collab.drawio.png

3.3 配置示例

@Configuration
public class TodoAgentConfig {
    
    @Bean
    public ChatClient todoEnabledAgent(ChatClient.Builder builder) {
        return builder
            // 1. 注册TodoWriteTool
            .defaultTools(TodoWriteTool.builder().build())
            
            // 2. 配置ChatMemory(必需!)
            .defaultAdvisors(
                // Tool调用顾问
                ToolCallAdvisor.builder()
                    .conversationHistoryEnabled(false)  // 禁用内部历史
                    .build(),
                    
                // 对话记忆顾问
                MessageChatMemoryAdvisor.builder(
                    MessageWindowChatMemory.builder()
                        .maxMessages(100)  // 保留足够的消息窗口
                        .build())
                    .build()
            )
            .build();
    }
}

⚠️ 关键配置

  • 必须启用ChatMemory,否则Todo状态无法持久化
  • maxMessages应设置足够大,确保Todo历史不被裁剪
  • ToolCallAdvisor.conversationHistoryEnabled(false)避免历史重复

3.4 Tool Result格式

TodoWriteTool返回JSON格式的当前状态:

{
  "success": true,
  "todos": [
    {"id": "1", "content": "分析需求文档", "status": "completed"},
    {"id": "2", "content": "设计数据库schema", "status": "completed"},
    {"id": "3", "content": "实现用户认证模块", "status": "in_progress"},
    {"id": "4", "content": "实现权限管理模块", "status": "pending"},
    {"id": "5", "content": "编写单元测试", "status": "pending"},
    {"id": "6", "content": "部署到测试环境", "status": "pending"}
  ],
  "currentTask": {
    "id": "3",
    "content": "实现用户认证模块",
    "status": "in_progress"
  },
  "progress": 0.33,
  "summary": "进度: 2/6 完成 (33%)"
}

四、事件驱动架构

4.1 设计目标

将Todo状态变化转换为应用事件,支持:

  • UI实时更新:前端显示进度条
  • 审计日志:记录任务执行历史
  • 通知推送:任务完成时发送提醒

4.2 事件定义

// 基础事件
public sealed interface TodoEvent permits 
    TodoListCreatedEvent, 
    TodoItemUpdatedEvent, 
    TodoListCompletedEvent {
    
    Instant timestamp();
    List<TodoItem> todos();
}

// 列表创建事件
public record TodoListCreatedEvent(
    Instant timestamp,
    List<TodoItem> todos,
    String sessionId
) implements TodoEvent {}

// 单项更新事件
public record TodoItemUpdatedEvent(
    Instant timestamp,
    List<TodoItem> todos,
    String itemId,
    TodoStatus oldStatus,
    TodoStatus newStatus
) implements TodoEvent {}

// 列表完成事件
public record TodoListCompletedEvent(
    Instant timestamp,
    List<TodoItem> todos,
    Duration totalDuration
) implements TodoEvent {}

4.3 事件发布配置

@Bean
public TodoWriteTool todoWriteTool(ApplicationEventPublisher eventPublisher) {
    return TodoWriteTool.builder()
        .todoEventHandler(event -> {
            // 发布到Spring事件总线
            eventPublisher.publishEvent(event);
            
            // 特殊处理:全部完成时
            if (event instanceof TodoItemUpdatedEvent e && 
                e.todos().stream().allMatch(t -> t.status() == TodoStatus.COMPLETED)) {
                eventPublisher.publishEvent(new TodoListCompletedEvent(
                    Instant.now(),
                    e.todos(),
                    calculateDuration(e.todos())
                ));
            }
        })
        .build();
}

4.4 事件监听示例

WebSocket实时推送

@Component
public class TodoWebSocketHandler {
    
    private final SimpMessagingTemplate messagingTemplate;
    
    @EventListener
    public void onTodoEvent(TodoEvent event) {
        // 推送到前端WebSocket
        messagingTemplate.convertAndSend(
            "/topic/todo/" + getSessionId(),
            TodoUpdateMessage.from(event)
        );
    }
}

// 前端订阅
// stompClient.subscribe('/topic/todo/{sessionId}', function(message) {
//     const data = JSON.parse(message.body);
//     updateProgressUI(data.progress, data.todos);
// });

审计日志记录

@Component
public class TodoAuditLogger {
    
    private final AuditLogRepository auditLogRepository;
    
    @EventListener
    public void onTodoItemUpdated(TodoItemUpdatedEvent event) {
        AuditLog log = AuditLog.builder()
            .eventType("TODO_UPDATE")
            .sessionId(getCurrentSessionId())
            .details(Map.of(
                "itemId", event.itemId(),
                "from", event.oldStatus().name(),
                "to", event.newStatus().name(),
                "progress", calculateProgress(event.todos())
            ))
            .timestamp(event.timestamp())
            .build();
        
        auditLogRepository.save(log);
    }
    
    @EventListener
    public void onTodoListCompleted(TodoListCompletedEvent event) {
        // 发送完成通知
        notificationService.send(Notification.builder()
            .title("任务完成")
            .content(String.format("已完成所有 %d 个任务,耗时 %s",
                event.todos().size(),
                formatDuration(event.totalDuration())))
            .build());
    }
}

五、与工作流引擎集成

5.1 场景:将Todo映射到工作流

当Agent的任务需要与外部系统协调时,可以将Todo映射到工作流引擎(如Camunda、Temporal):

part3-todo-workflow-engine.drawio.png

5.2 Camunda集成示例

@Service
public class TodoWorkflowService {
    
    private final RuntimeService runtimeService;
    private final TaskService taskService;
    private final TodoWriteTool todoWriteTool;
    
    /**
     * 将Todo列表启动为Camunda流程
     */
    public String startWorkflow(List<TodoItem> todos, String processKey) {
        // 创建流程变量
        Map<String, Object> variables = new HashMap<>();
        variables.put("todos", todos.stream()
            .map(t -> Map.of(
                "id", t.id(),
                "content", t.content(),
                "status", t.status().name()
            ))
            .collect(Collectors.toList()));
        
        // 启动流程实例
        ProcessInstance instance = runtimeService
            .startProcessInstanceByKey(processKey, variables);
        
        return instance.getId();
    }
    
    /**
     * 当Todo状态更新时,同步到Camunda
     */
    @EventListener
    public void onTodoItemUpdated(TodoItemUpdatedEvent event) {
        // 查找对应的工作流任务
        Task camundaTask = taskService.createTaskQuery()
            .processVariableValueEquals("todoId", event.itemId())
            .singleResult();
        
        if (camundaTask != null && event.newStatus() == TodoStatus.COMPLETED) {
            // 完成Camunda任务
            taskService.complete(camundaTask.getId());
        }
    }
}

5.3 Temporal集成示例

// Temporal Workflow定义
public interface TodoWorkflow {
    @WorkflowMethod
    void execute(List<TodoItem> todos);
}

// Workflow实现
public class TodoWorkflowImpl implements TodoWorkflow {
    
    @Override
    public void execute(List<TodoItem> todos) {
        for (TodoItem todo : todos) {
            // 等待外部信号(Agent完成该任务)
            boolean completed = Workflow.await(
                Duration.ofHours(24),
                () -> isTaskCompleted(todo.id())
            );
            
            if (!completed) {
                throw new ApplicationFailure(
                    "任务超时: " + todo.content(), "TIMEOUT");
            }
        }
    }
    
    @SignalMethod
    public void completeTask(String todoId) {
        // 接收Agent完成任务的信号
        completedTasks.add(todoId);
    }
}

// Agent侧调用
@Service
public class TodoTemporalService {
    
    private final WorkflowClient workflowClient;
    
    public void startWorkflow(List<TodoItem> todos) {
        TodoWorkflow workflow = workflowClient.newWorkflowStub(
            TodoWorkflow.class,
            WorkflowOptions.newBuilder()
                .setTaskQueue("TODO_TASK_QUEUE")
                .build()
        );
        
        // 异步启动
        WorkflowExecution execution = WorkflowClient.start(
            workflow::execute, todos);
    }
    
    public void notifyTaskCompleted(String workflowId, String todoId) {
        TodoWorkflow workflow = workflowClient.newWorkflowStub(
            TodoWorkflow.class, workflowId);
        workflow.completeTask(todoId);
    }
}

六、失败重试与异常处理

6.1 任务失败策略

当任务执行失败时,有几种处理方式:

part3-task-failure-strategies.drawio.png

6.2 自动重试实现

public class RetryableTodoWriteTool extends TodoWriteTool {
    
    private final int maxRetries;
    private final Map<String, Integer> retryCount = new ConcurrentHashMap<>();
    
    @Override
    public String execute(String arguments) {
        JsonNode args = objectMapper.readTree(arguments);
        String operation = args.get("operation").asText();
        
        if ("update_status".equals(operation)) {
            String itemId = args.get("id").asText();
            TodoStatus newStatus = TodoStatus.valueOf(args.get("status").asText());
            
            // 检查是否是失败导致的取消
            if (newStatus == TodoStatus.CANCELLED) {
                String reason = args.has("reason") ? args.get("reason").asText() : "";
                
                if (reason.contains("failed") && shouldRetry(itemId)) {
                    // 自动重试:重新标记为in_progress
                    return retryTask(itemId);
                }
            }
        }
        
        return super.execute(arguments);
    }
    
    private boolean shouldRetry(String itemId) {
        int current = retryCount.getOrDefault(itemId, 0);
        return current < maxRetries;
    }
    
    private String retryTask(String itemId) {
        retryCount.merge(itemId, 1, Integer::sum);
        
        // 重新设置状态
        return super.execute(objectMapper.writeValueAsString(Map.of(
            "operation", "update_status",
            "id", itemId,
            "status", "IN_PROGRESS"
        )));
    }
}

6.3 降级执行实现

public class FallbackTodoWriteTool extends TodoWriteTool {
    
    @EventListener
    public void onTaskFailed(TodoItemUpdatedEvent event) {
        if (event.newStatus() == TodoStatus.CANCELLED) {
            // 查找是否有对应的降级任务模板
            String fallbackTaskId = getFallbackTaskId(event.itemId());
            
            if (fallbackTaskId != null) {
                // 插入降级任务到下一个位置
                insertFallbackTask(event.itemId(), fallbackTaskId);
            }
        }
    }
    
    // 配置:任务ID -> 降级任务模板
    private final Map<String, FallbackTemplate> fallbackTemplates = Map.of(
        "deploy-production", new FallbackTemplate(
            "deploy-staging",
            "部署到预发环境(生产部署失败降级)",
            List.of("verify-staging")
        )
    );
}

6.4 用户介入策略

当自动策略无法处理时,通知用户决策:

@Component
public class TodoFailureHandler {
    
    private final AskUserQuestionTool askUserQuestionTool;
    private final TodoWriteTool todoWriteTool;
    
    @EventListener
    public void onTaskFailed(TodoItemUpdatedEvent event) {
        if (event.newStatus() != TodoStatus.CANCELLED) return;
        
        // 构建决策问题
        Question question = Question.single(
            "failure_action",
            "任务失败处理",
            String.format("任务「%s」执行失败,如何处理?", 
                getItemContent(event.itemId())),
            List.of(
                new Option("重试", "重新执行此任务", "retry"),
                new Option("跳过", "跳过此任务,继续执行后续任务", "skip"),
                new Option("中止", "停止整个任务列表的执行", "abort"),
                new Option("降级", "执行替代方案", "fallback")
            )
        );
        
        // 询问用户
        Map<String, List<String>> answers = 
            askUserQuestionTool.handle(List.of(question));
        String decision = answers.get("failure_action").get(0);
        
        // 执行决策
        executeDecision(decision, event.itemId());
    }
    
    private void executeDecision(String decision, String itemId) {
        switch (decision) {
            case "retry" -> todoWriteTool.retry(itemId);
            case "skip" -> todoWriteTool.skip(itemId);
            case "abort" -> todoWriteTool.abortAll();
            case "fallback" -> todoWriteTool.insertFallback(itemId);
        }
    }
}

七、最佳实践与踩坑指南

7.1 常见问题

问题原因解决方案
Todo状态丢失ChatMemory未配置或窗口过小配置MessageChatMemoryAdvisor,设置maxMessages
多个任务同时in_progress约束失效使用TodoWriteTool内置约束,不直接修改状态
任务列表过长初始规划过于细碎控制初始任务数在10个以内,复杂任务拆分到Subagent
无法恢复进度未持久化结合数据库存储Todo状态(见下文)

7.2 数据库持久化

跨会话保存任务进度:

@Entity
@Table(name = "todo_sessions")
public class TodoSession {
    @Id
    private String sessionId;
    
    @Column(columnDefinition = "JSON")
    private String todosJson;
    
    private Instant createdAt;
    private Instant updatedAt;
    
    // Getter/Setter...
}

@Service
public class PersistentTodoService {
    
    private final TodoSessionRepository repository;
    
    public void saveSession(String sessionId, List<TodoItem> todos) {
        TodoSession session = repository.findById(sessionId)
            .orElse(new TodoSession(sessionId));
        
        session.setTodosJson(objectMapper.writeValueAsString(todos));
        session.setUpdatedAt(Instant.now());
        
        repository.save(session);
    }
    
    public Optional<List<TodoItem>> loadSession(String sessionId) {
        return repository.findById(sessionId)
            .map(session -> {
                try {
                    return objectMapper.readValue(
                        session.getTodosJson(),
                        new TypeReference<List<TodoItem>>() {}
                    );
                } catch (JsonProcessingException e) {
                    throw new RuntimeException("Failed to parse todos", e);
                }
            });
    }
}

// 集成到TodoWriteTool
@Bean
public TodoWriteTool todoWriteTool(PersistentTodoService persistentService) {
    return TodoWriteTool.builder()
        .todoEventHandler(event -> {
            // 每次更新后持久化
            persistentService.saveSession(getCurrentSessionId(), event.todos());
        })
        .build();
}

7.3 调试技巧

打印Todo状态

TodoWriteTool todoWriteTool = TodoWriteTool.builder()
    .todoEventHandler(event -> {
        log.info("=== Todo Status Update ===");
        for (TodoItem item : event.todos()) {
            String marker = switch (item.status()) {
                case IN_PROGRESS -> "▶";
                case COMPLETED -> "✓";
                case CANCELLED -> "✗";
                case PENDING -> "○";
            };
            log.info("{} [{}] {}", marker, item.status(), item.content());
        }
        log.info("Progress: {}/{} ({})",
            event.todos().stream().filter(t -> t.status() == TodoStatus.COMPLETED).count(),
            event.todos().size(),
            String.format("%.0f%%", 
                100.0 * event.todos().stream()
                    .filter(t -> t.status() == TodoStatus.COMPLETED)
                    .count() / event.todos().size())
        );
    })
    .build();

输出示例

=== Todo Status Update ===
✓ [COMPLETED] 分析需求文档
✓ [COMPLETED] 设计数据库schema
▶ [IN_PROGRESS] 实现用户认证模块
○ [PENDING] 实现权限管理模块
○ [PENDING] 编写单元测试
○ [PENDING] 部署到测试环境
Progress: 2/6 (33%)

八、总结

TodoWriteTool的核心价值在于:

  1. 强制串行化 → 防止任务遗漏
  2. 状态可见性 → 进度透明可控
  3. 事件驱动 → 易于集成扩展
  4. 持久化支持 → 跨会话恢复

与其他模式的协作

模式与TodoWriteTool的关系
Agent SkillsSkill可以包含"创建Todo列表"的指令
AskUserQuestionTool任务失败时询问用户决策
Subagent Orchestration大任务拆分为Subagent任务
AutoMemoryTools保存跨会话的任务进度偏好

适用场景判断

part3-decision-tree.drawio.png


参考资料