1. 跨轮传递里,todo-state 为什么会被重复读
跨轮传递 todo-state 时,最典型的症状是:每一轮都把完整任务清单、已完成项、失败原因和下一步重新塞进 prompt,Claude Code 或 Codex CLI 的上下文迅速膨胀。TaoToken 的入口可以先固定:https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_content=todo_state_intro ,先拿 Key,再把请求地址设为 https://taotoken.net/api。很多长任务不是模型不会做,而是状态传递方式把预算吃掉了:越到后面,真正重要的约束和下一步被历史复述淹没,于是出现目标漂移、重复执行、工具调用来回打转。
从 Agent 状态管理开发者的视角看,todo-state 跨轮传递的 Token 消耗主要发生在两个地方:一是“跨轮读取”,也就是每一轮重新读取完整任务状态;二是“复述调用”,模型被要求把当前进度、已完成项、待办项再输出一遍,作为下一轮输入。单轮看起来不多,但长任务跑到 20 轮、50 轮后,重复读会变成上下文里的固定税。本文不讨论抽象概念,直接给出两样可复现产出:一份增量传递配置,一张重复读 Token 对照表。所有命令由读者在本地执行,Key 统一用YOUR_API_KEY占位,不要提交到仓库。
长任务上下文工程里常见四类手段:预算与卸载、压缩、todo-state 复述、跨会话记忆。本文把重点放在第三类,但会让它和前三类配合:预算决定哪些内容能留在当前轮,卸载把大块材料挪到文件或外部记忆,压缩把历史变成摘要,todo-state 只传差量,跨会话记忆只按需召回。这样做的目标不是让上下文“更短”这么简单,而是让每一轮都清楚:目标是什么、约束是什么、当前版本是什么、下一步只做什么。
2. 接入 TaoToken 前,先把 Key、Base URL、模型名分开管理
在改任何客户端配置之前,先把三件事分开:Key、Base URL、模型名。Key 从官网入口获取,建议先打开 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_content=todo_state_access ,登录后创建 Key。Base URL 统一写https://taotoken.net/api,注意这个地址在工具配置里不要加 UTM 参数,UTM 只用于官网入口和文档入口。模型名则按你实际使用的客户端填写,不要把一个客户端的模型名硬编码到另一个客户端里。
本地建议只保存环境变量名,不保存真实 Key。可以先用下面的方式确认当前 shell 里有哪些相关变量,输出时把值打码:
export TAOTOKEN_API_KEY="YOUR_API_KEY" env | grep -E 'TAOTOKEN|ANTHROPIC|OPENAI' | sed 's/=.*/=***/'如果你在团队里共享配置,推荐使用 1Password、Vault、CI Secret 或系统钥匙串,而不是把 Key 写进settings.json、config.toml、.env后提交。本文示例中的YOUR_API_KEY只是占位符,复制后必须替换。接入完成后,TaoToken 的价值不在于替你管理 todo-state,而在于把请求入口、Key 管理和模型选择统一起来,让你可以在客户端侧专注做增量传递、预算控制和对照实验。
3. Claude Code:settings.json 与 ANTHROPIC_* 的最小改法
Claude Code 侧使用settings.json和ANTHROPIC_*变量。用户级配置通常放在~/.claude/settings.json,项目级配置放在项目内的.claude/settings.json。如果两者同时存在,要确认优先级,避免项目级覆盖用户级后你以为还在用旧配置。最小配置如下:
{ "env": { "ANTHROPIC_BASE_URL": "https://taotoken.net/api", "ANTHROPIC_AUTH_TOKEN": "YOUR_API_KEY", "ANTHROPIC_MODEL": "claude-sonnet-4-20250514", "ANTHROPIC_SMALL_FAST_MODEL": "claude-3-5-haiku-20241022" } }如果你更喜欢用 shell 环境变量,也可以这样:
export ANTHROPIC_BASE_URL="https://taotoken.net/api" export ANTHROPIC_AUTH_TOKEN="YOUR_API_KEY" export ANTHROPIC_MODEL="claude-sonnet-4-20250514" export ANTHROPIC_SMALL_FAST_MODEL="claude-3-5-haiku-20241022"改完后重启终端和 Claude Code,避免旧进程继续读缓存配置。验证时不要只看“能不能启动”,还要看长任务里每轮是否仍然把完整 todo 列表塞进 prompt。如果仍然出现大段重复复述,问题通常不在 Base URL,而在你的 todo-state 传递协议。Claude Code 的ANTHROPIC_*只用于 Claude Code,不要把这些变量复制到 Codex 配置里,Codex 使用单独的config.toml和 provider 配置。
4. Codex CLI:config.toml 独立 provider,不要复用 ANTHROPIC_*
Codex CLI 侧使用config.toml,通常位于~/.codex/config.toml。它和 Claude Code 的配置体系不同,不要把ANTHROPIC_*套到 Codex。下面是一个独立 provider 示例:
model = "gpt-5-codex" model_provider = "taotoken" [model_providers.taotoken] name = "TaoToken" base_url = "https://taotoken.net/api" env_key = "TAOTOKEN_API_KEY" wire_api = "chat"环境变量单独设置:
export TAOTOKEN_API_KEY="YOUR_API_KEY"如果你的 Codex 版本对wire_api、模型名或 provider 字段有差异,以客户端实际要求为准,但原则不变:Codex 的 provider 只读TAOTOKEN_API_KEY,Base URL 仍然是https://taotoken.net/api。如果你同时用 Claude Code 和 Codex,建议用两个不同的 Key 或在 Key 名称上区分用途,比如TAOTOKEN_CLAUDE_KEY、TAOTOKEN_CODEX_KEY,排障时更容易定位是哪一个客户端在消耗。
5. CC Switch 三件套:profile、env、modelMap 的隔离写法
CC Switch 场景下,建议把配置拆成三件套:profile、env、modelMap。profile 决定当前激活的是 Claude Code 还是 Codex;env 只引用环境变量名,不写真实 Key;modelMap 把默认模型和快速模型映射到当前任务,避免业务代码里到处硬编码模型名。下面是一个示意配置,不同版本字段名可能不同,核心结构可以照这个思路迁移:
{ "profiles": [ { "id": "taotoken-claude", "provider": "claude", "baseUrl": "https://taotoken.net/api", "apiKeyEnv": "TAOTOKEN_API_KEY", "modelMap": { "default": "claude-sonnet-4-20250514", "fast": "claude-3-5-haiku-20241022" } }, { "id": "taotoken-codex", "provider": "codex", "baseUrl": "https://taotoken.net/api", "apiKeyEnv": "TAOTOKEN_API_KEY", "modelMap": { "default": "gpt-5-codex" } } ] }三件套的意义在于减少串配置:Claude Code 的 profile 只写 Claude 相关模型,Codex 的 profile 只写 Codex 相关模型;env 不泄露 Key;modelMap 可以按任务切换快速模型和默认模型。切换 profile 后,建议执行一次本地检查,确认目标客户端实际读到的 Base URL 是https://taotoken.net/api,而不是旧供应商地址。长任务排障时,先排除配置串用,再排查上下文工程。
6. todo-state 增量传递协议:快照、差量、摘要、引用
要减少 todo-state 重复读,关键不是让模型“记住别复述”,而是从协议上不提供需要复述的完整数据。可以把每轮状态拆成四层:快照、差量、摘要、引用。快照只保留任务 ID、目标摘要、硬约束、下一步和版本号;差量只保留新增、变更、删除、完成 ID、阻塞项;摘要只保留历史轮次的压缩结论;引用只保留外部记忆的路径或键,不把全文塞进 prompt。
下面是一份可运行的 Python 示例,用来生成增量 payload:
from dataclasses import dataclass, field, asdict from typing import Any import json import hashlib @dataclass class TodoItem: id: str title: str status: str # pending / running / done / blocked owner: str = "agent" evidence: str = "" updated_at: str = "" @dataclass class TodoState: task_id: str goal: str todos: list[TodoItem] next_action: str constraints: list[str] = field(default_factory=list) version: int = 1 def stable_json(obj: Any) -> str: return json.dumps(obj, ensure_ascii=False, sort_keys=True, separators=(",", ":")) def digest(obj: Any) -> str: return hashlib.sha256(stable_json(obj).encode("utf-8")).hexdigest()[:16] def compact_snapshot(state: TodoState) -> dict: return { "task_id": state.task_id, "goal_digest": digest(state.goal), "goal": state.goal if len(state.goal) <= 120 else state.goal[:117] + "...", "constraints": state.constraints, "next_action": state.next_action, "version": state.version, } def incremental_payload(prev: TodoState | None, curr: TodoState) -> dict: if prev is None: return { "mode": "full", "snapshot": compact_snapshot(curr), "todos": [asdict(t) for t in curr.todos], "digest": digest(asdict(curr)), } prev_map = {t.id: asdict(t) for t in prev.todos} curr_map = {t.id: asdict(t) for t in curr.todos} added = [v for k, v in curr_map.items() if k not in prev_map] changed = [v for k, v in curr_map.items() if k in prev_map and v != prev_map[k]] removed = [k for k in prev_map if k not in curr_map] done_ids = [t.id for t in curr.todos if t.status == "done"] blocked = [asdict(t) for t in curr.todos if t.status == "blocked"] return { "mode": "delta", "snapshot": compact_snapshot(curr), "base_version": prev.version, "current_version": curr.version, "added": added, "changed": changed, "removed": removed, "done_ids": done_ids, "blocked": blocked, "digest": digest(asdict(curr)), }接下来是把 payload 渲染成 prompt。注意这里明确要求模型不要复述完整 todo 列表:
def render_agent_turn(payload: dict, memory_refs: list[str], budget_remain: int) -> str: delta_part = { k: payload.get(k) for k in ["added", "changed", "removed", "done_ids", "blocked"] } return f"""你正在执行长任务。只依据以下增量状态继续,不要复述完整 todo 列表。 状态模式:{payload['mode']} 任务快照:{json.dumps(payload['snapshot'], ensure_ascii=False)} 增量:{json.dumps(delta_part, ensure_ascii=False)} 外部记忆引用:{json.dumps(memory_refs, ensure_ascii=False)} 剩余预算:{budget_remain} tokens 要求: 1. 如果状态冲突,以 current_version 和 digest 为准。 2. 只输出下一步动作、验证方式、需要更新的 todo id。 3. 不要重新抄写已完成项,除非用户明确询问。 """这个协议的关键点是:compact_snapshot每轮都带,但很短;incremental_payload只在状态变化时追加;完成项只传done_ids,不传完整 evidence;阻塞项单独传,便于模型优先处理。外部记忆用memory_refs引用,比如memory/task-001.md、notes/decision-003.md,需要细节时再按需读取,而不是每轮全文加载。
7. 重复读 Token 对照:全量复述与增量传递的本地实验
为了判断增量传递是否真的减少了重复读,可以做一个本地粗估。下面的脚本不依赖外部服务,只比较两种 prompt 构造方式的字符量,再按本地经验换算成粗略 token。它不适用于精确计费,但足够看出趋势:
def rough_tokens(text: str) -> int: # 本地粗估:中文、代码、JSON 混合场景误差较大,只用于对照趋势 return max(1, len(text) // 3) def render_full(state: TodoState) -> str: return f"""任务目标:{state.goal} 约束:{json.dumps(state.constraints, ensure_ascii=False)} 完整 todo:{json.dumps([asdict(t) for t in state.todos], ensure_ascii=False)} 下一步:{state.next_action} 请复述当前进度、已完成项、待办项,并给出下一步。 """ def build_demo_states() -> list[TodoState]: states = [] todos = [ TodoItem("T1", "梳理上下文预算", "done", evidence="budget.md"), TodoItem("T2", "实现 todo 差量", "running"), TodoItem("T3", "接入 TaoToken", "pending"), TodoItem("T4", "跑长任务对照", "pending"), TodoItem("T5", "记录目标漂移", "pending"), TodoItem("T6", "整理排障清单", "pending"), ] for version in range(1, 9): if version >= 2: todos[1].status = "done" if version >= 3: todos[2].status = "done" if version >= 4: todos[3].status = "running" if version >= 6: todos[4].status = "blocked" todos[4].evidence = "需要确认模型名" states.append(TodoState( task_id="task-001", goal="在不丢目标的前提下,降低长任务跨轮传递的重复读开销", todos=[TodoItem(**asdict(t)) for t in todos], next_action=f"处理第 {version} 轮变更", constraints=["不要复述已完成项", "每轮必须保留下一步", "状态冲突以版本号为准"], version=version, )) return states states = build_demo_states() prev = None full_tokens = 0 delta_tokens = 0 for s in states: full_tokens += rough_tokens(render_full(s)) payload = incremental_payload(prev, s) delta_tokens += rough_tokens(render_agent_turn(payload, ["memory/task-001.md"], 120000)) prev = s print("全量复述粗略 token:", full_tokens) print("增量传递粗略 token:", delta_tokens)在本地示例数据里,8 轮任务可能得到类似下面的对照趋势。实际数值会随 todo 数量、evidence 长度、模型和语言变化,关键是看“随轮次增长”的曲线:
| 轮次 | 全量复述粗略 token | 增量传递粗略 token | 状态版本 |
|---|---|---|---|
| 第 1 轮 | 980 | 980 | v1 |
| 第 2 轮 | 1150 | 320 | v2 |
| 第 3 轮 | 1250 | 360 | v3 |
| 第 4 轮 | 1420 | 420 | v4 |
| 第 5 轮 | 1600 | 450 | v5 |
| 第 6 轮 | 1810 | 560 | v6 |
| 第 7 轮 | 1980 | 580 | v7 |
| 第 8 轮 | 2160 | 610 | v8 |
全量复述的问题是线性增长:每多一轮,就多一份完整 todo 和历史说明。增量传递虽然也有快照和下一步,但增长更平缓,因为已完成项只保留 ID,阻塞项只在出现时传递,历史细节被压缩到外部引用。对于长任务,这能直接减少跨轮读取与复述调用的 Token 消耗,也能降低模型被旧信息带偏的概率。
8. 排障清单:目标丢失、上下文溢出、Key 串用、状态冲突
第一,目标丢失。每轮必须带goal_digest、硬约束和next_action,不要依赖完整历史。如果发现模型开始执行已经完成的任务,先检查done_ids是否没有被更新,再检查 prompt 是否要求它复述完整 todo。
第二,上下文溢出。不要把所有 evidence、日志、文件内容都放进 prompt。把大块材料写入外部记忆,只在当前轮需要时读取引用。预算不是固定值,可以按任务阶段调整:探索阶段多一些,执行阶段少一些,验证阶段只保留失败证据和验收标准。
第三,Key 串用。Claude Code 的ANTHROPIC_*只给 Claude Code 用,Codex 用config.toml里的 provider 和TAOTOKEN_API_KEY。如果出现 401 或模型不存在,先检查当前客户端实际读取的是哪个 profile、哪个环境变量、哪个 Base URL。Base URL 应统一是https://taotoken.net/api,不要在一个客户端里混入另一个客户端的变量名。
第四,状态冲突。增量传递必须带base_version、current_version和digest。如果模型收到的差量基于旧版本,它可能覆盖新状态。处理方式很简单:以current_version为准,冲突时重新生成差量,而不是让模型凭记忆猜。
第五,验证重复读是否下降。不要只看总 token,还要看每轮 prompt 里 todo 列表的长度、已完成项是否被完整复述、阻塞项是否重复出现。可以用第 7 节的本地脚本做趋势对照,再结合客户端实际用量观察。
9. 落地顺序:模型对话、Coding Plan、创建 Key、Claude Code 文档
如果你还没开始接入,建议按这个顺序走:先通过模型对话验证 TaoToken 的连通性和模型返回是否符合预期,地址是 https://taotoken.net/models/detail/chat?utm_source=taotoken_aicg_blog_end&utm_content=todo_state_chat ;然后根据长任务和 Coding 场景选择 Coding Plan,地址是 https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=todo_state_plan ;接着在控制台创建和管理 Key,地址是 https://taotoken.net/console/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=todo_state_keys ;最后按 Claude Code 文档完成settings.json和ANTHROPIC_*配置,地址是 https://taotoken.net/doc/ClaudeCodeAnthropic?utm_source=taotoken_aicg_blog_end&utm_content=todo_state_claude_doc 。官网入口是 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_content=todo_state_cta 。
回到 todo-state 跨轮传递本身,减少重复读不靠一句“请简洁”,而靠协议:快照保留目标和约束,差量只传变化,摘要压缩历史,引用按需召回。TaoToken 在这里的作用是统一请求入口和 Key 管理,让你能把精力放在状态协议和 Token 对照上。先把 Base URL 固定为https://taotoken.net/api,用YOUR_API_KEY占位跑通 Claude Code 或 Codex,再逐步替换全量复述。长任务跑稳之后,你会发现省下来的不只是 token,还有被重复信息稀释掉的注意力。