云计算百科
云计算领域专业知识百科平台

把每亿 Token 成本压到 20%:一个真实 Agent 框架的成本手术与实现解剖

这不是一篇"省钱小技巧"合集。它把一次真实的成本优化,从账本数字一直追到框架源码里的实现机制:哪一层在记账、哪一层在改写上下文、哪一次压缩其实亏钱、哪一个参数改了会让缓存前缀整段失效。

数据来源:四天窗口、990 次调用、79.5M token 的真实账本与代理统计,全部为实测值。金额单位统一为"美元 / 每 1 亿 token"。

适配对象:长会话 Agent、带 prompt cache 的按 token 计费 API,以及所有"账单很贵但说不清贵在哪"的 LLM 应用。

一、一笔说不清的账单

几乎所有团队都经历过同一幕:月度账单暴涨三倍,大家围在白板前猜原因。猜"是不是模型太贵",于是换便宜模型;猜"是不是用户在乱用",于是加限制;猜"是不是缓存没开",于是查配置。折腾一圈,账单通常只降了十几个百分点。

因为猜错方向了。账单的结构不按"模型"分,按输入是否命中缓存和输出分,而这三者的单价差了两个数量级。

先看一次实测的成本结构。把 1 亿 token 按真实混合比例摊开:

桶token 占比单价(USD / 百万)成本占总成本
命中输入 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 的比值漂亮得多。

于是得到两条互补的目标:

  • 把 M 压到绝对低,比值自然达标;
  • 不要为刷比值而延长会话——短会话在数学上根本不可能达到 98%。
  • 第二条需要一个推导。单轮命中率近似为 hit ≈ 1 − δ / C,其中 δ 是本轮追加的 token,C 是本轮上下文。于是 98% 等价于 δ ≤ 0.02 × C:

    上下文 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 的请求体大致按以下顺序组装:

  • 系统提示(身份、规则、工作区约定、注入的运行时快照);
  • AGENTS.md 等工作区指令;
  • 技能目录摘要与记忆视图(按会话状态生成);
  • 工具 schema 列表(按注册顺序序列化);
  • 历史消息(user / assistant / tool result);
  • 本轮追加。
  • 前六段里,前四段是稳定前缀,第五段是增量区,第六段是本轮增量。成本由此分成三类:稳定前缀命中(便宜)、历史增量(便宜,只要没被改写)、本轮追加(全价)。

    于是"缓存命中率"在实现上退化成一个非常具体的问题:前四段有没有被重写? 任何一处改动都会导致从字节 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。二者的差别不是效率,是数量级。所以治理子代理的三条动作是:

  • 研究、独立审计这类任务,用新上下文的子代理,不用 fork;
  • 必须并行时,让整批在同一分钟内以相同 provider 与模型发出,使第一个付冷启动、其余命中同一前缀;
  • 反复派同类活,复用同一个子代理会话,而不是每次新开——每次新开都重付一份系统提示与工具 schema 的固定头部。
  • 这条经验在工程上可以直接量化:新开一个子代理的固定头部约 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 守卫 折叠前先估算回本,负收益则跳过
    赞(0)
    未经允许不得转载:网硕互联帮助中心 » 把每亿 Token 成本压到 20%:一个真实 Agent 框架的成本手术与实现解剖
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!