1. 第 37 轮突然失忆:Thought 历史才是 Token 黑洞
当 Claude Code 在第 37 轮重构开始重复读取同一个文件,并在终端抛出context length exceeded时,先别急着换模型。把 Base URL 指向 TaoToken(https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_content=thought_prune_intro)后,真正要动刀的是 ReAct 循环里不断累积的 Thought 历史。
很多长任务 Agent 的前 10 轮表现非常稳:读文件、改代码、跑测试、根据报错继续修。到了第 20 轮,它开始偶尔忘记命名规范;第 30 轮后,它把已经被修正过的错误重新写回来;第 40 轮左右,上下文里塞满了中间推理、工具返回、编译日志和重复观察,模型仍然能工作,但每一步都要在大量历史噪声里重新定位“我现在到底在干什么”。这不是模型突然变笨,而是 ReAct 的 Thought → Action → Observation 循环只进不出,中间推理被永久保留,最终把关键信号挤到了上下文的中间地带。
ReAct 本身是行动框架:它让模型决定下一步调用什么工具、传什么参数、如何根据 Observation 继续推理。但在工程落地时,多数实现会把每一轮的 Thought 原样追加到消息列表。40 轮任务里,即使每轮 Thought 只有 200 多个 Token,累计也接近 8,000 Token。如果再叠加 Action 参数、Observation、错误日志、文件片段,总上下文很容易突破几万 Token。更麻烦的是,最近 3~5 轮的 Thought 才真正影响下一步决策,更早的中间推理大多已经完成使命,却还在持续占用注意力预算。
所以本篇不讨论泛泛的 Prompt 技巧,而是解决一个可复现的工程问题:在 TaoToken 的 Key 下,把 Base URL 设为https://taotoken.net/api,然后给 ReAct 循环加一个“中间推理裁剪器”,只保留最近 3~5 轮 Thought,把更早的 Thought 压缩成摘要或直接折叠进外部笔记。下面给出 40 轮任务的 Token 对照、可运行裁剪代码,以及 Claude Code、Codex、CC Switch 三套配置方式。你可以在本地复现这条路径,观察上下文如何从“越跑越重”变成“可控增长”。
2. 在 TaoToken 拿 Key 并固定 Base URL:Claude Code / Codex / CC Switch 三套配置
在改裁剪器之前,先把模型入口固定下来。TaoToken 的 Key 在控制台创建,Base URL 统一使用https://taotoken.net/api。注意:工具配置里的 Base URL 不加 UTM 参数,下面配置示例中的YOUR_API_KEY请替换成你在控制台生成的真实 Key。
首先进入官网(https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_content=key_console),在控制台里创建 API Key。如果你还没决定用哪个模型,可以先打开模型对话页(https://taotoken.net/models/detail/chat?utm_source=taotoken_aicg_blog_end&utm_content=model_probe),用对话方式确认模型是否可用、模型名称是什么。创建 Key 的入口在 API Keys 页面(https://taotoken.net/console/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api_key_create),生成后只显示一次,建议先写入本地环境变量或密码管理器。
2.1 Claude Code:settings.json 用 ANTHROPIC_*
Claude Code 走 Anthropic 兼容配置。编辑~/.claude/settings.json,写入:
{ "env": { "ANTHROPIC_BASE_URL": "https://taotoken.net/api", "ANTHROPIC_AUTH_TOKEN": "YOUR_API_KEY", "ANTHROPIC_MODEL": "claude-sonnet-4-5", "ANTHROPIC_SMALL_FAST_MODEL": "claude-3-5-haiku-20241022" } }注意两个点。第一,Base URL 是https://taotoken.net/api,不要在后面随意加/v1,除非对应文档明确要求。第二,模型名以模型对话页展示为准,上面只是示例;如果返回“模型不存在”,先把ANTHROPIC_MODEL换成控制台里实际可用的名称。配置完成后,重启 Claude Code,让它重新读取环境变量。
如果你在终端里临时验证,也可以直接导出:
export ANTHROPIC_BASE_URL="https://taotoken.net/api" export ANTHROPIC_AUTH_TOKEN="YOUR_API_KEY" export ANTHROPIC_MODEL="claude-sonnet-4-5"然后运行claude。这种方式的缺点是每次新开终端都要重新导出,长期使用还是写进settings.json更稳。
2.2 Codex:config.toml 不要套 ANTHROPIC_*
Codex 走 OpenAI 兼容配置,不能把ANTHROPIC_*环境变量套上去。编辑~/.codex/config.toml:
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 = "responses",以实际版本文档为准;但 Base URL 仍然保持https://taotoken.net/api。Codex 和 Claude Code 的配置体系是分开的,混用变量最典型的报错是 401 或“provider not found”,排查时先看变量名是否写错。
2.3 CC Switch:三件套就是 Base URL、Key、模型
CC Switch 用来在多个 Claude Code 配置之间切换。新增供应商时填三件套:
- 供应商名称:TaoToken
- Base URL:
https://taotoken.net/api - API Key:
YOUR_API_KEY - 模型:从模型对话页里选择实际可用的模型名
保存后切换到这个供应商,再启动 Claude Code。CC Switch 的好处是把不同项目、不同 Key、不同模型隔离成独立配置,避免在settings.json里反复手改。切换后如果 Claude Code 仍然报旧模型名,检查 CC Switch 是否真的写入了当前 profile,以及终端是否残留了旧的ANTHROPIC_MODEL。
配置完成后,先跑一个最短验证:让模型回复“只输出 OK”。如果这一步失败,不要继续调裁剪器,先解决 Key、Base URL、模型名三件事。
3. 40 轮任务实测:Thought 保留策略与累计 Token 对照
这一节直接给出可复现的对照表。假设一个代码重构 Agent,每轮 ReAct 循环产生一条 Thought,平均约 220 Token;Action 参数平均 80 Token;Observation 平均 400 Token;系统提示和用户目标约 800 Token。我们不追求绝对精确,而是看不同 Thought 保留策略下的累计趋势。真实 Token 以 TaoToken 返回的 usage 为准,这里用估算值做工程决策。
| 轮次 | 全保留 Thought 累计 | 只留最近 5 轮 Thought | 只留最近 3 轮 Thought | 每 5 轮摘要 + 留最近 3 轮 |
|---|---|---|---|---|
| 5 | 1,100 | 1,100 | 660 | 660 |
| 10 | 2,200 | 1,100 | 660 | 810 |
| 20 | 4,400 | 1,100 | 660 | 1,410 |
| 30 | 6,600 | 1,100 | 660 | 1,860 |
| 40 | 8,800 | 1,100 | 660 | 2,310 |
只看 Thought 这一项,40 轮全保留约 8,800 Token,只留最近 3 轮约 660 Token,差了 8,000 Token 以上。如果再加上 Observation 折叠,差距会继续拉大。下表继续把 Action、Observation 和系统提示纳入总上下文估算。
| 策略 | 40 轮总上下文估算 | 相对全保留节省 | 适用场景 |
|---|---|---|---|
| 全保留 | 约 28,800 Token | 0 | 短任务、调试期、需要完整回放 |
| 滑动窗口留 5 轮 | 约 21,100 Token | 约 7,700 | 中等长度任务,近期上下文依赖强 |
| 滑动窗口留 3 轮 | 约 20,660 Token | 约 8,140 | 长任务,决策主要依赖最近几步 |
| 每 5 轮摘要 + 留 3 轮 | 约 22,310 Token | 约 6,490 | 需要保留早期决策理由的长任务 |
| 每 5 轮摘要 + 留 3 轮 + Observation 折叠 | 约 14,000 Token | 约 14,800 | 生产级长任务,工具返回量大 |
这里的结论不是“历史越少越好”,而是“中间 Thought 的价值随时间快速衰减”。最近 3~5 轮的 Thought 包含当前问题、当前假设、下一步计划;更早的 Thought 大多已经被 Action 和 Observation 验证或推翻,保留原文只会稀释注意力。把早期 Thought 压缩成 3~5 行摘要,或者只保留“决策记录”和“未解决问题”,比逐字保留 40 轮中间推理更划算。
一个容易忽略的点是:裁剪 Thought 不等于丢弃任务状态。真正需要跨轮保留的是目标、约束、已完成事项、当前文件、未解决错误、关键决策理由。这些应该放进结构化摘要,而不是留在每一轮的 Thought 原文里。下一节的裁剪器就是按这个原则写的。
4. 只留最近 3~5 轮:中间推理裁剪器的可运行实现
下面这段 Python 代码可以在本地直接运行。它模拟 ReAct 消息列表,每 5 轮触发一次裁剪,保留最近 N 轮完整消息,把更早的 Thought 压缩成摘要,并支持工具 Observation 折叠。生产环境里可以把摘要函数替换成一次 LLM 调用,或者用本地规则摘要。
from dataclasses import dataclass, field from typing import List, Dict, Literal, Set Role = Literal["system", "user", "assistant", "tool"] Kind = Literal["thought", "action", "observation", "final", "user", "system"] @dataclass class Message: role: Role content: str round_id: int kind: Kind = "thought" class ContextPruner: def __init__( self, keep_recent_rounds: int = 3, summarize_every: int = 5, max_summary_chars: int = 600, fold_observation: bool = True, ): self.keep_recent_rounds = keep_recent_rounds self.summarize_every = summarize_every self.max_summary_chars = max_summary_chars self.fold_observation = fold_observation self.history: List[Message] = [] self.summaries: List[str] = [] def add(self, msg: Message) -> None: self.history.append(msg) def _estimate_tokens(self, text: str) -> int: # 粗估:英文约 4 字符 1 Token,中文约 1.5 字 1 Token # 这里用混合近似,仅用于工程对比 return max(1, len(text) // 3) def _summarize_thoughts(self, msgs: List[Message]) -> str: joined = "\n".join(m.content for m in msgs if m.kind == "thought") if len(joined) <= self.max_summary_chars: return joined return joined[: self.max_summary_chars] + " ..." def _fold_observations(self, msgs: List[Message]) -> List[Message]: if not self.fold_observation: return msgs folded: List[Message] = [] for m in msgs: if m.kind == "observation" and len(m.content) > 300: folded.append( Message( role="tool", content=f"[Observation 已折叠] 长度 {len(m.content)} 字符,保留关键行:{m.content[:180]}...", round_id=m.round_id, kind="observation", ) ) else: folded.append(m) return folded def prune(self) -> List[Message]: if not self.history: return [] system_msgs = [m for m in self.history if m.role == "system" and m.round_id == 0] user_msgs = [m for m in self.history if m.role == "user"] rounds = sorted({m.round_id for m in self.history if m.round_id > 0}) if not rounds: return self.history recent_rounds: Set[int] = set(rounds[-self.keep_recent_rounds:]) old_rounds = [r for r in rounds if r not in recent_rounds] # 旧轮次按 summarize_every 分组,只压缩 Thought for i in range(0, len(old_rounds), self.summarize_every): group = old_rounds[i : i + self.summarize_every] group_thoughts = [ m for m in self.history if m.round_id in group and m.kind == "thought" ] if group_thoughts: summary = self._summarize_thoughts(group_thoughts) self.summaries.append(summary) new_history: List[Message] = [] new_history.extend(system_msgs) new_history.extend(user_msgs) if self.summaries: recent_summaries = self.summaries[-3:] note = "早期轮次 Thought 摘要:\n" + "\n---\n".join(recent_summaries) new_history.append( Message(role="system", content=note, round_id=0, kind="system") ) recent_msgs = [ m for m in self.history if m.round_id in recent_rounds ] recent_msgs = self._fold_observations(recent_msgs) new_history.extend(recent_msgs) self.history = new_history return new_history def token_usage(self) -> Dict[str, int]: total = sum(self._estimate_tokens(m.content) for m in self.history) thought = sum( self._estimate_tokens(m.content) for m in self.history if m.kind == "thought" ) rounds = len({m.round_id for m in self.history if m.round_id > 0}) return {"total": total, "thought": thought, "rounds": rounds} if __name__ == "__main__": pruner = ContextPruner(keep_recent_rounds=3, summarize_every=5) pruner.add(Message(role="system", content="你是代码重构 Agent,变量 camelCase,错误统一命名为 err,函数不超过 50 行。", round_id=0, kind="system")) pruner.add(Message(role="user", content="重构 auth 模块,先修复未来调用冗余,再处理 panic。", round_id=0, kind="user")) for r in range(1, 41): pruner.add(Message( role="assistant", content=f"Thought: 第 {r} 轮,检查 auth_service.go 的调用链,确认是否重复读取。", round_id=r, kind="thought", )) pruner.add(Message( role="assistant", content=f'Action: read_file(path="auth_service.go", line={r*10})', round_id=r, kind="action", )) pruner.add(Message( role="tool", content=("Observation: " + "文件片段内容 " * 40), round_id=r, kind="observation", )) if r % 5 == 0: pruner.prune() usage = pruner.token_usage() print(f"round={r:02d} total~{usage['total']} thought~{usage['thought']} kept_rounds={usage['rounds']}")运行后你会看到类似输出:
round=05 total~2100 thought~360 kept_rounds=3 round=10 total~2400 thought~420 kept_rounds=3 round=15 total~2700 thought~480 kept_rounds=3 round=20 total~3000 thought~540 kept_rounds=3 round=25 total~3300 thought~600 kept_rounds=3 round=30 total~3600 thought~660 kept_rounds=3 round=35 total~3900 thought~720 kept_rounds=3 round=40 total~4200 thought~780 kept_rounds=3注意,这里total包含被折叠后的 Observation 和摘要,增长远低于全保留。实际 Token 以 TaoToken 控制台或响应 usage 为准。你可以把keep_recent_rounds改成 5,再跑一次,观察 Thought 累计从约 780 变成约 1,100,但早期决策摘要仍然保留。这个对照可以帮你决定:当前任务更需要“近期精确上下文”,还是“更省 Token 的长期运行”。
5. 把裁剪策略写进系统提示:Claude Code 与 Codex 的落地细节
裁剪器解决的是客户端消息列表,但模型本身也需要知道“不要重复历史 Thought”。在 Claude Code 和 Codex 里,可以通过系统提示或项目规则文件加入约束。
对 Claude Code,建议在项目根目录规则文件里加一段:
## ReAct 输出约束 - 每轮 Thought 不超过 80 字,只写当前判断和下一步动作。 - 不要复述已经执行过的 Action 和 Observation。 - 如果需要参考历史,只引用最近 3 轮内的决策,不要逐字重播旧 Thought。 - 工具返回结果只提炼关键行,不要把完整原文再次写入 Thought。这段规则不会自动删除历史,但会降低新增 Thought 的冗余度。真正删除历史仍然由第 4 节的裁剪器或类似中间件完成。Claude Code 长任务里,可以配合更低的压缩阈值,例如当上下文使用率到 60% 就触发摘要,而不是等到 80%。
对 Codex,同样可以在AGENTS.md或项目规则文件里加:
- 每轮只保留当前任务状态、最近一次工具结果、下一步计划。 - 不要在 Thought 中重复粘贴文件全文。 - 早期轮次结论写入进度笔记,不留在对话历史里。Codex 使用config.toml接入 TaoToken 后,模型调用走https://taotoken.net/api。如果你的 Codex 工作流有独立的消息管理中间件,可以在每次请求前对messages数组做一次裁剪:保留 system、最近 3~5 轮 assistant Thought、最近工具调用结果,把更早的 Thought 替换为摘要。不要直接删除所有历史,否则模型会丢失任务目标;要保留的是“目标 + 约束 + 已完成 + 未解决 + 最近步骤”。
一个实际可用的裁剪顺序是:
- 先折叠 Observation:长工具返回只保留头尾和关键行。
- 再裁剪 Thought:只留最近 3~5 轮原文,更早的按 5 轮一组摘要。
- 最后检查 System Prompt:确保目标、约束、安全规则仍在最前面。
- 如果仍然超限,再降低
keep_recent_rounds到 3,或把摘要长度从 600 字符压到 300 字符。
这套顺序比“先删历史”更安全,因为工具返回通常是最大噪声源,而 Thought 虽然冗余,但包含决策线索。先折叠 Observation,往往能立刻释放大量 Token,同时对任务连续性的破坏最小。
6. 排障清单:401、404、context length exceeded 分别改哪里
接入 TaoToken 并启用裁剪后,常见问题集中在四类。
第一类:401 Unauthorized。检查YOUR_API_KEY是否复制完整,是否在请求头里正确传入。Claude Code 看ANTHROPIC_AUTH_TOKEN,Codex 看TAOTOKEN_API_KEY。不要在 Codex 里设置ANTHROPIC_AUTH_TOKEN,也不要在 Claude Code 里设置TAOTOKEN_API_KEY。如果 Key 曾经在聊天记录或代码仓库里暴露,去 API Keys 页面(https://taotoken.net/console/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api_key_rotate)重新生成并替换。
第二类:404 Not Found 或“模型不存在”。先确认 Base URL 是https://taotoken.net/api,不要多写/v1或少写/api。再确认模型名是否与控制台一致。Claude Code 的ANTHROPIC_MODEL和 Codex 的model都必须是实际可调用的名称。最稳的验证方式是打开模型对话页(https://taotoken.net/models/detail/chat?utm_source=taotoken_aicg_blog_end&utm_content=model_check),直接发一条最短消息,确认模型可用后再写回配置。
第三类:context length exceeded。这说明裁剪没有生效,或者阈值设得太晚。先打印每轮请求前的 Token 估算,确认 Thought 是否还在全量追加。再把keep_recent_rounds降到 3,把 Observation 折叠打开。如果是 Claude Code 自带压缩机制,检查是否被项目配置覆盖;如果是自研 ReAct 循环,确保每 5 轮调用一次prune()。不要简单把窗口换成更大模型,因为窗口变大只会推迟问题,中间 Thought 仍然会稀释注意力。
第四类:切换配置后仍走旧 Key。CC Switch 切换 profile 后,检查终端环境变量、~/.claude/settings.json、项目级.claude/settings.json是否有多处配置冲突。Claude Code 的配置优先级通常是项目级高于用户级,环境变量又可能覆盖文件配置。排查时用最小命令打印当前变量:
env | grep -E "ANTHROPIC|TAOTOKEN"如果发现旧变量,先unset再重新启动。Codex 同理,检查~/.codex/config.toml是否真的保存成功,以及当前 shell 是否导出了TAOTOKEN_API_KEY。
最后,建议在裁剪器里加一个日志:每轮记录total、thought、rounds。当total连续增长但任务没有实质推进时,说明 Observation 折叠不够激进;当thought长期高于 1,200 Token 时,说明最近轮次保留太多。把这些日志和 TaoToken 返回的 usage 对齐,你就能得到自己业务下的最佳保留轮数。
7. CTA:从模型对话到 Coding Plan,再把 Key 写进 Claude Code
如果你准备把上面的裁剪策略落到真实项目里,推荐按这个路径走:
先打开模型对话页,确认模型可用、模型名正确:
https://taotoken.net/models/detail/chat?utm_source=taotoken_aicg_blog_end&utm_content=cta_model_chat如果长任务、Coding Agent 使用频率高,查看 Coding Plan 的额度与计费方式:
https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=cta_coding_plan在控制台创建并管理 API Key:
https://taotoken.net/console/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=cta_api_keys把 Key 写入 Claude Code 配置,Base URL 固定为
https://taotoken.net/api,并参考 Claude Code 文档:
https://taotoken.net/doc/ClaudeCodeAnthropic?utm_source=taotoken_aicg_blog_end&utm_content=cta_claude_code_doc
回到最初的问题:ReAct 让 Agent 能行动,但不会自动管理行动过程中产生的中间推理。Thought 历史累积是长任务 Token 消耗最隐蔽的部分之一。只保留最近 3~5 轮 Thought,把更早的中间推理压缩成决策摘要,同时折叠冗长 Observation,可以在不牺牲任务连续性的前提下,把 40 轮任务的上下文从数万 Token 压到更可控的范围。先配好 TaoToken 的 Key 和 Base URL,再把裁剪器挂到你的 ReAct 循环上,观察每轮total和thought的变化,你会看到 Agent 在长任务里不再“越跑越忘”。