 ClaudeCode:learn-claude-code 項(xiàng)目實(shí)戰(zhàn)筆記(3)TodoWrite 待辦寫(xiě)入與 TaoToken 配置)
1. 為什么長(zhǎng)鏈路任務(wù)里模型總在“裝忙”TodoWrite 要解決的真實(shí)痛點(diǎn)如果你跟著 learn-claude-code 的 s01、s02 一路寫(xiě)下來(lái)會(huì)發(fā)現(xiàn)一個(gè)很尷尬的現(xiàn)象單步任務(wù)讀文件、改一行、跑個(gè) bash它干得挺利索可一旦你丟給它“重構(gòu) hello.py加類(lèi)型注解、補(bǔ) docstring、再加 main guard”這種多步任務(wù)它就開(kāi)始表演了。前三步做得像模像樣第四步突然回頭把第一步又做了一遍或者干脆跳過(guò)某一步直接宣布“已完成”。這不是模型笨而是長(zhǎng)鏈路任務(wù)里上下文被工具結(jié)果不斷填滿(mǎn)系統(tǒng)提示的約束力被稀釋模型對(duì)“我現(xiàn)在做到哪了”這件事失去了清晰感知。TodoWrite 這個(gè)模塊要干的事說(shuō)白了就是給 Agent 裝一塊白板。模型每做一步必須先在白板上寫(xiě)清楚哪些任務(wù) pending、哪個(gè) in_progress、哪些 completed。這塊白板不依賴(lài)對(duì)話歷史而是獨(dú)立存在的一個(gè) Python 對(duì)象每次工具調(diào)用后把渲染結(jié)果塞回給模型看。這樣一來(lái)哪怕對(duì)話已經(jīng)滾了二十輪模型抬頭就能看到“哦任務(wù) 2 還在進(jìn)行中任務(wù) 3 還沒(méi)開(kāi)始”不會(huì)跑偏。我實(shí)測(cè)下來(lái)加了 TodoWrite 之后一個(gè) 6 步的 Python 包創(chuàng)建任務(wù)完成率從原來(lái)的“做一半就開(kāi)始即興發(fā)揮”變成了基本能按順序走完。關(guān)鍵不在于模型變聰明了而在于它有了一個(gè)外部的、結(jié)構(gòu)化的狀態(tài)錨點(diǎn)。這篇筆記就帶你從零把這個(gè)模塊寫(xiě)出來(lái)同時(shí)把 TaoToken 的 Key 和 API 通道配好讓 ClaudeCode 能真正跑起來(lái)。適合誰(shuí)看已經(jīng)寫(xiě)過(guò) s01/s02 的讀者或者手頭有一個(gè)能調(diào)通的 Agent loop、想加上任務(wù)規(guī)劃能力的開(kāi)發(fā)者。如果你還沒(méi)配過(guò) API 通道第三節(jié)的配置可以直接抄。2. TaoToken 前置統(tǒng)一 Key 與 API 通道怎么接進(jìn) learn-claude-codelearn-claude-code 的 s03 代碼里客戶(hù)端初始化是這樣的from anthropic import Anthropic import os client Anthropic(base_urlos.getenv(ANTHROPIC_BASE_URL)) MODEL os.environ[MODEL_ID]它讀兩個(gè)環(huán)境變量ANTHROPIC_BASE_URL和MODEL_ID。認(rèn)證 token 走的是ANTHROPIC_AUTH_TOKEN但代碼里有一行很關(guān)鍵if os.getenv(ANTHROPIC_BASE_URL): os.environ.pop(ANTHROPIC_AUTH_TOKEN, None)這行的意思是如果你設(shè)了自定義 base_url它就把 auth token 清掉改用 base_url 里攜帶的通道認(rèn)證。所以我們的接入思路很清晰——把 TaoToken 的 API 地址填進(jìn)ANTHROPIC_BASE_URL把 Key 通過(guò) TaoToken 的通道機(jī)制傳進(jìn)去模型 ID 填MODEL_ID。TaoToken 在這里扮演的角色是統(tǒng)一 Key 和 API 通道你不需要在代碼里硬編碼任何密鑰也不需要為不同模型維護(hù)多套 base_url。一個(gè) Key 走一個(gè)入口模型 ID 決定實(shí)際調(diào)用哪個(gè)模型。對(duì) learn-claude-code 這種教學(xué)項(xiàng)目來(lái)說(shuō)好處是你 clone 下來(lái)之后只改環(huán)境變量就能跑代碼本身一行不用動(dòng)。具體要準(zhǔn)備三樣?xùn)|西第一一個(gè) TaoToken 的 API Key。去控制臺(tái)創(chuàng)建一個(gè)復(fù)制出來(lái)形如sk-開(kāi)頭的一串字符。這個(gè) Key 不要提交到 git放.env里。第二確認(rèn)你要用的模型 ID。比如claude-sonnet-4-20250514或者你賬號(hào)下可用的其他模型標(biāo)識(shí)。這個(gè) ID 會(huì)傳給MODEL_ID環(huán)境變量最終由 TaoToken 通道路由到對(duì)應(yīng)模型。第三API 入口地址。TaoToken 的 API 地址是https://taotoken.net/api注意這個(gè)地址不帶任何查詢(xún)參數(shù)直接作為 base_url 使用。如果你在 Claude Code 或 Cline 這類(lèi)工具里配置Base URL 就填這個(gè)。這里要提醒一句不要把 Key 寫(xiě)死在 Python 文件里。learn-claude-code 用python-dotenv加載.env你就在項(xiàng)目根目錄建一個(gè).env把 Key 和模型 ID 放進(jìn)去。.gitignore里加上.env這是基本操作。配好之后你的 Agent 就有了一個(gè)穩(wěn)定的模型調(diào)用通道。接下來(lái)我們寫(xiě) TodoWrite 的代碼讓它在這個(gè)通道上跑起來(lái)。3. 可復(fù)制配置settings.json 與 config.toml 骨架 TodoWrite 工具注冊(cè)這一節(jié)給你兩份可直接抄的配置骨架以及 TodoWrite 在 Agent loop 里的注冊(cè)方式。先看 Claude Code 側(cè)的settings.json路徑是~/.claude/settings.jsonWindows 是C:\Users\你的用戶(hù)名\.claude\settings.json{ env: { ANTHROPIC_BASE_URL: https://taotoken.net/api, ANTHROPIC_AUTH_TOKEN: sk-你的TaoToken密鑰, ANTHROPIC_MODEL: claude-sonnet-4-20250514, ANTHROPIC_SMALL_FAST_MODEL: claude-sonnet-4-20250514 }, permissions: { allow: [ Bash(python:*), Read, Write, Edit ] } }如果你用的是 Codex 風(fēng)格的config.toml路徑是~/.codex/config.toml骨架如下model claude-sonnet-4-20250514 model_provider taotoken [model_providers.taotoken] name TaoToken base_url https://taotoken.net/api env_key TAOTOKEN_API_KEY [profiles.default] model claude-sonnet-4-20250514 provider taotoken注意env_key指向的環(huán)境變量名你在 shell 里 export 或者寫(xiě)進(jìn).env都行。三件套就是 Base URL、Key、Model ID缺一不可。Cline 的 MCP 配置也是同樣的邏輯Base URL 填https://taotoken.net/apiKey 填你的 TaoToken KeyModel ID 填模型標(biāo)識(shí)?,F(xiàn)在回到 learn-claude-code 的 s03 代碼。TodoWrite 的核心是一個(gè)TodoManager類(lèi)它維護(hù)一個(gè)items列表每個(gè) item 有id、text、status三個(gè)字段。status只允許pending、in_progress、completed三種值而且同一時(shí)間只能有一個(gè)in_progress。這個(gè)約束是硬性的違反就拋ValueError。class TodoManager: def __init__(self): self.items [] def update(self, items: list) - str: if len(items) 20: raise ValueError(Max 20 todos allowed) validated [] in_progress_count 0 for i, item in enumerate(items): text str(item.get(text, )).strip() status str(item.get(status, pending)).lower() item_id str(item.get(id, str(i 1))) if not text: raise ValueError(fItem {item_id}: text required) if status not in (pending, in_progress, completed): raise ValueError(fItem {item_id}: invalid status {status}) if status in_progress: in_progress_count 1 validated.append({id: item_id, text: text, status: status}) if in_progress_count 1: raise ValueError(Only one task can be in_progress at a time) self.items validated return self.render()render()方法把當(dāng)前任務(wù)列表渲染成帶標(biāo)記的文本[ ]表示 pending[]表示 in_progress[x]表示 completed最后附上完成計(jì)數(shù)。這個(gè)渲染結(jié)果會(huì)作為 tool_result 返回給模型模型下一輪就能看到自己的進(jìn)度。工具注冊(cè)部分在TOOLS列表里加一項(xiàng){ name: todo, description: Update task list. Track progress on multi-step tasks., input_schema: { type: object, properties: { items: { type: array, items: { type: object, properties: { id: {type: string}, text: {type: string}, status: {type: string, enum: [pending, in_progress, completed]} }, required: [id, text, status] } } }, required: [items] } }然后在TOOL_HANDLERS里掛上TOOL_HANDLERS { bash: lambda **kw: run_bash(kw[command]), read_file: lambda **kw: run_read(kw[path], kw.get(limit)), write_file: lambda **kw: run_write(kw[path], kw[content]), edit_file: lambda **kw: run_edit(kw[path], kw[old_text], kw[new_text]), todo: lambda **kw: TODO.update(kw[items]), }到這里TodoWrite 的工具注冊(cè)就完成了。模型在需要規(guī)劃多步任務(wù)時(shí)會(huì)主動(dòng)調(diào)用todo工具傳入一個(gè) items 數(shù)組。你的TodoManager校驗(yàn)后存儲(chǔ)并渲染結(jié)果回傳給模型。下一節(jié)我們驗(yàn)證它是否真的生效。4. 驗(yàn)證請(qǐng)求跑通 s03 并確認(rèn)待辦寫(xiě)入生效配置和代碼都就位后先確認(rèn)環(huán)境變量加載正確。在項(xiàng)目根目錄建.envANTHROPIC_BASE_URLhttps://taotoken.net/api ANTHROPIC_AUTH_TOKENsk-你的TaoToken密鑰 MODEL_IDclaude-sonnet-4-20250514然后跑一個(gè)最小驗(yàn)證腳本確認(rèn)通道能通import os from dotenv import load_dotenv from anthropic import Anthropic load_dotenv(overrideTrue) if os.getenv(ANTHROPIC_BASE_URL): os.environ.pop(ANTHROPIC_AUTH_TOKEN, None) client Anthropic(base_urlos.getenv(ANTHROPIC_BASE_URL)) resp client.messages.create( modelos.environ[MODEL_ID], max_tokens100, messages[{role: user, content: reply with OK only}] ) print(resp.content[0].text)預(yù)期輸出就是OK。如果這一步報(bào) 401說(shuō)明 Key 或 base_url 有問(wèn)題先解決再往下走。通道通了之后啟動(dòng) s03cd learn-claude-code python agents/s03_todo_write.py你會(huì)看到提示符s03 。輸入一個(gè)多步任務(wù)Refactor the file hello.py: add type hints, docstrings, and a main guard預(yù)期行為是模型第一輪就會(huì)調(diào)用todo工具創(chuàng)建一個(gè)包含 3 到 4 個(gè)條目的任務(wù)列表其中第一個(gè)標(biāo)記為in_progress其余為pending。終端會(huì)打印類(lèi)似 todo: [ ] #1: Read hello.py [] #2: Add type hints [ ] #3: Add docstrings [ ] #4: Add main guard (0/4 completed)然后模型開(kāi)始執(zhí)行第一個(gè)任務(wù)讀文件、改代碼。每完成一步它會(huì)再次調(diào)用todo把當(dāng)前任務(wù)標(biāo)記為completed下一個(gè)標(biāo)記為in_progress。你會(huì)在終端看到進(jìn)度不斷更新最后的渲染結(jié)果類(lèi)似[x] #1: Read hello.py [x] #2: Add type hints [x] #3: Add docstrings [x] #4: Add main guard (4/4 completed)如果你連續(xù)三輪模型都沒(méi)有調(diào)用todo工具nag reminder 會(huì)注入到 tool_result 里你會(huì)看到模型收到reminderUpdate your todos./reminder后重新調(diào)用 todo 更新進(jìn)度。這個(gè)機(jī)制在agent_loop里通過(guò)rounds_since_todo計(jì)數(shù)器實(shí)現(xiàn)used_todo False for block in response.content: if block.type tool_use: # ... 執(zhí)行工具 ... if block.name todo: used_todo True rounds_since_todo 0 if used_todo else rounds_since_todo 1 if rounds_since_todo 3: results.insert(0, {type: text, text: reminderUpdate your todos./reminder})驗(yàn)證待辦寫(xiě)入是否生效最直接的辦法是看終端輸出里有沒(méi)有 todo:開(kāi)頭的行以及渲染結(jié)果里的[ ]、[]、[x]標(biāo)記是否隨任務(wù)推進(jìn)而變化。如果模型從頭到尾沒(méi)調(diào)用 todo檢查T(mén)OOLS列表里是否正確注冊(cè)了todo項(xiàng)以及TOOL_HANDLERS里是否有對(duì)應(yīng)的 lambda。5. 本篇常見(jiàn)錯(cuò)排查401、local proxy failed、reading choices、OAuth這一節(jié)把我在配 TaoToken learn-claude-code 過(guò)程中踩過(guò)的坑列出來(lái)對(duì)照真實(shí)報(bào)錯(cuò)給解法。401 authentication_error最常見(jiàn)。報(bào)錯(cuò)信息通常是Error code: 401 - {error: {message: Invalid API key}}。原因有三個(gè)Key 復(fù)制時(shí)帶了空格或換行.env里變量名寫(xiě)錯(cuò)比如寫(xiě)成ANTHROPIC_API_KEY而不是ANTHROPIC_AUTH_TOKEN或者 base_url 末尾多了斜杠。檢查.env文件確保ANTHROPIC_AUTH_TOKENsk-xxx沒(méi)有引號(hào)、沒(méi)有多余空格base_url 就是https://taotoken.net/api不要加/v1或尾部斜杠。local proxy failed / connection refused如果你在 settings.json 里配了ANTHROPIC_BASE_URL但本地有殘留的代理設(shè)置可能會(huì)報(bào)local proxy failed。檢查環(huán)境變量里有沒(méi)有HTTP_PROXY、HTTPS_PROXY指向一個(gè)已經(jīng)關(guān)掉的本地端口。在 shell 里unset HTTP_PROXY HTTPS_PROXY再跑。另外確認(rèn)ANTHROPIC_BASE_URL沒(méi)有被其他工具的配置覆蓋。reading choices of undefined這個(gè)報(bào)錯(cuò)通常出現(xiàn)在用 OpenAI 兼容格式調(diào) Claude 模型時(shí)。learn-claude-code 用的是 Anthropic SDK返回結(jié)構(gòu)是response.content不是response.choices。如果你在代碼里混用了 OpenAI 的解析方式就會(huì)報(bào)這個(gè)。檢查你的agent_loop里是不是用了response.choices[0].message.content改成response.content并遍歷 block。OAuth token 相關(guān)報(bào)錯(cuò)如果你之前配過(guò) Claude Code 的 OAuth 登錄環(huán)境里可能殘留ANTHROPIC_AUTH_TOKEN或CLAUDE_CODE_OAUTH_TOKEN。learn-claude-code 的代碼里有一行os.environ.pop(ANTHROPIC_AUTH_TOKEN, None)但如果你在別的地方又設(shè)了 OAuth token可能會(huì)沖突。在.env里顯式清空ANTHROPIC_AUTH_TOKEN留空或者確保只走 TaoToken 的 Key 通道。模型 ID 不匹配報(bào)錯(cuò)model not found或invalid model。檢查MODEL_ID是否是你 TaoToken 賬號(hào)下可用的模型標(biāo)識(shí)。不同賬號(hào)可用的模型列表可能不同去控制臺(tái)確認(rèn)一下。另外注意模型 ID 大小寫(xiě)敏感不要自己拼。todo 工具不觸發(fā)模型一直不調(diào)用 todo只調(diào) bash。檢查T(mén)OOLS列表里 todo 的description是否清晰以及SYSTEM提示里有沒(méi)有寫(xiě)“Use the todo tool to plan multi-step tasks”。s03 的 SYSTEM 提示是SYSTEM fYou are a coding agent at {WORKDIR}. Use the todo tool to plan multi-step tasks. Mark in_progress before starting, completed when done. Prefer tools over prose.如果這段被改短了模型可能就不太主動(dòng)用 todo。把它加回去。排查順序建議先確認(rèn)通道通最小腳本返回 OK再確認(rèn)工具注冊(cè)TOOLS 和 TOOL_HANDLERS 都有 todo最后看模型行為SYSTEM 提示是否引導(dǎo)。三步都過(guò)了TodoWrite 基本就能穩(wěn)定工作。6. 把 TodoWrite 用起來(lái)從 s03 到真實(shí)編碼任務(wù)的接入建議TodoWrite 這個(gè)模塊本身不復(fù)雜但它解決的是一個(gè)很本質(zhì)的問(wèn)題讓 Agent 在多步任務(wù)里有可追蹤的狀態(tài)。你把它跑通之后可以試著做幾件事。第一把TodoManager的render()輸出格式改成你習(xí)慣的樣子。比如加個(gè)進(jìn)度條或者把 completed 的任務(wù)折疊起來(lái)只顯示計(jì)數(shù)。渲染結(jié)果會(huì)回傳給模型格式清晰對(duì)模型理解進(jìn)度有幫助。第二調(diào)整 nag reminder 的閾值。默認(rèn)是 3 輪你可以改成 2 輪讓模型更頻繁地更新或者改成 5 輪減少干擾。這個(gè)值在rounds_since_todo 3那行改。第三把 todo 工具和你的真實(shí)項(xiàng)目結(jié)合。比如你在做一個(gè) Django 重構(gòu)可以讓模型先列 todo每改一個(gè)文件就更新?tīng)顟B(tài)。這樣即使對(duì)話很長(zhǎng)你隨時(shí)能看到“現(xiàn)在做到哪個(gè)文件了”。如果你還沒(méi)配 TaoToken 的 Key去控制臺(tái)創(chuàng)建一個(gè)然后按第三節(jié)的 settings.json 或 config.toml 填好三件套。配好之后模型對(duì)話可以用來(lái)快速驗(yàn)證通道Coding Plan 適合長(zhǎng)期編碼任務(wù)API Keys 頁(yè)面管理你的密鑰。接入文檔里有各工具的詳細(xì)配置說(shuō)明。最后說(shuō)一個(gè)我踩過(guò)的坑.env文件不要提交到 git。learn-claude-code 的.gitignore里可能沒(méi)有默認(rèn)排除你自己加一行.env。Key 泄露了就去控制臺(tái)吊銷(xiāo)重發(fā)不要心存僥幸。TodoWrite 讓 Agent 有了計(jì)劃能力但計(jì)劃能不能執(zhí)行好取決于你的通道穩(wěn)不穩(wěn)、提示清不清晰。把這兩件事做好剩下的就是讓模型干活了。