把 Claude Code 当作日常开发伙伴之后我最先想明白的一件事是它带来的绝大部分翻车事故不是能力不够而是没有计划。前两周我在一个内部工具库里做一次跨五个模块的重构Claude 在第三个模块上已经把接口风格从“统一返回 Result”悄悄改成了“直接抛异常”代码写完跑起来一看完全偏离了最初定下的兼容约束。那一次我损失了大半天也记住了 TodoWrite 这个工具。learn-claude-code 系列写到 S03这一期专门拆 TodoWrite——一个看起来只是“列待办”的工具实际是 Agent 保持方向感的底层机制它把模糊目标变成显式清单把长对话容易丢失的目标重新固定到上下文里。这篇文章适合三类人刚开始用 Claude Code 做项目的新手、被长任务跑偏坑过的开发者、以及想在团队里推广 Agent 工作流的人。1. 迷失方向不是玄学Agent 为什么要靠一张任务清单兜底1.1 那次跑偏事故代码越写越顺方向越走越远我接到的是一个很常见的活儿把项目里的配置加载逻辑拆成独立模块保持对外接口不变同时让新模块可以被单元测试直接覆盖。任务本身不复杂但是涉及 config、loader、watch、cli、plugin 五个模块的联动。一开始 Claude 的表现很不错它先扫描了所有相关文件读完目录结构之后给出了实施方向。如果没有 TodoWrite接下来就是事故现场。它在改到第三个模块 loader 的时候为了代码“更优雅”擅自引入了新的错误处理约定把原来散落在各个模块的 Result 风格统一换成了异常抛出。单独看每一步代码都没问题甚至很漂亮但合在一起整个 cli 模块原本依赖的错误捕获逻辑全乱了。问题不在 Claude 不够聪明而在于它把“让 loader 更优雅”这个子目标当成了整个任务的最终目标。这种局部最优吞噬全局目标的现象我在长任务里见到过很多次而且越复杂的任务越明显。1.2 上下文窗口与注意力漂移初始目标会被淹没这个现象背后有 LLM 注意力机制的影子。在长会话里模型对上下文的注意力并不是均匀分配的更靠近末尾的对话和最近的工具执行结果往往权重更高。早期会话里那句“保持对外接口不变”在几百行工具输出之后已经很难对每一步决策形成足够的约束。做过搜索系统或者推荐系统的读者应该很好理解这是一次典型的“注意力漂移”。把目标写在对话开头就好比把一张便签贴在房间进门处人在客厅待久了根本看不见。Agent 也一样它不会凭空记住一个写在很久之前的约束除非这个约束被反复强化。这就是任务规划工具的切入点把目标拆成清单变成可以随时复查的显式状态。TodoWrite 每一次被调用都相当于把这张便签重新移到 Agent 眼前让它在做下一步决策前先对一下答案。1.3 计划不是给 Agent 的“紧箍咒”是给它的一副“导航路书”有人可能会担心给 Agent 列计划是不是限制了它的灵活性我一开始也这么想后来发现恰恰相反。没有清单的时候Claude 确实“自由”但这种自由是它被局部目标牵着走的自由有了清单之后它在步骤层面仍然可以自由选择实现方式只是不再允许偏离全局方向。这有点像给司机配导航导航不会规定你每个路口怎么踩油门但它会在你走错方向时提醒你。计划真正兜住的是 Agent 的全局一致性。所以我把 TodoWrite 在项目里的定位理解为一副路书——它把“我们最终要到哪”写进了 Agent 手里每一步决策都能回到这个列表上对答案。有了路书Agent 才能真正放心大胆地走因为它知道自己不会因为走太远而迷路。2. TodoWrite 的机制拆解它能记住什么又记不住什么2.1 工具调用长什么样title 与 todos 两个核心参数在 Claude Code 的日常交互里我通常不会手写 TodoWrite 的原始调用Claude 会在合适的时机自动调用它。但要想高效使用还是得看懂它的入参格式。从底层看它是 Claude Code 提供的一个工具调用function call核心参数有两个title 和 todos。title 是这张清单的名字用于在上下文里快速定位这张表todos 是一个字符串数组每一项是一条待办。Claude 的标记规范里支持 checkbox 语法未完成的用- [ ]完成之后改成- [x]。我平时在日志里看到的调用大概长这样{ tool: TodoWrite, title: 重构 config 模块并保持对外接口兼容, todos: [ - [ ] 梳理 config 模块现有接口与调用方, - [ ] 设计新模块目录结构与导出入口, - [ ] 迁移 loader 中的配置读取逻辑, - [ ] 补齐 watch 模块对配置变更的订阅, - [ ] 编写并跑通全量回归测试 ] }这里需要特别提醒一点todos 里的每一项本身应该是一句可以明确判断“完没完”的话而不是一个模糊的动作。“迁移 loader 中的配置读取逻辑”比“优化 loader”强得多因为前者完成与否可以立刻验证。2.2 它不是数据库而是上下文里的状态对象TodoWrite 最关键的性质是它把清单作为一份动态状态写进了 Claude 的对话上下文。Claude 在后续执行中每完成一项就会再次调用 TodoWrite把对应项的- [ ]改成- [x]然后再继续下一步。也就是说Claude Code 的规划能力并不来源于任何外部数据库而是通过反复把这个最新清单展示在上下文里让后续决策随时有机会回到这份路书上来。理解了这一点也就理解了它的边界清单不会持久化。你退出终端再开一个新会话上次的清单就没了它也不会被写进项目的某个文件除非你自己重定向输出或者用笔记记录。它更不是项目管理工具的替代品。别指望用 TodoWrite 管理跨多天的复杂项目它活在会话里服务的是单次对话中的执行一致性不是跨会话的工程管理。2.3 自动触发与主动干预别把规划完全交给 AgentClaude Code 本身有较强的任务规划自觉性遇到多步骤任务时通常会主动调用 TodoWrite。但我在实际使用中发现自动触发的清单质量并不稳定有时候它把任务拆得过细二十几个待办塞满上下文有时候又拆得太粗一个“处理配置兼容”直接涵盖了五个不同的约束。因此我更倾向于在任务开头就把规划要求讲清楚。比如我会在提示词里写“请先阅读仓库结构然后用 TodoWrite 建立 5 到 8 项任务清单每项必须包含可验证的完成标准。”这比事后修正清单要省得多。主动干预的另一个手段是终端里的 /todo 命令具体命令名以你当前 Claude Code 版本为准可以用claude --help确认。它可以把我当前上下文里的任务清单打印到屏幕上让我在对话任何时刻直接检查 Claude 的理解是否还在正轨上。发现问题就当场让它改不要等代码写完了再纠偏。3. 实战复盘一次完整重构任务中 TodoWrite 是怎么被用起来的3.1 任务背景与初始清单的建立为了把前面的原理讲透我拿一个最近真实跑过的任务做完整复盘。项目代号就叫 internal-cli是一个内部命令行工具功能比较简单读取配置文件、监听文件变更、触发插件化的任务流。这次任务的目标是把配置加载逻辑抽成独立模块并保持对外接口完全不变。我拿到需求之后的第一件事不是让 Claude 直接开干而是先让它读了项目根目录的 README 和 package.json搞清楚模块边界然后明确拒绝了它一开始看似周到的方案因为那个方案引入了新的配置格式。在我把“不要引入新配置格式”这个约束写进提示词之后Claude 重新调用了 TodoWrite任务清单如下{ tool: TodoWrite, title: 抽取配置模块internal-cli 重构计划, todos: [ - [ ] 盘点所有引用 config 加载逻辑的文件, - [ ] 在 src/config 目录下建立新模块并迁移基础加载函数, - [ ] 保持原入口文件导出名称不变更新内部引用路径, - [ ] 接入 watch 模块并验证热更新逻辑不受影响, - [ ] 编写覆盖新旧两套路径的回归测试用例, - [ ] 跑通 lint、typecheck 与全量测试 ] }注意这个清单里没有任何一项是“重构配置模块”这种大而空的说法每一项都能对应到一份文件、一个行为或一个验证动作。3.2 执行中清单的动态更新何时新增、何时勾掉整个任务全程Claude 一共调用 TodoWrite 七次。第一次建立清单随后每完成一块就更新一次。真正值得展开的是第三次和第五次的更新。第三次更新发生在迁移基础加载函数时。Claude 发现 loader 里有一段旧的环境变量合并逻辑被全局共享如果直接迁移会影响 watch 模块。如果按原始清单硬着头皮做完这个隐患会在最后一步测试时才暴露。但因为清单写得足够细它在执行当前项时立刻发现了边界问题于是没有把它当成一个“临时想法”随手处理而是先调用了一次 TodoWrite在清单里追加了一项- [ ] 拆分环境变量合并逻辑并单独验证兼容行为。这一步很关键——不是阻止 Agent 发现问题而是让新发现的问题进入显式规划而不是变成一次临时起意的代码改动。第五次更新在测试环节。两个模块的新旧路径测试都通过后Claude 把对应几项全部改成了- [x]但没有一次性把所有项都勾掉而是保留了最后一项“跑通 lint、typecheck 与全量测试”作为收尾检查点。这种“最后一个检查点不提前勾掉”的习惯我后来也沿用了下来。清单里的最后一项往往承担着对齐全局质量的责任提前勾掉等于默认路径畅通这在重构任务里是很危险的假设。3.3 同样任务没有清单时会发生什么为了说明差异我在同一个仓库里做过对照实验不写规划要求直接把任务描述丢给 Claude。结果是它在一开始也生成了一个类似 TodoWrite 的清单但因为我没有强调约束这个清单在第二次更新时就被悄悄改掉了——原来的“保持对外接口不变”消失取而代之的是几个“优化调用方式”。这就是我反复提到的局部目标吞噬全局目标的过程。对照实验当然不等于完全科学的结论但至少能说明如果 Agent 的清单本身就是漂移的它做出来的结果也必然漂移。TodoWrite 提供的不是一个静态的文件而是一条时时更新的约束流。它之所以有效是因为它把最初的目标反复拉回到当前决策点前面让漂移很难跨过步骤之间的缝隙。4. 拆解任务的方法论我是怎么把一个模糊需求变成可勾选清单的4.1 先定检查点再拆动作很多人在让 Agent 列计划时下意识列出的是一堆动作“读取文件”“重写函数”“跑测试”。这些不是目标只是路径。我自己摸索出的顺序是先定检查点这个任务完成到哪一步我可以判断“它已经做完了”。把这些可验证的检查点找出来再去补充每个检查点之间的动作。比如“抽取配置模块”这个任务检查点依次是所有引用路径已更新、旧接口行为不变、测试覆盖新旧两条路径、全量测试通过。每个检查点本身就是一个清单项。动作细节不需要进清单那是执行阶段分布的视角清单只需要勾勒出这一路的里程碑。这个习惯帮我解决了一个长期困扰任务描述一旦超过三句话我自己都会忘记当初想让 Agent 做什么更别提要求它保持方向了。4.2 每一项都要自带“完成标准”模糊动词是任务清单失效的第一原因。“优化”“调整”“完善”“重构”这些词本身没有完成边界Agent 做多做少都无法判断结果就是它什么时候收手完全看心情。我在列清单时会强迫自己把每个动词改成可以验证的表述常见做法是把动词换成“盘点、迁移、替换、验证、跑通”这类带明确动作对象的词并在末尾补上约束对象。给大家一个直观的对比表模糊清单项可验证清单项优化 loader 逻辑迁移 loader 配置读取逻辑保持现有接口签名不变处理配置兼容盘点所有旧配置字段的解析路径并补齐新模块映射完善测试编写覆盖新旧两套配置路径的回归用例重构 watch 模块接入新模块并验证文件变更能正常触发订阅回调这张表背后真正的原则是清单的每一项都应该能在十分钟之内被检验完成状态。如果一个清单项需要你坐下来思考良久才能判断做没做完那它就还是太大需要继续拆。4.3 任务粒度与清单长度的经验值拆到什么程度最好我根据这段时间的实测给出一个可参考的经验区间一个普通规模的功能任务或重构任务清单控制在 5 到 10 项。少于 5 项说明内部大概率藏着大石头多于 10 项清单本身就会变成新的认知负担同时也会消耗更多上下文窗口。单看一项的粒度理想情况是让 Claude 花几分钟到半小时完成最长不超过一两个小时。如果一项任务连续执行半小时还没有任何进展反馈说明它应该被拆成两项甚至三项。这不是一个硬性公式不同任务的颗粒度差异很大但可以作为日常使用时的一个自查标准。我自己每次拿到新任务都会先在心里过一遍这个数计划里有没有超过十项的大清单有没有一项能撑半天的巨型任务有一项就说明规划环节还没做到位。5. 实测中踩过的坑TodoWrite 不是万能的5.1 清单膨胀已完成项也会反向污染判断TodoWrite 每次更新都会把整张清单完整写回上下文这意味着清单越长每次更新消耗的 token 越多也更挤占上下文空间。我在一次跨模块改造中发现Claude 到后半程开始频繁调用文件搜索而不是直接根据已有信息判断一个重要原因就是前面累积了二十多项的历史待办把关键信息挤得太散。解决方式很简单定期重写清单。每当任务完成到一个阶段我会要求 Claude 把已完成的大块任务合并成一行历史记录然后只保留当前阶段需要盯着的待办。保持清单在 8 项以内往往比贪心记录所有历史更有效。这里有个细节不需要每次更新都全量重写只要在感觉上下文变臃肿时做一次“瘦身”即可。过度频繁地重写清单反而会把 Claude 的注意力从实现引到规划本身上。5.2 把它当成需求文档细节全部塞进去是个误区有人喜欢在 TodoWrite 里写很长的描述把约束、背景、验收条件全部塞进一个清单项结果是清单读起来像一篇小作文。其实 TodoWrite 应该只负责“接下来要做哪几件事”至于这件事的具体约束和背景有两种更好的放置方式一是写在项目代码周围的注释和说明里让 Claude 在做文件操作时自然读到二是放在对话的上下文里作为清单项的补充描述。清单一旦承担了所有信息携带的任务就会变得笨重Claude 每次思考都要从长篇细节里重新提取关键点反而容易漏掉真正的约束。我在早期犯过这个错一次数据库迁移任务里某个清单项写了近百字包含连接池配置、超时时间、重试规则。结果 Claude 在执行时只记住了其中一条其他全被忽略。从那以后我的清单项永远控制在二十字以内长约束一律放正文。5.3 多会话场景下的清单延续问题Claude Code 支持对话续接但 TodoWrite 的清单并不会跨会话保留。如果你开了一个新会话想继续昨天的重构任务直接说“继续上次的工作”Claude 往往只能根据 git 历史和你贴出来的信息重新理解目标。此时最好在最开始时明确要求它先阅读仓库状态再重建一份与当前进度一致的 TodoWrite 清单。跨天执行长任务时我还有个习惯结束会话前让 Claude 把当前清单以纯文本形式输出到终端我自己复制到项目根目录的一个 todo.md 里。第二天新会话开始时把这份 todo.md 的内容粘回去让它在此基础上继续。用文件作为持久化层恰好补上了 TodoWrite 不落盘的短板。这个方法也方便团队协作另一个同事接手时第一眼就知道这个任务当前卡在哪一步。5.4 计划先行并不等于不再调整清单要敢改最后一个坑是我自己最容易犯的过度相信初始计划。TodoWrite 的价值在于让目标显式化但它并不神圣。执行中一旦发现新的约束或更好的技术路径应该及时要求 Claude 更新清单而不是为了保持“计划完整”而强行走完原本的步骤。有一次重构中Claude 提出可以复用仓库里某个闲置工具函数避免额外引入依赖。如果我不让它改清单原本计划中“新增一个 helper 模块”这一项就会变成无效工作。让 Agent 修改清单和让 Agent 死守清单是两种不同的使用方式。前者是把它当协作者后者是把它当执行器。至少在 TodoWrite 这个工具的设计哲学里前者明显更贴合它的能力边界。计划是拿来用的不是拿来供着的该增就增该并就并只要最终目标还是那个目标就行。最后说点我现在的使用习惯。每次把新任务交给 Claude Code 前我都会先自己花两分钟在脑子里过一遍检查点然后把“请建立 5 到 8 项可验证的 TodoWrite 清单”直接写进任务描述。这不是不相信它的规划能力而是让规划一开始就经过人的经验校验。这一期 S03 写到这里下一期我打算聊聊 Claude Code 的上下文管理策略——也就是当项目大到你明显感觉它“够不着”时应该怎么喂信息。欢迎有同样实践经验的读者来评论区交流各自的清单模板互相抄抄作业。