背景

我在仿写 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 是会话内二选一吗?

是的。两套系统独立存在,一个会话只启用一套。但它们不是「一套的升级版」,而是并行两套,用开关切换。

TodoWriteTask 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 层限定死?

这是最核心的问题。我的理解有三层:

  1. 模型对自己的运行环境一无所知。「用 todo 还是 task」取决于运行时环境——是不是 TUI、有没有 fs.watch、任务要不要跨会话存活。这些只有 harness 知道,让模型选等于让它猜。
  2. 交互式场景恰恰需要 Task 的重能力,不是「放弃了轻量的」。 交互式会话能挂几天、有常驻任务面板、可能多 agent 协作。TodoWrite 的「轻」在这种场景下是不可用,不是优点。
  3. 给自主权的代价是全局不可预测性。 模型有时用 todo、有时用 task,进度散在两套系统里,崩溃恢复直接失效;行为不统一,测试、hook、agent 都没法稳定依赖;两套工具都暴露,prompt 膨胀、缓存命中率下降。

模型的自主权应该体现在「给定工具集里怎么编排」——规划顺序、决定要不要建任务、怎么拆解,这些是完全自由的。但它不能决定「有哪些工具」,那是环境层的事。

问题六:Task 功能完全覆盖 Todo,为什么还要两套?

如果按「简单任务 vs 复杂任务」来分,那确实冗余——交互式会话里 Task 完全能列简单的平铺清单。但分割线是运行环境,不是任务复杂度

非交互场景直接用 Task 是纯开销 + 副作用(见问题四)。那为什么不做「一套系统 + 持久化开关」?我判断是三个原因:

  1. 机制差异太大:TodoWrite 一个调用、数组进出、无状态;Task 是文件 + 锁 + watcher + hooks 的整套生态,硬合并是把简单事做成 800 行。
  2. 向后兼容:SDK 老用户、hook、自建 agent 都引用 TodoWrite 这个名字,直接摘掉会破坏现有生态。
  3. 工具名本身就是环境提示:暴露 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

更多推荐