Flowness / Flowness 真实问题

上下文只用了一半就被压缩:advisor 调用与我们自己的计数错误

在我们的记录里,含 advisor 调用的请求,顶层用量约为真实上下文的 2 倍,而在 9 月 14 日的 194 次压缩里,记下的“压缩前大小”基本等于这个数字,我们推断客户端是按它判断上下文已满的。调低压缩阈值后,一天 452 次自动压缩里有 194 次发生在真实上下文只有记录值一半的时候。

熟悉的朋友可以直接跳过这一段。

Flowness 是我们搭的多 agent 工程系统,由一个人和一群 AI agent 一起运行:人定方向、做取舍,agent 写代码、派活、审查。它从 2026 年 3 月下旬跑到现在,6 个多月;只算还留着会话记录的部分(大多是 7 月以后的),它就消耗了超过 1400 亿 Claude token,其中九成以上是缓存读取。这个系列把运行中真实遇到的问题一个个摊开写。真实环境里 agent 碰到的问题,大多非常琐碎:一次 grep 没匹配到,一道检查没有对象也记“通过”,一个用量字段被读错了含义。结果能不能信,往往就卡在这些地方。我们把它们写出来,是想和在做同样事情的人交流。

假设你的 agent 跑在 Claude Code 里,而你前一天刚把自动压缩的阈值调低。第二天一看,压缩次数涨了几十倍。你去翻成本统计,发现某个工具一天被调用一千多次、读了将近两亿 token,而且有一批压缩正好发生在它刚被调用之后。两条线索指向同一个工具。你会关掉它,然后等第二天压缩回落。

我们就是这么做的。2026 年 9 月 14 日(本文时刻均为 UTC),Flowness 服务器上的会话(按全部会话记录统计)一共发生 452 次自动上下文压缩,前两天(9 月 12 日和 13 日)分别只有 17 次和 11 次。其中 194 次压缩发生时,压缩记录上写的“压缩前大小”约为会话真实上下文的 2 倍。这些压缩的前一个请求里,模型都调用过 advisor 工具。追查过程中我们还发现,自己用来统计 advisor 用量的方法按日志行累加,得到的调用次数和 token 数都约为真实值的 5 倍。当天我们在编排程序派发的会话里关闭了 advisor,这类会话里的调用降到 0;但 advisor 在另外几类会话里继续出现,9 月 24 日的单日用量超过了修复前的任何一天。

这篇把缠在一起的三件事分开讲:含 advisor 调用的请求,顶层用量为什么约是真实上下文的 2 倍;我们按日志行累加的统计,为什么得到约为真实值 5 倍的调用次数;窗口调低在中间起了什么作用。最后一节有一段脚本,你可以直接在自己的会话记录上跑。

背景:先要知道的几样东西

Flowness(我们搭的多 agent 工程系统)里,有一类会话由程序自动启动:编排程序调用 Claude Code 的命令行,以后台会话(claude --bg)或一次性无界面进程(claude -p)的方式起一个会话,交给它一个任务。本文把这类会话叫作程序启动的会话。另外还有人在终端里手动打开的会话;这些会话还会再派出子 agent(通过 Agent 工具或 workflow 启动的一次性会话)和队员会话(Claude Code 的 Agent Teams 功能启动的协作会话)。后文关闭 advisor 时,这几类会话的结果不一样。

会话记录(transcript)是 Claude Code 为每个会话写下的 JSONL 文件,在 ~/.claude/projects/<项目>/<会话id>.jsonl;子 agent 的记录在 ~/.claude/projects/<项目>/<会话id>/subagents/ 下。下面对记录结构和字段含义的描述,都是我们对自己记录的观察和解读,没有引用产品文档。模型的每个内容块(一段思考、一次工具调用、一段文字)各写一行,同一个请求产生的多行各带一份这个请求的用量数据(usage);在案例会话里各行的 usage 相同,我们也见过同一请求较早的行用量不完整的情况,所以后面的脚本取最后一行。对含 advisor 的请求,usage 里除了顶层的几个 token 数,还有一个分段数组 iterations,每一段是一次模型推理,主模型的段标为 message,advisor 的段标为 advisor_message。一行不等于一个请求,顶层之下还有分段。下面两个错都出在没看清其中一个。

advisor 工具是 Claude Code 提供的一个服务端工具。从会话记录的形态看,主模型在处理一个请求的中途调用它,第二个模型读取对话后给出建议,再由主模型继续。在我们的记录里,advisor 那一段的读入量约等于当时的整段上下文(两者之比的中位数是 1.00),缓存读取和缓存写入字段全部为 0。这个观察覆盖 9 月 9 日到 10 月 5 日的 3149 个含 advisor 调用的请求。它被问了什么、答了什么,我们看不到:记录里 advisor 调用的输入字段恒为空,返回内容是加密的。

