为什么你说的「优化生效了」,不拿日志根本没人信
cache 命中、上下文瘦身、压缩生效——每一句声称都得能用 usage 日志还原;没观测的优化只是玄学。
立靶:你说优化生效了,可你看的是哪个数
前面几讲做了一连串动作:把静态前缀做厚去命中 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 | 仅未缓存部分,不含下面两项 | 1× |
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_tokens | history 怎么涨的、压缩有没有触发、撞不撞 context 上限 |
| 第 3 层 · 版本对照 | 同一组 fixture × N 次 × 改动前后 → 4 维指标对照表 | 这次改动到底有没有变好、好在哪一维、代价在哪一维 |
第 1 层是地基:一轮一行、机器可读,这正是长任务 Agent 通用的「tsv 记账」纪律,也是任何自动评估 harness 的输入。
第 2 层的关键在于曲线形状会说话:线性递增 = 无压缩、历史全保留;锯齿状回落 = 压缩 / truncation 触发了;突然阶跃 = 某轮注入了大块(一段大 tool_result 的 JSON、一次记忆灌入);平线 = 固定窗口截断。不用看内容,光看形状就能定位问题在哪一轮。
第 3 层的纪律最硬:先有 baseline 再改动,一次只动一个变量。改了 system 又顺手换了模型再去对照,归因等于零——你永远说不清是哪个动作带来的变化。
4 维评估:效率成本可机测,质量体验必须留人
光有 token 数还不够。一次改动到底「赚不赚」,要四个维度一起看。
| 维度 | 指标 | 怎么测 | 能否全自动 |
|---|---|---|---|
| 效率 | input/output tokens、cache 命中率、ttft、total | usage 字段 + 时间戳 | 纯机器 |
| 质量 | 响应相关性、任务完成率 | 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 视角——用它做判断和验收:
- 任何人来汇报「优化生效了」,先问一句「你看的是哪个口径的数、有没有 baseline 对照」。没有 baseline 的「变好了」一律按未验证处理。
- 验收一次 CE 改动,要四维一起要:不只看省了多少 token / 钱,还得看质量和体验那两列有没有人工抽查背书。只报效率和成本的周报,是危险信号。
- 警惕 Goodhart:定了自动指标之后,盯紧团队是在改善真实工作流,还是在讨好指标本身。指标服务于工作流,不能反过来。
- 延迟相关的承诺一律要 p50 / p95 两个数。只给平均值的延迟报告,等于没报。
工程视角——用它搭可观测:
- 第 1 层轮级日志常开,不是 debug 开关。出了问题才去开日志,等于没有 baseline,问题来了无从对照、无法归因。
- 写通用日志层先归一化 usage:不管底层是哪家口径,统一落「总量 + 缓存命中量 + 输出」三列,跨端点才能直接比。
- 失败轮和作废轮照记,单独标一列。提前生成 / 重试产生的作废调用也照付 input 费用,只记成功轮会漏掉一个成本黑洞。
- 观测一律在 API 返回层做,绝不在 prompt 层做——别指望让模型自报 token 数,它不知道自己的 token 数,让它报纯属幻觉。
- 评估守三条纪律:先 baseline 后改动、一次只动一个变量、
runs ≥ 3取均值(LLM 非确定性,单次结果不算数)。fixture 要覆盖被改动项服务的场景——你动的是 ASR 消歧表,fixture 里就得有错字样本。 - 观测的产出不是日志文件本身,是回填 CONTEXT.md:实测出来的真实 token、坐实的暗物质,都要写回设计账本。这才是 CONTEXT.md 作为活文档的运转方式。
顺带一提,这层并非要你从零造轮子。一些成熟的桌面客户端已经把「这一轮上下文构成」做成了用户可见的占比条(system / 工具 / 记忆 / 历史各占多少),一些 CLI 也提供了类似的 /context 命令——它们就是 CONTEXT.md 的运行期可视化版本。设计账本(静态文档)→ 对账单(每轮日志)→ 仪表盘(实时可视化),是同一件事的三级形态。你自己的 Agent 不必做到第三级,但第一级的轮级 jsonl 是底线。
收口
没观测的上下文等于不存在——你不能优化一个你看不见的东西。CONTEXT.md 是设计账本,usage 日志是对账单,对不上的差额就是暗物质。整个 Module E 到这里合上一个环:先把七层照亮、把账对平,再谈怎么省、怎么稳,而每一句「省了 / 稳了」,最终都要回到这张日志上接受检验。