ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

手搓Claude Code-第五章 todo_write:把 agent 任务清单落到本地文件

手搓Claude Code-第五章 todo_write:把 agent 任务清单落到本地文件 1. 为什么你的 agent 跑着跑着就忘了自己要干什么如果你正在自建 agent尤其是做 Claude Code 这类编码助手大概率遇到过这个场景给模型下了一个多步任务比如「重构 hello.py加类型注解、补 docstring、加 main guard」。模型第一步干得挺好第二步遇到一个 import 报错然后它就开始疯狂查这个报错查着查着类型注解忘了、docstring 忘了最后给你返回一个「已修复 import 问题」——你原本的任务它压根没做完。这不是模型笨是 Transformer 架构本身的注意力机制在长上下文里会涣散。注意这跟上下文窗口被打满是两码事。上下文窗口是物理截断超了就丢注意力涣散是即使内容还在窗口里模型对早期约束的权重也会衰减。表现一样条件 A 被忽略执行任务 C 时就没有约束。todo_write 就是工程上缓解这个问题的一个手段给 agent 一张待办表让它每做几步就回头更新一次用外部文件把「我该干什么」这件事钉住而不是指望模型自己记住。这一章我就把 todo_write 的 JSON schema、读写本地 todo 文件的完整代码、以及怎么用一次多步任务验证增删改查是否生效全部拆开讲一遍。适合已经在写 agent loop、想加规划能力的开发者小白也能跟着敲。核心检索词先摆出来Claude Code 的 todo_write 工具本质是一个让 agent 维护任务清单的函数调用它把「规划」从模型脑子里搬到本地文件里适合所有自建 agent 的场景。2. 接入前的准备TaoToken 与运行环境在动手写 todo_write 之前得先有一个能稳定调用 Claude 系列模型的通道。我这边用的是 TaoToken它提供兼容 Anthropic 接口的调用方式Base URL 和 Key 配好就能直接跑 Claude Code 或自建 agent。官网入口在 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content API 地址是 https://taotoken.net/api 注意 API 这条不加 UTM 参数。环境上你需要准备三样东西Python 3.10 以上、anthropic 官方 SDK、以及一个能写文件的本地目录。我建议单独建一个工作目录比如~/learn_claude_code/s05_todo_write因为 todo_write 会把任务清单落到本地文件目录乱了后面排查很痛苦。安装依赖就一行pip install anthropic然后配置环境变量。这里有个坑很多人直接把 Key 写死在代码里调试时改来改去容易漏。我习惯用.env加python-dotenv但为了让你复制就能跑下面直接读环境变量export ANTHROPIC_BASE_URLhttps://taotoken.net/api export ANTHROPIC_API_KEY你的Key如果你用的是 Claude Code 本体配置在~/.claude/settings.json里长这样{ env: { ANTHROPIC_BASE_URL: https://taotoken.net/api, ANTHROPIC_API_KEY: 你的Key, ANTHROPIC_MODEL: claude-sonnet-4-20250514 } }注意这里三件套必须齐全Base URL、Key、Model ID。少任何一个请求都会以 401 或 model not found 结束。Model ID 要跟你账号里可用的模型对齐别照抄别人的。自建 agent 这边初始化 client 的代码是import os from anthropic import Anthropic client Anthropic( base_urlos.environ[ANTHROPIC_BASE_URL], api_keyos.environ[ANTHROPIC_API_KEY], ) MODEL os.environ.get(ANTHROPIC_MODEL, claude-sonnet-4-20250514) WORKDIR os.path.abspath(./workspace) os.makedirs(WORKDIR, exist_okTrue)WORKDIR 一定要用绝对路径。我踩过的坑是用了相对路径agent 在子进程里执行时工作目录变了todo 文件写到了别的地方找半天找不到。绝对路径能省掉这类玄学问题。到这一步通道和环境就齐了。接下来才是本章的重点todo_write 本身怎么设计。3. todo_write 的 JSON Schema 与本地文件读写实现todo_write 的本质是一个工具函数模型通过 tool_use 调用它传入一个 todos 数组函数把数组标准化后写进本地文件同时返回一个简短的结果字符串给模型。整个设计分四块schema 定义、输入标准化、文件读写、注册到工具表。先说 schema。这是给模型看的「说明书」required 字段是在向模型强调哪些参数必须生成{ name: todo_write, description: Create and manage a task list for your current coding session., input_schema: { type: object, properties: { todos: { type: array, items: { type: object, properties: { content: {type: string}, status: { type: string, enum: [pending, in_progress, completed] } }, required: [content, status] } } }, required: [todos] } }status 只有三个合法值这个 enum 很关键。模型有时候会自己发明done、finished这种状态enum 能把它约束回来但约束不保证 100% 生效所以后端还得再校验一次。接下来是输入标准化。模型返回的 todos 可能是字符串、可能是 Python 字面量、也可能是残缺结构得统一转成 list[dict]import json import ast def _normalize_todos(todos): if isinstance(todos, str): try: todos json.loads(todos) except json.JSONDecodeError: try: todos ast.literal_eval(todos) except (SyntaxError, ValueError): return None, Error: todos must be a list or JSON array string if not isinstance(todos, list): return None, Error: todos must be a list for i, t in enumerate(todos): if not isinstance(t, dict): return None, fError: todos[{i}] must be an object if content not in t or status not in t: return None, fError: todos[{i}] missing content or status if t[status] not in (pending, in_progress, completed): return None, fError: todos[{i}] has invalid status {t[status]} return todos, None为什么先用 json 再用 ast因为 json 对标准格式更严格能挡住大部分脏输入ast 容错性更强能解析单引号、元组这类 Python 独有语法。只用 ast 的话模型输出再离谱都能混进来隐藏 bug 会变多。两层兜底先严后宽是实践中比较稳的顺序。然后是文件读写。todo 落到本地文件路径固定在 WORKDIR 下的.todos.jsonimport os TODO_FILE os.path.join(WORKDIR, .todos.json) CURRENT_TODOS: list[dict] [] def _save_todos(todos): with open(TODO_FILE, w, encodingutf-8) as f: json.dump(todos, f, ensure_asciiFalse, indent2) def _load_todos(): if not os.path.exists(TODO_FILE): return [] try: with open(TODO_FILE, r, encodingutf-8) as f: return json.load(f) except (json.JSONDecodeError, OSError): return [] def run_todo_write(todos) - str: global CURRENT_TODOS todos, error _normalize_todos(todos) if error: return error CURRENT_TODOS todos _save_todos(todos) lines [\n## Current Tasks] for t in CURRENT_TODOS: icon {pending: , in_progress: , completed: x}[t[status]] lines.append(f [{icon}] {t[content]}) print(\n.join(lines)) return fUpdated {len(CURRENT_TODOS)} tasks注意_load_todos在启动时调用一次把上次会话的清单恢复回来。这样 agent 重启后不会丢进度长任务续跑很实用。最后注册到工具表TOOLS [{ name: todo_write, description: Create and manage a task list for your current coding session., input_schema: { ... } # 上面那段 schema }] TOOL_HANDLERS { todo_write: run_todo_write, }到这里todo_write 的读写闭环就完成了。模型调用它它写文件、打印、返回结果模型下一轮就能看到「Updated N tasks」。4. 在 agent loop 里验证增删改查是否生效光有工具不够得让 agent loop 主动去用它。核心逻辑是每三轮提醒模型更新一次待办表一旦模型调用了 todo_write计数器归零。rounds_since_todo 0 def agent_loop(messages: list): global rounds_since_todo while True: if rounds_since_todo 3 and messages: messages.append({ role: user, content: reminderUpdate your todos./reminder }) rounds_since_todo 0 response client.messages.create( modelMODEL, systemSYSTEM, messagesmessages, toolsTOOLS, max_tokens8000, ) messages.append({role: assistant, content: response.content}) if response.stop_reason ! tool_use: return rounds_since_todo 1 results [] for block in response.content: if block.type ! tool_use: continue handler TOOL_HANDLERS.get(block.name) output handler(**block.input) if handler else fUnknown: {block.name} if block.name todo_write: rounds_since_todo 0 results.append({ type: tool_result, tool_use_id: block.id, content: output, }) messages.append({role: user, content: results})system 提示词也要改权重高得让模型重视待办表SYSTEM ( fYou are a coding agent at {WORKDIR}. Before starting any multi-step task, use todo_write to plan your steps. Update status as you go. )现在验证。先建一个测试文件workspace/hello.pydef greet(name): print(Hello, name) greet(world)然后给 agent 下任务messages [{ role: user, content: Refactor workspace/hello.py: add type hints, docstrings, and a main guard. }] agent_loop(messages)跑起来后观察终端。正常流程是模型先调 todo_write 列出三条任务状态都是 pending然后开始改文件改一条就把对应任务标 in_progress改完标 completed最后返回「All tasks are complete」。验证增删改查是否真的生效直接看.todos.jsoncat workspace/.todos.json你应该能看到类似这样的内容[ {content: Add type hints to greet(), status: completed}, {content: Add module and function docstrings, status: completed}, {content: Add main guard, status: completed} ]如果模型中途新增了任务比如发现要处理 import数组里会多一条。这就是「增」。改状态是「改」删任务一般发生在模型重新规划时会传一个更短的数组覆盖。查就是每次_load_todos恢复。实测下来加了 todo_write 之后模型跑偏的概率明显下降尤其是三步以上的任务。代价是 token 消耗变高因为每轮都要带上待办表而且每三轮多一次提醒。这个取舍后面会讲。5. 常见报错排查401、local proxy failed、reading choices、OAuth自建 agent 接 Claude 时报错集中在几个地方。我按真实遇到的顺序列一下。401 Unauthorized。最常见八成是 Key 没配或配错。检查ANTHROPIC_API_KEY是否生效注意别把 Base URL 和 Key 搞混。如果你用的是 Claude Code 本体检查~/.claude/settings.json里的 env 块三件套 Base URL、Key、Model ID 是否齐全。少 Model ID 有时不报 401而是报 model not found别被误导。local proxy failed。这个报错通常出现在你本地起了代理但没起来或者环境变量指向了一个不存在的本地端口。检查ANTHROPIC_BASE_URL是不是被别的工具改成了http://localhost:xxxx。正确值应该是https://taotoken.net/api。如果你同时装了多个 AI 工具它们可能互相覆盖环境变量用echo $ANTHROPIC_BASE_URL确认一下。reading choices 相关报错。这个一般出现在你混用了 OpenAI 格式的 SDK 去调 Anthropic 接口。Anthropic 的响应结构是content数组不是choices。检查你用的是anthropic包而不是openai包client 初始化方式也要对应。OAuth 相关报错。Claude Code 本体走的是 OAuth 登录如果你手动改了配置又没清缓存会报 token 失效。解决办法是删掉~/.claude/下的凭据缓存重新登录或者干脆用 API Key 模式绕开 OAuth。自建 agent 一般不涉及 OAuth如果你遇到了说明你可能在混用两套认证。todo_write 返回 Error: todos must be a list。这是模型输出格式不对_normalize_todos挡住了。看模型传进来的原始内容多半是它把 todos 包成了{todos: [...]}而不是直接传数组。检查 schema 里required: [todos]有没有写对以及 handler 调用时是不是handler(**block.input)**会把{todos: [...]}展开成todos[...]这个细节错了就会一直报错。文件写不进去。检查 WORKDIR 是否存在且有写权限。用绝对路径别用相对路径。如果.todos.json一直不生成在_save_todos里加个 print 看有没有被调用。排查顺序建议先确认 401 和 Base URL再确认 SDK 类型最后看 todo_write 自己的逻辑。大部分问题在前两步就能定位。6. 把 todo_write 用起来从验证到长期编码todo_write 跑通之后你会发现它不只是个「待办表」它其实是 agent 的短期记忆锚点。模型每轮都能从.todos.json里读到「我该干什么」注意力涣散的影响被外部文件抵消了一部分。如果你想把它用到长期编码或 Agent 场景建议配合 Coding Plan 一起用入口在 https://taotoken.net/api 对应的 coding-plan 页面适合需要连续多轮、跨会话的任务。验证模型本身的能力可以直接去模型对话页面试接入和排障的细节看接入文档和 API Keys 页面就够了。最后留一个我自己的经验todo_write 的提醒频率别设太密。三轮一次是原项目的选择我试过每轮都提醒token 消耗翻倍效果提升有限。另外todo 文件建议加个时间戳字段方便你回溯模型是什么时候改的清单排查「它为什么突然换任务」这类问题时特别有用。
返回列表