自动压缩(auto-compaction)在会话记录里留下一行压缩记录,之后会话的历史被一份摘要取代。我们观察到它在上下文变大时出现。压缩记录这一行的 subtype 是 compact_boundary;它的 compactMetadata 里有 preTokens 字段,是客户端记下的“压缩前上下文大小”。在我们的系统里,会话压缩之后需要重新读取之前读过的大文件。本文把“压缩前大小约为真实上下文 2 倍”的压缩叫作倍增压缩,精确判据写在文末的依据说明里。

压缩窗口是我们为程序启动的会话设置的上下文上限。9 月 13 日之前是 65 万 token;9 月 13 日下调,之后按会话类别调到 15 万到 25 万。

版本和模型。 本文逐段分析的那个案例会话运行在 Claude Code 2.1.270,主模型和 advisor 的模型都是 claude-sonnet-5。这三个值读自会话记录:消息行的 version 字段、message.model 字段,以及 usage.iterations 里 advisor_message 那一段的 model 字段。文中统计数字覆盖的其他会话,版本不全和它相同:9 月 10 日到 10 月 5 日的压缩记录上出现过 2.1.258、2.1.270、2.1.280、2.1.284、2.1.287 五个版本。模型也不全相同:同期的记录里见过 claude-opus-4-8、claude-fable-5-1、claude-opus-5、claude-opus-5-5、claude-sonnet-5、claude-sonnet-5-5 等。我们没有按版本或模型分开统计,不知道这些现象在哪些版本、哪些模型上有,哪些上没有。你在自己的记录上核对时,先看这几个字段。

图 1:9 月 14 日 13:09:34 的那个请求在记录里是 5 行,每行带同一份 usage,顶层数字是主模型两段之和,advisor 那一段只在 iterations 里。

经过

9 月 6 日:第一次注意到 advisor,只换了它的模型。 当时的成本调查发现,155 个按设定应该只用 Sonnet 的会话里,有 88 个出现了 Opus 的用量,来源是 advisor。当时关注的是 advisor 用了哪个模型,于是我们让 Sonnet 档的会话把 advisor 的模型也固定为 Sonnet。这一步只针对第二个模型是谁,没有针对调用次数;之后 advisor 仍然每天出现约 300 次。也没有人把 advisor 和压缩联系起来。这次调查已经记下了 advisor 在会话记录里的识别方式:usage.iterations 里类型为 advisor_message 的段。这条记录后来很要紧。

9 月 10 日到 13 日:倍增的压缩已经存在,但数量少。 事后复算,这四天分别有 14、6、15、10 次压缩的 preTokens 约为真实上下文的 2 倍。当时窗口是 65 万,这些压缩发生在 preTokens 62 万到 82 万、真实上下文 31 万到 41 万的时候。每天 6 到 15 次,没有人去看每次压缩时的真实上下文是多少。这四天的压缩和对应请求的用量,今天都能在记录里复算;当时只是没有人去看。

9 月 13 日:一次调查得出了错误的结论。 一份调查想统计 advisor 的调用次数,结论是 advisor 调用没有在会话记录里留下可以计数的字段,并把 Sonnet 档会话里出现 Opus 用量这件事,归因到模型的分配方式。它只查了消息正文和每行的顶层字段,没有查 usage 内部的数组。一周前的调查已经记录了正确的位置,这次没有复用。

9 月 13 日 15 时左右:调低窗口。 依据是两条:缓存读取占全部 token 的 97%,成本与上下文大小乘以调用次数成正比;再按前一天会话的峰值上下文分档。决策时记录的峰值是中位数 21.3 万、第 90 百分位 49.2 万。这些峰值都是用每行的顶层 usage 算出来的。窗口从 65 万降到 25 万;之后发现有一处配置没有生效,窗口并没有立即在全部会话上变化,后续又按会话类别调整为 15 万到 25 万。

9 月 14 日:压缩暴增。 11:11 到 13:32,一个程序启动的会话被压缩 18 次,其中 9 次的 preTokens 约为真实值的 2 倍。压缩之后,它把角色说明里一个名字相近的辅助命令当成 advisor 工具反复调用,向系统报告工具故障,并以 advisor 要求暂停为由拒绝执行协调 agent 的指令,最后被终止。这些行为与压缩之间的因果关系,我们没有分析。

