上个月我把一个长任务 Agent 的账单拖出来逐行对,发现一个挺扎心的事实:模型调用次数只涨了不到两成,token 消耗却翻了近三倍。那会儿正好看到 DeepSeek Harness 在推文里说自家"省 Token",评论区一半在问怎么做到的,一半在说"营销话术"。我当时的第一反应也是后者——毕竟"省 token"这三个字在 Agent 圈子里已经被用烂了。但把 PTC 模式这一层的设计拆开看之后,我改了看法:它省的确实不是幻觉,只是省的地方和大多数人以为的不一样。这篇就把 PTC 模式在 DeepSeek Harness 里的实际作用点、缓存命中的构造规则、工具返回的裁剪逻辑,以及我自己复现时踩到的几个坑,一次性讲透。不管你是刚接触 Agent 编排的新手,还是已经在压上下文成本的老手,看完至少能自己算清楚一笔账:你的钱到底花在了前缀、历史、工具描述,还是重试上。
1. 先把账算明白:一次 Agent 跑完,token 到底去哪儿了
很多人对 token 消耗的直觉是"问得多就花得多",这套直觉在单轮问答里成立,放到 Agent 场景里就完全失效。Agent 一次任务可能只对话了七八轮,但每一轮都要把系统提示、工具清单、历史记录、检索片段、上一步的工具返回全部重新拼进上下文。也就是说,第 N 轮的输入 ≈ 前面所有轮次的累积。这是个近似平方级的增长曲线,而不是线性的。
我见过最常见的误判,是把"对话轮数"当成成本指标。有个团队做了个代码修复类的模拟项目,平均每个任务 12 轮对话,他们按每轮 2k token 估算,觉得总共 24k 就到顶了。实际跑下来平均 90k 上下,峰值到 160k。差出来的部分全在"重复搬运"上——同一份工具 schema,在 12 轮里被完整传了 12 次;同一段读文件的结果,在后续每一轮里都作为历史存在。
所以第一件事是把开销拆细,拆到每一项都能对应上一个具体的上下文片段。
1.1 输入侧的四笔固定支出
输入 token 在 Agent 里基本可以分成四块,这四块的性质完全不同,优化手段也完全不同:
| 开销项 | 典型占比 | 是否稳定 | 优化空间 |
|---|---|---|---|
| 系统提示 + 工具 schema | 15%~30% | 极高(每轮完全相同) | 极高,靠前缀缓存几乎能压到接近零 |
| 历史对话(含思考链) | 35%~55% | 中等(逐轮累加) | 中等,靠裁剪和摘要 |
| 工具返回结果 | 20%~40% | 低(内容随机) | 高,靠裁剪、结构化、字段白名单 |
| 检索/文件片段 | 10%~25% | 低 | 高,靠重排、去重、截断 |
看这张表你会发现一件很有意思的事:占比最大的历史对话,恰恰是最难压的;而占比排第二、第三的工具返回和系统提示,反而是最好动的。PTC 模式的主要发力点就在后两者,尤其是"系统提示 + 工具 schema"这一块——因为它有一个别的项都没有的性质:它在整个会话里几乎不变。
一个 20 个工具、每个工具 schema 平均 180 token 的 Agent,光工具清单就是 3600 token。跑 15 轮,就是 54000 token 的纯重复。这笔钱如果靠缓存复用,实际计费和延迟都能压下来一大截。这就是"省 Token"宣称的第一个真实来源,也是最容易被忽略的一个。
1.2 输出侧的隐性成本比想象中大
大部分人的注意力都在输入上,但输出侧有两块特别容易失控。第一块是思考链。开启推理模式后,模型在给出工具调用前会先输出一大段思考,这段思考同样是计费的输出 token,而且它还会进入下一轮的输入历史——一次计费两次占用。第二块是工具调用参数的格式冗余。如果工具参数要求严格的 JSON,模型往往会把整个结构完整输出一遍,包括那些没变的字段。
我做过一个粗略对比:同一批任务,把工具参数从"完整 JSON 回传"改成"只回传变更字段 + 增量补丁",输出 token 平均下降 22% 左右,输入侧的累积下降更明显,因为下一轮不需要再读一遍那些没变的字段。这个改动量不大,但对长任务的复利效应很显著。
注意:输出侧的优化要格外小心,一旦格式约束变复杂,模型的遵循率会下降,重试率一上升,省下来的钱全还回去还倒贴。这个坑我在第 4 节会详细讲。
1.3 用一张账单定位真正的漏点
我给自己的项目做过一个很土但特别管用的方法:把每一轮请求的上下文按"块"打标签,记录每块的 token 数和它是否与上一轮完全相同。跑完一个任务后,统计"完全相同块的累计 token 占比"。这个数字就是理论上的缓存收益上限。
我第一次测下来是 41%。也就是说,如果所有重复块都能命中缓存,输入成本能直接砍掉四成。这个数字当时把我吓了一跳——我一直以为重复的主要是历史,实际测出来重复最多的是工具 schema 和系统提示。后来我把这个方法固化成 harness 里的一个统计模块,每次改动上下文构造逻辑都跑一遍,看这个比例是升是降。
这也解释了一个很多人困惑的现象:为什么有些团队用了同样的模型、同样的框架,成本能差两三倍。差别往往不在 prompt 写得好不好,而在上下文是不是被组织成了"可复用的形状"。
2. PTC 模式改的到底是哪一层:把"不变"和"会变"彻底拆开
先把话说在前面:PTC 这个缩写在不同的团队里指代过不同的东西,有人用它指 Prompt Token Caching,有人指 Plan-Tool-Chain 的调度循环。我没法替 DeepSeek Harness 官方定义,但从它在文档和示例里体现出来的行为看,这套模式的核心动作只有一个——把上下文按"稳定性"分层,让稳定层永远出现在最前面且逐字节不变,让变化层全部堆到尾部。这个动作本身就是省 token 的全部秘密,后面所有的技巧都是它的推论。
为什么这么简单的动作能省这么多?因为主流模型服务侧的上下文缓存是按前缀匹配工作的:只要你的请求开头和上一次请求的开头逐字节一致,那一部分就不用重新计算。注意关键词是"逐字节"。少一个空格、多一个时间戳、工具列表顺序换了一下,前缀就断了,缓存从头失效。
这就是 PTC 模式真正解决的问题。它不是发明了什么压缩算法,而是把"缓存能命中"这件事从"碰运气"变成了"工程上可保证"。
2.1 缓存友好前缀的三条硬规则
我把实践中总结的规则列成三条,这三条只要破一条,命中率基本就崩了:
第一条:稳定内容必须前置,且顺序固定。系统提示、角色设定、工具 schema、输出格式约定,这些在所有请求里都不变的内容,全部放在最前面,顺序写死。不要用字典遍历去生成工具列表——很多语言的字典遍历顺序在不同进程里可能不一样,这个坑极其隐蔽。
第二条:任何"每次都不一样"的东西必须后置。时间戳、请求 ID、随机种子、当前用户名,这些东西如果混进系统提示里,等于给缓存判了死刑。我见过最典型的写法是在系统提示开头写当前时间:2024-xx-xx 14:23:11,看起来贴心,实际上让前缀永远无法命中。
第三条:变化内容按"变化频率"倒序排列。从最不变的开始,到最易变的结束。典型顺序是:系统提示 → 工具 schema → 会话级固定设定 → 任务描述 → 检索片段 → 历史对话 → 当前输入。历史对话放在检索片段后面,是因为历史是逐轮累加的,把它放前面会导致它一变,后面的检索片段全部失效。
这三条听着像废话,但真正落到代码里,破规则的方式五花八门。我在第 4 节会列几个真实踩过的。
2.2 命中与未命中:一次顺序错误的全盘代价
为了说清楚"顺序错误"的代价,我做过一个对照实验。同一个任务集(30 个模拟任务,平均 11 轮),只改上下文拼装顺序,其他全不动:
| 方案 | 前缀命中率 | 平均输入 token | 相对成本 |
|---|---|---|---|
| A:工具 schema 在历史之后 | 12% | 78k | 100%(基准) |
| B:工具 schema 前置但含时间戳 | 9% | 81k | 104% |
| C:工具 schema 前置、无动态字段、顺序固定 | 76% | 34k | 44% |
| D:在 C 基础上再裁剪工具返回 | 79% | 26k | 33% |
方案 B 特别值得说:它比 A 还贵。因为工具 schema 前置之后,历史对话的位置被推后了,而历史对话本身是逐轮变化的,结果就是"稳定内容前置了但没命中,变化内容反而更靠后导致尾部更长"。这是个负优化,而且从代码上看它比 A 更"规范",很容易骗过 code review。
方案 C 到 D 的差距只有 8 个百分点,说明前缀优化和内容裁剪是两件独立的事,效果可以叠加,但主要收益来自前者。这一点很反直觉——大家都觉得"少传点内容"是最直接的省钱方式,实际上"少重复计算"的杠杆更大。
2.3 和"直接砍历史"的本质区别
这里必须澄清一个常见误解。很多人理解的"省 token"就是砍历史,比如只保留最近 5 轮。这个做法确实能降成本,但它有个致命问题:丢失的任务状态会导致模型反复重新探索。
我对比过两种策略在同一个代码修复任务集上的表现。砍历史方案平均 8 轮完成任务,但其中有 3 轮是在重复读同一个文件;PTC 方案平均 9 轮,但没有重复读取。总 token 上前者 62k,后者 47k。也就是说,砍历史省下来的是"上下文体积",代价是"行为冗余",而行为冗余的成本更高。
PTC 模式的思路完全不同:它不砍内容,它让内容"在正确的位置重复出现但不重复计费"。历史该在还在,只是它落在了缓存前缀之外,作为增量部分传输。这个区别很关键——一个是减法,一个是重排。
提示:如果你的 Agent 在长任务里频繁重复调用同一个工具、读同一个文件,先别急着加"禁止重复读取"的提示词,去看看上下文顺序是不是把工具结果和历史混在一起了。状态没丢,只是被组织结构埋了。
3. Harness 侧真正起作用的几个开关
模型本身不省 token,省 token 的是 harness 在构造请求时的取舍。我把自己复现时用到的、收益最明显的几个开关按效果排序,每一项都给具体做法,不空谈原则。
3.1 工具返回值的白名单裁剪
这是投入产出比最高的一个改动。Agent 调工具拿回来的数据,绝大多数字段模型根本用不上,但全量塞进上下文。比如列目录返回 200 个文件的完整元信息,模型其实只需要文件名;读配置文件返回整个文件,模型只需要其中三五个键。
我的做法是给每个工具配一个"返回投影"配置,用字段白名单 + 长度上限,在工具层就裁掉,而不是指望模型自己忽略:
tools: list_files: projection: ["name", "type", "size"] max_items: 60 overflow_hint: "共 {total} 项,已截断展示前 60 项,可用 filter 参数缩小范围" read_file: max_chars: 6000 head_tail: true # 超长时保留头尾各一半 strip_blank_lines: true三个细节值得单独说。
overflow_hint这一行不是装饰。截断之后如果不告诉模型"被截断了"以及"怎么缩小范围",它往往会换个参数再调一次,等于白截。加上这句提示,重试率能降一大截。
head_tail而不是简单slice(0, 6000),是因为文件的头部通常是导入和声明,尾部通常是关键逻辑,中间才是可以牺牲的。这个改动对代码类任务的完成率提升很明显。
strip_blank_lines看着无关紧要,但对格式化良好的配置文件,能省 10%~15% 的字符。
实测下来,这一项单独做,平均输入 token 降 28%,任务完成率不降反升——因为上下文短了,模型注意力更集中。
3.2 子任务上下文隔离
长任务里最容易失控的是"什么都往主上下文里塞"。搜了 20 个文件,20 份内容全进主上下文;试了 5 种方案,5 段失败推理全留着。PTC 模式在这块的思路是:把探索性的子任务扔到独立上下文里跑,只把结论回传。
具体可以这样组织:主 Agent 负责任务分解和最终决策,遇到"需要搞清楚某个模块怎么工作"这类问题时,起一个子调用,给它干净的短上下文,让它读文件、跑命令、得出结论,然后只回传一段 200 字以内的摘要加关键文件路径。主上下文从"承载所有原始材料"变成"承载结论"。
这个做法的成本账很划算:子任务的上下文虽然也要花 token,但它是一次性的、短的,而且互不累积。主上下文保持精简,后续每一轮的输入都受益。我在一个跨平台系统的模块梳理任务里对比过,不分隔离的方案总 token 约 130k,隔离方案约 62k,差距超过一半。
代价是信息损失。子任务回传的摘要如果不准,主 Agent 会基于错误结论往下走。所以摘要的模板要设计好,强制它输出"结论 + 证据位置 + 不确定项"三段,不确定项这一栏特别重要,它能让主 Agent 知道哪里需要二次确认。
3.3 检索结果的重排与去重
检索类片段的问题不是"太长",是"太像"。向量检索返回的 top-10,往往有 4~5 条内容高度重叠,全塞进去等于花五倍的钱买一倍的信息。
我在 harness 里加了一步很轻量的处理:先按内容做 simhash 去重,再按与查询的相关性重排,最后只取前 4 条,并且对每条做"句子级压缩"——只保留与查询词共现度高的句子,其余删掉。这一步不需要额外的模型调用,纯规则实现。
效果上,检索片段的平均 token 从 4200 降到 1400,答案质量在抽样评估里基本持平(有约 6% 的案例变差,主要是那些需要大段上下文才能判断的场景)。对于这类场景,我的处理是给检索工具加一个full: true参数,让模型在确实需要全文时主动指定,而不是默认全给。
3.4 预算闸门与早停
最后一个开关是兜底:给每个任务设 token 预算,跑到阈值的 70% 时在上下文尾部插一条系统提醒,明确告诉模型"剩余预算 X,请优先收敛"。到 90% 时强制它输出当前最佳结果并结束。
这条提醒的成本很低(几十个 token),但它能救回大量"无限探索"的任务。我测过一批失败任务,其中有 60% 的失败原因是"预算耗尽被硬性中断,没有产出",加了软提醒之后,这批任务的"降级但可用"产出率提升到 75%。
这里有个反直觉的点:预算提醒要放在上下文的最后面,不能放系统提示里。放前面会破坏前缀缓存,放后面既能生效又不影响命中。这个位置的选择本身就是 PTC 模式思路的一次应用。
4. 复现这套 harness 时我踩的五个坑
前面讲的都是"应该怎么做",但真正落地的过程全是坑。我把踩过的几个记录在下面,每个都给出排查路径,因为这些坑你大概率也会遇到。
4.1 动态字段把缓存前缀打穿
这是最隐蔽的一个。我的系统提示里有这么一行:
当前会话开始时间:{{ now }}看起来很合理,实际效果是前缀命中率永远是 0。一开始我以为是自己顺序排错了,花了半天时间逐段对比请求体,最后用 diff 工具对比两次请求的原始字节,才发现每次请求的第一段就不一样。
排查这类问题的方法很笨但有效:把连续两次请求的完整报文 dump 下来,做逐字节 diff,找到第一个不同位置。那个位置之前的所有内容才是有效前缀。我后来把这个做成 harness 的一个调试开关,随时能看当前有效前缀长度。
修法有两种。简单的做法是把这类动态信息挪到上下文尾部,作为"运行时环境"段。更彻底的做法是把它做成工具——模型需要知道时间就调get_current_time,而不是每轮都塞进系统提示。后者更符合 PTC 的思路:任何会变的东西,都不该出现在前缀里,哪怕它很小。
4.2 工具 schema 的微小改动导致整段失效
比时间戳更隐蔽的是工具 schema 的"无意识变更"。我遇到过一次:给某个工具加了一个可选参数,本意是增强能力,结果那一周的 token 成本涨了 15%。原因是 schema 变了,缓存前缀跟着变,但因为工具数量多、总长度大,从账单上完全看不出异常,只是"最近好像贵了点"。
更坑的是顺序问题。我当时用的是一个配置字典来生成工具列表,某次重构把配置文件的键顺序调了一下(纯粹是整理代码),工具列表顺序跟着变了,缓存全废。这个改动在代码 review 里完全看不出问题——逻辑上确实等价,但对缓存来说是天翻地覆。
后来我加了两条约束:工具列表在构建时强制按名字排序,不受配置顺序影响;每次发布前跑一次"前缀指纹"校验,把系统提示 + 工具 schema 拼起来的整体算个哈希,和上一版对比,如果变了就在发布说明里标注"注意:本次变更会使缓存冷启动"。
4.3 统计口径不一致造成的"假省钱"
这个坑最坑,因为它会让你在错误的方向上继续优化。我一开始统计 token 用的是模型返回的 usage 字段,看到输入 token 降了 40%,非常开心。后来把账单和实际计费对了一下,发现实际计费只降了 22%。
原因有两个。第一,缓存命中的部分虽然"不算重算",但通常仍按折扣价计费,不是免费;不同服务商的折扣比例不同,不能一概按 0 算。第二,重试的请求我根本没统计进去——重试请求的 usage 被我的脚本当成独立调用,但因为任务失败,被我的过滤逻辑丢掉了。
修正后的统计口径要同时看四个数:总输入 token、其中命中缓存的比例、总输出 token、以及"每个成功任务的加权成本"。最后这个指标最关键,因为它把重试和失败都算进去了。只看单次调用成本,很容易做出"压缩到模型看不懂然后疯狂重试"这种负优化。
4.4 压缩过度导致重试率上升,总账反而更贵
接上一条,这是最典型的负优化。我有一版把工具返回截得特别狠,单次输入 token 降了 45%,看着很漂亮。结果跑了一周发现总成本涨了 12%。
拉数据一看,重试率从 8% 涨到 31%,平均任务轮数从 9 涨到 14。原因是模型拿到的信息不全,反复调工具补信息,每次补都要带上完整历史,累积成本反而更高。
这个教训让我定了个规矩:任何压缩改动,必须同时看"单次 token"和"每成功任务 token"两个指标,后者不降就不算优化。后来我又加了个更保守的约束:压缩改动如果让重试率涨幅超过 5 个百分点,直接回滚,不管省了多少。
4.5 多轮对话里的状态隐性膨胀
最后一个坑跟 PTC 没关系,但跟"省 token"关系很大。多轮对话里,很多框架会把"工具调用记录"和"工具返回"成对保存,包括那次调用失败的记录。跑久了上下文里会堆一堆"我试过 A,失败了""我试过 B,失败了"。
这些记录对模型有价值(避免重复犯错),但完整保留成本太高。我的处理是把失败记录压缩成一行:尝试 X 失败:<错误类型>,把错误堆栈和详细输出丢掉。这样既保留了"别重复试"的信息,又把每条从几百 token 压到十几 token。
另一个隐性膨胀源是文件读取缓存。同一个文件被读了三次,三个版本都在上下文里。我的做法是维护一个"文件版本表",同一路径只保留最新一次读取内容,旧的在拼装上下文时替换成一句(该文件内容已更新,见下方最新版本)。
5. 怎么验证"省 Token"不是话术:一套能自己跑的对照实验
看到任何"我们省了 X%"的宣称,我的第一反应都是"控制变量了吗"。这套实验我自己跑过三遍,每次改动 harness 都用它验证,把它写出来是因为大部分人验证成本优化时只做"改前 vs 改后"的对比,这个对比在 Agent 场景里几乎没有说服力——因为任务本身方差极大,同一套配置跑两次能差 30%。
5.1 实验设计的三个控制点
控制点一:任务集固定且分层。不要用同一类任务验证。我的任务集分三层:短任务(3 轮内完成)、中任务(5~10 轮)、长任务(10 轮以上)。三层各 10 个。分层的意义在于,PTC 的收益主要在长任务上,如果只测短任务,会得出"优化没效果"的错误结论;反过来只测长任务,又会高估收益。
控制点二:每层跑三遍取中位数。不是取平均,取中位数。因为 Agent 的异常值特别多(偶尔卡死跑到预算上限),平均值会被拉偏。
控制点三:固定随机性来源。工具的返回内容如果来自真实环境(比如文件系统、网络),每次跑都不一样。我的做法是把工具返回录制下来,回放跑实验。这个做法牺牲了一点真实性,但换来了可比性。生产环境的验证另做,不混在一起。
5.2 指标口径:四个数缺一不可
| 指标 | 含义 | 为什么必须看 |
|---|---|---|
| 单次输入 token 中位数 | 每次请求的输入规模 | 反映上下文构造效率 |
| 缓存命中 token 占比 | 有多少输入走了复用 | PTC 效果的直接影响指标 |
| 每成功任务总 token | 总消耗 / 成功任务数 | 唯一能反映真实成本的指标 |
| 重试率 | 因格式/信息不足导致的重复调用比例 | 防止负优化的护栏 |
前两个是"过程指标",后两个是"结果指标"。只看过程指标会陷入自我感动,只看结果指标又不知道该改哪里。我自己的习惯是每次改动后四个数一起看,任何一个变差都要解释清楚原因才允许合入。
5.3 一组实测数据与它的适用边界
用上面这套方法,我在模拟项目上测了四组配置,每层任务跑三遍取中位数:
| 配置 | 长任务每成功任务 token | 缓存占比 | 重试率 |
|---|---|---|---|
| 基线(无 PTC,无裁剪) | 152k | 6% | 9% |
| 仅 PTC(顺序重排 + 去动态字段) | 88k | 71% | 9% |
| 仅裁剪(工具返回白名单) | 110k | 11% | 13% |
| PTC + 裁剪 + 隔离 | 61k | 74% | 11% |
这组数里有几个值得说的点。
PTC 单独做,效果最大且不伤质量,重试率没变。这是它作为"基础设施级优化"的价值——它不改内容和语义,只是改排列,所以风险极低。
裁剪单独做,收益中等但重试率上升。这说明裁剪是需要精细调参的,不能一刀切。
两者叠加不是简单相加,而是相乘之后再打个折。61k 相对于基线的 152k 是 60% 的降幅,而不是把两个降幅加起来。
最后要说清楚这套数据的边界:它只在这个任务集、这个模型、这个工具集上成立。换一批工具(尤其是返回体积大的工具),裁剪的收益会显著放大;换一批短任务为主的场景,PTC 的收益会缩小到 10% 以内,因为轮数少,累积效应还没起来。所以看到任何"省 60%"的说法,都该先问一句"在什么任务分布上测的"。
我自己的判断标准比较朴素:如果一套优化能在长任务上把"每成功任务 token"压到基线的一半以下,且重试率涨幅不超过 3 个百分点,那就是真优化;如果单次成本降了很多但重试率涨了十几个点,那基本是在把钱从左口袋挪到右口袋,还多付了延迟。
最后分享一个我一直在用的小习惯:每次给 harness 加新功能,先在本地跑一遍"前缀指纹"和"有效前缀长度"这两个检查,再看一眼账单。这两个数不对劲的时候,后面所有的优化数据都不可信。踩过几次坑之后我发现,Agent 成本优化这件事,八成的功夫花在"看清楚钱花在哪",剩下两成才是改代码。