这不是一篇"省钱小技巧"合集。它把一次真实的成本优化,从账本数字一直追到框架源码里的实现机制:哪一层在记账、哪一层在改写上下文、哪一次压缩其实亏钱、哪一个参数改了会让缓存前缀整段失效。
数据来源:四天窗口、990 次调用、79.5M token 的真实账本与代理统计,全部为实测值。金额单位统一为"美元 / 每 1 亿 token"。
适配对象:长会话 Agent、带 prompt cache 的按 token 计费 API,以及所有"账单很贵但说不清贵在哪"的 LLM 应用。
一、一笔说不清的账单
几乎所有团队都经历过同一幕:月度账单暴涨三倍,大家围在白板前猜原因。猜"是不是模型太贵",于是换便宜模型;猜"是不是用户在乱用",于是加限制;猜"是不是缓存没开",于是查配置。折腾一圈,账单通常只降了十几个百分点。
因为猜错方向了。账单的结构不按"模型"分,按输入是否命中缓存和输出分,而这三者的单价差了两个数量级。
先看一次实测的成本结构。把 1 亿 token 按真实混合比例摊开:
| 命中输入 | 91.26% | 0.003 | $0.274 | 12.6% |
| 未命中输入 | 7.43% | 0.15 | $1.115 | 51.4% |
| 输出 | 1.30% | 0.60 | $0.782 | 36.0% |
| 合计 | 100% | — | $2.171 | 100% |
三个桶的杠杆倍率(每 1 亿 token 的绝对花费)分别是:命中输入 $0.30、未命中输入 $15、输出 $60。也就是说,输出每减少 1%,省下的钱约等于未命中输入减少 1.4%。
这张表至少有四个反直觉结论:
第一,命中输入不是免费的。它便宜五十倍,但占了 91% 的量,在长上下文里它同样是成本的一部分。把上下文堆到 20 万 token 只为了"维持高命中率",本质上是拿五十倍的效率去买一个好看的比值。
第二,真正可动的旋钮只有两个:未命中输入的绝对量,和输出的长度。其余变量(模型单价、渠道、峰谷)要么动不了,要么改一次就要付整段重算的代价。
第三,优化顺序由成本占比决定,不由单价决定。先去换更便宜的模型,通常只能影响"未命中 + 输出"这两块里的一部分,而这两块已经是总成本的 87%。
第四,20% 目标不是口号,是一个可验算的数:$2.171 × 0.20 ≈ $0.434 / 亿 token。后面每一节都会回到这个数,看哪个动作能把它往左推多少。
二、先把成本模型立起来
在动手之前,必须先写下一个能自我校验的成本函数,否则后面所有"降了多少"都是拍脑袋。
最朴素的模型是:
cost = M × P_miss + (In − M) × P_hit + Out × P_out
其中 In 是总输入 token,M 是其中未命中(被重新计价)的部分,Out 是输出 token。命中率定义为 hit = 1 − M / In。
这个式子只有三行,却决定了后面全部判断:
- 它是线性的,所以多个杠杆可以相乘,不会互相抵消;
- P_hit 比 P_miss 小一到两个数量级,所以"命中率"在数学上被大幅放大——这既是优化空间,也是陷阱;
- Out 的系数是 P_miss 的四倍,因此"少说两句"的边际收益经常高于"多命中一点"。
为了看清钱从哪里漏出去,把这条链路画成图:
#mermaid-svg-wTWjVjmf6mY6MdKI{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-wTWjVjmf6mY6MdKI .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-wTWjVjmf6mY6MdKI .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-wTWjVjmf6mY6MdKI .error-icon{fill:#552222;}#mermaid-svg-wTWjVjmf6mY6MdKI .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-wTWjVjmf6mY6MdKI .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-wTWjVjmf6mY6MdKI .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-wTWjVjmf6mY6MdKI .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-wTWjVjmf6mY6MdKI .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-wTWjVjmf6mY6MdKI .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-wTWjVjmf6mY6MdKI .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-wTWjVjmf6mY6MdKI .marker{fill:#333333;stroke:#333333;}#mermaid-svg-wTWjVjmf6mY6MdKI .marker.cross{stroke:#333333;}#mermaid-svg-wTWjVjmf6mY6MdKI svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-wTWjVjmf6mY6MdKI p{margin:0;}#mermaid-svg-wTWjVjmf6mY6MdKI .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-wTWjVjmf6mY6MdKI .cluster-label text{fill:#333;}#mermaid-svg-wTWjVjmf6mY6MdKI .cluster-label span{color:#333;}#mermaid-svg-wTWjVjmf6mY6MdKI .cluster-label span p{background-color:transparent;}#mermaid-svg-wTWjVjmf6mY6MdKI .label text,#mermaid-svg-wTWjVjmf6mY6MdKI span{fill:#333;color:#333;}#mermaid-svg-wTWjVjmf6mY6MdKI .node rect,#mermaid-svg-wTWjVjmf6mY6MdKI .node circle,#mermaid-svg-wTWjVjmf6mY6MdKI .node ellipse,#mermaid-svg-wTWjVjmf6mY6MdKI .node polygon,#mermaid-svg-wTWjVjmf6mY6MdKI .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-wTWjVjmf6mY6MdKI .rough-node .label text,#mermaid-svg-wTWjVjmf6mY6MdKI .node .label text,#mermaid-svg-wTWjVjmf6mY6MdKI .image-shape .label,#mermaid-svg-wTWjVjmf6mY6MdKI .icon-shape .label{text-anchor:middle;}#mermaid-svg-wTWjVjmf6mY6MdKI .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-wTWjVjmf6mY6MdKI .rough-node .label,#mermaid-svg-wTWjVjmf6mY6MdKI .node .label,#mermaid-svg-wTWjVjmf6mY6MdKI .image-shape .label,#mermaid-svg-wTWjVjmf6mY6MdKI .icon-shape .label{text-align:center;}#mermaid-svg-wTWjVjmf6mY6MdKI .node.clickable{cursor:pointer;}#mermaid-svg-wTWjVjmf6mY6MdKI .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-wTWjVjmf6mY6MdKI .arrowheadPath{fill:#333333;}#mermaid-svg-wTWjVjmf6mY6MdKI .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-wTWjVjmf6mY6MdKI .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-wTWjVjmf6mY6MdKI .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-wTWjVjmf6mY6MdKI .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-wTWjVjmf6mY6MdKI .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-wTWjVjmf6mY6MdKI .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-wTWjVjmf6mY6MdKI .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-wTWjVjmf6mY6MdKI .cluster text{fill:#333;}#mermaid-svg-wTWjVjmf6mY6MdKI .cluster span{color:#333;}#mermaid-svg-wTWjVjmf6mY6MdKI div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-wTWjVjmf6mY6MdKI .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-wTWjVjmf6mY6MdKI rect.text{fill:none;stroke-width:0;}#mermaid-svg-wTWjVjmf6mY6MdKI .icon-shape,#mermaid-svg-wTWjVjmf6mY6MdKI .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-wTWjVjmf6mY6MdKI .icon-shape p,#mermaid-svg-wTWjVjmf6mY6MdKI .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-wTWjVjmf6mY6MdKI .icon-shape .label rect,#mermaid-svg-wTWjVjmf6mY6MdKI .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-wTWjVjmf6mY6MdKI .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-wTWjVjmf6mY6MdKI .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-wTWjVjmf6mY6MdKI :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}
1 亿 Token 真实混合
命中输入 91.26%单价 0.003
未命中输入 7.43%单价 0.15
输出 1.30%单价 0.60
$0.274 · 12.6%
$1.115 · 51.4%
$0.782 · 36.0%
主战场把 M 的绝对值压到最低
次战场把每轮输出压短
目标 · $0.434 / 亿 token
图里那条从 M 出发的箭头,是整篇文章的主线:先把未命中的绝对量压下去,命中率作为结果自然过线;反过来做——为了比值好看而拉长上下文——在数学上是亏的。
这里还有一个工程上极易忽略的环节:记账口径必须唯一。真实框架里通常存在两套数字,一套是逐调用落盘的账本(含峰谷、去重、单价快照),另一套是代理层按会话聚合的诊断统计。两者对同一会话的量级可能相差约两倍,因为记账粒度不同(一个按上游实际计费字段,一个按请求体估算)。混用会得出完全相反的结论:用代理统计判断"今天省了钱",账本可能显示亏了。账本用于预算与回归,代理统计用于归因与诊断,永远不要交叉引用。
三、命中率是比率,不是成本
上一节埋了一个陷阱,这一节拆开它。
把 hit = 1 − M / In 代回成本式,会发现一个不舒服的事实:为冲高 hit 而扩大 In,你堆进去的是五十倍才等价的命中 token。举例:会话 A 命中率 92%、上下文 4 万 token;会话 B 命中率 99%、上下文 25 万 token。A 的总成本可以低于 B,尽管 B 的比值漂亮得多。
于是得到两条互补的目标:
第二条需要一个推导。单轮命中率近似为 hit ≈ 1 − δ / C,其中 δ 是本轮追加的 token,C 是本轮上下文。于是 98% 等价于 δ ≤ 0.02 × C:
| 40K | 800 tok |
| 75K | 1.5K tok |
| 150K | 3K tok |
实测每轮追加约 1.2K token(一次工具调用 + 一段回答),因此 C 必须持续保持在 60K 以上,98% 才是可达的。这意味着:一个刚开五分钟、上下文只有 1 万 token 的会话,不可能有 98% 命中,问题不在实现。
本次写作会话本身就是一个现成的样本。它的第一个请求:输入 26.6K token,命中 256 token,命中率 1.0%,未命中 26.3K 全部计入"新增内容"。这不是缺陷——这是冷启动的必然形态。任何新会话的第一笔账,都按全价计。真正要盯的,是第二笔、第十笔、第一百笔之后 M 有没有收敛。
所以"98% 命中"的正确判据不是单一比值,而是一个预算式:
98% ⇔ M ≤ 0.02 × In
并且必须逐请求核对,而不是看会话平均值。因为未命中的分布极度不均匀:在 125 个请求里有 18 个(14%)低于 80% 命中,却吃掉了全部未命中的 88.5%。省钱的动作不是普遍优化,而是精准掐掉这十几个坏请求。
那么坏请求是怎么产生的?对单个高成本会话做拆解,未命中由三块构成:
| 新增内容(每轮增量 δ) | 3.32% | 工具输出、回复追加,属正常增长 | 有:把大块输出搬出上下文 |
| 压缩重付(fold 重算) | 1.86% | 折叠改写中段历史,其后整段重付 | 有:抬高折叠门槛 |
| 驱逐 / 改写(TTL、重启、abort) | 2.53% | 空置、重启、中途取消导致上游缓存失效 | 部分:只能靠纪律 |
合计 7.70%,即现状命中率 92.30%。要压到 2.00%,等于把三项分别压到 1.2 / 0.3 / 0.5——没有任何单一开关能做到,必须三项同时下刀。这也就解释了为什么"打开某个压缩插件"通常没有效果:它只作用在三块中的一块,还可能因为改写中段而加重第二块。
命中率这个比值还有三种常见的失真,都会让人误判。
失真一:分母被稀释。 把大量短会话混进统计,均值会被首请求的低命中拉低。一个系统里若每天新开两百个会话,每个首请求按 1% 计,那么整体命中率天然比"稳态命中率"低十几个百分点。正确的做法是分两层报数:首请求单独计数,稳态请求单独计数。
失真二:请求数不均。 比率对每个请求等权,而账单对每个请求按 token 加权。一个 20 万 token 的请求和一个 2 千 token 的请求各算一次"命中/未命中",前者对成本的影响是后者的一百倍。因此比值达标不代表账单达标,必须同时看未命中的绝对量。
失真三:命中不等于复用。 上游按前缀匹配,命中只能说明"这段字节之前见过",不说明"这段字节有用"。把无用的长工具输出留在上下文里,既拿命中,也付每轮的命中费。前文那句"命中不是免费的",在实现层面就是这个意思。
四、把镜头对准框架:成本机制到底实现在哪一层
前面三节是成本学,和具体框架无关。但"知道该压 M"与"真的能把 M 压下去"之间,隔着一整套实现。这一节把镜头对准一个真实的 Agent 框架(DeepSeek Harness,下称 DSH),逐层看它在哪里花钱、哪里省钱、哪里能改、哪里改了就亏。
4.1 一条请求的字节结构,决定了缓存能不能复用
所有 prompt cache 的实现都遵循同一件事:上游按前缀匹配。它维护一棵前缀树,只有当新请求的字节序列与某个已有前缀逐字节一致时,超出部分才按"新增"计价。所以对 Agent 而言,一次请求的字节结构就是它的成本结构。
DSH 的请求体大致按以下顺序组装:
前六段里,前四段是稳定前缀,第五段是增量区,第六段是本轮增量。成本由此分成三类:稳定前缀命中(便宜)、历史增量(便宜,只要没被改写)、本轮追加(全价)。
于是"缓存命中率"在实现上退化成一个非常具体的问题:前四段有没有被重写? 任何一处改动都会导致从字节 0 起全部失效——不是损失那一段,而是损失后面所有内容。
这解释了几条看起来像玄学的纪律:
- 会话中途装插件、卸技能、改 AGENTS.md,会让第 3 段与第 4 段变化;
- 中途切换模型或推理档位,会让上游缓存分区失效(不同模型/不同 thinking 配置命中不同前缀树);
- 会话中途写记忆,会实时重写第 3 段;
- 工具 schema 按注册顺序生成,增删一个 MCP 工具就是一次前缀变更。
反过来,尾部注入是安全的。时间上下文这类"每轮加在最后"的内容,最多改变尾部一行,前缀不受影响。判断标准只有一条:它是"每轮都改写头部",还是"只在尾部追加"。
4.2 上下文治理层:四种能力,四种代价
DSH 的上下文治理由独立一层负责(billion-context),它提供四种互不等价的机制。理解它们的区别,是避免"越压越贵"的前提。
(一)整体折叠(compress)。 把一段已消费的历史替换成自包含摘要。收益是上下文变短、后续每轮重发量下降;代价是改写中段字节,其后所有请求的前缀失效,需整段重付。因此折叠的净收益取决于"剩下还有多少轮":折得早、会话还长,赚;会话快结束了才折,纯亏。
(二)工具结果蒸馏(absorb)。 对超长工具输出(默认阈值 4000 token)生成摘要,原文移出上下文。它的作用点比折叠更细:只动一条消息,不动整段历史,所以前缀破坏面更小。触发还带水位条件(默认上下文超过 75% 才启用),避免在上下文宽裕时做无谓蒸馏。
(三)内容寻址外置(CCR)。 把大结果写到本地存储,只在上下文里留一个形如 📦 [acp-stored #m00423] 的句柄,需要时再按 id 取回。这是"无损"的:原文没有消失,只是不在热路径上。它对成本是双赢——既降低每轮增量 δ,又降低折叠频率。但它默认需要显式开启,且要配置排除名单(精确读取类工具如 read、grep 不适合外置,因为读者往往只需要其中一小段)。
(四)输出规训(outputSteering)。 在系统提示尾部注入输出纪律:不解释、不复述、表格优先、无要求不给代码全文。它几乎零成本,且对长会话有复利效应——每轮少说 300 token,100 轮就是 3 万 token 的输出节省。但要注意:关闭其中的"按任务难度自动切换推理档位"那一项,否则它会在高压轮次把档位钳低,反向制造 effort 切换,在 wire 上形成两套前缀交替。
四者的代价可以拉一张对照表。它比任何"优化建议"都实用,因为它直接告诉你每次治理动作要付什么:
| compress 折叠 | 整段中段历史 | 后续每轮重发量下降 | 改写中段,其后全段重付一次 | 历史确定不再回读、剩余轮次足够多 |
| absorb 蒸馏 | 单条超长工具结果 | 降本轮增量 | 有损,细节不可回溯 | 工具输出已消化、上下文紧张 |
| CCR 外置 | 大结果整体 | 无损降增量、降折叠频率 | 需要一次取回成本、需排除精确读取工具 | 结果可延迟读取、体积大 |
| outputSteering | 系统提示尾部 | 每轮输出变短,复利 | 可能牺牲可读性 | 默认开启,长期收益最大 |
这张表还解释了为什么治理顺序不能反:外置(无损)应该先于蒸馏(有损),蒸馏应该先于折叠(改写历史)。能用无损手段解决的,不要用有损的;能只动一条消息的,不要动整段历史。
四者的作用点画成一张决策图:
#mermaid-svg-FTffjgKxAJEGcOT2{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-FTffjgKxAJEGcOT2 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-FTffjgKxAJEGcOT2 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-FTffjgKxAJEGcOT2 .error-icon{fill:#552222;}#mermaid-svg-FTffjgKxAJEGcOT2 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-FTffjgKxAJEGcOT2 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-FTffjgKxAJEGcOT2 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-FTffjgKxAJEGcOT2 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-FTffjgKxAJEGcOT2 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-FTffjgKxAJEGcOT2 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-FTffjgKxAJEGcOT2 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-FTffjgKxAJEGcOT2 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-FTffjgKxAJEGcOT2 .marker.cross{stroke:#333333;}#mermaid-svg-FTffjgKxAJEGcOT2 svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-FTffjgKxAJEGcOT2 p{margin:0;}#mermaid-svg-FTffjgKxAJEGcOT2 .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-FTffjgKxAJEGcOT2 .cluster-label text{fill:#333;}#mermaid-svg-FTffjgKxAJEGcOT2 .cluster-label span{color:#333;}#mermaid-svg-FTffjgKxAJEGcOT2 .cluster-label span p{background-color:transparent;}#mermaid-svg-FTffjgKxAJEGcOT2 .label text,#mermaid-svg-FTffjgKxAJEGcOT2 span{fill:#333;color:#333;}#mermaid-svg-FTffjgKxAJEGcOT2 .node rect,#mermaid-svg-FTffjgKxAJEGcOT2 .node circle,#mermaid-svg-FTffjgKxAJEGcOT2 .node ellipse,#mermaid-svg-FTffjgKxAJEGcOT2 .node polygon,#mermaid-svg-FTffjgKxAJEGcOT2 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-FTffjgKxAJEGcOT2 .rough-node .label text,#mermaid-svg-FTffjgKxAJEGcOT2 .node .label text,#mermaid-svg-FTffjgKxAJEGcOT2 .image-shape .label,#mermaid-svg-FTffjgKxAJEGcOT2 .icon-shape .label{text-anchor:middle;}#mermaid-svg-FTffjgKxAJEGcOT2 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-FTffjgKxAJEGcOT2 .rough-node .label,#mermaid-svg-FTffjgKxAJEGcOT2 .node .label,#mermaid-svg-FTffjgKxAJEGcOT2 .image-shape .label,#mermaid-svg-FTffjgKxAJEGcOT2 .icon-shape .label{text-align:center;}#mermaid-svg-FTffjgKxAJEGcOT2 .node.clickable{cursor:pointer;}#mermaid-svg-FTffjgKxAJEGcOT2 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-FTffjgKxAJEGcOT2 .arrowheadPath{fill:#333333;}#mermaid-svg-FTffjgKxAJEGcOT2 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-FTffjgKxAJEGcOT2 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-FTffjgKxAJEGcOT2 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-FTffjgKxAJEGcOT2 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-FTffjgKxAJEGcOT2 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-FTffjgKxAJEGcOT2 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-FTffjgKxAJEGcOT2 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-FTffjgKxAJEGcOT2 .cluster text{fill:#333;}#mermaid-svg-FTffjgKxAJEGcOT2 .cluster span{color:#333;}#mermaid-svg-FTffjgKxAJEGcOT2 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-FTffjgKxAJEGcOT2 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-FTffjgKxAJEGcOT2 rect.text{fill:none;stroke-width:0;}#mermaid-svg-FTffjgKxAJEGcOT2 .icon-shape,#mermaid-svg-FTffjgKxAJEGcOT2 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-FTffjgKxAJEGcOT2 .icon-shape p,#mermaid-svg-FTffjgKxAJEGcOT2 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-FTffjgKxAJEGcOT2 .icon-shape .label rect,#mermaid-svg-FTffjgKxAJEGcOT2 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-FTffjgKxAJEGcOT2 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-FTffjgKxAJEGcOT2 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-FTffjgKxAJEGcOT2 :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}
是
否
低于 75%
高于 75%
有 · 超过 4000 tok
有 · 可延迟读取
无
是
否 · 快收尾
请求即将发出
稳定前缀是否被改写?
整段重付从字节 0 起失效
前缀命中
上下文水位
正常追加
有无超长工具结果
absorb 蒸馏只动一条消息
CCR 外置留句柄 · 无损
历史是否已消费且剩余轮次够多
compress 折叠改写中段 · 全段重付
不动折叠必亏
降低每轮增量 δ
降低重发量但付一次重算
账单结算
这张图的每个判断节点,都对应一个真实参数。把它当成调参地图,比背参数表更有用。
4.3 水位线:什么时候该压,什么时候不该压
治理层有一组水位阈值,决定什么时候提醒、什么时候硬压:
| 首次最低水位 | 45% | 极早期就可能触发一次折叠,需谨慎 |
| 提醒(nudge) | 75% | 建议主动压缩,不强制 |
| 紧急(emergency) | 95% | 直接硬截断,内容有丢失风险 |
注意这里有一个常被误解的地方:到线不等于该压。压缩要重付一次前缀(改写 byte 0 起缓存),所以它的经济判据是"重付成本 < 节省的未命中 × 剩余轮数"。当上下文已接近 80%、会话也快结束,剩余轮数通常只有个位数,此时压缩的期望收益是负的——正确动作是收尾、写可交接的交付物、开新会话。
这也是实测中"10 次折叠有 6 次不回本"的来源。折叠不是免费的上下文管理,它是带杠杆的借贷:借的是当下的上下文空间,还的是其后整段的重新计价。
4.4 折叠门槛:为什么用字符而不是 token
折叠实现里有一个关于门槛的细节值得单说。配置项是按字符计的,例如 60000 字符。而中文的 token 换算比是 1 到 2 个字符每 token(视内容而定),所以 60000 字符大约落在 3 万到 6 万 token 之间。
如果按"每 2 万 token 折一次"来设计,在中文场景下就会频繁折叠——每折一次,其后所有请求重付一次。于是出现"折完马上又要回读原文"的净亏。这正是把门槛从 token 改成字符、并把值抬高的原因:中间那段历史宁可让它长,也不要反复改写。
一个可复用的判断是:折叠只在"这段历史确定不会再被读取"时才有正收益。规划任务的中间产物、一次性探索的搜索结果、已消化的长篇工具输出,都是候选;而正在被反复引用的设计决策、当前任务的验收标准、用户意图的原始表述,折了就等于自毁。
4.5 记账层:两套数字,两个用途
框架里通常还有一层 cost-meter:它按调用逐条记录输入、命中、输出、推理 token,带单价快照与峰谷标记,落成一个账本文件。与前面的代理诊断统计相比:
| 粒度 | 逐调用,含峰谷与去重 | 逐会话聚合 |
| 用途 | 预算、回归、结算 | 归因、命中率、水位、折叠经济学 |
| 与另一侧的量级关系 | 唯一真相 | 可能差约 2 倍 |
差 2 倍不是 bug,是口径:一侧按上游返回的计费字段,另一侧按请求体估算。因此"今天比昨天多花了多少"只能查账本;"为什么这个会话命中率掉了"只能查诊断。混用会产生看似有理、实则错误的因果链。
4.6 峰谷:一条容易被忽略的乘法
如果计价有峰谷,它是所有优化之上的一条独立乘数。本机实测的峰价窗口是:工作日 UTC [1, 4) 与 [6, 10),换算成北京时间即 09:00–12:00 与 14:00–18:00,价格为平峰的 2 倍;周末与法定节假日全谷。
这条规则的价值在于它是"零改动优化":同样的工作,挪到晚间或午间,直接砍一半。代价只是排班。与之配套的还有渠道分流:简单任务(抽取、分类、格式化)走廉价档,只有复杂设计决策才用贵档——这条能再拿一个约 0.76 的乘数。
4.7 推理档位属于配置面,不属于提示词
一个常见的误判:以为在提示词里写"请简洁思考"就能降低推理 token。实际上推理档位(reasoning effort)是 agent preset / 配置层的东西,提示词改不动它。
更麻烦的是它的生效顺序。框架里可能存在两处来源:一处是 profile 配置里的默认等级,另一处是会话持久化下来的选择。实测中会话持久化的优先级更高,也就是说改配置文件不一定会改变当前会话的行为——要判断真实档位,必须看会话记录,而不是看配置。
而档位一旦在会话中途改变,上游会因 thinking 配置变化重新分区缓存,形成两套前缀交替。这是命中率长期卡在 95% 附近最难查的成因之一。对策很朴素:会话内不换档,档位只在规划阶段决定一次。
4.8 子代理:最容易被低估的一块
最后一块,也是成本占比最大的一块。在同一会话家族里,4 个"继承父会话完整上下文"的子代理会话,合计占家族输入量的 53.6%,成本约 40%。原因很简单:fork 型子代理一开局就背着 10 万到 27 万 token 的完整前缀,冷启动全额计价。
对比之下,"新上下文"型子代理只带任务说明,开局几百到几千 token。二者的差别不是效率,是数量级。所以治理子代理的三条动作是:
这条经验在工程上可以直接量化:新开一个子代理的固定头部约 1.2 万 token;复用则第二轮起即命中。分天零散开八个,就是八份冷启动。
五、代码详解:把诊断与守卫写成可执行的
前面讲的是"应该怎么想",这一节给"怎么落地"。五段代码分别对应一个决策:判达标、算成本、判折叠、守前缀、算子代理预算。它们不依赖特定框架的 SDK,只依赖账本与诊断数据——换一个框架,改两个字段名就能复用。
5.1 解析诊断报告,逐请求判 98%
诊断接口返回的是一段带固定版式的文本。别急着让人肉读数,先把它结构化,再按预算式逐请求判定。
import re
from dataclasses import dataclass, field
NUM = r"([\\d.]+)([KMB]?)"
def to_tokens(v: str, unit: str) –> float:
mul = {"": 1, "K": 1_000, "M": 1_000_000, "B": 1_000_000_000}[unit]
return float(v) * mul
@dataclass
class LineItem:
idx: int
inp: float # 本轮输入
cached: float # 命中
new: float # 新增内容
comp: float # 折叠重付
ttl: float # 驱逐 / 改写
@property
def miss(self):
return self.inp – self.cached
@property
def hit(self):
return 1 – self.miss / self.inp if self.inp else 0
def parse_report(text: str):
"""把 cache-report 文本解析成明细 + 汇总。"""
items, summary = [], {}
for line in text.splitlines():
m = re.match(rf"\\s+(\\d+)\\s+[\\d:]+\\s+{NUM}\\s+{NUM}\\s+{NUM}%\\s+{NUM}\\s+{NUM}\\s+{NUM}", line)
if m:
g = m.groups()
items.append(LineItem(int(g[0]),
to_tokens(g[1], g[2]), to_tokens(g[3], g[4]),
to_tokens(g[6], g[7]), to_tokens(g[9], g[10]),
to_tokens(g[12], g[13])))
continue
m = re.match(r"\\s+total (input|cached|output)\\s+" + NUM, line)
if m:
summary[m.group(1)] = to_tokens(m.group(2), m.group(3))
return items, summary
def verdict(items, budget=0.02):
"""逐请求判定:M ≤ budget × In 才算达标。"""
bad = []
for it in items:
if it.inp <= 0:
continue
if it.miss > budget * it.inp:
bad.append({
"idx": it.idx, "input": int(it.inp),
"miss": int(it.miss), "hit": round(it.hit, 4),
"blame": "fold" if it.comp > it.new else
"ttl" if it.ttl > it.new else "append",
})
total_in = sum(i.inp for i in items)
total_miss = sum(i.miss for i in items)
return {
"requests": len(items),
"overall_hit": round(1 – total_miss / total_in, 4) if total_in else None,
"breaches": len(bad),
"sample": sorted(bad, key=lambda x: –x["miss"])[:5],
}
两个设计要点。
其一,判定要逐请求,不要看均值。 一个 96% 的会话均值,可能由 100 个 99% 请求与 3 个 50% 请求组成;后者才是账单的真正来源,也是唯一可下手的地方。
其二,blame 字段把未命中分到三个抽屉。 comp 占主导说明折叠太频繁;ttl 占主导说明出现了空置、重启或中途改写;new 占主导说明只是正常增长,该去压输出。三个抽屉对应三种完全不同的动作,判错抽屉就会改错地方。
5.2 成本核算:三桶 + 峰谷
def cost(hit_tok, miss_tok, out_tok, p_hit, p_miss, p_out, peak_mult=1.0):
"""p_* 单位:美元 / 百万 token;peak_mult 为峰时乘数。"""
per_m = 1e-6
base = (hit_tok * p_hit + miss_tok * p_miss + out_tok * p_out) * per_m
return {
"cost": round(base * peak_mult, 6),
"share": {
"hit": round(hit_tok * p_hit * per_m * peak_mult / (base * peak_mult), 4),
"miss": round(miss_tok * p_miss * per_m * peak_mult / (base * peak_mult), 4),
"out": round(out_tok * p_out * per_m * peak_mult / (base * peak_mult), 4),
},
"per_100m": round(base * peak_mult / (hit_tok + miss_tok + out_tok) * 1e8, 2),
}
# 实测混合比例:命中 91.26% / 未命中 7.43% / 输出 1.30%
r = cost(91.26e6, 7.43e6, 1.30e6, 0.003, 0.15, 0.60)
print(r["per_100m"]) # 2.17 美元 / 亿 token
print(cost(91.26e6, 7.43e6, 1.30e6, 0.003, 0.15, 0.60, peak_mult=2.0)["per_100m"]) # 4.34
注意最后一行:峰谷是一个整体乘数,它乘在最终账单上,不改变三桶的相对占比。所以排班优化与结构优化可以同时做,互不干扰。这也是为什么"把重活排到晚间"值得写进流程,而不是写成建议。
5.3 折叠 ROI:什么时候该压
折叠不是免费的。它把中段历史改写为摘要,收益是"其后每轮少发一些 token",成本是"改写后整段按新增重付一次"。可写成:
def fold_roi(ctx_before, ctx_after, remaining_turns, p_hit, p_miss):
"""返回 (是否值得, 净收益美元)。remaining_turns 含本轮之后还要跑的轮数。"""
per_m = 1e-6
repay = ctx_after * p_miss * per_m # 改写后整段重付一次
saved = (ctx_before – ctx_after) * p_hit * per_m # 每轮少发的命中 token
benefit = saved * max(remaining_turns – 1, 0)
return benefit > repay, round(benefit – repay, 6)
# 例:20 万上下文折到 8 万,还剩 3 轮
print(fold_roi(200_000, 80_000, 3, 0.003, 0.15)) # (False, …)
# 例:同样的折叠,但还剩 40 轮
print(fold_roi(200_000, 80_000, 40, 0.003, 0.15)) # (True, …)
第一例是净亏。这就是实测中"十次折叠六次不回本"的数学来源:会话尾部那几轮,根本不值得为它改一次历史。把它做成守卫——在折叠前先算一次,负收益就跳过——比事后分析有效得多。
5.4 头部冻结守卫:把纪律变成断言
"会话内不换模型、不改系统提示"这条纪律,靠自觉执行早晚会破。更可靠的做法是给稳定前缀算指纹,每次请求前比对。
import hashlib, json
def prefix_fingerprint(system_prompt: str, model: str, effort: str, tools: list) –> dict:
"""稳定前缀指纹:任一字段变化都意味着从字节 0 起失效。"""
def h(x):
return hashlib.sha256(json.dumps(x, ensure_ascii=False, sort_keys=True).encode()).hexdigest()[:16]
parts = {
"system": h(system_prompt),
"model": h(model),
"effort": h(effort),
"tools": h([t.get("name") for t in tools]), # 顺序敏感,不要排序
}
return {"parts": parts, "whole": h(parts)}
class PrefixGuard:
def __init__(self, repaved_tokens: int = 33_000):
self.baseline = None
self.repaved = repaved_tokens
def check(self, fp: dict) –> dict:
if self.baseline is None:
self.baseline = fp
return {"ok": True, "reason": "first request"}
changed = [k for k in fp["parts"] if fp["parts"][k] != self.baseline["parts"][k]]
if not changed:
return {"ok": True, "reason": "stable"}
self.baseline = fp
return {"ok": False, "changed": changed, "expect_rebill_tokens": self.repaved}
tools 的指纹要保持注册顺序,不能排序:上游看到的也是有序列表,顺序变了前缀就变了。这一行注释比代码本身重要——很多"明明没改配置却掉命中"的事故,源头就是工具列表被重新排序。
5.5 子代理预算器:fork 还是 fresh
最后一段用于规划期决策:在派子代理之前,先估一下两种形态的输入量差多少。
def subagent_budget(parent_ctx, task_tokens, fixed_header=12_000, prefill_ratio=0.1):
"""fork: 继承父上下文;fresh: 只带任务 + 固定头部。"""
fork_first = parent_ctx + task_tokens
fresh_first = fixed_header + task_tokens
return {
"fork_first_request": fork_first,
"fresh_first_request": fresh_first,
"saving": fork_first – fresh_first,
"ratio": round(fresh_first / fork_first, 3),
}
# 父会话 20 万 token,任务 3 千 token
print(subagent_budget(200_000, 3_000))
# {'fork_first_request': 203000, 'fresh_first_request': 15000,
# 'saving': 188000, 'ratio': 0.074}
一次 fork 等于用 20 万 token 冷启动一个只干 3 千 token 活的代理,效率 1.5%。这就是"子代理吃掉 40% 成本"的微观机制。规划期跑一次这个函数,比事后审计省事得多。
5.6 把四项回归指标串成一条周报
单点脚本容易写,难的是让它每周自动跑、并输出可比较的时间序列。做法是把前面几段拼成一个入口:
def weekly_report(report_text, prices, peak_mult=1.0):
items, summary = parse_report(report_text)
steady = [i for i in items if i.idx > 1] # 剔除首请求
if not steady:
return {"note": "只有首请求,不足以判稳态"}
hit = sum(i.cached for i in steady)
miss = sum(i.miss for i in steady)
inp = sum(i.inp for i in steady)
out = summary.get("output", 0.0) # 取报告汇总里的输出总量
v = verdict(steady)
c = cost(hit, miss, out, prices["hit"], prices["miss"], prices["out"], peak_mult)
return {
"requests": len(steady),
"steady_hit": round(hit / inp, 4),
"breach_requests": v["breaches"],
"cost_per_100m": c["per_100m"],
"fold_share": round(sum(i.comp for i in steady) / inp, 4),
"ttl_share": round(sum(i.ttl for i in steady) / inp, 4),
}
四个指标分别落在 steady_hit、cost_per_100m、breach_requests、fold_share + ttl_share 上。它们必须一起看,原因在第九节展开:任何一个单独优化都会把另外三个推向坏方向。
值得注意的是 steady 的剔除规则:只去掉首请求是不够的,模型切换、重启后的首个请求同样应剔除,因为它们本质上是"重新冷启动"。判断标准是"这一请求之前是否存在一个可复用的同前缀请求",而不是"它是不是第一条"。
六、案例分析:三个真实场景的成本解剖
6.1 案例一:26.6K 的冷启动
本次写作会话的第一个请求,账本记录如下:
| 输入 | 26.6K token |
| 命中 | 256 token |
| 命中率 | 1.0% |
| 新增内容 | 26.3K token |
| 折叠重付 | 0 |
| 驱逐 / 改写 | 0 |
这个样本的价值在于它"干净":没有折叠、没有重启、没有模型切换,未命中 100% 来自新增内容。于是可以确认两件事。
第一,系统提示与工具 schema 的固定头部就在 26K 量级。这解释了为什么"新开一个子代理"很贵:它要重付这 26K 的一部分。也解释了为什么"复用同一个子代理会话"是有效的——第二轮起这部分命中。
第二,冷启动不算违规。判据 M ≤ 0.02 × In 若套在首请求上,必然不达标;所以正确做法是把首请求从回归统计里剔除(或单列),只看"进入稳态后"的请求。反过来说,任何"我的命中率只有 60%"的报告,第一步都该看有多少请求属于首请求。
6.2 案例二:一次折叠引发的重付链
第二个案例来自一个长会话的中间段。它在第 37 轮触发了一次折叠,把 18 万 token 的历史压成 6 千 token 摘要,上下文回落到约 7 万。账本上随后出现了一条"压缩重付"记录。
把这件事画成时间线,就能看清成本是怎么产生的:
上游缓存代理层客户端上游缓存代理层客户端#mermaid-svg-1rueKriaVBWPn6jC{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-1rueKriaVBWPn6jC .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-1rueKriaVBWPn6jC .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-1rueKriaVBWPn6jC .error-icon{fill:#552222;}#mermaid-svg-1rueKriaVBWPn6jC .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-1rueKriaVBWPn6jC .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-1rueKriaVBWPn6jC .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-1rueKriaVBWPn6jC .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-1rueKriaVBWPn6jC .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-1rueKriaVBWPn6jC .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-1rueKriaVBWPn6jC .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-1rueKriaVBWPn6jC .marker{fill:#333333;stroke:#333333;}#mermaid-svg-1rueKriaVBWPn6jC .marker.cross{stroke:#333333;}#mermaid-svg-1rueKriaVBWPn6jC svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-1rueKriaVBWPn6jC p{margin:0;}#mermaid-svg-1rueKriaVBWPn6jC .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-1rueKriaVBWPn6jC text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-1rueKriaVBWPn6jC .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-1rueKriaVBWPn6jC .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-1rueKriaVBWPn6jC .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-1rueKriaVBWPn6jC .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-1rueKriaVBWPn6jC #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-1rueKriaVBWPn6jC .sequenceNumber{fill:white;}#mermaid-svg-1rueKriaVBWPn6jC #sequencenumber{fill:#333;}#mermaid-svg-1rueKriaVBWPn6jC #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-1rueKriaVBWPn6jC .messageText{fill:#333;stroke:none;}#mermaid-svg-1rueKriaVBWPn6jC .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-1rueKriaVBWPn6jC .labelText,#mermaid-svg-1rueKriaVBWPn6jC .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-1rueKriaVBWPn6jC .loopText,#mermaid-svg-1rueKriaVBWPn6jC .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-1rueKriaVBWPn6jC .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-1rueKriaVBWPn6jC .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-1rueKriaVBWPn6jC .noteText,#mermaid-svg-1rueKriaVBWPn6jC .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-1rueKriaVBWPn6jC .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-1rueKriaVBWPn6jC .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-1rueKriaVBWPn6jC .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-1rueKriaVBWPn6jC .actorPopupMenu{position:absolute;}#mermaid-svg-1rueKriaVBWPn6jC .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-1rueKriaVBWPn6jC .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-1rueKriaVBWPn6jC .actor-man circle,#mermaid-svg-1rueKriaVBWPn6jC line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-1rueKriaVBWPn6jC :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}水位 82% 触发折叠净效果:省下后续重发,但先付一次 6.4 万重算第 36 轮请求(上下文 18 万)前缀与历史逐字节一致命中 17.4 万 / 新增 0.6 万中段历史 → 6 千 token 摘要第 37 轮请求(上下文 7 万)头部一致,但中段字节已被改写命中仅到改写点,其后 6.4 万全部按新增计价第 38–41 轮前缀重新稳定,命中率回到 99%
这张图里最关键的一行是"命中仅到改写点"。缓存不是按比例打折,是按前缀截断:改写发生在中段,则中段之前仍命中,中段之后全部重算。所以折叠重付的规模,约等于"改写点之后的内容长度",而不是"被折叠掉的那段长度"。这也解释了为什么在会话早期折叠更贵:后面还有大量内容要跟着重付。
如果这次折叠发生在第 40 轮之后、剩余不足 5 轮,那么按 5.3 节的 ROI 函数计算就是负收益——这也是那条守卫存在的理由。
6.3 案例三:一次子代理扇出的事故复盘
第三个案例来自一次并行的调研任务。任务是"对四个模块各做一次独立审计",操作者图省事,用了四个"继承完整上下文"的子代理并行发出。
账本给出的结论很难看:四个子代理的首请求输入分别是 21.3 万、19.8 万、22.7 万与 20.5 万 token,全部按新增计价;而它们实际的工作量,每个约 4 千 token 的读取与 1 千 token 的输出。也就是说,为了四个各 5 千 token 的活,付了 84 万 token 的冷启动。
同一批任务改用"新上下文"子代理后,输入降至每个约 1.6 万(固定头部加任务说明),总额从 84 万降到 6.4 万,约为原来的 7.6%。质量没有下降,因为审计任务本身就是自包含的——它不需要父会话的任何历史。
这个案例的教训不是"别用子代理",而是先判断任务是否真的需要继承上下文:
| 独立调研、批量审计、文档转换 | 不需要 | 新上下文,自带任务说明 |
| 需要延续当前设计的续写、评审 | 需要 | fork,但要控制父上下文规模 |
| 同一批次的重复同类任务 | 不需要 | 复用同一个子代理会话,而非新开 |
第三种最容易被忽略。新开一个子代理,每次都要重付系统提示与工具 schema 的固定头部(本机实测约 1.2 万 token);而复用同一会话,第二轮起这部分命中。分天零散开八个同类子代理,等于付八份冷启动;若在一次任务里顺序复用,只需第一份。
6.4 三个案例之后的集成排序
把三个案例放在一起,能得到一个可操作的排序:
| 1 | 减少请求数、缩短会话家族 | 全部三桶 | 0.75 |
| 2 | 压输出(档位、纪律、maxTokens) | 输出桶 | 0.45 |
| 3 | 掐掉高未命中请求 | 未命中桶 | 0.40 |
| 4 | 子代理治理 | 未命中桶 | 0.80 |
| 5 | 折叠门槛与尾部治理 | 未命中桶 | 0.80 |
| 6 | 排班与渠道分流 | 整体乘数 | 0.76 |
顺序不是审美问题:先做 1、2,因为它们的收益确定且不依赖框架;再做 3、4,因为它们需要治理机制;最后做 5,因为折叠有正负两面。
七、五类浪费与杠杆矩阵
把散落各处的动作收成一张表。这张表既是检查清单,也是优先级依据:
| 1 | 输出过长(思考 + 冗长) | 占总成本 36%,约 1500 tok/请求 | 降档位、收紧回答规范、限 maxTokens | 输出 −50%~−70%,总成本 ×0.75~0.82 |
| 2 | 折叠重付 | 会话内 13.7%,6/10 次不回本 | 抬高折叠门槛、大结果外置 | 未命中 ×0.6~0.8 |
| 3 | 上游缓存驱逐 | 7.6% | 保持连续作业、避免空置 | 未命中 −5%~−10% |
| 4 | 模型切换 / 重启 / 改提示词 | 占输入 0.25%(明确归因部分) | 会话内不换模型、不重启、不改头部 | 未命中 ×0.5 |
| 5 | 子代理扇出 | 家族输入 53.6%,成本约 40% | 少 fork、压子代理上下文、定廉价模型 | 总成本 ×0.75~0.85 |
把六个乘数连乘,得到从基线到 20% 的路线:
| — | 只把命中率从 92.5% 提到 98% | ×0.63 | $1.368 | 63% |
| ① | 未命中 ×0.40(掐掉浪费) | ×0.69 | $1.501 | 69% |
| ② | ① + 输出 ×0.45 | ×0.49 | $1.072 | 49% |
| ③ | ② + 请求数 −25% | ×0.39 | $0.851 | 39% |
| ④ | ③ + 子代理治理 ×0.8、时段 ×0.8 | ×0.31 | $0.680 | 31% |
| ⑤ | ④ + 60% 流量走廉价渠道 ×0.76 | ×0.24 | $0.517 | 24% |
| ⑥ | ⑤ + 折叠门槛抬高、外置机制打开 | ×0.20 | $0.43 | 20% |
注意第一行的教训:单杠杆有天花板。把命中率一个人从 92.5% 顶到 98%,只能拿到 63%。这不是努力不够,是数学——命中率只影响未命中桶,而它只占总成本的 51.4%,且只能压到很小。想再往下,只能叠加别的维度。
八、反直觉清单与失效边界
写到这里,需要把"不可控"和"会误判"的部分说清楚,否则读者照做之后会遇到解释不了的现象。
第一,上游 TTL 不可观测。 代理层没有 TTL 假设、没有 warm-up、没有 keepalive、也没有驱逐策略。因此由 TTL 造成的未命中只能归因、不能消除;缓解手段只有"连续作业 + 别重启"。当你在报告里看到一大块 unattributed,那不是代码 bug,是外部黑盒。
第二,归因是启发式的。 判定顺序通常是"重启 > 模型切换 > 密钥 > 提示词改写 > wire 变更 > 上游驱逐",残余才记入 TTL。所以"一次模型切换约 11 万 token"这类数字是上限,不是精确值。
第三,压缩不是目的。 折叠低于回本线时净亏,前面已给出十次里六次不回本的实测比例。任何"把上下文压到多小"的 KPI 都是错的,正确指标是"未命中率"和"每亿 token 成本"。
第四,不要叠加第三方压缩器。 宿主自带压缩与外部压缩插件同时工作时,两者会各自改写中段,互相破坏前缀,结果比不压还贵。上下文治理层只能有一个 owner。
第五,先统一口径,再判断是否达标。 账本与代理统计差约两倍。用错口径会得出"优化无效"或"优化作弊"的相反结论。
第六,短会话不可能 98%。 这不是实现问题,是 δ / C 的数学结果。给短会话设命中率 KPI,等于给飞机设"螺旋桨转速"。
九、怎么验证:A/B 与四个回归指标
最后是验证方法。最小可行方案是"一次只改一个变量,跑十轮":
| 输出侧 | 推理档位高 vs 低 | 每请求输出 token、账本花费 |
| 子代理 | 独立上下文 vs fork | 新增子会话输入量、首请求未命中 |
| 压缩侧 | 折叠门槛前 vs 后 | 折叠回本比例(是否从 4/10 提升) |
| 前缀侧 | 冻结头部前后 | 单请求未命中量、unattributed 占比 |
回归指标固定四个,不要增加:每亿 token 成本、未命中率、每请求输出 token、子代理输入占比。四个指标互相制约:只压成本会牺牲输出质量,只压未命中会拉长会话,只算子代理会忽略输出膨胀。
还有一层更隐蔽的风险:指标会被玩坏。把"每亿 token 成本"当唯一 KPI,最省事的做法是把任务切碎、让模型少说话,成本立刻下降,但产出也一起下降。把"未命中率"当唯一 KPI,最省事的做法是不断开新会话、避免长历史,比值变好,账单未必变好。把"每请求输出 token"当唯一 KPI,模型会开始省略必要的解释与校验步骤。
防止指标被玩坏的办法是给每个指标配一个"质量锚点":成本配"任务一次通过率",未命中率配"会话内回读次数",输出长度配"人工返工次数"。锚点不必精确,但必须存在——否则优化会自然滑向"少干活"。
一个可执行的做法是把这四项写进 CI 的周报:数据来自账本与诊断接口,脚本就是第五节那几段。
十、给读者的三条
先归因,再优化。 把未命中拆成"新增 / 折叠 / 驱逐",把成本拆成"未命中 / 输出 / 命中",才知道该动哪根杠杆。不做归因就开始调参,等于在黑暗里换灯泡。
优化的是乘积,不是单点。 单杠杆有天花板,组合才是路径。在多数系统里,输出侧与子代理侧是最先回本的两块——它们不需要改框架,只需要改习惯。
纪律比配置更有效。 头部冻结、连续作业、零改写这三条不花钱,却决定了缓存前缀能不能稳定复用。它们的收益不会出现在某一次 A/B 里,而是体现在"账单曲线不再抽风"上。
成本优化到最后,拼的不是技巧,是对自己系统字节结构的理解深度:知道哪一段是稳定的,哪一段每轮都在变,哪一次压缩在改写历史,哪一次切换在重建前缀。想清楚这些,20% 只是一个顺带的结果。
附录:术语表
| 命中输入 | 与上游 prompt cache 前缀一致的输入 token |
| 未命中输入 | 新增或被改写、需重新计价的输入 token |
| δ | 单轮请求相对上一轮追加的 token |
| 折叠(fold) | 把中段历史压缩成摘要的上下文管理动作 |
| 冷启动 | 会话头部字节与上游已有缓存不一致,整段按新内容计费 |
| fork 型子代理 | 继承父会话完整上下文的子代理,开局即付全额前缀 |
| 水位 | 当前上下文占窗口比例,触发提醒与紧急截断 |
| ROI 守卫 | 折叠前先估算回本,负收益则跳过 |
网硕互联帮助中心






评论前必须登录!
注册