同一天,负责人问协调 agent:执行会话读完任务包就有 100 多 K,是不是上下文太多了?问题里举的例子是另一个会话。我们逐段读了这个会话的记录。它起步时上下文是 76,921 token。13:09:34,一个请求调用了 advisor,主模型在调用前后各读了一遍上下文,两段分别是 124,519 和 127,361 token,这个请求的顶层 usage 记为两者之和 251,880。13:15:09,这个会话被自动压缩,preTokens 是 254,808,而压缩前最后一次真实上下文是 127,361。

图 2:顶层用量记为两段之和 251,880,5 分 35 秒后的自动压缩把压缩前大小记为 254,808,真实上下文是 127,361,三根条按同一比例绘制。

同一天的处理。 负责人批准:程序启动的会话一律关闭 advisor,人手动打开的会话保留。13:58,我们提交改动,在四条程序启动会话的路径上设置环境变量 CLAUDE_CODE_DISABLE_ADVISOR_TOOL=1,并给每日成本报表加上两项统计:按主模型分段计算真实上下文;压缩前大小超过真实上下文 1.3 倍的,记为“虚假压缩”。这个 1.3 倍判据和上面倍增压缩的判据,在我们的全部数据里只有 6 次结果不同。

为什么当时看起来没问题

我们当时手里有三个数字,正好对应开头的三件事。每个先写当时的读法,再写它实际的含义。

证据一:顶层用量。 当时的读法是这样的。会话记录里每一行的 usage 顶层有输入、缓存读取、缓存写入几个数,它们是 Claude Code 写下的,不是我们算的。事后我们复核了 3149 个含 advisor 调用的请求:顶层 usage 每一次都精确等于主模型各段推理的上下文之和,advisor 那一段不计入顶层。它是严格的加法,不多不少。要算读入了多少 token,它就是对的数。9 月 13 日决定把窗口降到多少时,我们按前一天会话的峰值上下文分档,峰值就是从这个数里取的最大值。

实际发生的:主模型在 advisor 调用前读一遍上下文、调用后再读一遍,顶层数字就是两遍之和,中位数是真实上下文的 1.99 倍。记录里主模型有两段、各带一份读入量,按这两份算并没有多算;被放大的是“上下文有多大”这个判断。在那些倍增的压缩里,preTokens 与前一个请求顶层数字之比的中位数是 1.013。所以我们推断,客户端是依据顶层数字判断“上下文已满”的。这一步我们没有看客户端代码,也没有做对照实验,证据是这两个比值。其中 1.013 是在倍增压缩的判据选出的样本里算的,判据本身就要求 preTokens 与顶层用量相差不超过 10%,所以它只说明被选出的这些压缩里两者一致;更独立一些的证据是证据三里压缩前大小与真实上下文之比的双峰分布。

同一个数字,拿来回答“读了多少”是对的,拿来回答“上下文有多大”就大了一倍。9 月 13 日我们按前一天的峰值给窗口分档时,用它回答的是后一个问题。用按主模型分段的方法重算 9 月 13 日已提交报表里的峰值,任务会话那一类的两个峰值都降了:

任务会话的峰值上下文 已提交报表(顶层口径) 重算(主模型分段口径)
中位数 25.9 万 19.8 万
第 90 百分位 52.3 万 33.3 万

“任务会话那一类”指报表里按首条消息归为任务会话的那一类(修复、评审、执行等;不含协调、验证、续接类会话,也不含另一条工作线的会话)。决策时工作日志记录的是 21.3 万和 49.2 万,统计时间窗和口径与已提交报表可能不同,差异原因我们没有核对。修正后的第 90 百分位仍高于 25 万,所以降低窗口本身仍有依据;但越过阈值的会话里,有一部分是因为 advisor 调用让顶层数字翻倍。

证据二:advisor 的调用次数和 token 数。 当时的读法:协调 agent 给出的数字在下表的中间一列(9 月 14 日那格里,当时给出的只有截至 13:35 的数)。我们用成本报表脚本里同口径的函数复现了这些数字,每一个都能对上。日报脚本能逐位复现,这些数字就被当作了准确数字。

advisor 调用 按日志行累加 按请求 ID 去重
9 月 12 日 1612 次,无缓存输入 2.77 亿 token 305 次,0.53 亿
9 月 13 日 1725 次,2.67 亿 330 次,0.52 亿
9 月 14 日 当时给出的是截至 13:35 的 1451 次、1.64 亿;我们事后算出全天 1672 次、1.92 亿 全天 310 次、0.36 亿

