Learn Claude Code

Python 复刻agents/s03_todo_write.py

跑长任务时 Claude 自己写一份结构化清单——每条待办、当前状态、做完打勾。这份清单是写到内存里的一份工作板,模型每隔几轮回头看一眼,避免任务长到一半忘了前面还有什么没做。


3.1 清单是什么

清单是一份外置的目标记录。模型在长任务里有一个已知的弱点:注意力会偏向最近的内容,看到后面就开始忘前面。一份 phase 1 执行计划有七个脚本要按依赖顺序跑——do0 装包并设主口径开关、do1 灌华证 ESG、do2 灌 CSMAR 财务 + 治理 + KZ + Soe、do3 合并 MDA 版管理者短视主义、do4 拼面板含 PC 主口径构造、do5 描述统计、do6 基准回归——等跑到 do6 时模型的注意力在最近做完的 do5 描述统计上,前面 do2 里"剔除金融业 J""ROA 不可得用 Cash 替代""WW 不可得用 KZ 替代"这种细节已经离它的注意力焦点很远。

清单解决这件事的方式是把任务目标变成一份独立可查的状态记录。每条待办是一个结构化条目,含编号、内容描述、当前状态。状态分待办、进行中、完成三种。模型把清单写到工具调用对应的内存表里,下一轮可以读回,不依赖自己的注意力。

3.2 清单怎么用

模型调用 TodoWrite 这个工具来读写清单。每次调用时传入的是一份完整的新清单——含所有待办的最新状态,不是只传变更的那部分。代码把这份新清单写到状态文件里,把旧的整份覆盖掉。

同时把清单的"渲染版本"作为工具结果回传给模型。渲染版本是你在屏幕上看到的那种带勾、带状态颜色的清单格式。这样模型在下一轮看到清单的当前样貌,就跟你看到的视角一致。

清单的状态文件不随会话关闭而保留。本会话结束清单也跟着结束。需要跨会话保留任务的场景用第 07 节讲的任务系统。

3.3 一次真实运行的清单演化

下面这段记录的是 phase 1 那次三小时会话里清单的关键演化点。模型在第 1 轮就把七步全部写进清单,后续每跑完一个 do-file 就更新一次状态。

轮 1:用户说"按 docs/01_实证研究设计.md 跑 phase 1 七个 do-file"。模型调用 TodoWrite 一次性写出七条:

- [ ] do0_setup:装 reghdfe / estout / winsor2,设主口径开关(PC=A2, ESG=华证 0-1, M=MDA, FC=KZ)
- [ ] do1_esg_import:灌华证 ESG(按年度年末值,0-1 标准化,含 E/S/G 三分项)
- [ ] do2_csmar_vars:灌 CSMAR 财务 + 治理(Size/Lev/Cash/Growth/Top1/Hindex/Soe/Age/KZ)
- [ ] do3_myopia_merge:合并 MDA 版管理者短视(胡楠 2021 词频)
- [ ] do4_panel_assemble:拼主面板 + PC 主口径构造 + 样本筛选
- [ ] do5_desc_corr:表 1 描述统计 + 表 2 Pearson 相关性
- [ ] do6_baseline:表 3 基准回归 + 表 4 ESG 三分项 + 表 5 PC 内部分解

轮 12:do0 跑完,模型更新清单——把 do0 改成 [x],do1 改成 [~](进行中),其它保持 [ ]。do0 的关键产物(reghdfe / ftools / estout / winsor2 全部 OK)作为附注挂在该条目下。

轮 38:do4 跑到中间步骤。模型在工具结果里看见 PC 七合一原始 76305 obs,缩尾后保留同样行数,merge ESG + CSMAR + Myopia 后保留 76305 行;样本筛选阶段陆续 drop —— 2009-2023 后 →(保留) → 剔金融 J → 剔 ST → 关键变量非缺失 → 控制变量 1%/99% winsor → 最终 26441 行。模型把"剔金融 J""剔 ST""关键变量非缺失"这几条作为子节点挂在 do4 下。

轮 47:跑 do6 时模型回头查清单。它注意到清单里 do4 的子节点写着"PC 主口径 = z(SD_A2_ln) + z(Bank)",于是 reghdfe 命令里 PC 解释变量名直接用 PC(不是 PC_A1)。如果没有这条状态记录,跑到 do6 时模型可能记不清主口径名字、得回去查 do4 源码。

轮 51:do6 跑完,三张表(table3_baseline.rtf, table4_esg_subscores.rtf, table5_pc_decomp.rtf)落盘。模型把 do6 改成 [x],并在子节点里附上关键结果:PC(A2 主) β=0.0004(不显著)、PC(A1) β=0.0018***、PC(C 熵值法) β=-0.0024(不显著);E 分项与 G 分项 PC 系数也不显著;股权侧 vs 债权侧检验 p=0.0002。

轮 52:清单七条全部 [x],模型用一段总结向用户回报 phase 1 完成。这条总结之所以能写得稳准——观测数对得上、关键发现摆得明白、跨脚本的依赖说得清楚——靠的就是清单一直摆在那儿、每跑完一步都更新一次。

3.4 整份替换的设计

每次提交一份完整快照,不走增量补丁的形式。目的是让清单状态在任何一个时点都是一致的。如果允许增量更新,比如只把第 5 条标完成、其它条目不重传,模型可能在更新过程中遗漏其它字段,让清单进入不一致状态。整份替换强制每次提交都是完整的,要么全部正确要么全部失败,没有中间状态。

代价是模型每次写清单时必须重写所有条目,对它的注意力是一种负担。但这种负担让清单状态可靠,长任务里值得。

清单系统还有一条隐藏的协议:Claude Code 每隔几轮在对话里塞一条系统提醒"请回头查清单"。这条提醒不显示给你看,是直接注入对话里给模型看的。靠这条提醒,模型在长任务中后段也能想起回头查 do4 拼面板时具体剔除了哪些样本、do6 里 firm 和 year 双向固定效应是否都加了。

3.5 容易踩的坑

整份替换的语义意味着模型漏写一条等于把它从清单里删掉。这种漏写在长任务里偶尔会发生,没法靠代码层防御,只能靠模型自己规范。

清单不持久化跨会话。今天关掉 Claude Code 明天回来开新会话,原来那份清单已经不在了。如果你的实证推进跨周——phase 1 七个 do 一周跑完、phase 2 四个 do 下周开干——应该用任务系统,TodoWrite 不适合这种场景。

优先级排序由模型主观决定。模型看上下文判断什么先做,你可以显式说"先按 do0→do6 顺序跑",但如果你没说清楚模型自己挑顺序,结果可能不合你的预期——比如它跳过 do3 直接拼面板,导致 myopia 字段缺失、do4 的 merge 1:1 寡淡无果。

3.6 知识地图

关键词含义容易误会的点
TodoWrite模型用来读写清单的工具是给模型用的工具,不是给用户的功能;用户只是看渲染结果
状态字段标识每条是待办、进行中、完成"进行中"不限于一条;模型可以并行标几条进行中
整份替换每次提交都是完整新清单覆盖旧的漏写一条等于删除,不是 append
系统提醒每隔几轮注入"请查清单"的内部消息没这条提醒模型在长任务里会忘掉回查清单