Claude Code 的 Todo 和 Task 我的学习记录
背景
我在仿写 Claude Code(CC)的过程中,读到 s12 课程里对 CC 任务系统的分析,冒出一串疑问:CC 的 Todo 和 Task 是什么关系?是会话内二选一吗?谁来决定用哪个?决定后还能改吗?……把这次自己的这些疑问和学习整理成文,作为问题记录和学习记录。
先给结论:CC 中 Task System 和 TodoWrite 是两套独立的系统,同时存在。一个会话只启用其中一套,靠 isTodoV2Enabled() 这个开关切换。这个选择由 harness 层(CC 运行环境)在会话启动时决定,LLM 没有决策权,会话内不会中途切换。
说明:以上结论基于课程 README 对 CC 源码(
utils/tasks.ts、Task 系列工具、hooks/useTaskListWatcher.ts)的分析,非官方文档直接结论,可能随版本变化。如需权威依据,建议直接核对源码。
问题一:Todo 和 Task 是会话内二选一吗?
是的。两套系统独立存在,一个会话只启用一套。但它们不是「一套的升级版」,而是并行两套,用开关切换。
| TodoWrite | Task System | |
|---|---|---|
| 定位 | 当前任务的轻量执行清单 | 可恢复的任务系统 |
| 存储 | 进程内内存 | ~/.claude/tasks/{taskListId}/{id}.json,每任务一个文件 |
| 依赖 | 无 | blockedBy / blocks 依赖图 |
| 生命周期 | 当前会话 | 跨会话持久化 |
| 并发 | 无保护 | 文件锁、owner 认领 |
问题二:什么时候决定用哪个?谁来决定?
会话启动时就确定了,跟对话内容无关,是确定性规则:
- 交互式会话(终端里跑
claude实时对话)→ 默认 Task (V2) - 非交互式 / SDK 调用 → 默认 TodoWrite (V1)
- 环境变量
CLAUDE_CODE_ENABLE_TASKS→ 强制启用 Task
不是大模型决定的。 LLM 只是被动使用 harness 暴露给它的工具:交互式会话里它只能看到 TaskCreate / TaskGet / TaskUpdate / TaskList;非交互式里只能看到 TodoWrite / TodoRead。「用 todo 还是 task」这个问题对模型来说根本不存在,它没有选择权。
决定后会话内不会更改,想换模式只能重开会话,或用环境变量控制。
问题三:交互式会话和非交互式会话指的是什么?
不是「新版 vs 老版」,是调用 CC 的方式不同:
- 交互式:人在终端前敲
claude进入持续对话界面,有 spinner、权限确认弹窗、任务面板,会话可以挂很久。CC 桌面客户端、IDE 插件都属于这一类。 - 非交互式:一次性调用,没有持续对话界面,没有人可点弹窗。典型场景:
claude -p "..."(print 模式跑完即退出)、管道喂输入、CI/CD 流水线自动化调用、用 Agent SDK 在代码里驱动 CC 引擎。
区分的关键是:人在终端前实时对话 vs 脚本/流水线/代码自动调用。
问题四:保留 TodoWrite 只是为了兼容老场景吗?
一半是兼容,一半是非交互场景根本用不上 Task 的重型能力。
Task 比 TodoWrite 强在:文件锁并发保护、owner 认领、fs.watch 响应式监听、activeForm 在 spinner 显示、生命周期 hooks。这些面向的是交互式 UI 和多 agent 协作。但在脚本 / CI 场景里:
- 没有界面需要
fs.watch实时刷新; - 没有 spinner 需要显示 activeForm;
- 没有多 agent 并发,不需要 owner 和锁;
- 最要命的是:Task 持久化到磁盘,每次跑完会在
~/.claude/tasks/留下一堆跨会话残留文件。TodoWrite 在内存里,跑完即散,零副作用。
所以非交互场景用 TodoWrite,不是因为「任务简单」,而是因为「环境不需要那些重能力,且轻量无副作用」。
问题五:为什么不给大模型自主权,harness 层限定死?
这是最核心的问题。我的理解有三层:
- 模型对自己的运行环境一无所知。「用 todo 还是 task」取决于运行时环境——是不是 TUI、有没有
fs.watch、任务要不要跨会话存活。这些只有 harness 知道,让模型选等于让它猜。 - 交互式场景恰恰需要 Task 的重能力,不是「放弃了轻量的」。 交互式会话能挂几天、有常驻任务面板、可能多 agent 协作。TodoWrite 的「轻」在这种场景下是不可用,不是优点。
- 给自主权的代价是全局不可预测性。 模型有时用 todo、有时用 task,进度散在两套系统里,崩溃恢复直接失效;行为不统一,测试、hook、agent 都没法稳定依赖;两套工具都暴露,prompt 膨胀、缓存命中率下降。
模型的自主权应该体现在「给定工具集里怎么编排」——规划顺序、决定要不要建任务、怎么拆解,这些是完全自由的。但它不能决定「有哪些工具」,那是环境层的事。
问题六:Task 功能完全覆盖 Todo,为什么还要两套?
如果按「简单任务 vs 复杂任务」来分,那确实冗余——交互式会话里 Task 完全能列简单的平铺清单。但分割线是运行环境,不是任务复杂度。
非交互场景直接用 Task 是纯开销 + 副作用(见问题四)。那为什么不做「一套系统 + 持久化开关」?我判断是三个原因:
- 机制差异太大:TodoWrite 一个调用、数组进出、无状态;Task 是文件 + 锁 + watcher + hooks 的整套生态,硬合并是把简单事做成 800 行。
- 向后兼容:SDK 老用户、hook、自建 agent 都引用
TodoWrite这个名字,直接摘掉会破坏现有生态。 - 工具名本身就是环境提示:暴露 Task 暗示「这里是持久任务系统」,暴露 TodoWrite 暗示「这里是轻量即跑即弃」,模型对所处环境的感知更准。
总结
- Todo 和 Task 是两套独立系统,会话内二选一,由 harness 层按运行环境(交互 / 非交互)决定,LLM 无决策权。
- 交互式 = Task,非交互式 = TodoWrite,
CLAUDE_CODE_ENABLE_TASKS可强制启用 Task。 - 保留 TodoWrite 既有兼容成分,也有「非交互场景用 Task 是纯开销」的机制合理性。
- 不给模型自主权,是因为「用哪套」是环境事实而非策略选项。
- 对我自己开发的智能体:简单来说没有非交互场景,直接锁死用 Task 即可,不必纠结两套并存;也可以尝试把todo工具和task工具的区分在systemprompt中向大模型说明清楚,让大模型自己根据简单或复杂场景进行灵活调用。
如果你也想弄懂 Claude Code 这类 Coding Agent 到底是怎么工作的,这个仓库也许能帮你少走一些弯路。目前我的github的项目(OpenAI SDK 版)已经更新到第十二课了,我学习的课程为learn-claude-code的v2版本课程,大家一起学习,共同进步。如果对你有帮助,请为我点一个Star,谢谢。
项目地址:https://github.com/peijiping/learn-claude-code-langchain
更多推荐
所有评论(0)