实际发生的:复现只说明方法稳定,方法本身有偏差。同一个请求在会话记录里写成 5 行以上,每行带同一份 usage,统计按行累加。按请求 ID 去重之后的数字在右列;同一时间窗对比,9 月 14 日全天按行统计与去重后相差约 5.4 倍。我们的会话记录里另有一种类型为 cost-state 的记录,按模型分项,看起来是客户端自己的累计用量。我们挑了 6 个会话对账(挑选条件:主模型为 Sonnet、advisor 模型为 Opus、至少 3 次 advisor 调用):去重后的结果与它逐位相等,按行累加的结果约为它的 5 倍。正确的去重方法 9 月 6 日的调查就用过,当时对账 10 个会话全部一致,后来写报表脚本时没有沿用。

这个错误影响的是我们对 advisor 有多贵的判断。按行统计时,我们曾估计 advisor 约占每天读入成本的四分之一;去重后约为八分之一。分子(advisor)被放大约 5 倍,分母里的主模型缓存读取按行统计时也被放大了约 2 倍,所以比例只降了一半。这个比例沿用了一个旧估算:无缓存输入的价格约为缓存读取的 10 倍。它只比较了读入量,没有计入缓存写入和输出。有一个数不受重复计数影响:每次调用平均读入的 token 数,9 月 14 日约为 11.5 万,12 日和 13 日约为 17 万和 16 万。按行重复也不影响峰值,因为峰值取的是最大值,一行重复几遍最大值不变;所以 9 月 13 日按峰值分档那一步,受的是证据一的影响,不是证据二的。窗口决策的另外两条依据(缓存读取占 97%、成本与上下文大小乘以调用次数成正比)是否受按行累加影响,我们没有核对。

证据三:压缩的总次数。 当时能看到的是总数:前两天 17 次和 11 次,9 月 14 日 452 次,窗口刚降。没有人把每次压缩的压缩前大小与当时的真实上下文对照。我们的预期是,关掉 advisor 后压缩会回到每天十几次。

实际发生的:窗口调低是原因之一。窗口 65 万时,倍增压缩每天 6 到 15 次,发生在真实上下文 31 万到 41 万的时候。窗口降到 20 万到 25 万后,真实上下文 10 万到 12.5 万左右的会话,调用一次 advisor,顶层数字就会越过窗口;我们观察到这类会话随后出现压缩。9 月 14 日 13:40 之前的 159 次倍增压缩,发生时真实上下文的中位数是 13 万。9 月 14 日一天有 194 次。当天看到的“压缩暴增”同时有两个原因:窗口调低,以及 advisor 让数字翻倍。只看总次数,无法区分。把每次压缩的 preTokens 除以压缩前的真实上下文,可以把压缩分成两类:比值接近 2 的,和接近 1 的。9 月 14 日 13:40 之前的压缩里,这个比值的分布是双峰的:118 次在 1.0 附近,156 次在 2.0 附近(按比值数,与上面 159 次的倍增判据口径不同),只有 14 次落在中间。

当时看不见的那一层:同一份记录,三个问题。 三个数字都能在记录里找到。251,880 个 token 确实被主模型读过;1612 确实是记录里带着 advisor 用量的行数;窗口确实调低了。错的是我们拿每个数字去回答的问题:拿“读了多少”回答“上下文有多大”,拿“有多少行”回答“有多少请求”,拿“阈值降了”回答“为什么暴增”。

前两个错有一个通用的名字:重复计数(double counting),同一样东西被数了不止一次。同一段上下文被主模型读了两遍,两遍加在一起当成了上下文的大小;同一个请求写成 5 行,5 行加在一起当成了 5 次调用。底下还有一层是字段含义被误读:顶层 usage 回答的是“这次请求读入了多少”,我们拿它回答“这个会话的上下文有多大”。这两个名字都不新。agent 系统里新的是两点。第一,做判断的是 agent(证据二那组数字就是协调 agent 给出并据此判断的),它手里的每条证据都是真的,只是被拿去回答了另一个问题。第二,这些字段是 Claude Code 写的,我们没有查过它的文档,也没有它们的定义;而我们这边,9 月 6 日的调查、报表脚本、9 月 13 日的调查各自对同一份记录统计过一次,报表脚本没有沿用 9 月 6 日那次调查用过的按请求 ID 去重,9 月 13 日的调查也没有复用一周前记下的字段位置。

图 3:顶层用量翻倍加上窗口调低,造成了 9 月 14 日 452 次压缩里的 194 次倍增压缩;按行累加影响的是我们对 advisor 成本的估计,不影响峰值。

我们试过的

