为什么你说的「优化生效了」,不拿日志根本没人信

cache 命中、上下文瘦身、压缩生效——每一句声称都得能用 usage 日志还原;没观测的优化只是玄学。

Module E · 第 6 讲

立靶:你说优化生效了,可你看的是哪个数

前面几讲做了一连串动作:把静态前缀做厚去命中 cache,把映射表挪出 prompt 省 token,给历史接上压缩。每一步你都觉得「这下省了」。但有人追问一句——省了多少,你怎么知道的? 多半答不上来,或者随手报一个数字,而那个数字往往是面板上最显眼、却最不靠谱的那一个。

这一讲是整个 Module E 的收口。前面每一讲都埋了一句「这里应该会省 / 应该会快」,全是设计期的假设。可观测与评估,就是把这些口头账全部换成数字的那一层:模型这一轮实际收到了多少 token,cache 到底命中了没有,改完之后是真变好还是只是你想变好。

核心问题只有一个:你声称的每一次优化,拿什么证明? CE 的原则里有一条「迭代优化不能凭感觉」——没有观测的 CE,本质上是玄学。CONTEXT.md 是你的设计期账本(你以为模型看到什么),usage 日志是运行期对账单(模型实际看到什么)。两者必须能对上;对不上的差额,就是上下文里的暗物质

接手任何一个 Agent,先问两个问题就能判断它的 CE 成熟度:「每轮 usage 落在哪?」「CONTEXT.md 估的 token 和日志对过账吗?」两个都答不上来,说明这个项目的上下文工程还停在设计稿阶段。

框架:先读懂 usage,再搭三层观测,最后用 4 维评估

usage 字段:两家口径正好相反,读错差一个数量级

最容易栽跟头的不是观测搭得有多复杂,而是第一步——把 usage 字段读对。同一个「token 用量」,两大类 API 口径相反,混用必出错。

Anthropic 口径下四个字段互斥、不重叠

字段含义计价(相对 input)
input_tokens仅未缓存部分,不含下面两项
cache_creation_input_tokens本轮新写入 cache 的部分1.25×(5min)/ 2×(1h)
cache_read_input_tokens本轮从 cache 读出的部分0.1×
output_tokens输出output 价

真实 input 总量 = input + cache_creation + cache_read。最致命的误读是看到 input_tokens: 800 就说「这轮才 800」——cache_read 里可能还躺着几千 token。别被最小的那个字段骗了。

而 OpenAI 兼容口径正好反过来:prompt_tokens含缓存的全量prompt_tokens_details.cached_tokens 是其中命中缓存的子集,completion_tokens 是输出。Anthropic 的 input_tokens 是「扣掉缓存后的余量」,OpenAI 的 prompt_tokens 是「含缓存的总量」——一个减了、一个没减。跨端点比数据、写一个通用日志层时,必须先归一化成「总量 + 缓存命中量」两列,否则同一通对话两边日志能差出整整一个 cache 的量。

三层观测:从一行日志到一张对照表

观测不是某个高级看板,而是三个层级叠起来,每一层回答一类问题。

层级长什么样回答什么
第 1 层 · 轮级快照每轮一行 jsonl/tsv:{turn, ts, prompt_tokens, completion_tokens, cached, ttft, duration, cancelled}这一轮模型看到多少、花了多少、等了多久
第 2 层 · 会话曲线一通对话一条线,x=轮次,y=prompt_tokenshistory 怎么涨的、压缩有没有触发、撞不撞 context 上限
第 3 层 · 版本对照同一组 fixture × N 次 × 改动前后 → 4 维指标对照表这次改动到底有没有变好、好在哪一维、代价在哪一维

第 1 层是地基:一轮一行、机器可读,这正是长任务 Agent 通用的「tsv 记账」纪律,也是任何自动评估 harness 的输入。

第 2 层的关键在于曲线形状会说话:线性递增 = 无压缩、历史全保留;锯齿状回落 = 压缩 / truncation 触发了;突然阶跃 = 某轮注入了大块(一段大 tool_result 的 JSON、一次记忆灌入);平线 = 固定窗口截断。不用看内容,光看形状就能定位问题在哪一轮。

第 3 层的纪律最硬:先有 baseline 再改动,一次只动一个变量。改了 system 又顺手换了模型再去对照,归因等于零——你永远说不清是哪个动作带来的变化。

4 维评估:效率成本可机测,质量体验必须留人

光有 token 数还不够。一次改动到底「赚不赚」,要四个维度一起看。

维度指标怎么测能否全自动
效率input/output tokens、cache 命中率、ttft、totalusage 字段 + 时间戳纯机器
质量响应相关性、任务完成率smoke 断言 + LLM-as-judge + 人工抽查半自动
体验满意度、首轮体感延迟(看 p95 不看平均)真实交互 + 用户反馈必须有人
成本单 turn / 月度成本usage × 单价累加纯机器

三条落地要点:

第一,效率和成本可以全自动跑,质量和体验必须留人工兜底。质量维可以用 LLM-as-judge 做半自动化,但 judge 本身要定期人工校准,否则 judge 悄悄漂移了没人发现。

