资讯动态

Agent harness 经 TaoToken 接入后,todo-state 复述能缓解目标丢失吗

发布时间:2026/9/18 13:23:49 来源:尧图企业网站定制
1. 先问“谁在消耗 Token”harness 层其实有四笔账长任务跑到三十几轮Agent 突然回过头去执行一个早就被否决的方案或者在第 N 轮把需求里“不要改数据库 schema”这条约束忘得干干净净。很多人的第一反应是模型不行但真正的原因往往在 Agent harness 层历史的组织方式、目标的保存位置、什么时候做压缩、要不要召回旧会话。在把 Agent harness 的模型访问凭据切到 TaoTokenhttps://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_contentharness_intro 之后我才有机会把这些开销一笔一笔拆开看因为所有请求都走同一个 Base URL计量口径统一了才谈得上对比。先建立一个基本认知一次 Agent 循环里的 Token 消耗不是“一次推理那么点”。它至少包含五份固定开销系统提示、工具 schema、安全策略、输出格式约束。这部分每轮都在跟任务长短无关属于“坐着不动也要付的钱”。滚动历史每一轮的模型输出、工具调用参数、工具返回结果全都追加进上下文。工具返回是膨胀最快的部分一次文件读取或一次测试输出就可能上千 Token。压缩调用摘要历史不是免费的它本身就是一次额外的模型请求输入是待压缩的原文输出是摘要。todo-state 复述如果 harness 选择每轮把当前任务状态重新注入那么不管历史多长这份状态都会固定占据一段窗口并且每轮重复计费。跨会话记忆召回检索出来的旧片段被塞回上下文召回的条数、粒度、去重策略直接决定这笔开销的上限。把这五笔账摆在一起就能理解为什么“换个更便宜的接入点”和“改 harness 策略”是两件事但又互相影响前者决定单价后者决定你每轮要付多少份单价。TaoToken 在这件事上的价值是提供一个统一的 OpenAI 兼容 / Anthropic 兼容入口Base URL 固定为https://taotoken.net/api这样 Claude Code、Codex、以及自己写的 harness 都能指向同一个地址做对照实验不用为每个工具单独配一套计量逻辑。Key 在官网控制台拿https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_contentharness_keys 。下面按“配置 → 预算 → 压缩 → todo-state 复述 → 记忆 → 对照记录”的顺序走一遍每一步都给可复制的片段你可以直接在本地跑。2. 接入配置Claude Code、Codex 与 CC Switch 三件套配置这一步的目标只有一个让 harness 发出的每一个请求都打到https://taotoken.net/api并且用的是你自己的 Key。三件套永远是同一组Base URL、API Key、模型名。不同工具只是把这组值放到不同位置。2.1 Claude Codesettings.json 与 ANTHROPIC_*Claude Code 走的是 Anthropic 协议族所以用它自己的ANTHROPIC_*变量。推荐写进~/.claude/settings.json而不是散在 shell 里方便版本管理{ 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-haiku-4-5 } }如果偏好环境变量方式等价写法是export ANTHROPIC_BASE_URLhttps://taotoken.net/api export ANTHROPIC_AUTH_TOKENYOUR_API_KEY export ANTHROPIC_MODELclaude-sonnet-4-5注意ANTHROPIC_BASE_URL只填到域名和/api不要自己再拼/v1/messages客户端会补路径。配完后用一次最普通的对话验证连通性再进长任务避免把配置问题和任务问题混在一起排查。2.2 Codexconfig.toml不要套 ANTHROPIC_*这是最常见的踩坑点Codex 是另一套协议与配置体系把ANTHROPIC_*变量塞给它不会生效。Codex 用~/.codex/config.toml自定义 provider 的字段是base_url和env_keymodel gpt-5-codex model_provider taotoken [model_providers.taotoken] name TaoToken base_url https://taotoken.net/api env_key TAOTOKEN_API_KEY wire_api responses然后只在环境里提供它需要的那把钥匙export TAOTOKEN_API_KEYYOUR_API_KEY两个文件的分工要记清楚Claude Code 读ANTHROPIC_*Codex 读config.toml里的env_key指向的变量。混用是无效配置而且报错信息通常很含糊会浪费你大量时间。2.3 CC Switch三件套的集中管理如果你同时跑 Claude Code、Codex还有自己写的 harness用 CC Switch 这类配置切换工具比手改文件稳。它管的就是三件套字段填什么说明Base URLhttps://taotoken.net/api所有工具统一填这个不要带额外路径API KeyYOUR_API_KEY从官网控制台创建不要在客户端里存明文到共享目录模型名按工具选对应模型Claude Code 一套、Codex 一套切换 profile 时一起换切换后一定要做一次“配置自检”发一条最短的 hello 请求确认返回正常再开始长任务。否则你会在第 20 轮才发现请求根本没出去。3. 上下文预算与卸载把“装不下”变成“可计算”上下文溢出不是突然发生的它是预算失控累积到阈值的结果。harness 层能做的第一件事是给不同类别的内容分配显式配额而不是让历史无限增长然后指望模型自己扛。一个可落地的预算表长这样# budget.py —— 上下文预算账本伪代码按需改成你的 harness BUDGET { system_and_tools: 4000, # 系统提示 工具 schema硬性占用 todo_state: 600, # 每轮复述的目标与进度 memory_recall: 1500, # 跨会话记忆召回上限 recent_turns: 12000, # 最近若干轮原文保真区 compressed_history: 3000, # 早期历史摘要 working_space: 2000, # 留给本轮工具返回 reserve: 1500, # 安全余量防止估算偏差 } TOTAL_WINDOW sum(BUDGET.values()) def plan_context(items): items: [{kind: ..., tokens: ..., payload: ...}] kept, unloaded [], [] for it in sorted(items, keylambda x: x[tokens]): if used(kept) it[tokens] BUDGET[it[kind]]: kept.append(it) else: unloaded.append(it) return kept, unloaded关键不是这张表的具体数字而是“超配额的内容去了哪里”。正确做法是卸载offload到本地磁盘或对象存储在上下文里只留一个可寻回的指针例如[tool_output:unloaded idrun-1842 path./.harness/artifacts/run-1842.log summarypytest 输出3 failed 12 passed失败项见 log 尾部 tokens37]这样历史里保留的是“这件事发生过、结论是什么、原文在哪”而不是几千行日志。Agent 需要细节时可以按id或path主动取回一小段而不是一次性全塞回去。指针化卸载带来两个直接收益滚动历史的增长曲线从线性变成近似常数以及压缩调用的输入变短压缩本身也便宜了。代价是 harness 必须实现“取回”工具否则 Agent 会对着指针干瞪眼。4. 压缩什么时候压、压什么、压完怎么验证没丢目标压缩是最容易被误用的机制。常见错误是“上下文快满了就压一次”结果压缩触发点太靠后压缩调用本身就已经接近窗口上限容易失败或截断。更稳的策略是双阈值# compress.py —— 双阈值触发 压缩后校验伪代码 SOFT 0.65 # 达到窗口 65% 开始规划压缩 HARD 0.82 # 达到 82% 必须压缩否则拒绝新工具调用 COMPRESS_PROMPT 你在压缩 Agent 的历史记录。只输出以下四段不要复述过程 1. 已确认的决策含被否决的方案及否决理由 2. 仍然有效的硬约束版本、路径、禁止修改项 3. 已完成的步骤及验证结果 4. 当前未解决的分支与下一步 删除所有重复的工具原始输出它们已卸载为指针。 def maybe_compress(ctx): ratio ctx.tokens / ctx.window if ratio SOFT: return ctx if ratio HARD: ctx.freeze_new_tool_calls() summary call_model(COMPRESS_PROMPT, inputctx.oldest_span()) ctx.replace_span_with(summary) assert_goal_preserved(ctx) # 见下 return ctxassert_goal_preserved是很多人漏掉的一步。压缩最危险的副作用是“把目标也压没了”摘要里只剩干巴巴的步骤原始需求中的验收条件被丢掉。一个低成本的校验方式是压缩前后各跑一次目标抽取比较关键字段是否一致。def extract_goal(text): 返回结构化目标字段固定便于 diff return call_model( 从文本中抽取objective / constraints / acceptance / rejected。 缺失字段填 null不要编造。, inputtext, ) def assert_goal_preserved(ctx): before ctx.goal_snapshot # 压缩前保存的快照 after extract_goal(ctx.summary) for field in (objective, constraints, acceptance): if before.get(field) and not after.get(field): ctx.restore_field(field, before[field]) # 回填不覆盖已有内容换句话说压缩的验收标准不是“摘要读起来通顺”而是“目标字段没有丢”。这一条用结构化比对就能自动化不需要人工逐条读。5. todo-state 复述它缓解的是目标漂移不是上下文溢出回到主线问题todo-state 复述能缓解目标丢失吗答案是能但缓解的是目标漂移不是上下文溢出。这两件事经常被混为一谈导致期望错位。它的作用机制是“位置换稳定性”。模型对上下文不同位置的注意力并不均匀放在几万 Token 之前的原始需求随着历史增长会逐渐被稀释而每轮固定注入在固定位置的 todo-state相当于把目标从“深埋在历史里”搬到“每轮都看得见”。目标漂移这类失败模式会明显减少。但它有几个必须承认的代价第一它是固定开销。每轮都注入就每轮都计费。如果 todo-state 复述写了 800 Token跑 60 轮就是接近 5 万 Token 的纯重复输入。这也是为什么我在开头强调“谁在消耗 Token”复述不是免费的它是用可预测的小额重复成本换目标稳定性。第二它救不了溢出。如果历史本身已经撑爆窗口加一段复述只会让情况更糟。正确顺序是先用预算与卸载把总量压下来再用复述稳住目标。顺序反了就是给一艘漏水的船加桅杆。第三复述的质量决定效果上限。把 todo-state 写成“正在处理第 3 步”基本无效写成结构化的目标 约束 验收条件 已否决项才真正起效。下面是一个每轮注入的 todo-state 模板可以直接放进 harness# todo_state.py —— 每轮注入的结构化状态伪代码 TODO_TEMPLATE [TASK STATE] objective: {objective} constraints: {constraints} # 不可违反项逐条列出 acceptance: {acceptance} # 判定完成的条件 done: {done} # 已完成附验证方式 rejected: {rejected} # 已否决方案 否决原因防止回退 next: {next} open_questions: {open_questions} def render_todo_state(state): text TODO_TEMPLATE.format(**state) if estimate_tokens(text) BUDGET[todo_state]: # 超预算时优先保留 objective / constraints / rejected state trim_by_priority( state, keep[objective, constraints, rejected, acceptance], ) text TODO_TEMPLATE.format(**state) return text注意rejected这一项。长任务里最典型的“目标丢失”表现不是忘了要做什么而是重新捡起已经被否决的方案然后团队又花一轮把它否决掉。把否决理由记进 todo-state等于给 Agent 一个防回退的锚。还有两个开关值得做成配置便于做对照实验开关取值影响todo_state.enabledon / off关掉即回到纯历史模式用于对照目标漂移率todo_state.intervalevery_turn / every_n_turns每轮注入最稳但最贵隔轮注入省 Token 但稳定性下降todo_state.max_tokens数值超预算时按优先级裁剪优先保 constraints 与 rejectedcompress.soft_ratio0~1提前触发压缩配合复述使用效果更稳memory.top_k整数召回条数上限直接决定记忆那笔账的大小6. 跨会话记忆召回也要计费别把它当免费午餐跨会话记忆常被当成“有了就更聪明”但从 Token 视角看它是把过去的开销搬到现在来付。召回 5 条历史片段每条 300 Token一轮就多 1500 Token 输入而其中可能只有一条真正相关。可控的做法是给召回设三道闸第一道是检索范围闸只检索与当前 objective 相关的命名空间不要全库扫。第二道是条数与长度闸top_k加上每条最大长度超长片段先截断再入库。第三道是去重与时效闸同一结论只保留最新版本被否决的方案降权而不是删除因为它对防回退有价值。存储侧建议先用最朴素的方式起步本地文件或本地 SQLite让命令由读者在自己的机器上执行不要把这些状态库接到生产环境上去。# 本地建一个最小的记忆表自己执行 sqlite3 ./.harness/memory.db SQL CREATE TABLE IF NOT EXISTS memory ( id TEXT PRIMARY KEY, scope TEXT NOT NULL, -- 按 objective 或项目划分 kind TEXT NOT NULL, -- decision / constraint / rejected / fact content TEXT NOT NULL, created_at INTEGER NOT NULL, superseded_by TEXT ); CREATE INDEX IF NOT EXISTS idx_memory_scope ON memory(scope, kind); SQL召回时在 harness 侧做一次预算裁剪把“记忆”这栏控制在预算表内# recall.py —— 召回结果先裁剪再进上下文伪代码 def recall(query, scope, budget_tokens): hits search_memory(query, scopescope, top_k8) hits drop_superseded(hits) # 去掉已被取代的旧版本 hits dedupe_by_conclusion(hits) # 同一结论只留一条 picked, used [], 0 for h in sorted(hits, keylambda x: x.score, reverseTrue): cost estimate_tokens(h.content) if used cost budget_tokens: break picked.append(h) used cost return picked一个容易忽略的细节召回内容进入上下文后也会被后续的压缩再次处理。如果压缩提示词没有区分“原始历史”和“召回记忆”摘要里可能出现记忆的二手转述信息层级就乱了。建议在上下文里给不同来源打显式标签比如[RECALL]、[HISTORY]、[TOOL]压缩时按标签分别处理。7. 改前 / 改后对照记录把“感觉好多了”换成可填的表没有对照记录所有优化都是主观感受。下面这份台账模板建议每跑一次长任务就填一行字段固定便于横向比较。注意这里给的是记录结构具体数值需要你在自己的任务上测不要照抄别人的数字。记录项改前纯历史无复述改后预算 压缩 复述 记忆闸任务总轮数填写实测填写实测目标漂移次数复述已否决方案、偏离 objective填写实测填写实测上下文溢出中断次数填写实测填写实测单轮平均输入 Token从调用日志统计从调用日志统计压缩触发次数与总开销记录调用次数 × 单次成本同口径记录复述累计开销0未启用轮数 × 单轮复述 Token记忆召回累计开销0未启用召回次数 × 平均召回 Token任务是否达成 acceptance是 / 否是 / 否判定“目标漂移”需要一个客观口径否则每次都要靠人回忆。建议直接从工具调用序列里自动检测# drift_check.py —— 客观检测目标漂移伪代码 def detect_drift(trace, state): signals [] for step in trace: if step.action in state.rejected_actions: signals.append((rejected_action_replay, step.index)) if step.target_file and step.target_file in state.forbidden_paths: signals.append((constraint_violation, step.index)) if step.index % 10 0: goal_now extract_goal(step.context) if goal_now[objective] ! state.objective: signals.append((objective_shift, step.index)) return signals这样跑完一次任务你能拿到的是三条硬数据漂移事件发生在第几轮、类型是什么、对应哪次上下文变更。把这张表和预算表放在一起看通常能立刻定位是哪一栏配额设置不合理。如果只允许保留一个观察指标我会选“第 10 轮之后的目标漂移次数”。因为前十轮历史还短几乎不会漂移真正体现 harness 设计水平的是后半程。8. 上线前检查清单与下一步把上面几节压缩成一份可执行的检查清单Base URL 是否统一为https://taotoken.net/api且没有重复拼接路径Claude Code 用的是ANTHROPIC_*Codex 用的是config.toml的base_urlenv_key两者没有混用Key 用YOUR_API_KEY占位的方式管理不写死在会被提交的文件里预算表里每一栏都有明确数值且reserve不为零卸载后的内容留了可寻回的指针且 harness 实现了取回工具压缩有软硬双阈值压缩后会做目标字段的自动校验与回填todo-state 每轮注入包含rejected字段且在超预算时按优先级裁剪记忆召回有 scope、top_k、长度三道闸且去重与时效处理到位漂移检测是自动化的不依赖人工回忆每跑一次长任务填一行对照记录。回到最初的问题todo-state 复述能缓解目标丢失吗能。但它的定位是“稳定器”不是“扩容方案”。真正让长任务活下来的组合是预算与卸载控制总量压缩降低历史体积todo-state 复述锚定目标记忆召补充跨会话信息四者各管一段。任何一环单独上效果都会打折扣。配置和 Key 的门槛其实不高先把接入跑通再按上面的顺序一项一项打开开关做对照你会比看十篇原理分析更快找到自己任务的最优参数组合先在网页端试模型对话确认模型与响应风格符合预期https://taotoken.net/models/detail/chat?utm_sourcetaotoken_aicg_blog_endutm_contentcta_chat长任务跑得多可以看 Coding Plan 的额度与形态是否匹配你的轮次规模https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcta_plan然后到控制台创建 API Key把YOUR_API_KEY换成真实值https://taotoken.net/console/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentcta_keysClaude Code 侧的settings.json字段与 Base URL 写法以文档为准https://taotoken.net/doc/ClaudeCodeAnthropic?utm_sourcetaotoken_aicg_blog_endutm_contentcta_claudecode再补一句实践建议先只开todo_state.enabled跑一次长任务记录漂移次数再叠加压缩与预算再跑一次。每次只改一个变量对照表才有意义。把这两次记录贴在一起你就有了自己任务上的第一手结论而不是别人文章里的经验值。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价