按时间写。每一件事都针对一个具体现象,各自只覆盖了问题的一个环节。

9 月 6 日,把 Sonnet 档会话里 advisor 的模型固定为 Sonnet。 当时的问题是 advisor 用了哪个模型,这一步只针对第二个模型是谁,没有针对调用次数和用量翻倍。

9 月 13 日,压缩窗口从 65 万降到 25 万(之后按类别调为 15 万到 25 万)。 目的是降低每次调用的读入量,实际降了多少没有单独测;同时让倍增压缩从每天个位数到十几次变成一天 194 次。它不是一个错误的决定,修正后的第 90 百分位仍高于 25 万;它是一个依据缓存读取占 97% 做出、窗口档位又按被放大的峰值选定的决定,并且把另一个问题放大了。

9 月 14 日,四条程序启动路径设置 CLAUDE_CODE_DISABLE_ADVISOR_TOOL=1。 附一个长期保留在测试集里的测试,检查这四处设置都在。编排程序派发的后台会话里,advisor 降到 0,数字见下一节。这是机制,不是提示词;这次提交只覆盖这四条路径。

同一天,报表按主模型分段计算真实上下文,增加虚假压缩统计。 上下文口径修正了,这对应证据一;调用次数仍按行统计,仍然只读顶层会话记录,证据二没有修。

9 月 24 日,新增的一条后台启动路径同样带上这个变量,测试覆盖。 防止新路径遗漏。

9 月 25 日,在给 workflow 子 agent 的派发信里写“不调用 advisor”。 这是提示词层面的要求。9 月 30 日到 10 月 3 日,子 agent 的会话记录里每天仍有 25 到 116 个含 advisor 的请求;这些子 agent 里有多少来自那条工作线,我们没有归因。

9 月 25 日,加一个全局钩子。 agent 在会话里自己输入 claude --bg 启动新会话时,命令必须带这个变量和窗口设置。起因记在这次提交的说明里:有一条工作线的 agent 自己输入命令启动会话,没有经过程序启动路径,这些会话既没有关闭 advisor,也没有我们设置的窗口上限,三天里调用 advisor 数十次,上下文涨到约 96 万(这组数字我们没有用会话记录重新核对)。钩子的测试今天通过。

图 4:用真实会话验证过关闭有效的只有编排程序派发这一条路径,队员会话和子 agent 上没有机制层面的措施,手动会话按负责人的决定有意保留。

现在的状态

截至 2026 年 10 月 6 日,按三件事分开说。

第一件:在编排程序派发的后台会话里,advisor 调用降到 0;全系统的倍增压缩从 9 月 23 日起为 0,原因没有验证。 我们把编排程序的派发记录(里面有完整启动命令和会话 ID)与会话记录逐一对照。修复之前派发、有记录的 290 个会话中,206 个(71%)调用过 advisor,共 444 个请求。修复之后派发的 239 个会话,启动命令全部带有该变量,没有一个出现 advisor 调用。派发记录(包括执行、修复、评审等各类会话)到 9 月 24 日为止,此后这条路径没有新的派发。修复之后的 239 个会话里有 210 个是评审会话;修复之前派发的评审会话,在我们核对的范围内全部调用过 advisor。claude -p 包装器等另外几处接线,我们没有单独做对照。

倍增压缩从 9 月 23 日起连续为 0。9 月 15 日有 20 次,来源没有单独归因;9 月 17 日到 21 日每天 35 到 69 次,按会话首条消息的形态判断,来自程序启动路径之外的会话;这比 9 月 10 日到 13 日还多,原因我们没有单独分析。9 月 23 日之后为 0 的原因,我们没有验证:可能是这些会话的上下文没有走到窗口,也可能是客户端行为有变化。我们没有按客户端版本分开统计过倍增压缩,所以也不能把这个变化归到某次版本更替上。顶层用量翻倍这个现象还在记录里:10 月 4 日之后的请求里,含 advisor 的请求顶层用量中位数仍是真实上下文的 1.99 倍。

第三件:压缩总次数没有马上回落。 我们当时预期,关掉 advisor 后压缩会回到每天十几次。9 月 15 日、17 日到 21 日每天仍有 95 到 347 次(9 月 16 日只有 1 次,原因没有查),其中大部分是真实上下文确实接近窗口的正常压缩(例如 9 月 18 日的 347 次里有 283 次),原因是窗口调低,和 advisor 无关。9 月 22 日到 10 月 3 日每天 0 到 31 次,与预期同量级。10 月 4 日出现 251 次,其中 248 次的压缩前大小与真实上下文相符,另外 3 次满足 1.3 倍判据但不满足倍增压缩的判据;这些压缩的来源我们没有查。窗口调低带来的压缩成本,在本次修复范围之外,还没有处理。