第二,延迟看 p95,不看平均。一个语音场景 ttft 平均 800ms 看着很健康,但 p95 若是 3s,用户记住的就是那次 3 秒的尴尬沉默。体验由尾部决定,平均值会撒谎。

第三,四个维度经常互相打架:砍 token 提了效率,却伤了质量;提前生成降了延迟,却涨了成本。所以对照表必须四维一起看,单维报喜 = 没做完评估

数据 / case:三个脱敏现场

面板口径把真实流量低估了近两个数量级。 某团队把自己某个成熟 Agent 产品的全部历史会话用量加总,结果发现常用统计面板上显示的那个 token 数,只取了四字段里最小的那一两个(input + output),完全没算 cache 的两兄弟。真实流量比面板数字高出近两个数量级。这不是玩具题——「别被最小的字段骗了」在真实账本上的差距,可以达到约 90 倍。顺带一提,这个产品 input_tokens 占真实总量不到 1%,反而印证了它的 cache 工程做得极好:超过九成的输入都走了 0.1× 的缓存读。报一个 token 数字,先报清是哪种口径——展示口径、流量口径、计费口径是三个完全不同的数。

框架黑盒挡得住 message,挡不住 metrics。 某语音 Agent 用的框架把对话历史托管在内部,代码里根本看不见那个数组,于是有人下结论「history 增长测不了,框架是黑盒」。其实不然:框架在每次 LLM 调用后都会发一个 metrics 事件,里面带着 prompt_tokens。挂一个监听器,每轮把这个数字抄进 jsonl,就能从外面把黑盒的输入量逐轮还原,完全不用改框架内部。把逐轮 prompt_tokens 连成线,是线性递增就坐实了「无压缩全保留」,再按斜率外推到预期轮数,就知道一通长对话离 context 上限还有多远、要不要自己接管压缩。黑盒挡住的是内容,不是输入量。

指标全绿,照样可能翻车。 假设你给某个 Agent 接好了全套观测,一个月做了三次优化,看板显示单 turn 成本降了 55%、ttft 降了 30%,你准备在周报里宣布胜利。等一下——这份周报缺了质量和体验两维,恰好是两个纯机器测不了的维度。而成本降 55% 的手段(砍 prompt、压输出上限、关掉提前生成)每一条都可能在悄悄伤通话质量和接通体验,看板上根本没有那两列,伤了也不显示。这就是 Goodhart 陷阱:指标一旦成为目标,就不再是好指标。给了你一套自动指标,本能就是去优化指标本身,而不是去优化背后那个真实的工作流。

可操作做法:PM 与工程两条线

PM 视角——用它做判断和验收:

  1. 任何人来汇报「优化生效了」,先问一句「你看的是哪个口径的数、有没有 baseline 对照」。没有 baseline 的「变好了」一律按未验证处理。
  2. 验收一次 CE 改动,要四维一起要:不只看省了多少 token / 钱,还得看质量和体验那两列有没有人工抽查背书。只报效率和成本的周报,是危险信号。
  3. 警惕 Goodhart:定了自动指标之后,盯紧团队是在改善真实工作流,还是在讨好指标本身。指标服务于工作流,不能反过来。
  4. 延迟相关的承诺一律要 p50 / p95 两个数。只给平均值的延迟报告,等于没报。

工程视角——用它搭可观测:

  1. 第 1 层轮级日志常开,不是 debug 开关。出了问题才去开日志,等于没有 baseline,问题来了无从对照、无法归因。
  2. 写通用日志层先归一化 usage:不管底层是哪家口径,统一落「总量 + 缓存命中量 + 输出」三列,跨端点才能直接比。
  3. 失败轮和作废轮照记,单独标一列。提前生成 / 重试产生的作废调用也照付 input 费用,只记成功轮会漏掉一个成本黑洞。
  4. 观测一律在 API 返回层做,绝不在 prompt 层做——别指望让模型自报 token 数,它不知道自己的 token 数,让它报纯属幻觉。
  5. 评估守三条纪律:先 baseline 后改动、一次只动一个变量、runs ≥ 3 取均值(LLM 非确定性,单次结果不算数)。fixture 要覆盖被改动项服务的场景——你动的是 ASR 消歧表,fixture 里就得有错字样本。
  6. 观测的产出不是日志文件本身,是回填 CONTEXT.md:实测出来的真实 token、坐实的暗物质,都要写回设计账本。这才是 CONTEXT.md 作为活文档的运转方式。

顺带一提,这层并非要你从零造轮子。一些成熟的桌面客户端已经把「这一轮上下文构成」做成了用户可见的占比条(system / 工具 / 记忆 / 历史各占多少),一些 CLI 也提供了类似的 /context 命令——它们就是 CONTEXT.md 的运行期可视化版本。设计账本(静态文档)→ 对账单(每轮日志)→ 仪表盘(实时可视化),是同一件事的三级形态。你自己的 Agent 不必做到第三级,但第一级的轮级 jsonl 是底线。

收口

没观测的上下文等于不存在——你不能优化一个你看不见的东西。CONTEXT.md 是设计账本,usage 日志是对账单,对不上的差额就是暗物质。整个 Module E 到这里合上一个环:先把七层照亮、把账对平,再谈怎么省、怎么稳,而每一句「省了 / 稳了」,最终都要回到这张日志上接受检验。