一个可靠的Agent必须"知道自己在做什么"——TodoWriteTool为Agent提供了结构化的任务列表管理能力,让复杂任务的执行过程可追踪、可恢复、可审计。
环境准备
本文示例代码基于以下技术栈:
| 组件 | 版本要求 |
|---|---|
| JDK | 17+ |
| Spring Boot | 3.2+ |
| Spring AI | 2.0.0-M3+ |
| spring-ai-agent-utils | 0.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状态
设计意图:
- 强制串行化:防止Agent分散注意力
- 显式状态:每个任务状态清晰可见
- 可审计性:执行历史可追溯
二、状态机与生命周期
2.1 状态定义
public enum TodoStatus {
PENDING, // 待处理
IN_PROGRESS, // 进行中(唯一性约束)
COMPLETED, // 已完成
CANCELLED // 已取消
}
2.2 状态转换图
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的状态需要持久化到对话历史中,原因:
- 跨轮次保持:用户可能在多个对话轮次中逐步完成任务
- 上下文同步:LLM需要看到最新的任务状态
- 恢复能力:会话中断后可恢复进度
3.2 工作原理
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):
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 任务失败策略
当任务执行失败时,有几种处理方式:
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的核心价值在于:
- 强制串行化 → 防止任务遗漏
- 状态可见性 → 进度透明可控
- 事件驱动 → 易于集成扩展
- 持久化支持 → 跨会话恢复
与其他模式的协作:
| 模式 | 与TodoWriteTool的关系 |
|---|---|
| Agent Skills | Skill可以包含"创建Todo列表"的指令 |
| AskUserQuestionTool | 任务失败时询问用户决策 |
| Subagent Orchestration | 大任务拆分为Subagent任务 |
| AutoMemoryTools | 保存跨会话的任务进度偏好 |
适用场景判断:
参考资料
- Spring AI Agent Utils - TodoWriteTool 文档
- LLM List Position Bias 研究
- Camunda BPMN 工作流
- Temporal Workflow