图 5:红褐的倍增压缩从 9 月 23 日起为 0(原因没有验证),灰的其余压缩在窗口调低之后一直存在。

advisor 在其他会话里继续出现。 9 月 15 日之后,顶层会话记录里调用过 advisor 的 284 个会话中,246 个的首条消息是队员会话的形态;其余 38 个里,17 个的首条消息是手写的自然语言,形态像手动会话,其余没有逐个归类。9 月 21 日到 10 月 3 日,advisor 用量的主体在子 agent 的会话记录里。我们推断,这些会话继承了派出它们的手动会话的设置,而手动会话按负责人的决定保留了 advisor;这一点没有做对照实验。9 月 24 日,这台服务器上含 advisor 的请求有 511 个,其中 457 个在子 agent 的记录里,advisor 的无缓存输入 1.43 亿 token,高于修复前任何一天(最高为 0.53 亿)。这一天每个含 advisor 的请求平均读入约 28 万,比修复前高,原因我们没有分析。

第二件:报表脚本还有两个计数问题。 一是 advisor 调用次数仍按行统计;二是它只读每个项目目录下的顶层会话记录,不读子 agent 的记录。9 月 24 日的报表写着 311 次、1.48 亿,去重后的全口径是 511 个请求、1.43 亿。token 数接近,是因为按行多算和漏掉子 agent 这两个误差方向相反、碰巧抵消;次数差了 200。另外,定时生成的日报每天在 UTC 14:20 运行,按 UTC 日期过滤,所以只覆盖当天前 14 个多小时。

advisor 的建议质量没有评估过。 记录里 advisor 调用的输入字段恒为空,返回内容是加密的,我们看不到它被问了什么、答了什么。

如果是你的系统

下面五条。第一条是一段脚本,今晚就能在自己的会话记录上跑;我们自己还没做完的,在条目里标出。

先跑这段脚本,看三样东西:同一个请求有几行,含 advisor 的请求各段多大,每次压缩的比值是多少。 把它存成 check_compaction.py,对一份会话记录运行:

python3 check_compaction.py ~/.claude/projects/<项目>/<会话id>.jsonl
#!/usr/bin/env python3
# 用法:python3 check_compaction.py ~/.claude/projects/<项目>/<会话id>.jsonl
# 读一份 Claude Code 会话记录:按 requestId 去重,算每个请求的真实上下文,
# 再把每次自动压缩记下的 preTokens 除以压缩前最后一次真实上下文。
import json, sys

def ctx(u):
    # 一段推理的上下文 = 输入 + 缓存读取 + 缓存写入
    return ((u.get("input_tokens") or 0) + (u.get("cache_read_input_tokens") or 0)
            + (u.get("cache_creation_input_tokens") or 0))

requests, order, compactions = {}, [], []
srv = {}   # 请求里 advisor 的服务端工具调用块数(每个内容块一行,要跨行累加)
with open(sys.argv[1], encoding="utf-8", errors="replace") as f:
    for line in f:
        if not line.strip():
            continue
        try:
            row = json.loads(line)
        except ValueError:          # 正在写入的最后一行可能不完整
            continue
        if not isinstance(row, dict):
            continue
        if row.get("type") == "assistant":
            rid = row.get("requestId")
            if not rid:
                continue
            for c in (row.get("message") or {}).get("content") or []:
                if isinstance(c, dict) and c.get("type") == "server_tool_use" and c.get("name") == "advisor":
                    srv[rid] = srv.get(rid, 0) + 1
            u = (row.get("message") or {}).get("usage") or {}
            its = u.get("iterations") or []
            main = [ctx(i) for i in its if i.get("type") == "message"]
            first_ts = requests[rid]["ts"] if rid in requests else row.get("timestamp")
            if rid not in requests:             # 同一个请求写成多行,只算一次;用量取最后一行
                order.append(rid)
            requests[rid] = {
                "ts": first_ts,
                "top": ctx(u),                          # 顶层:主模型各段之和
                "real": max(main) if main else ctx(u),  # 真实上下文:主模型各段中的最大值
                "segments": main,
                "advisor": sum(i.get("type") == "advisor_message" for i in its),
            }
        elif row.get("subtype") == "compact_boundary":
            pre = (row.get("compactMetadata") or {}).get("preTokens")
            compactions.append((row.get("timestamp"), pre, order[-1] if order else None))

for rid in requests:
    requests[rid]["advisor"] = max(requests[rid]["advisor"], srv.get(rid, 0))
print(f"去重后的请求数:{len(requests)}")
print("含 advisor 调用的请求:")
for rid in order:
    r = requests[rid]
    if r["advisor"]:
        segs = " + ".join(f"{s:,}" for s in r["segments"])
        print(f"  {r['ts']}  主模型各段 {segs} = 顶层 {r['top']:,};真实上下文 {r['real']:,}")
print("自动压缩:")
for ts, pre, rid in compactions:
    r = requests.get(rid)
    if r and pre and r["real"]:
        print(f"  {ts}  preTokens {pre:,};压缩前真实上下文 {r['real']:,};"
              f"比值 {pre / r['real']:.2f};前一个请求调用 advisor {r['advisor']} 次")

它做三件事。按 requestId 去重:同一个请求会写成多行,每行带一份 usage,脚本取最后一行;我们的记录里含 advisor 调用的请求平均约 5 行(9 月 14 日 1672 行对 310 个请求;不含的请求平均约 2 行),按行累加 advisor 的调用次数和 token,约为真实值的 5 倍。算真实上下文:含 advisor 调用的请求,顶层用量不等于上下文大小;在我们核对过的含 advisor 请求里,顶层数字是主模型各段之和,脚本用“各段中的最大值”作为这个请求的真实上下文。算比值:找出 subtype 为 compact_boundary 的压缩记录,把 preTokens 除以压缩前最后一个请求的真实上下文。

在本文的案例会话上运行,输出节选如下:

含 advisor 调用的请求:
  2026-09-14T13:09:34.042Z  主模型各段 124,519 + 127,361 = 顶层 251,880;真实上下文 127,361
自动压缩:
  2026-09-14T13:15:09.815Z  preTokens 254,808;压缩前真实上下文 127,361;比值 2.00;前一个请求调用 advisor 1 次
  2026-09-14T13:24:25.899Z  preTokens 218,477;压缩前真实上下文 212,823;比值 1.03;前一个请求调用 advisor 0 次

这个会话一共 6 次自动压缩:2 次比值为 2.00,前一个请求都调用过 advisor;另外 4 次在 1.02 到 1.13 之间,前一个请求都没有调用 advisor。读你自己的输出时:比值接近 2 的,看前一个请求有没有调用 advisor。脚本数 advisor 调用时取两种痕迹中的较大值:usage.iterations 里的 advisor_message 段,和内容块里名为 advisor 的服务端工具调用(server_tool_use),因为少数请求只有后者(我们 9 月 14 日的 194 次倍增压缩里有 4 次是这样)。按这个口径,我们的 194 次全部有;在服务器上 9 月 9 日之后修改过、含压缩记录的 854 份会话记录(含子 agent 记录)上跑,比值不小于 1.8 的 516 次压缩全部显示至少 1 次 advisor,没有一份报错。前一个请求没有用量记录的压缩,脚本不打印,所以输出行数可能少于压缩记录的条数。比值接近 1 的,压缩前大小与真实上下文相符,不是 advisor 造成的倍增,我们在 9 月 15 日到 21 日核对过,其中大部分是窗口调低造成的,10 月 4 日那 248 次的来源没有查。三点提醒:脚本只在我们自己的记录上跑过(版本 2.1.258 到 2.1.287 为主),字段名在别的版本上可能不同;子 agent 的记录在 <会话id>/subagents/ 下,要分别运行;会话记录里有客户端自己的累计用量记录时(我们的记录里是类型为 cost-state 的那一行),拿几个会话对一次账,我们对了 6 个,去重口径逐位相等。

算成本时,分清你要的是“上下文有多大”还是“实际读入了多少”。 前者取主模型各段中的最大值,后者取主模型各段之和;advisor 那一段的读入量不在顶层里,要另从 iterations 里取。基于顶层数字计算的峰值和窗口阈值,需要按这个口径重算一次。

调低阈值之前,先找出只在高占用时才会出问题的机制。 一个偶发的问题,在阈值降低后可能变成高频问题。调整阈值时,把压缩按“真实上下文是否接近窗口”分成两类分别计数,这样能看出总数变化里各有多少来自哪个原因。我们是事后才这么分的;分开之前,暴增被我们全算在 advisor 上。

关闭一个功能时,列出所有会话启动路径,然后用真实会话验证,不只用读源码的测试。 我们在编排程序派发的路径上关掉了 advisor,这条路径上的效果是 239 个会话 0 次调用;agent 自己输入的启动命令、队员会话和子 agent 是另外几条路径,advisor 在那里继续出现。可以检查:你系统里一个会话一共有几种启动方式,每种方式下这个设置是否生效。我们长期保留的那个测试读取源代码,确认四处设置都在。它保证设置不会被改掉,不能证明真实会话里不再调用 advisor。后者需要把派发记录里的启动命令和会话记录里的实际调用逐一对照。子 agent 和队员会话这两条路径上,我们到今天没有机制层面的措施。

改动之前写下预期,改动之后对照。 我们事先写下了预期:关掉之后,压缩会回到每天十几次。事后对照发现前一周没有做到,才把窗口调低作为另一个独立原因分了出来。9 月 15 日、17 日到 21 日每天有 95 到 347 次压缩;没有事先写下的预期,这些数字容易被看成修复没生效,有了预期,才看出需要另找原因。

还没想明白的

有两件事我们到今天没有答案,想听听你的做法。

第一件关于字段的定义。我们用一个第三方客户端写下的字段做了阈值决策,这个字段对“读了多少”是准的、对“上下文有多大”大了一倍,而我们是在压缩暴增之后才回头查它的构成。对 advisor 读入量和缓存的描述,我们只依据自己的观测,没有引用产品文档。如果你的系统也依赖别人写的用量字段,你在把它用于决策之前,有没有一个固定的步骤去确认它回答的是你问的那个问题?是读文档、拿已知案例校准,还是像我们这次一样事后对账才发现?

第二件关于在哪一层关掉一个工具。我们在程序启动路径上用环境变量关了 advisor,对手动输入的启动命令加了钩子,对 workflow 子 agent 只写了提示词,对队员会话和 Agent 工具派出的子 agent 没有机制层面的措施,而手动会话是有意保留的。同一时期,9 月 24 日 advisor 的用量比修复前任何一天都高,主体在子 agent 里,原因我们没有分析。我们卡在这里:子 agent 和队员会话继承的是派出它们的手动会话的设置(这是推断,没有做对照实验),而手动会话要保留 advisor。如果你的系统里也有手动会话派出自动会话这条链,你是怎么让一个设置只对派生出来的会话生效、不对手动会话生效的?

还有两个小一点的问题。倍增压缩从 9 月 23 日起为 0,我们没有验证原因;如果你在自己的记录里看到含 advisor 请求的 preTokens 行为前后有变化,或者在某个版本上跑上面的脚本得到不一样的形态,我们想知道。另外,advisor 给的建议值不值它读入的那一整段上下文,我们没有评估过;记录里看不到它被问了什么、答了什么。如果你评估过,是怎么评的?


依据说明:本文的数字主要来自 Flowness 服务器上 2026 年 9 月 9 日之后修改过的 10,896 个会话记录文件(含 3,797 个子 agent 记录),按请求 ID 去重,时刻均为 UTC;另有编排程序的派发记录、git 提交历史、每日成本报表和当前代码。倍增压缩的精确判据是:压缩前最后一个请求的顶层用量,不小于它主模型各段中最大一段的 1.8 倍,并且压缩记录的 preTokens 与这个顶层用量相差不超过 10%。9 月 6 日调查的数字、9 月 13 日决策时的峰值和那次 10 个会话的对账,来自当时的调查记录与工作日志,我们没有重新核对。“客户端依据顶层数字判断上下文已满”是由比值推断的,我们没有查看客户端代码。对 advisor 读入量和缓存的描述,只依据我们自己的观测,没有引用产品文档。文中提到的记录字段名(requestId、usage.iterations、compact_boundary、preTokens、cost-state、version、model)和文件位置,在我们机器上的一份会话记录里核对过存在,字段的含义仍是我们的解读。案例会话的版本(Claude Code 2.1.270)和模型(主模型与 advisor 均为 claude-sonnet-5)读自该会话的记录;其他会话的版本分布(2.1.258 到 2.1.287)读自同期压缩记录上的 version 字段,我们没有按版本分开统计任何现象。“如果是你的系统”里的脚本在案例会话的记录上运行过,输出中的四个数与正文一致;也在服务器上 9 月 9 日之后修改过、含压缩记录的 854 份会话记录上运行过,没有报错,比值不小于 1.8 的压缩全部显示至少 1 次 advisor。脚本数 advisor 调用时,同时数 usage.iterations 里的 advisor_message 段和内容块里名为 advisor 的服务端工具调用,因为少数请求只有后者。状态截至 2026 年 10 月 6 日。


每篇末尾的问题我们自己还没有答案。你有做法,或者在自己的系统里见过同样的事,欢迎写信到 hi@natureblueee.com。